commit c60cc7ca752d4da4f3a63768c5cf098e69c9c913 Author: upstream-mirror Date: Tue Sep 15 21:19:12 2026 +0500 snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted) diff --git a/.docker/Dockerfile-alpine b/.docker/Dockerfile-alpine new file mode 100644 index 0000000..6e1848e --- /dev/null +++ b/.docker/Dockerfile-alpine @@ -0,0 +1,35 @@ +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. +# +# To get SQLite and Docker Volumes working with this image, mount the volume where SQLite should be written to at: +# +# /home/ory/sqlite/some-file. + +RUN addgroup -S ory; \ + adduser -S ory -G ory -D -u 10000 -h /home/ory -s /bin/nologin; \ + chown -R ory:ory /home/ory +RUN apk --update upgrade && apk --no-cache --update-cache --upgrade --latest add ca-certificates + +WORKDIR /home/ory + +COPY kratos /usr/bin/kratos + +# By creating the sqlite folder as the ory user, the mounted volume will be owned by ory:ory, which +# is required for read/write of SQLite. +RUN mkdir -p /var/lib/sqlite +RUN chown ory:ory /var/lib/sqlite +VOLUME /var/lib/sqlite + +# Exposing the ory home directory to simplify passing in Kratos configuration (e.g. if the file $HOME/.kratos.yaml +# exists, it will be automatically used as the configuration file). +VOLUME /home/ory + +# Declare the standard ports used by Kratos (4433 for public service endpoint, 4434 for admin service endpoint) +EXPOSE 4433 4434 + +USER 10000 + +ENTRYPOINT ["kratos"] +CMD ["serve"] diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build new file mode 100644 index 0000000..57e5e49 --- /dev/null +++ b/.docker/Dockerfile-build @@ -0,0 +1,46 @@ +FROM golang:1.26-trixie AS builder + +RUN apt-get update && apt-get upgrade -y &&\ + mkdir -p /var/lib/sqlite + +WORKDIR /go/src/github.com/ory/kratos + +COPY oryx/go.mod oryx/go.mod +COPY oryx/go.sum oryx/go.sum + + +COPY go.mod go.mod +COPY go.sum go.sum +COPY pkg/client-go/go.* pkg/client-go/ + +ENV CGO_ENABLED 0 + +RUN go mod download + +COPY . . + +ARG VERSION +ARG COMMIT +ARG BUILD_DATE + +RUN --mount=type=cache,target=/root/.cache/go-build go build \ + -ldflags="-X 'github.com/ory/kratos/driver/config.Version=${VERSION}' -X 'github.com/ory/kratos/driver/config.Date=${BUILD_DATE}' -X 'github.com/ory/kratos/driver/config.Commit=${COMMIT}'" \ + -o /usr/bin/kratos + +######################### +# Kratos is built with CGO_ENABLED=0, so it needs no glibc at runtime. The static +# base carries none, which keeps this image free of the unfixable libc6 +# advisories the base-nossl variant ships with, and matches the base the released +# images use (.docker/Dockerfile-distroless-static). +FROM gcr.io/distroless/static-debian12:nonroot AS runner + +COPY --from=builder --chown=nonroot:nonroot /var/lib/sqlite /var/lib/sqlite +COPY --from=builder --chown=nonroot:nonroot /usr/bin/kratos /usr/bin/kratos + +VOLUME /var/lib/sqlite + +# Declare the standard ports used by Kratos (4433 for public service endpoint, 4434 for admin service endpoint) +EXPOSE 4433 4434 + +ENTRYPOINT ["kratos"] +CMD ["serve"] diff --git a/.docker/Dockerfile-debug b/.docker/Dockerfile-debug new file mode 100644 index 0000000..b42a5e7 --- /dev/null +++ b/.docker/Dockerfile-debug @@ -0,0 +1,18 @@ +FROM golang:1.26-trixie +ENV CGO_ENABLED 0 + +RUN apt-get update && apt-get install -y --no-install-recommends inotify-tools psmisc +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +COPY script/debug-entrypoint.sh /entrypoint.sh + +VOLUME /dockerdev + +WORKDIR /dockerdev + +ENV DELVE_PORT 40000 +ENV SERVICE_NAME service + +EXPOSE 8000 $DELVE_PORT + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.docker/Dockerfile-distroless-static b/.docker/Dockerfile-distroless-static new file mode 100644 index 0000000..5697233 --- /dev/null +++ b/.docker/Dockerfile-distroless-static @@ -0,0 +1,7 @@ +FROM gcr.io/distroless/static-debian12:nonroot + +COPY kratos /usr/bin/kratos +EXPOSE 4433 4434 + +ENTRYPOINT ["kratos"] +CMD ["serve"] diff --git a/.docker/docker-compose.template.dbg b/.docker/docker-compose.template.dbg new file mode 100644 index 0000000..e298132 --- /dev/null +++ b/.docker/docker-compose.template.dbg @@ -0,0 +1,17 @@ +version: '3.7' + +services: + ${SERVICE_NAME}: + build: + dockerfile: ./.docker/Dockerfile-debug + ports: + - ${REMOTE_DEBUGGING_PORT}:40000 + security_opt: + - apparmor=unconfined + cap_add: + - SYS_PTRACE + volumes: + - type: bind + source: ${SERVICE_ROOT} + target: /dockerdev + read_only: false diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b8e6ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +docs +.releaser +.github +.circleci +tmp +scripts +.idea +.git/ +database.yaml +contrib/quickstart +node_modules/ +./quickstart.yml +./quickstart-*.yml +.bin/ +test/ +pgked.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a543c21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.go] +indent_size = 4 +indent_style = tab diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ef90d00 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @aeneasr @ory/product-development diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c440360 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/FUNDING.yml + +# These are supported funding model platforms + +# github: +patreon: _ory +open_collective: ory diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 0000000..652db61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,122 @@ +# 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" +labels: + - bug +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" + options: + - 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 + 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 + Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/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" + 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!" + id: describe-bug + type: textarea + validations: + required: true + - attributes: + description: | + Clear, formatted, and easy to follow steps to reproduce the behavior: + placeholder: | + Steps to reproduce the behavior: + + 1. Run `docker run ....` + 2. Make API Request to with `curl ...` + 3. Request fails with response: `{"some": "error"}` + 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 + automatically formatted into code, so no need for backticks. Please + redact any sensitive information" + label: "Relevant log output" + render: shell + placeholder: | + log=error .... + id: logs + type: textarea + - attributes: + description: + "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" + render: yml + placeholder: | + server: + admin: + port: 1234 + id: config + type: textarea + - attributes: + 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?" + options: + - Ory Network + - macOS + - Linux + - Windows + - FreeBSD + - Other + id: operating-system + type: dropdown + - attributes: + label: "In which environment are you deploying?" + options: + - Ory Network + - Docker + - "Docker Compose" + - "Kubernetes with Helm" + - Kubernetes + - Binary + - Other + id: deployment + type: dropdown + - attributes: + 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 new file mode 100644 index 0000000..3f60848 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml @@ -0,0 +1,125 @@ +# 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." +labels: + - rfc +name: "Design Document" +body: + - attributes: + value: | + Thank you for writing this design document. + + One of the key elements of Ory's software engineering culture is the use of defining software designs through design docs. These are relatively informal documents that the primary author or authors of a software system or application create before they embark on the coding project. The design doc documents the high level implementation strategy and key design decisions with emphasis on the trade-offs that were considered during those decisions. + + Ory is leaning heavily on [Google's design docs process](https://www.industrialempathy.com/posts/design-docs-at-google/) + and [Golang Proposals](https://github.com/golang/proposal). + + Writing a design doc before contributing your change ensures that your ideas are checked with + the community and maintainers. It will save you a lot of time developing things that might need to be changed + after code reviews, and your pull requests will be merged faster. + type: markdown + - attributes: + label: "Preflight checklist" + options: + - 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 + 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 + Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/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" + 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" + id: scope + type: textarea + validations: + required: true + + - 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" + id: goals + type: textarea + validations: + required: true + + - attributes: + description: | + This section should start with an overview and then go into details. + 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" + id: design + type: textarea + validations: + required: true + + - 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" + 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" + 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" + id: pseudocode + type: textarea + + - attributes: + description: | + One of the primary factors that would influence the shape of a software design and hence the design doc, is the degree of constraint of the solution space. + + On one end of the extreme is the “greenfield software project”, where all we know are the goals, and the solution can be whatever makes the most sense. Such a document may be wide-ranging, but it also needs to quickly define a set of rules that allow zooming in on a manageable set of solutions. + + 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" + id: constrait + type: textarea + + - attributes: + description: | + This section lists alternative designs that would have reasonably achieved similar outcomes. The focus should be on the trade-offs that each respective design makes and how those trade-offs led to the decision to select the design that is the primary topic of the document. + + While it is fine to be succinct about a solution that ended up not being selected, this section is one of the most important ones as it shows very explicitly why the selected solution is the best given the project goals and how other solutions, that the reader may be wondering about, introduce trade-offs that are less desirable given the goals. + + label: Alternatives considered + id: alternatives + type: textarea diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml new file mode 100644 index 0000000..fbbb238 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -0,0 +1,86 @@ +# 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" +labels: + - feat +name: "Feature Request" +body: + - attributes: + value: | + Thank you for suggesting an idea for this project! + + 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" + options: + - 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 + 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 + Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/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" + id: ory-network-project + type: input + - attributes: + 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 [...]" + id: problem + type: textarea + validations: + required: true + - attributes: + description: | + Describe the solution you'd like + placeholder: | + A clear and concise description of what you want to happen. + label: "Describe your ideal solution" + id: solution + type: textarea + validations: + required: true + - attributes: + description: "Describe alternatives you've considered" + label: "Workarounds or alternatives" + id: alternatives + type: textarea + validations: + required: true + - attributes: + 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." + label: Additional Context + id: additional + type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4f556b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/config.yml + +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 + discuss ideas. + - name: Ory Chat + url: https://www.ory.com/chat + about: + Hang out with other Ory community members to ask and answer questions. diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..c6cf23b --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,16 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/auto_assign.yml + +# Set to true to add reviewers to pull requests +addReviewers: true + +# Set to true to add assignees to pull requests +addAssignees: true + +# A list of reviewers to be added to pull requests (GitHub user name) +assignees: + - ory/maintainers + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +numberOfReviewers: 0 diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..8ada3f0 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,8 @@ +name: "CodeQL config" + +queries: + - uses: security-and-quality + +paths-ignore: + - "/test/" + - "/pkg/testhelpers" diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000..4fed118 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,6 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/config.yml + +todo: + keyword: "@todo" + label: todo diff --git a/.github/conventional_commits.json b/.github/conventional_commits.json new file mode 100644 index 0000000..c879da8 --- /dev/null +++ b/.github/conventional_commits.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://raw.githubusercontent.com/ory/ci/master/conventional_commit_config/dist/config.schema.json", + "addTypes": ["improvement", "perf"], + "addScopes": ["cli", "sql"] +} diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..8246b68 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,67 @@ +[ + { + "name": "package/2fa", + "color": "0A28FD", + "aliases": ["module:2fa"] + }, + { + "name": "package/cli", + "color": "0A28FD", + "aliases": ["module:cli"] + }, + { + "name": "package/courier", + "color": "0A28FD", + "aliases": ["module:courier"] + }, + { + "name": "package/courier", + "color": "0A28FD", + "aliases": ["module:docs"] + }, + { + "name": "package/selfservice/errorx", + "color": "0A28FD", + "aliases": ["module:errorx"] + }, + { + "name": "package/identity", + "color": "0A28FD", + "aliases": ["module:identity"] + }, + { + "name": "package/persistence/sql", + "color": "0A28FD", + "aliases": ["module:migrations"] + }, + { + "name": "package/selfservice", + "color": "0A28FD", + "aliases": ["module:selfservice"] + }, + { + "name": "package/selfservice/oidc", + "color": "0A28FD", + "aliases": ["module:ss/oidc"] + }, + { + "name": "package/selfservice/password", + "color": "0A28FD", + "aliases": ["module:ss/password"] + }, + { + "name": "package/selfservice/verification", + "color": "0A28FD", + "aliases": ["module:verification"] + }, + { + "name": "package/selfservice/recovery", + "color": "0A28FD", + "aliases": [] + }, + { + "name": "package/session", + "color": "0A28FD", + "aliases": ["module:session"] + } +] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..32c141f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,60 @@ + + +## Related issue(s) + + + +## Checklist + + + +- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md). +- [ ] I have referenced an issue containing the design document if my change + introduces a new feature. +- [ ] I am following the + [contributing code guidelines](../blob/master/CONTRIBUTING.md#contributing-code). +- [ ] I have read the [security policy](../security/policy). +- [ ] I confirm that this pull request does not address a security + vulnerability. If this pull request addresses a security vulnerability, I + confirm that I got the approval (please contact + [security@ory.com](mailto:security@ory.com)) from the maintainers to push + the changes. +- [ ] I have added tests that prove my fix is effective or that my feature + works. +- [ ] I have added or changed [the documentation](https://github.com/ory/docs). + +## Further Comments + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..eb81e2d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,385 @@ +name: CI +on: + push: + branches: + - master + tags: + - "*" + pull_request: + +# Cancel in-progress runs in current workflow. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CGO_ENABLED: "0" + +jobs: + test: + name: Run tests and lints + runs-on: ubuntu-latest + services: + postgres: + image: postgres:18 + env: + POSTGRES_DB: postgres + POSTGRES_PASSWORD: test + POSTGRES_USER: test + ports: + - 5432:5432 + mysql: + image: mysql:9.6 + env: + MYSQL_ROOT_PASSWORD: test + ports: + - 3306:3306 + env: + TEST_DATABASE_POSTGRESQL: "postgres://test:test@localhost:5432/postgres?sslmode=disable" + TEST_DATABASE_MYSQL: "mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true" + TEST_DATABASE_COCKROACHDB: "cockroach://root@localhost:26257/defaultdb?sslmode=disable" + steps: + - run: | + docker create --name cockroach -p 26257:26257 \ + cockroachdb/cockroach:latest-v25.4 start-single-node --insecure \ + || true + docker start cockroach + name: Start CockroachDB + - run: docker pull oryd/hydra:v2.2.0 + name: Pull Hydra + - uses: ory/ci/checkout@master + with: + fetch-depth: 2 + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - run: go list -json > go.list + - name: Run nancy + uses: sonatype-nexus-community/nancy-github-action@v1.0.3 + with: + nancyVersion: v1.0.42 + - run: | + sudo apt-get update + name: apt-get update + - run: npm install + name: Install node deps + - name: Run golangci-lint + if: ${{ github.ref_type != 'tag' }} + uses: golangci/golangci-lint-action@v9 + env: + GOGC: 100 + with: + args: --timeout 10m0s + version: latest + only-new-issues: "true" + - name: Build Kratos + run: make install + - name: Run go tests + run: make test-coverage + - name: Submit to Codecov + run: | + bash <(curl -s https://codecov.io/bash) + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + test-e2e: + name: Run end-to-end tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:18 + env: + POSTGRES_DB: postgres + POSTGRES_PASSWORD: test + POSTGRES_USER: test + ports: + - 5432:5432 + mysql: + image: mysql:9.6 + env: + MYSQL_ROOT_PASSWORD: test + ports: + - 3306:3306 + mailslurper: + image: oryd/mailslurper:latest-smtps + ports: + - 4436:4436 + - 4437:4437 + - 1025:1025 + env: + TEST_DATABASE_POSTGRESQL: "postgres://test:test@localhost:5432/postgres?sslmode=disable" + TEST_DATABASE_MYSQL: "mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true" + TEST_DATABASE_COCKROACHDB: "cockroach://root@localhost:26257/defaultdb?sslmode=disable" + strategy: + fail-fast: false + matrix: + database: ["postgres", "sqlite"] + # "cockroach", "mysql" TODO: fix tests and uncomment + steps: + - uses: actions/setup-node@v6 + with: + node-version: "24" + - run: | + docker create --name cockroach -p 26257:26257 \ + cockroachdb/cockroach:latest-v25.4 start-single-node --insecure + docker start cockroach + 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: ory/ci/checkout@master + with: + fetch-depth: 2 + - run: | + sudo apt-get update + name: apt-get update + - run: | + npm ci + cd test/e2e; npm ci + npm i -g expo-cli + name: Install node deps + - run: | + sudo apt-get install -y moreutils gettext + name: Install tools + - name: Setup Go + uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - name: Install selfservice-ui-react-native + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-react-native + path: react-native-ui + - run: | + cd react-native-ui + npm install + + - name: Install selfservice-ui-node + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-node + path: node-ui + - run: | + cd node-ui + npm install --legacy-peer-deps + + - name: Install selfservice-ui-react-nextjs + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-react-nextjs + path: react-ui + - run: | + cd react-ui + npm ci + + - run: | + echo 'RN_UI_PATH='"$(realpath react-native-ui)" >> "$GITHUB_ENV" + echo 'NODE_UI_PATH='"$(realpath node-ui)" >> "$GITHUB_ENV" + echo 'REACT_UI_PATH='"$(realpath react-ui)" >> "$GITHUB_ENV" + - name: "Run Cypress tests" + run: ./test/e2e/run.sh ${{ matrix.database }} + env: + RN_UI_PATH: react-native-ui + NODE_UI_PATH: node-ui + REACT_UI_PATH: react-ui + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: cypress-${{ matrix.database }}-logs + path: test/e2e/*.e2e.log + + test-e2e-playwright: + name: Run Playwright end-to-end tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:18 + env: + POSTGRES_DB: postgres + POSTGRES_PASSWORD: test + POSTGRES_USER: test + ports: + - 5432:5432 + mysql: + image: mysql:9.6 + env: + MYSQL_ROOT_PASSWORD: test + ports: + - 3306:3306 + mailslurper: + image: oryd/mailslurper:latest-smtps + ports: + - 4436:4436 + - 4437:4437 + - 1025:1025 + env: + TEST_DATABASE_POSTGRESQL: "postgres://test:test@localhost:5432/postgres?sslmode=disable" + TEST_DATABASE_MYSQL: "mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true" + TEST_DATABASE_COCKROACHDB: "cockroach://root@localhost:26257/defaultdb?sslmode=disable" + strategy: + fail-fast: false + matrix: + database: ["postgres", "cockroach", "sqlite", "mysql"] + steps: + - uses: actions/setup-node@v6 + with: + node-version: "24" + - run: | + docker create --name cockroach -p 26257:26257 \ + cockroachdb/cockroach:latest-v25.4 start-single-node --insecure + docker start cockroach + name: Start CockroachDB + - uses: ory/ci/checkout@master + with: + fetch-depth: 2 + - run: | + sudo apt-get update + name: apt-get update + - run: | + npm ci + cd test/e2e; npm ci + npx playwright install --with-deps + npm i -g expo-cli + name: Install node deps + - run: | + sudo apt-get install -y moreutils gettext + name: Install tools + - name: Setup Go + uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - run: go build . + - name: Install selfservice-ui-react-native + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-react-native + path: react-native-ui + - run: | + cd react-native-ui + npm install + + - name: Install selfservice-ui-node + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-node + path: node-ui + - run: | + cd node-ui + npm install --legacy-peer-deps + + - name: Install selfservice-ui-react-nextjs + uses: actions/checkout@v7 + with: + repository: ory/kratos-selfservice-ui-react-nextjs + path: react-ui + - run: | + cd react-ui + npm ci + + - run: | + echo 'RN_UI_PATH='"$(realpath react-native-ui)" >> "$GITHUB_ENV" + echo 'NODE_UI_PATH='"$(realpath node-ui)" >> "$GITHUB_ENV" + echo 'REACT_UI_PATH='"$(realpath react-ui)" >> "$GITHUB_ENV" + + - name: "Set up environment" + run: test/e2e/run.sh --only-setup + - name: "Run Playwright tests" + run: | + cd test/e2e + npm run playwright + env: + DB: ${{ matrix.database }} + RN_UI_PATH: react-native-ui + NODE_UI_PATH: node-ui + REACT_UI_PATH: react-ui + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-${{ matrix.database }}-logs + path: test/e2e/*.e2e.log + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-test-results-${{ matrix.database }}-${{ github.sha }} + path: | + test/e2e/test-results/ + test/e2e/playwright-report/ + + docs-cli: + runs-on: ubuntu-latest + name: Build CLI docs + needs: + - test + steps: + - uses: ory/ci/docs/cli-next@master + with: + token: ${{ secrets.ORY_BOT_PAT }} + arg: "." + output-dir: docs/kratos + + release: + name: Generate release + runs-on: ubuntu-latest + if: ${{ github.ref_type == 'tag' }} + needs: + - test + - test-e2e + steps: + - uses: ory/ci/releaser@master + with: + token: ${{ secrets.ORY_BOT_PAT }} + goreleaser_key: ${{ secrets.GORELEASER_KEY }} + cosign_pwd: ${{ secrets.COSIGN_PWD }} + docker_username: ${{ secrets.DOCKERHUB_USERNAME }} + docker_password: ${{ secrets.DOCKERHUB_PASSWORD }} + + newsletter-draft: + name: Draft newsletter + runs-on: ubuntu-latest + if: ${{ github.ref_type == 'tag' }} + needs: + - release + steps: + - uses: ory/ci/newsletter@master + with: + mailchimp_list_id: f605a41b53 + mailchmip_segment_id: 6479477 + mailchimp_api_key: ${{ secrets.MAILCHIMP_API_KEY }} + draft: "true" + ssh_key: ${{ secrets.ORY_BOT_SSH_KEY }} + + slack-approval-notification: + name: Pending approval Slack notification + runs-on: ubuntu-latest + if: ${{ github.ref_type == 'tag' }} + needs: + - newsletter-draft + steps: + - uses: ory/ci/newsletter/slack-notify@master + with: + slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + + newsletter-send: + name: Send newsletter + runs-on: ubuntu-latest + needs: + - newsletter-draft + if: ${{ github.ref_type == 'tag' }} + environment: production + steps: + - uses: ory/ci/newsletter@master + with: + mailchimp_list_id: f605a41b53 + mailchmip_segment_id: 6479477 + mailchimp_api_key: ${{ secrets.MAILCHIMP_API_KEY }} + draft: "false" + ssh_key: ${{ secrets.ORY_BOT_SSH_KEY }} diff --git a/.github/workflows/closed_references.yml b/.github/workflows/closed_references.yml new file mode 100644 index 0000000..de5efcf --- /dev/null +++ b/.github/workflows/closed_references.yml @@ -0,0 +1,30 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/closed_references.yml + +name: Closed Reference Notifier + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + issueLimit: + description: Max. number of issues to create + required: true + default: "5" + +jobs: + find_closed_references: + if: github.repository_owner == 'ory' + runs-on: ubuntu-latest + name: Find closed references + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: "24" + - uses: ory/closed-reference-notifier@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issueLabels: upstream,good first issue,help wanted + issueLimit: ${{ github.event.inputs.issueLimit || '5' }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..0107870 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: "26 21 * * 3" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: ["go", "javascript"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + config-file: ./.github/codeql/codeql-config.yml + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # 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@v4 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/conventional_commits.yml b/.github/workflows/conventional_commits.yml new file mode 100644 index 0000000..2032849 --- /dev/null +++ b/.github/workflows/conventional_commits.yml @@ -0,0 +1,59 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/conventional_commits.yml + +name: Conventional commits + +# This GitHub CI Action enforces that pull request titles follow conventional commits. +# More info at https://www.conventionalcommits.org. +# +# The Ory-wide defaults for commit titles and scopes are below. +# Your repository can add/replace elements via a configuration file at the path below. +# More info at https://github.com/ory/ci/blob/master/conventional_commit_config/README.md + +on: + pull_request_target: + types: + - edited + - opened + - ready_for_review + - reopened + # pull_request: # for debugging, uses config in local branch but supports only Pull Requests from this repo + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - id: config + uses: ory/ci/conventional_commit_config@master + with: + config_path: .github/conventional_commits.json + default_types: | + feat + fix + revert + docs + style + refactor + test + build + autogen + security + ci + chore + default_scopes: | + deps + docs + default_require_scope: false + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: ${{ steps.config.outputs.types }} + scopes: ${{ steps.config.outputs.scopes }} + requireScope: ${{ steps.config.outputs.requireScope }} + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject should start with a lowercase letter, yours is uppercase: + "{subject}" diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml new file mode 100644 index 0000000..dbd337b --- /dev/null +++ b/.github/workflows/cve-scan.yaml @@ -0,0 +1,114 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/server/.github/workflows/cve-scan.yaml + +name: Docker Image Scanners +on: + workflow_dispatch: + push: + branches: + - "master" + tags: + - "v*.*.*" + pull_request: + branches: + - "master" + +permissions: + contents: read + security-events: write + +jobs: + scanners: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Setup Env + id: vars + shell: bash + run: | + # Store values in local variables + SHA_SHORT=$(git rev-parse --short HEAD) + REPO_NAME=${{ github.event.repository.name }} + + IMAGE_NAME="oryd/${REPO_NAME}:${SHA_SHORT}" + + # Output values for debugging + echo "Values to be set:" + echo "SHA_SHORT: ${SHA_SHORT}" + echo "REPO_NAME: ${REPO_NAME}" + echo "IMAGE_NAME: ${IMAGE_NAME}" + + # Set GitHub Environment variables + echo "SHA_SHORT=${SHA_SHORT}" >> "${GITHUB_ENV}" + echo "IMAGE_NAME=${IMAGE_NAME}" >> "${GITHUB_ENV}" + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + with: + driver: docker + - name: Build images + shell: bash + run: | + IMAGE_TAG="${{ env.SHA_SHORT }}" make docker + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Anchore Scanner + uses: anchore/scan-action@v7 + id: grype-scan + with: + image: ${{ env.IMAGE_NAME }} + fail-build: true + severity-cutoff: high + add-cpes-if-none: true + - name: Inspect action SARIF report + shell: bash + if: ${{ always() }} + run: | + echo "::group::Anchore Scan Details" + jq '.runs[0].results' ${{ steps.grype-scan.outputs.sarif }} + echo "::endgroup::" + - name: Anchore upload scan SARIF report + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ steps.grype-scan.outputs.sarif }} + - name: Kubescape scanner + uses: kubescape/github-action@main + id: kubescape + with: + image: ${{ env.IMAGE_NAME }} + verbose: true + format: pretty-printer + # can't whitelist CVE yet: https://github.com/kubescape/kubescape/pull/1568 + severityThreshold: critical + - name: Dockle Linter + uses: erzz/dockle-action@v1 + if: ${{ always() }} + with: + image: ${{ env.IMAGE_NAME }} + exit-code: 42 + failure-threshold: high + - name: Hadolint + uses: hadolint/hadolint-action@v3.3.0 + id: hadolint + if: ${{ always() }} + with: + dockerfile: .docker/Dockerfile-build + verbose: true + format: "json" + failure-threshold: "error" + - name: View Hadolint results + if: ${{ always() }} + shell: bash + run: | + echo "::group::Hadolint Scan Details" + echo "${HADOLINT_RESULTS}" | jq '.' + echo "::endgroup::" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..9ae81d2 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,21 @@ +name: Format + +on: + pull_request: + merge_group: + +env: + CGO_ENABLED: "0" + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - run: make format + - name: Indicate formatting issues + run: git diff HEAD --exit-code --color diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 0000000..ed551b6 --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,25 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/labels.yml + +name: Synchronize Issue Labels + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + milestone: + if: github.repository_owner == 'ory' + name: Synchronize Issue Labels + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Synchronize Issue Labels + uses: ory/label-sync-action@v0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + dry: false + forced: true diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml new file mode 100644 index 0000000..e8431e4 --- /dev/null +++ b/.github/workflows/milestone.yml @@ -0,0 +1,33 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/server/.github/workflows/milestone.yml + +name: Generate and Publish Milestone Document + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + milestone: + if: github.repository_owner == 'ory' + name: Generate and Publish Milestone Document + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + token: ${{ secrets.TOKEN_PRIVILEGED }} + - name: Milestone Documentation Generator + uses: ory/milestone-action@v0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputFile: docs/docs/milestones.md + - name: Commit Milestone Documentation + uses: EndBug/add-and-commit@v10.0.0 + with: + message: "autogen(docs): update milestone document" + author_name: aeneasr + author_email: "3372410+aeneasr@users.noreply.github.com" + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_PRIVILEGED }} diff --git a/.github/workflows/pm.yml b/.github/workflows/pm.yml new file mode 100644 index 0000000..bcf2a71 --- /dev/null +++ b/.github/workflows/pm.yml @@ -0,0 +1,30 @@ +name: Synchronize with product board + +on: + issues: + types: + - opened + pull_request: + types: + - opened + - ready_for_review + +jobs: + automate: + if: github.event.pull_request.head.repo.fork == false + name: Add issue to project + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: ory-corp/planning-automation-action@v0.2 + with: + project: 5 + organization: ory-corp + token: ${{ secrets.ORY_BOT_PAT }} + todoLabel: "Needs Triage" + statusName: Status + prStatusValue: "Needs Triage" + issueStatusValue: "Needs Triage" + includeEffort: "false" + monthlyMilestoneName: Roadmap Monthly + quarterlyMilestoneName: Roadmap diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..c1f52b1 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,47 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/stale.yml + +name: "Close Stale Issues" +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + if: github.repository_owner == 'ory' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: | + Hello contributors! + + I am marking this issue as stale as it has not received any engagement from the community or maintainers for a year. That does not imply that the issue has no merit! If you feel strongly about this issue + + - open a PR referencing and resolving the issue; + - leave a comment on it and discuss ideas on how you could contribute towards resolving it; + - leave a comment and describe in detail why this issue is critical for your use case; + - open a new issue with updated details and a plan for resolving the issue. + + Throughout its lifetime, Ory has received over 10.000 issues and PRs. To sustain that growth, we need to prioritize and focus on issues that are important to the community. A good indication of importance, and thus priority, is activity on a topic. + + Unfortunately, [burnout](https://www.jeffgeerling.com/blog/2016/why-i-close-prs-oss-project-maintainer-notes) has become a [topic](https://opensource.guide/best-practices/#its-okay-to-hit-pause) of [concern](https://docs.brew.sh/Maintainers-Avoiding-Burnout) amongst open-source projects. + + It can lead to severe personal and health issues as well as [opening](https://haacked.com/archive/2019/05/28/maintainer-burnout/) catastrophic [attack vectors](https://www.gradiant.org/en/blog/open-source-maintainer-burnout-as-an-attack-surface/). + + The motivation for this automation is to help prioritize issues in the backlog and not ignore, reject, or belittle anyone. + + If this issue was marked as stale erroneously you can exempt it by adding the `backlog` label, assigning someone, or setting a milestone for it. + + Thank you for your understanding and to anyone who participated in the conversation! And as written above, please do participate in the conversation if this topic is important to you! + + Thank you 🙏✌️ + stale-issue-label: "stale" + exempt-issue-labels: "bug,blocking,docs,backlog" + days-before-stale: 365 + days-before-close: 30 + exempt-milestones: true + exempt-assignees: true + only-pr-labels: "stale" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42d798e --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +cover.out +.idea/ +tmp/ +ddl/ +.DS_Store +/kratos +packrd/ +*-packr.go +dist/ +node_modules +.bin/ +test/e2e/cypress/videos +test/e2e/cypress/screenshots +test/e2e/.bin +pkged.go +coverage.* +schema.sql +*.sqlite +heap_profiler/ +goroutine_dump/ +inflight_trace_dump/ + +contrib/quickstart/kratos/oidc + +e2e/*.log +e2e/kratos.*.yml +e2e/proxy.json +e2e/cypress/downloads + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +internal/httpclient/*.o +internal/httpclient/*.a +internal/httpclient/*.so + +# Folders +internal/httpclient/_obj +internal/httpclient/_test + +# Architecture specific extensions/prefixes +internal/httpclient/*.[568vq] +internal/httpclient/[568vq].out + +internal/httpclient/*.cgo1.go +internal/httpclient/*.cgo2.c +internal/httpclient/_cgo_defun.c +internal/httpclient/_cgo_gotypes.go +internal/httpclient/_cgo_export.* + +internal/httpclient/_testmain.go + +internal/httpclient/*.exe +internal/httpclient/*.test +internal/httpclient/*.prof + +test/e2e/hydra-login-consent/hydra-login-consent + +persistence/sql/migrations/sql/schema.sql + +test/e2e/hydra-kratos-login-consent/hydra-kratos-login-consent + +*.log +test/e2e/proxy.json +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 new file mode 100644 index 0000000..f8d8c8a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,39 @@ +version: "2" + +linters: + enable: + - gosec + - govet + - errcheck + - ineffassign + - staticcheck + - unused + - forbidigo + disable: + - bodyclose # too many false negatives + + settings: + gosec: + excludes: + - G101 + - G117 + - G306 + - G704 + - G705 + forbidigo: + forbid: + - pattern: "http\\.DefaultClient" + msg: "Use reg.HTTPClient(ctx) or httpx.NewResilientClient() — http.DefaultClient bypasses SSRF protection and retry logic." + - pattern: "retryablehttp\\.NewClient\\b" + msg: "Use httpx.NewResilientClient() — retryablehttp.NewClient() bypasses SSRF protection and registry configuration." + exclusions: + rules: + - linters: + - staticcheck + text: "SA1019" # we do use deprecated APIs on purpose sometimes + - linters: + - forbidigo + path: "pkg/(httpclient|client-go)/" + - linters: + - forbidigo + path: "_test\\.go$" diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..fba417d --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,23 @@ +version: 2 + +includes: + - from_url: + url: https://raw.githubusercontent.com/ory/xgoreleaser/master/build.tmpl.yml + +variables: + brew_name: kratos + brew_description: "The Ory Identity Platform (Ory Kratos)" + buildinfo_hash: "github.com/ory/kratos/driver/config.Commit" + buildinfo_tag: "github.com/ory/kratos/driver/config.Version" + buildinfo_date: "github.com/ory/kratos/driver/config.Date" + dockerfile_alpine: ".docker/Dockerfile-alpine" + dockerfile_static: ".docker/Dockerfile-distroless-static" + +project_name: kratos + +after: + hooks: + - cmd: "bash <(curl -s https://raw.githubusercontent.com/ory/xgoreleaser/master/docs.sh)" + env: + - "TAG_VERSION={{ .Tag }}" + - "DOCS_VERSION={{ .Major }}.{{ .Minor }}" diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000..bb9b645 --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,9 @@ +#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 + - vulnerability: CVE-2023-4813 + - vulnerability: CVE-2023-4806 + - vulnerability: CVE-2025-0395 # no fix available diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..5c231ee --- /dev/null +++ b/.mailmap @@ -0,0 +1,13 @@ +Aeneas Rekkas +Aeneas Rekkas <3372410+aeneasr@users.noreply.github.com> +Aeneas Rekkas +Aeneas Rekkas +Ajay Kelkar +Alano Terblanche <18033717+Benehiko@users.noreply.github.com> +Alano Terblanche +Jonas Hungershausen +Matt Bonnell <64976795+mbonnell-wish@users.noreply.github.com> +Nick Ufer +Nick Ufer +Patrik Neu +Patrik Neu diff --git a/.nancy-ignore b/.nancy-ignore new file mode 100644 index 0000000..d89987e --- /dev/null +++ b/.nancy-ignore @@ -0,0 +1,15 @@ +# HashiCorp Consul related CVEs +# Consul is not used but instead an indirect dependency to the pf13/viper config backend which is not actively used. +CVE-2020-7219 +CVE-2019-12291 +CVE-2020-7955 +CVE-2020-12797 +CVE-2020-13250 +CVE-2020-13170 +# End HashiCorp Consul + +# etcd issues - can be ignored because etcd is not used. +CVE-2020-15114 +CVE-2020-15136 +CVE-2020-15115 +# end diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..fb457f3 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.19.0 diff --git a/.orycli.yml b/.orycli.yml new file mode 100644 index 0000000..e8e73dc --- /dev/null +++ b/.orycli.yml @@ -0,0 +1,7 @@ +project: kratos + +pre_release_hooks: + - make sdk + - ./script/render-schemas.sh + - git config --unset user.email + - git config --unset user.name diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2a1893d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +.schema/ +.github/ISSUE_TEMPLATE +oryx \ No newline at end of file diff --git a/.reference-ignore b/.reference-ignore new file mode 100644 index 0000000..eee2a89 --- /dev/null +++ b/.reference-ignore @@ -0,0 +1,3 @@ +**/node_modules +docs +CHANGELOG.md diff --git a/.reports/dep-licenses.csv b/.reports/dep-licenses.csv new file mode 100644 index 0000000..9bc0b35 --- /dev/null +++ b/.reports/dep-licenses.csv @@ -0,0 +1,8 @@ +"module name","licenses" + +"github.com/arbovm/levenshtein","BSD-3-Clause" +"github.com/ory/x","Apache-2.0" +"github.com/stretchr/testify","MIT" +"go.opentelemetry.io/otel/sdk","Apache-2.0" +"golang.org/x/text","BSD-3-Clause" + diff --git a/.schema/api.openapi.json b/.schema/api.openapi.json new file mode 100644 index 0000000..4297cfe --- /dev/null +++ b/.schema/api.openapi.json @@ -0,0 +1,3239 @@ +{ + "components": { + "responses": { + "emptyResponse": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201." + }, + "errorContainer": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorContainer" + } + } + }, + "description": "User-facing error response" + }, + "identityList": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Identity" + }, + "type": "array" + } + } + }, + "description": "A list of identities." + }, + "identityResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + }, + "description": "A single identity." + } + }, + "schemas": { + "CompleteSelfServiceBrowserSettingsProfileStrategyFlow": { + "description": "nolint:deadcode,unused", + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "traits": { + "description": "Traits contains all of the identity's traits.", + "type": "object" + } + }, + "type": "object" + }, + "CompleteSelfServiceLoginFlowWithPasswordMethod": { + "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" + }, + "password": { + "description": "The user's password.", + "type": "string" + } + }, + "type": "object" + }, + "CompleteSelfServiceSettingsFlowWithPasswordMethod": { + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token\n\ntype: string", + "type": "string" + }, + "password": { + "description": "Password is the updated password\n\ntype: string", + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "CreateIdentity": { + "properties": { + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits.", + "type": "string" + }, + "traits": { + "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_url`.", + "type": "object" + } + }, + "required": [ + "schema_id", + "traits" + ], + "type": "object" + }, + "CreateRecoveryLink": { + "properties": { + "expires_in": { + "description": "Link Expires In\n\nThe recovery link will expire at that point in time. Defaults to the configuration value of\n`selfservice.flows.recovery.request_lifespan`.", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "type": "string" + }, + "identity_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "identity_id" + ], + "type": "object" + }, + "CredentialsType": { + "description": "and so on.", + "title": "CredentialsType represents several different credential types, like password credentials, passwordless credentials,", + "type": "string" + }, + "ID": { + "format": "int64", + "type": "integer" + }, + "Identity": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "recovery_addresses": { + "description": "RecoveryAddresses contains all the addresses that can be used to recover an identity.", + "items": { + "$ref": "#/components/schemas/RecoveryAddress" + }, + "type": "array", + "x-omitempty": true + }, + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits.", + "type": "string" + }, + "schema_url": { + "description": "SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.\n\nformat: url", + "type": "string" + }, + "traits": { + "$ref": "#/components/schemas/Traits" + }, + "verifiable_addresses": { + "description": "VerifiableAddresses contains all the addresses that can be verified by the user.", + "items": { + "$ref": "#/components/schemas/VerifiableAddress" + }, + "type": "array", + "x-omitempty": true + } + }, + "required": [ + "id", + "schema_id", + "schema_url", + "traits" + ], + "type": "object" + }, + "NullTime": { + "format": "date-time", + "title": "NullTime implements sql.NullTime functionality.", + "type": "string" + }, + "RecoveryAddress": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "value": { + "type": "string" + }, + "via": { + "$ref": "#/components/schemas/RecoveryAddressType" + } + }, + "required": [ + "id", + "value", + "via" + ], + "type": "object" + }, + "RecoveryAddressType": { + "type": "string" + }, + "State": { + "type": "string" + }, + "Traits": { + "type": "object" + }, + "Type": { + "description": "The flow type can either be `api` or `browser`.", + "title": "Type is the flow type.", + "type": "string" + }, + "UUID": { + "format": "uuid4", + "type": "string" + }, + "UpdateIdentity": { + "properties": { + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set\nwill update the Identity's SchemaID.", + "type": "string" + }, + "traits": { + "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_id`.", + "type": "object" + } + }, + "required": [ + "traits" + ], + "type": "object" + }, + "VerifiableAddress": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "status": { + "$ref": "#/components/schemas/VerifiableAddressStatus" + }, + "value": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "verified_at": { + "$ref": "#/components/schemas/NullTime" + }, + "via": { + "$ref": "#/components/schemas/VerifiableAddressType" + } + }, + "required": [ + "id", + "value", + "verified", + "via", + "status" + ], + "type": "object" + }, + "VerifiableAddressStatus": { + "type": "string" + }, + "VerifiableAddressType": { + "type": "string" + }, + "completeSelfServiceBrowserSettingsOIDCFlowPayload": { + "properties": { + "flow": { + "description": "Flow ID is the flow's ID.\n\nin: query", + "type": "string" + }, + "link": { + "description": "Link this provider\n\nEither this or `unlink` must be set.\n\ntype: string\nin: body", + "type": "string" + }, + "unlink": { + "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "completeSelfServiceRecoveryFlowWithLinkMethod": { + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "email": { + "description": "Email to Recover\n\nNeeds to be set when initiating the flow. If the email is a registered\nrecovery email, a recovery link will be sent. If the email is not known,\na email with details on what happened will be sent instead.\n\nformat: email\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "completeSelfServiceVerificationFlowWithLinkMethod": { + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "email": { + "description": "Email to Verify\n\nNeeds to be set when initiating the flow. If the email is a registered\nverification email, a verification link will be sent. If the email is not known,\na email with details on what happened will be sent instead.\n\nformat: email\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "errorContainer": { + "properties": { + "errors": { + "description": "Errors in the container", + "items": { + "type": "object" + }, + "type": "array" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "errors" + ], + "type": "object" + }, + "genericError": { + "description": "Error responses are sent when an error (e.g. unauthorized, bad request, ...) occurred.", + "properties": { + "error": { + "$ref": "#/components/schemas/genericErrorPayload" + } + }, + "title": "Error response", + "type": "object" + }, + "genericErrorPayload": { + "properties": { + "code": { + "description": "Code represents the error status code (404, 403, 401, ...).", + "example": 404, + "format": "int64", + "type": "integer" + }, + "debug": { + "description": "Debug contains debug information. This is usually not available and has to be enabled.", + "example": "The database adapter was unable to find the element", + "type": "string" + }, + "details": { + "additionalProperties": true, + "type": "object" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "healthNotReadyStatus": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "type": "object" + }, + "healthStatus": { + "properties": { + "status": { + "description": "Status always contains \"ok\".", + "type": "string" + } + }, + "type": "object" + }, + "jsonSchema": { + "description": "Raw JSON Schema", + "type": "object" + }, + "loginFlow": { + "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": { + "$ref": "#/components/schemas/CredentialsType" + }, + "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.", + "format": "date-time", + "type": "string" + }, + "forced": { + "description": "Forced stores whether this login flow should enforce re-authentication.", + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow started.", + "format": "date-time", + "type": "string" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/Type" + }, + "ui": { + "$ref": "#/components/schemas/uiContainer" + } + }, + "required": [ + "id", + "type", + "expires_at", + "issued_at", + "request_url", + "ui" + ], + "title": "Login Flow", + "type": "object" + }, + "loginViaApiResponse": { + "description": "The Response for Login Flows via API", + "properties": { + "session": { + "$ref": "#/components/schemas/session" + }, + "session_token": { + "description": "The Session Token\n\nA session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization\nHeader:\n\nAuthorization: bearer ${session-token}\n\nThe session token is only issued for API flows, not for Browser flows!", + "type": "string" + } + }, + "required": [ + "session_token", + "session" + ], + "type": "object" + }, + "recoveryFlow": { + "description": "This request is used when an identity wants to recover their account.\n\nWe recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery)", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,\na new request has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the request occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/recoveryFlowMethod" + }, + "description": "Methods contains context for all account recovery methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods", + "state" + ], + "title": "A Recovery Flow", + "type": "object" + }, + "recoveryFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/recoveryFlowMethodConfig" + }, + "method": { + "description": "Method contains the request credentials type.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "recoveryFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "recoveryFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "recoveryLink": { + "properties": { + "expires_at": { + "description": "Recovery Link Expires At\n\nThe timestamp when the recovery link expires.", + "format": "date-time", + "type": "string" + }, + "recovery_link": { + "description": "Recovery Link\n\nThis link can be used to recover the account.", + "type": "string" + } + }, + "required": [ + "recovery_link" + ], + "type": "object" + }, + "registrationFlow": { + "properties": { + "active": { + "$ref": "#/components/schemas/CredentialsType" + }, + "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.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/registrationFlowMethod" + }, + "description": "Methods contains context for all enabled registration methods. If a registration flow has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods" + ], + "type": "object" + }, + "registrationFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/registrationFlowMethodConfig" + }, + "method": { + "$ref": "#/components/schemas/CredentialsType" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "registrationFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "description": "Providers is set for the \"oidc\" registration method.", + "items": { + "$ref": "#/components/schemas/uiNodes" + }, + "type": "array" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "registrationFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "description": "Providers is set for the \"oidc\" registration method.", + "items": { + "$ref": "#/components/schemas/uiNodes" + }, + "type": "array" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "registrationViaApiResponse": { + "description": "The Response for Registration Flows via API", + "properties": { + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "session": { + "$ref": "#/components/schemas/session" + }, + "session_token": { + "description": "The Session Token\n\nThis field is only set when the session hook is configured as a post-registration hook.\n\nA session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization\nHeader:\n\nAuthorization: bearer ${session-token}\n\nThe session token is only issued for API flows, not for Browser flows!", + "type": "string" + } + }, + "required": [ + "session_token", + "identity" + ], + "type": "object" + }, + "revokeSession": { + "properties": { + "session_token": { + "description": "The Session Token\n\nInvalidate this session token.", + "type": "string" + } + }, + "required": [ + "session_token" + ], + "type": "object" + }, + "session": { + "properties": { + "active": { + "type": "boolean" + }, + "authenticated_at": { + "format": "date-time", + "type": "string" + }, + "expires_at": { + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "issued_at": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "expires_at", + "authenticated_at", + "issued_at", + "identity" + ], + "type": "object" + }, + "settingsFlow": { + "description": "This flow is used when an identity wants to update settings\n(e.g. profile data, passwords, ...) in a selfservice manner.\n\nWe recommend reading the [User Settings Documentation](../self-service/flows/user-settings)", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting,\na new flow has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/settingsFlowMethod" + }, + "description": "Methods contains context for all enabled registration methods. If a settings flow has been\nprocessed, but for example the first name is empty, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods", + "identity", + "state" + ], + "title": "Flow represents a Settings Flow", + "type": "object" + }, + "settingsFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/settingsFlowMethodConfig" + }, + "method": { + "description": "Method is the name of this flow method.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "settingsFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsProfileFormConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsViaApiResponse": { + "description": "The Response for Settings Flows via API", + "properties": { + "flow": { + "$ref": "#/components/schemas/settingsFlow" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + } + }, + "required": [ + "flow", + "identity" + ], + "type": "object" + }, + "uiContainer": { + "description": "Container represents a HTML Form. The container can work with both HTTP Form and JSON requests", + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "uiNode": { + "description": "Nodes are represented as HTML elements or their native UI equivalents. For example,\na node can be an `\u003cimg\u003e` tag, or an `\u003cinput element\u003e` but also `some plain text`.", + "properties": { + "attributes": { + "$ref": "#/components/schemas/uiNodeAttributes" + }, + "group": { + "$ref": "#/components/schemas/uiNodeGroup" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "type": { + "$ref": "#/components/schemas/uiNodeType" + } + }, + "required": [ + "type", + "group", + "attributes", + "messages" + ], + "title": "Node represents a flow's nodes", + "type": "object" + }, + "uiNodeAnchorAttributes": { + "properties": { + "href": { + "description": "The link's href (destination) URL.\n\nformat: uri", + "type": "string" + }, + "title": { + "$ref": "#/components/schemas/uiText" + } + }, + "required": [ + "href", + "title" + ], + "title": "AnchorAttributes represents the attributes of an anchor node.", + "type": "object" + }, + "uiNodeAttributes": { + "oneOf": [ + { + "$ref": "#/components/schemas/uiNodeInputAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeTextAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeImageAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeAnchorAttributes" + } + ], + "title": "Attributes represents a list of attributes (e.g. `href=\"foo\"` for links)." + }, + "uiNodeGroup": { + "type": "string" + }, + "uiNodeImageAttributes": { + "properties": { + "src": { + "description": "The image's source URL.\n\nformat: uri", + "type": "string" + } + }, + "required": [ + "src" + ], + "title": "ImageAttributes represents the attributes of an image node.", + "type": "object" + }, + "uiNodeInputAttributeType": { + "type": "string" + }, + "uiNodeInputAttributes": { + "description": "InputAttributes represents the attributes of an input node", + "properties": { + "disabled": { + "description": "Sets the input's disabled field to true or false.", + "type": "boolean" + }, + "label": { + "$ref": "#/components/schemas/uiText" + }, + "name": { + "description": "The input's element name.", + "type": "string" + }, + "pattern": { + "description": "The input's pattern.", + "type": "string" + }, + "required": { + "description": "Mark this input field as required.", + "type": "boolean" + }, + "type": { + "$ref": "#/components/schemas/uiNodeInputAttributeType" + }, + "value": { + "$ref": "#/components/schemas/uiNodeInputAttributesValue", + "description": "The input's value.", + "nullable": true + } + }, + "required": [ + "name", + "type", + "disabled" + ], + "type": "object" + }, + "uiNodeInputAttributesValue": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "uiNodeTextAttributes": { + "properties": { + "text": { + "$ref": "#/components/schemas/uiText" + } + }, + "required": [ + "text" + ], + "title": "TextAttributes represents the attributes of a text node.", + "type": "object" + }, + "uiNodeType": { + "type": "string" + }, + "uiNodes": { + "items": { + "$ref": "#/components/schemas/uiNode" + }, + "type": "array" + }, + "uiText": { + "properties": { + "context": { + "description": "The message's context. Useful when customizing messages.", + "type": "object" + }, + "id": { + "$ref": "#/components/schemas/ID" + }, + "text": { + "description": "The message text. Written in american english.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/uiTextType" + } + }, + "required": [ + "id", + "text", + "type" + ], + "type": "object" + }, + "uiTextType": { + "type": "string" + }, + "uiTexts": { + "items": { + "$ref": "#/components/schemas/uiText" + }, + "type": "array" + }, + "verificationFlow": { + "description": "Used to verify an out-of-band communication\nchannel such as an email address or a phone number.\n\nFor more information head over to: https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address,\na new request has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the request occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/verificationFlowMethod" + }, + "description": "Methods contains context for all account verification methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "type", + "methods", + "state" + ], + "title": "A Verification Flow", + "type": "object" + }, + "verificationFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/verificationFlowMethodConfig" + }, + "method": { + "description": "Method contains the request credentials type.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "verificationFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "verificationFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "version": { + "properties": { + "version": { + "description": "Version is the service's version.", + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "sessionCookie": { + "in": "cookie", + "name": "ory_kratos_session", + "type": "apiKey" + }, + "sessionToken": { + "in": "header", + "name": "X-Session-Token", + "type": "apiKey" + } + } + }, + "info": { + "contact": { + "email": "hi@ory.sh" + }, + "description": "Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs\nare exposed on different ports. Public APIs can face the public internet without any protection\nwhile administrative APIs should never be exposed without prior authorization. To protect\nthe administative API port you should use something like Nginx, Ory Oathkeeper, or any other\ntechnology capable of authorizing incoming requests.\n", + "license": { + "name": "Apache 2.0" + }, + "title": "Ory Kratos API", + "version": "" + }, + "openapi": "3.0.3", + "paths": { + "/health/alive": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming\nHTTP requests. This status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isAlive", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Always \"ok\".", + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + } + } + }, + "description": "Ory Kratos is ready to accept connections." + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Check HTTP Server Status", + "tags": [ + "admin" + ] + } + }, + "/health/ready": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g.\nthe database) are responsive as well.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of Ory Kratos, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isReady", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Always \"ok\".", + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + } + } + }, + "description": "Ory Kratos is ready to accept requests." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "required": [ + "errors" + ], + "type": "object" + } + } + }, + "description": "Ory Kratos is not yet ready to accept requests." + } + }, + "summary": "Check HTTP Server and Database Status", + "tags": [ + "admin" + ] + } + }, + "/identities": { + "get": { + "description": "Lists all identities. Does not support search at the moment.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "listIdentities", + "parameters": [ + { + "description": "Items per Page\n\nThis is the number of items per page.", + "in": "query", + "name": "per_page", + "schema": { + "default": 100, + "format": "int64", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Pagination Page", + "in": "query", + "name": "page", + "schema": { + "default": 0, + "format": "int64", + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/identityList" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "List Identities", + "tags": [ + "admin" + ] + }, + "post": { + "description": "This endpoint creates an identity. It is NOT possible to set an identity's credentials (password, ...)\nusing this method! A way to achieve that will be introduced in the future.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "createIdentity", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateIdentity" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "201": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Create an Identity", + "tags": [ + "admin" + ] + } + }, + "/identities/{id}": { + "delete": { + "description": "Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.\nThis endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is\nassumed that is has been deleted already.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "deleteIdentity", + "parameters": [ + { + "description": "ID is the identity's ID.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Delete an Identity", + "tags": [ + "admin" + ] + }, + "get": { + "description": "Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "getIdentity", + "parameters": [ + { + "description": "ID must be set to the ID of identity you want to get", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get an Identity", + "tags": [ + "admin" + ] + }, + "put": { + "description": "This endpoint updates an identity. It is NOT possible to set an identity's credentials (password, ...)\nusing this method! A way to achieve that will be introduced in the future.\n\nThe full identity payload (except credentials) is expected. This endpoint does not support patching.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "updateIdentity", + "parameters": [ + { + "description": "ID must be set to the ID of identity you want to update", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateIdentity" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Update an Identity", + "tags": [ + "admin" + ] + } + }, + "/metrics/prometheus": { + "get": { + "description": "```\nmetadata:\nannotations:\nprometheus.io/port: \"4434\"\nprometheus.io/path: \"/metrics/prometheus\"\n```", + "operationId": "prometheus", + "responses": { + "200": { + "$ref": "#/components/responses/emptyResponse" + } + }, + "summary": "Get snapshot metrics from the Kratos service. If you're using k8s, you can then add annotations to\nyour deployment like so:", + "tags": [ + "admin" + ] + } + }, + "/recovery/link": { + "post": { + "description": "This endpoint creates a recovery link which should be given to the user in order for them to recover\n(or activate) their account.", + "operationId": "createRecoveryLink", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRecoveryLink" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryLink" + } + } + }, + "description": "recoveryLink" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Create a Recovery Link", + "tags": [ + "admin" + ] + } + }, + "/schemas/{id}": { + "get": { + "description": "Get a Traits Schema Definition", + "operationId": "getSchema", + "parameters": [ + { + "description": "ID must be set to the ID of schema you want to get", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonSchema" + } + } + }, + "description": "jsonSchema" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/browser/flows/logout": { + "get": { + "description": "This endpoint initializes a logout flow.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nOn successful logout, the browser will be redirected (HTTP 302 Found) to the `return_to` parameter of the initial request\nor fall back to `urls.default_return_to`.\n\nMore information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout).", + "operationId": "initializeSelfServiceBrowserLogoutFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Browser-Based Logout User Flow", + "tags": [ + "public" + ] + } + }, + "/self-service/browser/flows/registration/strategies/oidc/settings/connections": { + "post": { + "description": "This endpoint completes a browser-based settings flow. This is usually achieved by POSTing data to this\nendpoint.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceBrowserSettingsOIDCSettingsFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete the Browser-Based Settings Flow for the OpenID Connect Strategy", + "tags": [ + "public" + ] + } + }, + "/self-service/errors": { + "get": { + "description": "This endpoint returns the error associated with a user-facing self service errors.\n\nThis endpoint supports stub values to help you implement the error UI:\n\n`?error=stub:500` - returns a stub 500 (Internal Server Error) error.\n\nMore information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors).", + "operationId": "getSelfServiceError", + "parameters": [ + { + "description": "Error is the container's ID", + "in": "query", + "name": "error", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/errorContainer" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get User-Facing Self-Service Errors", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/login/api": { + "get": { + "description": "This endpoint initiates a login flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing login flow call `/self-service/login/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks, including CSRF login attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceLoginViaAPIFlow", + "parameters": [ + { + "description": "Refresh a login session\n\nIf set to true, this will refresh an existing login session by\nasking the user to sign in again. This will reset the\nauthenticated_at time of the session.", + "in": "query", + "name": "refresh", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Login Flow for API clients", + "tags": [ + "public" + ] + } + }, + "/self-service/login/browser": { + "get": { + "description": "This endpoint initializes a browser-based user login flow. Once initialized, the browser will be redirected to\n`selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter\n`?refresh=true` was set.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceLoginViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Login Flow for browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/login/flows": { + "get": { + "description": "This endpoint returns a login flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "getSelfServiceLoginFlow", + "parameters": [ + { + "description": "The Login Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/login?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Login Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/login/methods/password": { + "post": { + "description": "Use this endpoint to complete a login flow by sending an identity's identifier and password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI flows expect `application/json` to be sent in the body and responds with\nHTTP 200 and a application/json body with the session token on success;\nHTTP 302 redirect to a fresh login flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after login URL or the `return_to` value if it was set and if the login succeeded;\na HTTP 302 redirect to the login UI URL with the flow ID containing the validation errors otherwise.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "completeSelfServiceLoginFlowWithPasswordMethod", + "parameters": [ + { + "description": "The Flow ID", + "in": "query", + "name": "flow", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceLoginFlowWithPasswordMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceLoginFlowWithPasswordMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginViaApiResponse" + } + } + }, + "description": "loginViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Login Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/api": { + "get": { + "description": "This endpoint initiates a recovery flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error.\n\nTo fetch an existing recovery flow call `/self-service/recovery/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "initializeSelfServiceRecoveryViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Recovery Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/browser": { + "get": { + "description": "This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to\n`selfservice.flows.recovery.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists, the browser is returned to the configured return URL.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "initializeSelfServiceRecoveryViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Recovery Flow for Browser Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/flows": { + "get": { + "description": "This endpoint returns a recovery flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "getSelfServiceRecoveryFlow", + "parameters": [ + { + "description": "The Flow ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/recovery?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get information about a recovery flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/recovery/methods/link": { + "post": { + "description": "Use this endpoint to complete a recovery flow using the link method. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid\nand a HTTP 302 Found redirect with a fresh recovery flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients it returns a HTTP 302 Found redirect to the Recovery UI URL with the Recovery Flow ID appended.\n`sent_email` is the success state after `choose_method` and allows the user to request another recovery email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a recovery link\")\ndoes not have any API capabilities. The server responds with a HTTP 302 Found redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Recover UI URL with\na new Recovery Flow ID which contains an error message that the recovery link was invalid.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "completeSelfServiceRecoveryFlowWithLinkMethod", + "parameters": [ + { + "description": "Recovery Token\n\nThe recovery token which completes the recovery request. If the token\nis invalid (e.g. expired) an error will be shown to the end-user.", + "in": "query", + "name": "token", + "schema": { + "type": "string" + } + }, + { + "description": "The Flow ID\n\nformat: uuid", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceRecoveryFlowWithLinkMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceRecoveryFlowWithLinkMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Recovery Flow with Link Method", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/api": { + "get": { + "description": "This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing registration flow call `/self-service/registration/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceRegistrationViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Registration Flow for API clients", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/browser": { + "get": { + "description": "This endpoint initializes a browser-based user registration flow. Once initialized, the browser will be redirected to\n`selfservice.flows.registration.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter\n`?refresh=true` was set.\n\n:::note\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceRegistrationViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Registration Flow for browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/flows": { + "get": { + "description": "This endpoint returns a registration flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "getSelfServiceRegistrationFlow", + "parameters": [ + { + "description": "The Registration Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/registration?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Registration Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/registration/methods/password": { + "post": { + "description": "Use this endpoint to complete a registration flow by sending an identity's traits and password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and a application/json body with the created identity success - if the session hook is configured the\n`session` and `session_token` will also be included;\nHTTP 302 redirect to a fresh registration flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after registration URL or the `return_to` value if it was set and if the registration succeeded;\na HTTP 302 redirect to the registration UI URL with the flow ID containing the validation errors otherwise.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "completeSelfServiceRegistrationFlowWithPasswordMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object" + } + } + }, + "x-originalParamName": "Payload" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationViaApiResponse" + } + } + }, + "description": "registrationViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Registration Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/api": { + "get": { + "description": "This endpoint initiates a settings flow for API clients such as mobile devices, smart TVs, and so on.\nYou must provide a valid Ory Kratos Session Token for this endpoint to respond with HTTP 200 OK.\n\nTo fetch an existing settings flow call `/self-service/settings/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "initializeSelfServiceSettingsViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Initialize Settings Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/browser": { + "get": { + "description": "This endpoint initializes a browser-based user settings flow. Once initialized, the browser will be redirected to\n`selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid\nOry Kratos Session Cookie is included in the request, a login flow will be initialized.\n\n:::note\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "initializeSelfServiceSettingsViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Initialize Settings Flow for Browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/flows": { + "get": { + "description": "When accessing this endpoint through Ory Kratos' Public API you must ensure that either the Ory Kratos Session Cookie\nor the Ory Kratos Session Token are set. The public endpoint does not return 404 status codes\nbut instead 403 or 500 to improve data privacy.\n\nYou can access this endpoint without credentials when using Ory Kratos' Admin API.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "getSelfServiceSettingsFlow", + "parameters": [ + { + "description": "ID is the Settings Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/settings?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Get Settings Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/settings/methods/password": { + "post": { + "description": "Use this endpoint to complete a settings flow by sending an identity's updated password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI-initiated flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and an application/json body with the session token on success;\nHTTP 302 redirect to a fresh settings flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\nHTTP 401 when the endpoint is called without a valid session token.\nHTTP 403 when `selfservice.flows.settings.privileged_session_max_age` was reached.\nImplies that the user needs to re-authenticate.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after settings URL or the `return_to` value if it was set and if the flow succeeded;\na HTTP 302 redirect to the Settings UI URL with the flow ID containing the validation errors otherwise.\na HTTP 302 redirect to the login endpoint when `selfservice.flows.settings.privileged_session_max_age` was reached.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceSettingsFlowWithPasswordMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceSettingsFlowWithPasswordMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceSettingsFlowWithPasswordMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsViaApiResponse" + } + } + }, + "description": "settingsViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Complete Settings Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/methods/profile": { + "post": { + "description": "Use this endpoint to complete a settings flow by sending an identity's updated traits. This endpoint\nbehaves differently for API and browser flows.\n\nAPI-initiated flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and an application/json body with the session token on success;\nHTTP 302 redirect to a fresh settings flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\nHTTP 401 when the endpoint is called without a valid session token.\nHTTP 403 when `selfservice.flows.settings.privileged_session_max_age` was reached and a sensitive field was\nupdated (e.g. recovery email). Implies that the user needs to re-authenticate.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after settings URL or the `return_to` value if it was set and if the flow succeeded;\na HTTP 302 redirect to the settings UI URL with the flow ID containing the validation errors otherwise.\na HTTP 302 redirect to the login endpoint when `selfservice.flows.settings.privileged_session_max_age` was reached.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceSettingsFlowWithProfileMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object" + } + } + }, + "x-originalParamName": "Payload" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Complete Settings Flow with Profile Method", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/api": { + "get": { + "description": "This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on.\n\nTo fetch an existing verification flow call `/self-service/verification/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore 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).", + "operationId": "initializeSelfServiceVerificationViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Verification Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/browser": { + "get": { + "description": "This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to\n`selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore 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).", + "operationId": "initializeSelfServiceVerificationViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Verification Flow for Browser Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/flows": { + "get": { + "description": "This endpoint returns a verification flow's context with, for example, error details and other information.\n\nMore 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).", + "operationId": "getSelfServiceVerificationFlow", + "parameters": [ + { + "description": "The Flow ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/verification?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Verification Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/verification/methods/link": { + "post": { + "description": "Use this endpoint to complete a verification flow using the link method. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid\nand a HTTP 302 Found redirect with a fresh verification flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients it returns a HTTP 302 Found redirect to the Verification UI URL with the Verification Flow ID appended.\n`sent_email` is the success state after `choose_method` and allows the user to request another verification email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a verification link\")\ndoes not have any API capabilities. The server responds with a HTTP 302 Found redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Verification UI URL with\na new Verification Flow ID which contains an error message that the verification link was invalid.\n\nMore 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).", + "operationId": "completeSelfServiceVerificationFlowWithLinkMethod", + "parameters": [ + { + "description": "Verification Token\n\nThe verification token which completes the verification request. If the token\nis invalid (e.g. expired) an error will be shown to the end-user.", + "in": "query", + "name": "token", + "schema": { + "type": "string" + } + }, + { + "description": "The Flow ID\n\nformat: uuid", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceVerificationFlowWithLinkMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceVerificationFlowWithLinkMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Verification Flow with Link Method", + "tags": [ + "public" + ] + } + }, + "/sessions": { + "delete": { + "description": "Use this endpoint to revoke a session using its token. This endpoint is particularly useful for API clients\nsuch as mobile apps to log the user out of the system and invalidate the session.\n\nThis endpoint does not remove any HTTP Cookies - use the Self-Service Logout Flow instead.", + "operationId": "revokeSession", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/revokeSession" + } + } + }, + "required": true, + "x-originalParamName": "Body" + }, + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Revoke and Invalidate a Session", + "tags": [ + "public" + ] + } + }, + "/sessions/whoami": { + "get": { + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nThis endpoint is useful for reverse proxies and API Gateways.", + "operationId": "whoami", + "parameters": [ + { + "in": "header", + "name": "Cookie", + "schema": { + "type": "string" + } + }, + { + "description": "in: authorization", + "in": "query", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/session" + } + } + }, + "description": "session" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Check Who the Current HTTP Session Belongs To", + "tags": [ + "public" + ] + } + }, + "/version": { + "get": { + "description": "This endpoint returns the version of Ory Kratos.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the version will never\nrefer to the cluster state, only to a single instance.", + "operationId": "getVersion", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "version": { + "description": "The version of Ory Kratos.", + "type": "string" + } + }, + "required": [ + "version" + ], + "type": "object" + } + } + }, + "description": "Returns the Ory Kratos version." + } + }, + "summary": "Return Running Software Version.", + "tags": [ + "admin" + ] + } + } + }, + "servers": [ + { + "url": "https://{tenant}.tenants.oryapis.com/api/kratos/{api}", + "variables": { + "api": { + "default": "public", + "description": "Target the public or administrative API.", + "enum": [ + "public", + "admin" + ] + }, + "tenant": { + "default": "demo", + "description": "Tenant ID as provided by Ory Cloud." + } + } + } + ], + "tags": [ + { + "description": "All administrative API endpoints exposed at the admin API port.", + "externalDocs": { + "url": "https://www.ory.sh/kratos/docs/reference/api" + }, + "name": "admin" + }, + { + "description": "All public API endpoints exposed at the public API port.", + "externalDocs": { + "url": "https://www.ory.sh/kratos/docs/reference/api" + }, + "name": "public" + } + ], + "x-forwarded-proto": "string", + "x-request-id": "string" +} diff --git a/.schema/openapi.json b/.schema/openapi.json new file mode 100644 index 0000000..ca625ad --- /dev/null +++ b/.schema/openapi.json @@ -0,0 +1,3285 @@ +{ + "components": { + "responses": { + "emptyResponse": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201." + }, + "errorContainer": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorContainer" + } + } + }, + "description": "User-facing error response" + }, + "identityList": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Identity" + }, + "type": "array" + } + } + }, + "description": "A list of identities." + }, + "identityResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + }, + "description": "A single identity." + }, + "schemaResponse": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "The raw identity traits schema" + } + }, + "schemas": { + "CompleteSelfServiceBrowserSettingsProfileStrategyFlow": { + "description": "nolint:deadcode,unused", + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "traits": { + "description": "Traits contains all of the identity's traits.", + "type": "object" + } + }, + "type": "object" + }, + "CompleteSelfServiceLoginFlowWithPasswordMethod": { + "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" + }, + "password": { + "description": "The user's password.", + "type": "string" + } + }, + "type": "object" + }, + "CompleteSelfServiceSettingsFlowWithPasswordMethod": { + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token\n\ntype: string", + "type": "string" + }, + "password": { + "description": "Password is the updated password\n\ntype: string", + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "CreateIdentity": { + "properties": { + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits.", + "type": "string" + }, + "traits": { + "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_url`.", + "type": "object" + } + }, + "required": [ + "schema_id", + "traits" + ], + "type": "object" + }, + "CreateRecoveryLink": { + "properties": { + "expires_in": { + "description": "Link Expires In\n\nThe recovery link will expire at that point in time. Defaults to the configuration value of\n`selfservice.flows.recovery.request_lifespan`.", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "type": "string" + }, + "identity_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "identity_id" + ], + "type": "object" + }, + "CredentialsType": { + "description": "and so on.", + "title": "CredentialsType represents several different credential types, like password credentials, passwordless credentials,", + "type": "string" + }, + "ID": { + "format": "int64", + "type": "integer" + }, + "Identity": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "recovery_addresses": { + "description": "RecoveryAddresses contains all the addresses that can be used to recover an identity.", + "items": { + "$ref": "#/components/schemas/RecoveryAddress" + }, + "type": "array", + "x-omitempty": true + }, + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits.", + "type": "string" + }, + "schema_url": { + "description": "SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.\n\nformat: url", + "type": "string" + }, + "traits": { + "$ref": "#/components/schemas/Traits" + }, + "verifiable_addresses": { + "description": "VerifiableAddresses contains all the addresses that can be verified by the user.", + "items": { + "$ref": "#/components/schemas/VerifiableAddress" + }, + "type": "array", + "x-omitempty": true + } + }, + "required": [ + "id", + "schema_id", + "schema_url", + "traits" + ], + "type": "object" + }, + "NullTime": { + "format": "date-time", + "title": "NullTime implements sql.NullTime functionality.", + "type": "string" + }, + "RecoveryAddress": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "value": { + "type": "string" + }, + "via": { + "$ref": "#/components/schemas/RecoveryAddressType" + } + }, + "required": [ + "id", + "value", + "via" + ], + "type": "object" + }, + "RecoveryAddressType": { + "type": "string" + }, + "State": { + "type": "string" + }, + "Traits": { + "type": "object" + }, + "Type": { + "description": "The flow type can either be `api` or `browser`.", + "title": "Type is the flow type.", + "type": "string" + }, + "UUID": { + "format": "uuid4", + "type": "string" + }, + "UpdateIdentity": { + "properties": { + "schema_id": { + "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set\nwill update the Identity's SchemaID.", + "type": "string" + }, + "traits": { + "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_id`.", + "type": "object" + } + }, + "required": [ + "traits" + ], + "type": "object" + }, + "VerifiableAddress": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "status": { + "$ref": "#/components/schemas/VerifiableAddressStatus" + }, + "value": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "verified_at": { + "$ref": "#/components/schemas/NullTime" + }, + "via": { + "$ref": "#/components/schemas/VerifiableAddressType" + } + }, + "required": [ + "id", + "value", + "verified", + "via", + "status" + ], + "type": "object" + }, + "VerifiableAddressStatus": { + "type": "string" + }, + "VerifiableAddressType": { + "type": "string" + }, + "completeSelfServiceBrowserSettingsOIDCFlowPayload": { + "properties": { + "flow": { + "description": "Flow ID is the flow's ID.\n\nin: query", + "type": "string" + }, + "link": { + "description": "Link this provider\n\nEither this or `unlink` must be set.\n\ntype: string\nin: body", + "type": "string" + }, + "unlink": { + "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "completeSelfServiceRecoveryFlowWithLinkMethod": { + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "email": { + "description": "Email to Recover\n\nNeeds to be set when initiating the flow. If the email is a registered\nrecovery email, a recovery link will be sent. If the email is not known,\na email with details on what happened will be sent instead.\n\nformat: email\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "completeSelfServiceVerificationFlowWithLinkMethod": { + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "email": { + "description": "Email to Verify\n\nNeeds to be set when initiating the flow. If the email is a registered\nverification email, a verification link will be sent. If the email is not known,\na email with details on what happened will be sent instead.\n\nformat: email\nin: body", + "type": "string" + } + }, + "type": "object" + }, + "errorContainer": { + "properties": { + "errors": { + "description": "Errors in the container", + "type": "object" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "errors" + ], + "type": "object" + }, + "form": { + "description": "HTMLForm represents a HTML Form. The container can work with both HTTP Form and JSON requests", + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "genericError": { + "description": "Error responses are sent when an error (e.g. unauthorized, bad request, ...) occurred.", + "properties": { + "error": { + "$ref": "#/components/schemas/genericErrorPayload" + } + }, + "title": "Error response", + "type": "object" + }, + "genericErrorPayload": { + "properties": { + "code": { + "description": "Code represents the error status code (404, 403, 401, ...).", + "example": 404, + "format": "int64", + "type": "integer" + }, + "debug": { + "description": "Debug contains debug information. This is usually not available and has to be enabled.", + "example": "The database adapter was unable to find the element", + "type": "string" + }, + "details": { + "additionalProperties": true, + "type": "object" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "healthNotReadyStatus": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "type": "object" + }, + "healthStatus": { + "properties": { + "status": { + "description": "Status always contains \"ok\".", + "type": "string" + } + }, + "type": "object" + }, + "loginFlow": { + "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": { + "$ref": "#/components/schemas/CredentialsType" + }, + "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.", + "format": "date-time", + "type": "string" + }, + "forced": { + "description": "Forced stores whether this login flow should enforce re-authentication.", + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow started.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/loginFlowMethod" + }, + "description": "List of login methods\n\nThis is the list of available login methods with their required form fields, such as `identifier` and `password`\nfor the password login method. This will also contain error messages such as \"password can not be empty\".", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods" + ], + "title": "Login Flow", + "type": "object" + }, + "loginFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/loginFlowMethodConfig" + }, + "method": { + "$ref": "#/components/schemas/CredentialsType" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "loginFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "loginFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "loginViaApiResponse": { + "description": "The Response for Login Flows via API", + "properties": { + "session": { + "$ref": "#/components/schemas/session" + }, + "session_token": { + "description": "The Session Token\n\nA session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization\nHeader:\n\nAuthorization: bearer ${session-token}\n\nThe session token is only issued for API flows, not for Browser flows!", + "type": "string" + } + }, + "required": [ + "session_token", + "session" + ], + "type": "object" + }, + "recoveryFlow": { + "description": "This request is used when an identity wants to recover their account.\n\nWe recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery)", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,\na new request has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the request occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/recoveryFlowMethod" + }, + "description": "Methods contains context for all account recovery methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods", + "state" + ], + "title": "A Recovery Flow", + "type": "object" + }, + "recoveryFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/recoveryFlowMethodConfig" + }, + "method": { + "description": "Method contains the request credentials type.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "recoveryFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "recoveryFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "recoveryLink": { + "properties": { + "expires_at": { + "description": "Recovery Link Expires At\n\nThe timestamp when the recovery link expires.", + "format": "date-time", + "type": "string" + }, + "recovery_link": { + "description": "Recovery Link\n\nThis link can be used to recover the account.", + "type": "string" + } + }, + "required": [ + "recovery_link" + ], + "type": "object" + }, + "registrationFlow": { + "properties": { + "active": { + "$ref": "#/components/schemas/CredentialsType" + }, + "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.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/registrationFlowMethod" + }, + "description": "Methods contains context for all enabled registration methods. If a registration flow has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods" + ], + "type": "object" + }, + "registrationFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/registrationFlowMethodConfig" + }, + "method": { + "$ref": "#/components/schemas/CredentialsType" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "registrationFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "description": "Providers is set for the \"oidc\" registration method.", + "items": { + "$ref": "#/components/schemas/uiNodes" + }, + "type": "array" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "registrationFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + }, + "providers": { + "description": "Providers is set for the \"oidc\" registration method.", + "items": { + "$ref": "#/components/schemas/uiNodes" + }, + "type": "array" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "registrationViaApiResponse": { + "description": "The Response for Registration Flows via API", + "properties": { + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "session": { + "$ref": "#/components/schemas/session" + }, + "session_token": { + "description": "The Session Token\n\nThis field is only set when the session hook is configured as a post-registration hook.\n\nA session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization\nHeader:\n\nAuthorization: bearer ${session-token}\n\nThe session token is only issued for API flows, not for Browser flows!", + "type": "string" + } + }, + "required": [ + "session_token", + "identity" + ], + "type": "object" + }, + "revokeSession": { + "properties": { + "session_token": { + "description": "The Session Token\n\nInvalidate this session token.", + "type": "string" + } + }, + "required": [ + "session_token" + ], + "type": "object" + }, + "session": { + "properties": { + "active": { + "type": "boolean" + }, + "authenticated_at": { + "format": "date-time", + "type": "string" + }, + "expires_at": { + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "issued_at": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "expires_at", + "authenticated_at", + "issued_at", + "identity" + ], + "type": "object" + }, + "settingsFlow": { + "description": "This flow is used when an identity wants to update settings\n(e.g. profile data, passwords, ...) in a selfservice manner.\n\nWe recommend reading the [User Settings Documentation](../self-service/flows/user-settings)", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting,\na new flow has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the flow occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/settingsFlowMethod" + }, + "description": "Methods contains context for all enabled registration methods. If a settings flow has been\nprocessed, but for example the first name is empty, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "id", + "expires_at", + "issued_at", + "request_url", + "methods", + "identity", + "state" + ], + "title": "Flow represents a Settings Flow", + "type": "object" + }, + "settingsFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/settingsFlowMethodConfig" + }, + "method": { + "description": "Method is the name of this flow method.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "settingsFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsProfileFormConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "settingsViaApiResponse": { + "description": "The Response for Settings Flows via API", + "properties": { + "flow": { + "$ref": "#/components/schemas/settingsFlow" + }, + "identity": { + "$ref": "#/components/schemas/Identity" + } + }, + "required": [ + "flow", + "identity" + ], + "type": "object" + }, + "uiNode": { + "description": "Nodes are represented as HTML elements or their native UI equivalents. For example,\na node can be an `\u003cimg\u003e` tag, or an `\u003cinput element\u003e` but also `some plain text`.", + "properties": { + "attributes": { + "$ref": "#/components/schemas/uiNodeAttributes" + }, + "group": { + "$ref": "#/components/schemas/uiNodeGroup" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "type": { + "$ref": "#/components/schemas/uiNodeType" + } + }, + "required": [ + "type", + "attributes" + ], + "title": "Node represents a flow's nodes", + "type": "object" + }, + "uiNodeAnchorAttributes": { + "properties": { + "href": { + "description": "The link's href (destination) URL.\n\nformat: uri", + "type": "string" + }, + "title": { + "$ref": "#/components/schemas/uiText" + } + }, + "required": [ + "href", + "title" + ], + "title": "AnchorAttributes represents the attributes of an anchor node.", + "type": "object" + }, + "uiNodeAttributes": { + "oneOf": [ + { + "$ref": "#/components/schemas/uiNodeInputAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeTextAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeImageAttributes" + }, + { + "$ref": "#/components/schemas/uiNodeAnchorAttributes" + } + ], + "title": "Attributes represents a list of attributes (e.g. `href=\"foo\"` for links)." + }, + "uiNodeGroup": { + "type": "string" + }, + "uiNodeImageAttributes": { + "properties": { + "src": { + "description": "The image's source URL.\n\nformat: uri", + "type": "string" + } + }, + "required": [ + "src" + ], + "title": "ImageAttributes represents the attributes of an image node.", + "type": "object" + }, + "uiNodeInputAttributeType": { + "type": "string" + }, + "uiNodeInputAttributes": { + "description": "InputAttributes represents the attributes of an input node", + "properties": { + "disabled": { + "description": "Sets the input's disabled field to true or false.", + "type": "boolean" + }, + "label": { + "$ref": "#/components/schemas/uiText" + }, + "name": { + "description": "The input's element name.", + "type": "string" + }, + "pattern": { + "description": "The input's pattern.", + "type": "string" + }, + "required": { + "description": "Mark this input field as required.", + "type": "boolean" + }, + "type": { + "$ref": "#/components/schemas/uiNodeInputAttributeType" + }, + "value": { + "description": "The input's value.", + "nullable": true, + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "required": [ + "name", + "type", + "disabled" + ], + "type": "object" + }, + "uiNodeTextAttributes": { + "properties": { + "text": { + "$ref": "#/components/schemas/uiText" + } + }, + "required": [ + "text" + ], + "title": "TextAttributes represents the attributes of a text node.", + "type": "object" + }, + "uiNodeType": { + "type": "string" + }, + "uiNodes": { + "items": { + "$ref": "#/components/schemas/uiNode" + }, + "type": "array" + }, + "uiText": { + "properties": { + "context": { + "description": "The message's context. Useful when customizing messages.", + "type": "object" + }, + "id": { + "$ref": "#/components/schemas/ID" + }, + "text": { + "description": "The message text. Written in american english.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/uiTextType" + } + }, + "required": [ + "id", + "text", + "type" + ], + "type": "object" + }, + "uiTextType": { + "type": "string" + }, + "uiTexts": { + "items": { + "$ref": "#/components/schemas/uiText" + }, + "type": "array" + }, + "verificationFlow": { + "description": "Used to verify an out-of-band communication\nchannel such as an email address or a phone number.\n\nFor more information head over to: https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation", + "properties": { + "active": { + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.", + "type": "string" + }, + "expires_at": { + "description": "ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address,\na new request has to be initiated.", + "format": "date-time", + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "issued_at": { + "description": "IssuedAt is the time (UTC) when the request occurred.", + "format": "date-time", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "methods": { + "additionalProperties": { + "$ref": "#/components/schemas/verificationFlowMethod" + }, + "description": "Methods contains context for all account verification methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.", + "type": "object" + }, + "request_url": { + "description": "RequestURL is the initial URL that was requested from Ory Kratos. It can be used\nto forward information contained in the URL's path or query for example.", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/State" + }, + "type": { + "$ref": "#/components/schemas/Type" + } + }, + "required": [ + "methods", + "state" + ], + "title": "A Verification Flow", + "type": "object" + }, + "verificationFlowMethod": { + "properties": { + "config": { + "$ref": "#/components/schemas/verificationFlowMethodConfig" + }, + "method": { + "description": "Method contains the request credentials type.", + "type": "string" + } + }, + "required": [ + "method", + "config" + ], + "type": "object" + }, + "verificationFlowMethodConfig": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "verificationFlowMethodConfigPayload": { + "properties": { + "action": { + "description": "Action should be used as the form action URL `\u003cform action=\"{{ .Action }}\" method=\"post\"\u003e`.", + "type": "string" + }, + "messages": { + "$ref": "#/components/schemas/uiTexts" + }, + "method": { + "description": "Method is the form method (e.g. POST)", + "type": "string" + }, + "nodes": { + "$ref": "#/components/schemas/uiNodes" + } + }, + "required": [ + "action", + "method", + "nodes" + ], + "type": "object" + }, + "version": { + "properties": { + "version": { + "description": "Version is the service's version.", + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "sessionCookie": { + "in": "cookie", + "name": "ory_kratos_session", + "type": "apiKey" + }, + "sessionToken": { + "in": "header", + "name": "X-Session-Token", + "type": "apiKey" + } + } + }, + "info": { + "contact": { + "email": "hi@ory.sh" + }, + "description": "Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs\nare exposed on different ports. Public APIs can face the public internet without any protection\nwhile administrative APIs should never be exposed without prior authorization. To protect\nthe administative API port you should use something like Nginx, Ory Oathkeeper, or any other\ntechnology capable of authorizing incoming requests.\n", + "license": { + "name": "Apache 2.0" + }, + "title": "Ory Kratos API", + "version": "" + }, + "openapi": "3.0.3", + "paths": { + "/health/alive": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming\nHTTP requests. This status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isAlive", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/healthStatus" + } + } + }, + "description": "Ory Kratos is ready to accept connections." + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Check HTTP Server Status", + "tags": [ + "admin" + ] + } + }, + "/health/ready": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g.\nthe database) are responsive as well.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of Ory Kratos, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isReady", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Always \"ok\".", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Ory Kratos is ready to accept requests." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Ory Kratos is not yet ready to accept requests." + } + }, + "summary": "Check HTTP Server and Database Status", + "tags": [ + "admin" + ] + } + }, + "/identities": { + "get": { + "description": "Lists all identities. Does not support search at the moment.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "listIdentities", + "parameters": [ + { + "description": "Items per Page\n\nThis is the number of items per page.", + "in": "query", + "name": "per_page", + "schema": { + "default": 100, + "format": "int64", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Pagination Page", + "in": "query", + "name": "page", + "schema": { + "default": 0, + "format": "int64", + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/identityList" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "List Identities", + "tags": [ + "admin" + ] + }, + "post": { + "description": "This endpoint creates an identity. It is NOT possible to set an identity's credentials (password, ...)\nusing this method! A way to achieve that will be introduced in the future.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "createIdentity", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateIdentity" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "201": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Create an Identity", + "tags": [ + "admin" + ] + } + }, + "/identities/{id}": { + "delete": { + "description": "Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.\nThis endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is\nassumed that is has been deleted already.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "deleteIdentity", + "parameters": [ + { + "description": "ID is the identity's ID.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Delete an Identity", + "tags": [ + "admin" + ] + }, + "get": { + "description": "Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "getIdentity", + "parameters": [ + { + "description": "ID must be set to the ID of identity you want to get", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get an Identity", + "tags": [ + "admin" + ] + }, + "put": { + "description": "This endpoint updates an identity. It is NOT possible to set an identity's credentials (password, ...)\nusing this method! A way to achieve that will be introduced in the future.\n\nThe full identity payload (except credentials) is expected. This endpoint does not support patching.\n\nLearn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "operationId": "updateIdentity", + "parameters": [ + { + "description": "ID must be set to the ID of identity you want to update", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateIdentity" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/identityResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Update an Identity", + "tags": [ + "admin" + ] + } + }, + "/metrics/prometheus": { + "get": { + "description": "```\nmetadata:\nannotations:\nprometheus.io/port: \"4434\"\nprometheus.io/path: \"/metrics/prometheus\"\n```", + "operationId": "prometheus", + "responses": { + "200": { + "$ref": "#/components/responses/emptyResponse" + } + }, + "summary": "Get snapshot metrics from the Kratos service. If you're using k8s, you can then add annotations to\nyour deployment like so:", + "tags": [ + "admin" + ] + } + }, + "/recovery/link": { + "post": { + "description": "This endpoint creates a recovery link which should be given to the user in order for them to recover\n(or activate) their account.", + "operationId": "createRecoveryLink", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRecoveryLink" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryLink" + } + } + }, + "description": "recoveryLink" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Create a Recovery Link", + "tags": [ + "admin" + ] + } + }, + "/schemas/{id}": { + "get": { + "description": "Get a Traits Schema Definition", + "operationId": "getSchema", + "parameters": [ + { + "description": "ID must be set to the ID of schema you want to get", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/schemaResponse" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/browser/flows/logout": { + "get": { + "description": "This endpoint initializes a logout flow.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nOn successful logout, the browser will be redirected (HTTP 302 Found) to the `return_to` parameter of the initial request\nor fall back to `urls.default_return_to`.\n\nMore information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout).", + "operationId": "initializeSelfServiceBrowserLogoutFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Browser-Based Logout User Flow", + "tags": [ + "public" + ] + } + }, + "/self-service/browser/flows/registration/strategies/oidc/settings/connections": { + "post": { + "description": "This endpoint completes a browser-based settings flow. This is usually achieved by POSTing data to this\nendpoint.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceBrowserSettingsOIDCSettingsFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete the Browser-Based Settings Flow for the OpenID Connect Strategy", + "tags": [ + "public" + ] + } + }, + "/self-service/errors": { + "get": { + "description": "This endpoint returns the error associated with a user-facing self service errors.\n\nThis endpoint supports stub values to help you implement the error UI:\n\n`?error=stub:500` - returns a stub 500 (Internal Server Error) error.\n\nMore information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors).", + "operationId": "getSelfServiceError", + "parameters": [ + { + "description": "Error is the container's ID", + "in": "query", + "name": "error", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/errorContainer" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get User-Facing Self-Service Errors", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/login/api": { + "get": { + "description": "This endpoint initiates a login flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing login flow call `/self-service/login/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks, including CSRF login attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceLoginViaAPIFlow", + "parameters": [ + { + "description": "Refresh a login session\n\nIf set to true, this will refresh an existing login session by\nasking the user to sign in again. This will reset the\nauthenticated_at time of the session.", + "in": "query", + "name": "refresh", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Login Flow for API clients", + "tags": [ + "public" + ] + } + }, + "/self-service/login/browser": { + "get": { + "description": "This endpoint initializes a browser-based user login flow. Once initialized, the browser will be redirected to\n`selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter\n`?refresh=true` was set.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceLoginViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Login Flow for browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/login/flows": { + "get": { + "description": "This endpoint returns a login flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "getSelfServiceLoginFlow", + "parameters": [ + { + "description": "The Login Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/login?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Login Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/login/methods/password": { + "post": { + "description": "Use this endpoint to complete a login flow by sending an identity's identifier and password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI flows expect `application/json` to be sent in the body and responds with\nHTTP 200 and a application/json body with the session token on success;\nHTTP 302 redirect to a fresh login flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after login URL or the `return_to` value if it was set and if the login succeeded;\na HTTP 302 redirect to the login UI URL with the flow ID containing the validation errors otherwise.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "completeSelfServiceLoginFlowWithPasswordMethod", + "parameters": [ + { + "description": "The Flow ID", + "in": "query", + "name": "flow", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceLoginFlowWithPasswordMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceLoginFlowWithPasswordMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginViaApiResponse" + } + } + }, + "description": "loginViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginFlow" + } + } + }, + "description": "loginFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Login Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/api": { + "get": { + "description": "This endpoint initiates a recovery flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error.\n\nTo fetch an existing recovery flow call `/self-service/recovery/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "initializeSelfServiceRecoveryViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Recovery Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/browser": { + "get": { + "description": "This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to\n`selfservice.flows.recovery.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists, the browser is returned to the configured return URL.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "initializeSelfServiceRecoveryViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Recovery Flow for Browser Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/recovery/flows": { + "get": { + "description": "This endpoint returns a recovery flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "getSelfServiceRecoveryFlow", + "parameters": [ + { + "description": "The Flow ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/recovery?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get information about a recovery flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/recovery/methods/link": { + "post": { + "description": "Use this endpoint to complete a recovery flow using the link method. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid\nand a HTTP 302 Found redirect with a fresh recovery flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients it returns a HTTP 302 Found redirect to the Recovery UI URL with the Recovery Flow ID appended.\n`sent_email` is the success state after `choose_method` and allows the user to request another recovery email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a recovery link\")\ndoes not have any API capabilities. The server responds with a HTTP 302 Found redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Recover UI URL with\na new Recovery Flow ID which contains an error message that the recovery link was invalid.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery.mdx).", + "operationId": "completeSelfServiceRecoveryFlowWithLinkMethod", + "parameters": [ + { + "description": "Recovery Token\n\nThe recovery token which completes the recovery request. If the token\nis invalid (e.g. expired) an error will be shown to the end-user.", + "in": "query", + "name": "token", + "schema": { + "type": "string" + } + }, + { + "description": "The Flow ID\n\nformat: uuid", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceRecoveryFlowWithLinkMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceRecoveryFlowWithLinkMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/recoveryFlow" + } + } + }, + "description": "recoveryFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Recovery Flow with Link Method", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/api": { + "get": { + "description": "This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing registration flow call `/self-service/registration/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceRegistrationViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Registration Flow for API clients", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/browser": { + "get": { + "description": "This endpoint initializes a browser-based user registration flow. Once initialized, the browser will be redirected to\n`selfservice.flows.registration.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter\n`?refresh=true` was set.\n\n:::note\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceRegistrationViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Registration Flow for browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/registration/flows": { + "get": { + "description": "This endpoint returns a registration flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "getSelfServiceRegistrationFlow", + "parameters": [ + { + "description": "The Registration Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/registration?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Registration Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/registration/methods/password": { + "post": { + "description": "Use this endpoint to complete a registration flow by sending an identity's traits and password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and a application/json body with the created identity success - if the session hook is configured the\n`session` and `session_token` will also be included;\nHTTP 302 redirect to a fresh registration flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after registration URL or the `return_to` value if it was set and if the registration succeeded;\na HTTP 302 redirect to the registration UI URL with the flow ID containing the validation errors otherwise.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "completeSelfServiceRegistrationFlowWithPasswordMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object" + } + } + }, + "x-originalParamName": "Payload" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationViaApiResponse" + } + } + }, + "description": "registrationViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/registrationFlow" + } + } + }, + "description": "registrationFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Registration Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/api": { + "get": { + "description": "This endpoint initiates a settings flow for API clients such as mobile devices, smart TVs, and so on.\nYou must provide a valid Ory Kratos Session Token for this endpoint to respond with HTTP 200 OK.\n\nTo fetch an existing settings flow call `/self-service/settings/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "initializeSelfServiceSettingsViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Initialize Settings Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/browser": { + "get": { + "description": "This endpoint initializes a browser-based user settings flow. Once initialized, the browser will be redirected to\n`selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid\nOry Kratos Session Cookie is included in the request, a login flow will be initialized.\n\n:::note\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\n:::\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "initializeSelfServiceSettingsViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Initialize Settings Flow for Browsers", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/flows": { + "get": { + "description": "When accessing this endpoint through Ory Kratos' Public API you must ensure that either the Ory Kratos Session Cookie\nor the Ory Kratos Session Token are set. The public endpoint does not return 404 status codes\nbut instead 403 or 500 to improve data privacy.\n\nYou can access this endpoint without credentials when using Ory Kratos' Admin API.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "getSelfServiceSettingsFlow", + "parameters": [ + { + "description": "ID is the Settings Flow ID\n\nThe value for this parameter comes from `flow` URL Query parameter sent to your\napplication (e.g. `/settings?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Get Settings Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/settings/methods/password": { + "post": { + "description": "Use this endpoint to complete a settings flow by sending an identity's updated password. This endpoint\nbehaves differently for API and browser flows.\n\nAPI-initiated flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and an application/json body with the session token on success;\nHTTP 302 redirect to a fresh settings flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\nHTTP 401 when the endpoint is called without a valid session token.\nHTTP 403 when `selfservice.flows.settings.privileged_session_max_age` was reached.\nImplies that the user needs to re-authenticate.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after settings URL or the `return_to` value if it was set and if the flow succeeded;\na HTTP 302 redirect to the Settings UI URL with the flow ID containing the validation errors otherwise.\na HTTP 302 redirect to the login endpoint when `selfservice.flows.settings.privileged_session_max_age` was reached.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceSettingsFlowWithPasswordMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceSettingsFlowWithPasswordMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CompleteSelfServiceSettingsFlowWithPasswordMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsViaApiResponse" + } + } + }, + "description": "settingsViaApiResponse" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Complete Settings Flow with Username/Email Password Method", + "tags": [ + "public" + ] + } + }, + "/self-service/settings/methods/profile": { + "post": { + "description": "Use this endpoint to complete a settings flow by sending an identity's updated traits. This endpoint\nbehaves differently for API and browser flows.\n\nAPI-initiated flows expect `application/json` to be sent in the body and respond with\nHTTP 200 and an application/json body with the session token on success;\nHTTP 302 redirect to a fresh settings flow if the original flow expired with the appropriate error messages set;\nHTTP 400 on form validation errors.\nHTTP 401 when the endpoint is called without a valid session token.\nHTTP 403 when `selfservice.flows.settings.privileged_session_max_age` was reached and a sensitive field was\nupdated (e.g. recovery email). Implies that the user needs to re-authenticate.\n\nBrowser flows expect `application/x-www-form-urlencoded` to be sent in the body and responds with\na HTTP 302 redirect to the post/after settings URL or the `return_to` value if it was set and if the flow succeeded;\na HTTP 302 redirect to the settings UI URL with the flow ID containing the validation errors otherwise.\na HTTP 302 redirect to the login endpoint when `selfservice.flows.settings.privileged_session_max_age` was reached.\n\nMore information can be found at [Ory Kratos User Settings \u0026 Profile Management Documentation](../self-service/flows/user-settings).", + "operationId": "completeSelfServiceSettingsFlowWithProfileMethod", + "parameters": [ + { + "description": "Flow is flow ID.", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object" + } + } + }, + "x-originalParamName": "Payload" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/settingsFlow" + } + } + }, + "description": "settingsFlow" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Complete Settings Flow with Profile Method", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/api": { + "get": { + "description": "This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on.\n\nTo fetch an existing verification flow call `/self-service/verification/flows?flow=\u003cflow_id\u003e`.\n\n:::warning\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\n:::\n\nMore 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).", + "operationId": "initializeSelfServiceVerificationViaAPIFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Verification Flow for API Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/browser": { + "get": { + "description": "This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to\n`selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore 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).", + "operationId": "initializeSelfServiceVerificationViaBrowserFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Initialize Verification Flow for Browser Clients", + "tags": [ + "public" + ] + } + }, + "/self-service/verification/flows": { + "get": { + "description": "This endpoint returns a verification flow's context with, for example, error details and other information.\n\nMore 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).", + "operationId": "getSelfServiceVerificationFlow", + "parameters": [ + { + "description": "The Flow ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/verification?flow=abcde`).", + "in": "query", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Get Verification Flow", + "tags": [ + "public", + "admin" + ] + } + }, + "/self-service/verification/methods/link": { + "post": { + "description": "Use this endpoint to complete a verification flow using the link method. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid\nand a HTTP 302 Found redirect with a fresh verification flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients it returns a HTTP 302 Found redirect to the Verification UI URL with the Verification Flow ID appended.\n`sent_email` is the success state after `choose_method` and allows the user to request another verification email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a verification link\")\ndoes not have any API capabilities. The server responds with a HTTP 302 Found redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Verification UI URL with\na new Verification Flow ID which contains an error message that the verification link was invalid.\n\nMore 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).", + "operationId": "completeSelfServiceVerificationFlowWithLinkMethod", + "parameters": [ + { + "description": "Verification Token\n\nThe verification token which completes the verification request. If the token\nis invalid (e.g. expired) an error will be shown to the end-user.", + "in": "query", + "name": "token", + "schema": { + "type": "string" + } + }, + { + "description": "The Flow ID\n\nformat: uuid", + "in": "query", + "name": "flow", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceVerificationFlowWithLinkMethod" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/completeSelfServiceVerificationFlowWithLinkMethod" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verificationFlow" + } + } + }, + "description": "verificationFlow" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Complete Verification Flow with Link Method", + "tags": [ + "public" + ] + } + }, + "/sessions": { + "delete": { + "description": "Use this endpoint to revoke a session using its token. This endpoint is particularly useful for API clients\nsuch as mobile apps to log the user out of the system and invalidate the session.\n\nThis endpoint does not remove any HTTP Cookies - use the Self-Service Logout Flow instead.", + "operationId": "revokeSession", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/revokeSession" + } + } + }, + "required": true, + "x-originalParamName": "Body" + }, + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Revoke and Invalidate a Session", + "tags": [ + "public" + ] + } + }, + "/sessions/whoami": { + "get": { + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nThis endpoint is useful for reverse proxies and API Gateways.", + "operationId": "whoami", + "parameters": [ + { + "in": "header", + "name": "Cookie", + "schema": { + "type": "string" + } + }, + { + "description": "in: authorization", + "in": "query", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/session" + } + } + }, + "description": "session" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "security": [ + { + "sessionToken": [] + } + ], + "summary": "Check Who the Current HTTP Session Belongs To", + "tags": [ + "public" + ] + } + }, + "/version": { + "get": { + "description": "This endpoint returns the version of Ory Kratos.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the version will never\nrefer to the cluster state, only to a single instance.", + "operationId": "getVersion", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "version": { + "description": "The version of Ory Kratos.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Returns the Ory Kratos version." + } + }, + "summary": "Return Running Software Version.", + "tags": [ + "admin" + ] + } + } + }, + "servers": [ + { + "url": "https://{tenant}.tenants.oryapis.com/api/kratos/{api}", + "variables": { + "api": { + "default": "public", + "description": "Target the public or administrative API.", + "enum": [ + "public", + "admin" + ] + }, + "tenant": { + "default": "demo", + "description": "Tenant ID as provided by Ory Cloud." + } + } + } + ], + "tags": [ + { + "description": "All administrative API endpoints exposed at the admin API port.", + "externalDocs": { + "url": "https://www.ory.sh/kratos/docs/reference/api" + }, + "name": "admin" + }, + { + "description": "All public API endpoints exposed at the public API port.", + "externalDocs": { + "url": "https://www.ory.sh/kratos/docs/reference/api" + }, + "name": "public" + } + ], + "x-forwarded-proto": "string", + "x-request-id": "string" +} diff --git a/.schema/openapi/gen.go.yml b/.schema/openapi/gen.go.yml new file mode 100644 index 0000000..1e7377d --- /dev/null +++ b/.schema/openapi/gen.go.yml @@ -0,0 +1,7 @@ +disallowAdditionalPropertiesIfNotPresent: false +packageName: client +generateInterfaces: true +structPrefix: true +enumClassPrefix: true +useOneOfDiscriminatorLookup: true +isGoSubmodule: false diff --git a/.schema/openapi/gen.typescript.yml b/.schema/openapi/gen.typescript.yml new file mode 100644 index 0000000..301478f --- /dev/null +++ b/.schema/openapi/gen.typescript.yml @@ -0,0 +1,11 @@ +npmName: "@ory/kratos-client" +npmVersion: 0.0.0 +# typescriptThreePlus: true +#npmRepository: https://github.com/ory/sdk.git +supportsES6: true +ensureUniqueParams: true +modelPropertyNaming: original +disallowAdditionalPropertiesIfNotPresent: false +withInterfaces: false +useSingleRequestParameter: true +enumUnknownDefaultCase: true diff --git a/.schema/openapi/patches/common.yaml b/.schema/openapi/patches/common.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.schema/openapi/patches/common.yaml @@ -0,0 +1 @@ +[] diff --git a/.schema/openapi/patches/courier.yaml b/.schema/openapi/patches/courier.yaml new file mode 100644 index 0000000..31f7bf8 --- /dev/null +++ b/.schema/openapi/patches/courier.yaml @@ -0,0 +1,29 @@ +# Makes courierMessageStatus a string enum +- op: remove + path: /components/schemas/courierMessageStatus/format +- op: replace + path: /components/schemas/courierMessageStatus/type + value: string +- op: add + path: /components/schemas/courierMessageStatus/enum + value: + - queued + - sent + - processing + - abandoned +# Makes courierMessageType a string enum +- op: remove + path: /components/schemas/courierMessageType/format +- op: replace + path: /components/schemas/courierMessageType/type + value: string +- op: add + path: /components/schemas/courierMessageType/enum + value: + - email + - phone +# Fix courierMessageStatus query parameter in listMessages endpoint +- op: replace + path: /paths/~1admin~1courier~1messages/get/parameters/2/schema + value: + $ref: "#/components/schemas/courierMessageStatus" diff --git a/.schema/openapi/patches/generic_error.yaml b/.schema/openapi/patches/generic_error.yaml new file mode 100644 index 0000000..a1e7fe3 --- /dev/null +++ b/.schema/openapi/patches/generic_error.yaml @@ -0,0 +1,6 @@ +- op: add + path: /paths/~1sessions~1whoami/get/parameters/0/example + value: MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj +- op: add + path: /paths/~1sessions~1whoami/get/parameters/1/example + value: ory_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f== diff --git a/.schema/openapi/patches/identity.yaml b/.schema/openapi/patches/identity.yaml new file mode 100644 index 0000000..1132912 --- /dev/null +++ b/.schema/openapi/patches/identity.yaml @@ -0,0 +1,13 @@ +- op: remove + path: /components/schemas/updateIdentityBody/properties/metadata_admin/type +- op: remove + path: /components/schemas/updateIdentityBody/properties/metadata_public/type +- op: remove + path: /components/schemas/createIdentityBody/properties/metadata_admin/type +- op: remove + path: /components/schemas/createIdentityBody/properties/metadata_public/type +- op: remove + path: /components/schemas/nullJsonRawMessage/type +- op: add + path: /components/schemas/nullJsonRawMessage/nullable + value: true diff --git a/.schema/openapi/patches/meta.yaml b/.schema/openapi/patches/meta.yaml new file mode 100644 index 0000000..2c91844 --- /dev/null +++ b/.schema/openapi/patches/meta.yaml @@ -0,0 +1,23 @@ +- op: replace + path: /info + value: + title: Ory Identities API + description: | + 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. + version: >- + {{ getenv "CIRCLE_TAG" }} + license: + name: Apache 2.0 + contact: + email: "office@ory.sh" +- op: replace + path: /tags + value: + - name: identity + description: APIs for managing identities. + - name: frontend + description: Endpoints used by frontend applications (e.g. Single-Page-App, Native Apps, Server Apps, ...) to manage a user's own profile. + - name: courier + description: APIs for managing email and SMS message delivery. + - name: metadata + description: Server Metadata provides relevant information about the running server. Only available when self-hosting this service. diff --git a/.schema/openapi/patches/nulls.yaml b/.schema/openapi/patches/nulls.yaml new file mode 100644 index 0000000..fbdeba2 --- /dev/null +++ b/.schema/openapi/patches/nulls.yaml @@ -0,0 +1,37 @@ +- op: replace + path: "#/components/schemas/NullUUID" + value: + type: string + format: uuid4 + nullable: true +- op: replace + path: "#/components/schemas/NullTime" + value: + format: date-time + type: string + nullable: true +- op: replace + path: "#/components/schemas/Time" + value: + format: date-time + type: string +- op: replace + path: "#/components/schemas/NullString" + value: + type: string + nullable: true +- op: replace + path: "#/components/schemas/NullBool" + value: + type: boolean + nullable: true +- op: replace + path: "#/components/schemas/NullInt" + value: + type: integer + nullable: true +- op: replace + path: "#/components/schemas/nullInt64" + value: + type: integer + nullable: true diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml new file mode 100644 index 0000000..f564809 --- /dev/null +++ b/.schema/openapi/patches/schema.yaml @@ -0,0 +1,141 @@ +# Makes uiNodeAttributes polymorph +- op: remove + path: /components/schemas/uiNodeAttributes/type +- op: add + path: /components/schemas/uiNodeAttributes/discriminator + value: + propertyName: node_type + mapping: + input: "#/components/schemas/uiNodeInputAttributes" + text: "#/components/schemas/uiNodeTextAttributes" + img: "#/components/schemas/uiNodeImageAttributes" + a: "#/components/schemas/uiNodeAnchorAttributes" + script: "#/components/schemas/uiNodeScriptAttributes" + div: "#/components/schemas/uiNodeDivisionAttributes" +- op: add + path: /components/schemas/uiNodeAttributes/oneOf + value: + - "$ref": "#/components/schemas/uiNodeInputAttributes" + - "$ref": "#/components/schemas/uiNodeTextAttributes" + - "$ref": "#/components/schemas/uiNodeImageAttributes" + - "$ref": "#/components/schemas/uiNodeAnchorAttributes" + - "$ref": "#/components/schemas/uiNodeScriptAttributes" + - "$ref": "#/components/schemas/uiNodeDivisionAttributes" + +- op: replace + path: /components/schemas/uiNodeDivisionAttributes/properties/node_type/enum + value: + - div + +- op: replace + path: /components/schemas/uiNodeInputAttributes/properties/node_type/enum + value: + - input + +- op: replace + path: /components/schemas/uiNodeTextAttributes/properties/node_type/enum + value: + - text + +- op: replace + path: /components/schemas/uiNodeImageAttributes/properties/node_type/enum + value: + - img + +- op: replace + path: /components/schemas/uiNodeAnchorAttributes/properties/node_type/enum + value: + - a + +- op: replace + path: /components/schemas/uiNodeScriptAttributes/properties/node_type/enum + value: + - script + +# Makes the uiNodeInputAttributes value attribute polymorph +- op: add + path: /components/schemas/uiNodeInputAttributes/properties/value/nullable + value: true + +- op: replace + path: /components/schemas/flowError/properties/error + value: + type: object + +- op: remove + path: "#/components/schemas/identityTraits/type" + +- op: add + path: /components/schemas/continueWith/discriminator + value: + propertyName: action + mapping: + show_verification_ui: "#/components/schemas/continueWithVerificationUi" + 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" + show_pin_entry_ui: "#/components/schemas/continueWithDeviceAuthnPinEntryUi" + +- op: add + path: /components/schemas/continueWith/oneOf + value: + - "$ref": "#/components/schemas/continueWithVerificationUi" + - "$ref": "#/components/schemas/continueWithSetOrySessionToken" + - "$ref": "#/components/schemas/continueWithSettingsUi" + - "$ref": "#/components/schemas/continueWithRecoveryUi" + - "$ref": "#/components/schemas/continueWithRedirectBrowserTo" + - "$ref": "#/components/schemas/continueWithDeviceAuthnPinEntryUi" + +# DeviceAuthn: constrain UserVerification to its known values. go-swagger renders +# the `type UserVerification string` alias as a plain string, so the enum is +# pinned here. The valid values are pin, platform, and none. +- op: add + path: /components/schemas/UserVerification/enum + value: + - pin + - platform + - none + +# DeviceAuthn: constrain KeyState and DeviceType to their known values, for the +# same reason as UserVerification above. +- op: add + path: /components/schemas/KeyState/enum + value: + - initial + - confirmed + - locked + +- op: add + path: /components/schemas/DeviceType/enum + value: + - Android + - iOS + +# Without explicit variable names, generators derive the "iOS" identifier as +# I_OS / IOs. The names apply verbatim in every generated language. +- op: add + path: /components/schemas/DeviceType/x-enum-varnames + value: + - ANDROID + - IOS + +# DeviceAuthn: the at-rest pin_secret ciphertext is server-internal and must +# never be returned to clients. Mark it write-only so the SDK and docs flag it +# as not-readable (Swagger 2.0 cannot express writeOnly, so this lands in the +# OpenAPI 3.0 spec only). +- op: add + path: /components/schemas/PINConfig/properties/pin_secret/writeOnly + value: true + +# DeviceAuthn: attach a description to the `data` property. go-swagger +# drops a description next to a bare $ref, so wrap the $ref in allOf to keep the +# reference and carry the description. +- op: replace + path: /components/schemas/continueWithDeviceAuthnPinEntryUi/properties/data + value: + description: >- + The one-time HPKE-sealed pin_secret material the device opens to bind the + user's PIN after a PIN-protected DeviceAuthn enrollment or secret rotation. + allOf: + - "$ref": "#/components/schemas/continueWithDeviceAuthnPinEntryUiData" diff --git a/.schema/openapi/patches/security.yaml b/.schema/openapi/patches/security.yaml new file mode 100644 index 0000000..9318c64 --- /dev/null +++ b/.schema/openapi/patches/security.yaml @@ -0,0 +1,3 @@ +- op: replace + path: /components/schemas/genericError/properties/details/additionalProperties + value: false diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml new file mode 100644 index 0000000..16ac21e --- /dev/null +++ b/.schema/openapi/patches/selfservice.yaml @@ -0,0 +1,187 @@ +# Makes updateLoginFlowPayload polymorph +#- op: remove +# path: /components/schemas/updateLoginFlowBody/type +#- op: add +# path: /components/schemas/updateLoginFlowBody/oneOf +# value: +# - "$ref": "#/components/schemas/updateLoginFlowWithPasswordMethod" + +# Makes updateRegistrationFlowPayload polymorph + +# All modifications for the registration flow +- op: remove + path: /components/schemas/updateRegistrationFlowBody/type +- op: add + path: /components/schemas/updateRegistrationFlowBody/oneOf + value: + - "$ref": "#/components/schemas/updateRegistrationFlowWithPasswordMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithOidcMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithSamlMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" +- op: add + path: /components/schemas/updateRegistrationFlowBody/discriminator + value: + propertyName: method + mapping: + password: "#/components/schemas/updateRegistrationFlowWithPasswordMethod" + oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" + saml: "#/components/schemas/updateRegistrationFlowWithSamlMethod" + webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" + passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod" +- op: add + path: /components/schemas/registrationFlowState + value: + 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 +- op: remove + path: /components/schemas/updateLoginFlowBody/type +- op: add + path: /components/schemas/updateLoginFlowBody/oneOf + value: + - "$ref": "#/components/schemas/updateLoginFlowWithPasswordMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithOidcMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithSamlMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithTotpMethod" + - "$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: + propertyName: method + mapping: + password: "#/components/schemas/updateLoginFlowWithPasswordMethod" + oidc: "#/components/schemas/updateLoginFlowWithOidcMethod" + saml: "#/components/schemas/updateLoginFlowWithSamlMethod" + totp: "#/components/schemas/updateLoginFlowWithTotpMethod" + 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 + value: + 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 +- op: remove + path: /components/schemas/updateRecoveryFlowBody/type +- op: add + path: /components/schemas/updateRecoveryFlowBody/oneOf + value: + - "$ref": "#/components/schemas/updateRecoveryFlowWithLinkMethod" + - "$ref": "#/components/schemas/updateRecoveryFlowWithCodeMethod" +- op: add + path: /components/schemas/updateRecoveryFlowBody/discriminator + value: + propertyName: method + mapping: + link: "#/components/schemas/updateRecoveryFlowWithLinkMethod" + code: "#/components/schemas/updateRecoveryFlowWithCodeMethod" +- op: add + path: /components/schemas/recoveryFlowState + type: string + value: + 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 +- op: remove + path: /components/schemas/updateVerificationFlowBody/type +- op: add + path: /components/schemas/updateVerificationFlowBody/oneOf + value: + - "$ref": "#/components/schemas/updateVerificationFlowWithLinkMethod" + - "$ref": "#/components/schemas/updateVerificationFlowWithCodeMethod" +- op: add + path: /components/schemas/updateVerificationFlowBody/discriminator + value: + propertyName: method + mapping: + link: "#/components/schemas/updateVerificationFlowWithLinkMethod" + code: "#/components/schemas/updateVerificationFlowWithCodeMethod" +- op: add + path: /components/schemas/verificationFlowState + type: string + value: + 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 +- op: remove + path: /components/schemas/updateSettingsFlowBody/type +- op: add + path: /components/schemas/updateSettingsFlowBody/oneOf + value: + - "$ref": "#/components/schemas/updateSettingsFlowWithPasswordMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithProfileMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithSamlMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithTotpMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithLookupMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithPasskeyMethod" +- op: add + path: /components/schemas/updateSettingsFlowBody/discriminator + value: + propertyName: method + mapping: + password: "#/components/schemas/updateSettingsFlowWithPasswordMethod" + profile: "#/components/schemas/updateSettingsFlowWithProfileMethod" + oidc: "#/components/schemas/updateSettingsFlowWithOidcMethod" + saml: "#/components/schemas/updateSettingsFlowWithSamlMethod" + totp: "#/components/schemas/updateSettingsFlowWithTotpMethod" + webauthn: "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" + passkey: "#/components/schemas/updateSettingsFlowWithPasskeyMethod" + lookup_secret: "#/components/schemas/updateSettingsFlowWithLookupMethod" +- op: add + path: /components/schemas/settingsFlowState + value: + 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 + +# Some issues with AdditionalProperties +- op: remove + path: "#/components/schemas/OAuth2LoginRequest/properties/AdditionalProperties" +- op: remove + path: "#/components/schemas/OAuth2ConsentRequestOpenIDConnectContext/properties/AdditionalProperties" +- op: remove + path: "#/components/schemas/OAuth2Client/properties/AdditionalProperties" diff --git a/.schema/openapi/patches/session.yaml b/.schema/openapi/patches/session.yaml new file mode 100644 index 0000000..5db330d --- /dev/null +++ b/.schema/openapi/patches/session.yaml @@ -0,0 +1,13 @@ +- op: add + path: /paths/~1sessions~1whoami/get/parameters/0/example + value: MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj +- op: add + path: /paths/~1sessions~1whoami/get/parameters/1/example + value: ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f== +- op: add + path: /components/schemas/authenticatorAssuranceLevel/enum + value: + - aal0 + - aal1 + - aal2 + - aal3 diff --git a/.schema/version.schema.json b/.schema/version.schema.json new file mode 100644 index 0000000..f82ab21 --- /dev/null +++ b/.schema/version.schema.json @@ -0,0 +1,601 @@ +{ + "$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.3.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.3.0/.schemastore/config.schema.json" + } + ] + }, + { + "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": [ + { + "properties": { + "version": { + "const": "v1.0.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.0.0/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.13.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.13.0/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.11.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.11.1/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.11.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.11.0/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.10.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.10.1/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.10.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.10.0/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.9.0-alpha.3" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.9.0-alpha.3/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.9.0-alpha.2" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.9.0-alpha.2/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.4.6-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.4.6-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.0-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.0-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.1-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.1-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.2-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.2-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.3-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.3-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.4-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.4-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.5.5-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.5.5-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.0-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.6.0-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.0-alpha.2" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.6.0-alpha.2/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.1-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.6.1-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.2-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.6.2-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.3-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.6.3-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.0-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.0-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.1-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.1-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.3-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.3-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.4-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.4-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.5-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.5-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.6-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.7.6-alpha.1/driver/config/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.0-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.8.0-alpha.1/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.0-alpha.2" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.8.0-alpha.2/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.0-alpha.3" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.8.0-alpha.3/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.2-alpha.1" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v0.8.2-alpha.1/embedx/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "version": { + "type": "string", + "maxLength": 0 + } + }, + "required": [ + "version" + ] + }, + { + "not": { + "properties": { + "version": {} + }, + "required": [ + "version" + ] + } + } + ] + }, + { + "$ref": "#/oneOf/0/allOf/1" + } + ] + } + ], + "title": "All Versions for Ory Kratos Configuration", + "type": "object" +} \ No newline at end of file diff --git a/.schemastore/README.md b/.schemastore/README.md new file mode 100644 index 0000000..6c96b02 --- /dev/null +++ b/.schemastore/README.md @@ -0,0 +1,3 @@ +The config schema is generated from the internal one at +`embedx/config.schema.json`, so in case of changes to the config schema, +please edit that internal schema instead. diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json new file mode 100644 index 0000000..2d2740d --- /dev/null +++ b/.schemastore/config.schema.json @@ -0,0 +1,3190 @@ +{ + "$id": "https://github.com/ory/kratos/embedx/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ory Kratos Configuration", + "type": "object", + "definitions": { + "baseUrl": { + "title": "Base URL", + "description": "The URL where the endpoint is exposed at. This domain is used to generate redirects, form URLs, and more.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/", + "https://my-app.com/.ory/kratos/public" + ] + }, + "socket": { + "type": "object", + "additionalProperties": false, + "description": "Sets the permissions of the unix socket", + "properties": { + "owner": { + "type": "string", + "description": "Owner of unix socket. If empty, the owner will be the user running Kratos.", + "default": "" + }, + "group": { + "type": "string", + "description": "Group of unix socket. If empty, the group will be the primary group of the user running Kratos.", + "default": "" + }, + "mode": { + "type": "integer", + "description": "Mode of unix socket in numeric form", + "default": 493, + "minimum": 0, + "maximum": 511 + } + } + }, + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceRequireVerifiedAddressHook": { + "type": "object", + "properties": { + "hook": { + "const": "require_verified_address" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerificationHook": { + "type": "object", + "properties": { + "hook": { + "const": "verification" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceShowVerificationUIHook": { + "type": "object", + "properties": { + "hook": { + "const": "show_verification_ui" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "b2bSSOHook": { + "type": "object", + "properties": { + "hook": { + "enum": ["b2b_sso", "organization"] + }, + "config": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": false, + "required": ["hook", "config"] + }, + "webHookAuthBasicAuthProperties": { + "properties": { + "type": { + "const": "basic_auth" + }, + "config": { + "type": "object", + "properties": { + "user": { + "type": "string", + "description": "user name for basic auth" + }, + "password": { + "type": "string", + "description": "password for basic auth" + } + }, + "additionalProperties": false, + "required": ["user", "password"] + } + }, + "additionalProperties": false, + "required": ["type", "config"] + }, + "httpRequestConfig": { + "type": "object", + "properties": { + "url": { + "title": "HTTP address of API endpoint", + "description": "This URL will be used to send the emails to.", + "examples": ["https://example.com/api/v1/email"], + "type": "string", + "pattern": "^https?://" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc). Defaults to POST.", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to request", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string", + "format": "uri", + "pattern": "^(http|https|file|base64)://", + "description": "URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods which support HTTP body payloads", + "default": "base64://ZnVuY3Rpb24oY3R4KSB7CiAgcmVjaXBpZW50OiBjdHgucmVjaXBpZW50LAogIHRlbXBsYXRlX3R5cGU6IGN0eC50ZW1wbGF0ZV90eXBlLAogIHRvOiBpZiAidGVtcGxhdGVfZGF0YSIgaW4gY3R4ICYmICJ0byIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS50byBlbHNlIG51bGwsCiAgcmVjb3ZlcnlfY29kZTogaWYgInRlbXBsYXRlX2RhdGEiIGluIGN0eCAmJiAicmVjb3ZlcnlfY29kZSIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV9jb2RlIGVsc2UgbnVsbCwKICByZWNvdmVyeV91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInJlY292ZXJ5X3VybCIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV91cmwgZWxzZSBudWxsLAogIHZlcmlmaWNhdGlvbl91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl91cmwiIGluIGN0eC50ZW1wbGF0ZV9kYXRhIHRoZW4gY3R4LnRlbXBsYXRlX2RhdGEudmVyaWZpY2F0aW9uX3VybCBlbHNlIG51bGwsCiAgdmVyaWZpY2F0aW9uX2NvZGU6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl9jb2RlIiBpbiBjdHgudGVtcGxhdGVfZGF0YSB0aGVuIGN0eC50ZW1wbGF0ZV9kYXRhLnZlcmlmaWNhdGlvbl9jb2RlIGVsc2UgbnVsbCwKICBzdWJqZWN0OiBjdHguc3ViamVjdCwKICBib2R5OiBjdHguYm9keQp9Cg==", + "examples": [ + "file:///path/to/body.jsonnet", + "file://./body.jsonnet", + "base64://ZnVuY3Rpb24oY3R4KSB7CiAgaWRlbnRpdHlfaWQ6IGlmIGN0eFsiaWRlbnRpdHkiXSAhPSBudWxsIHRoZW4gY3R4LmlkZW50aXR5LmlkLAp9=", + "https://oryapis.com/default_body.jsonnet" + ] + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism to use for auth with the HTTP email provider", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + }, + "additionalProperties": false + }, + "webHookAuthApiKeyProperties": { + "properties": { + "type": { + "const": "api_key" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the api key" + }, + "value": { + "type": "string", + "description": "The value of the api key" + }, + "in": { + "type": "string", + "description": "How the api key should be transferred", + "enum": ["header", "cookie"] + } + }, + "additionalProperties": false, + "required": ["name", "value", "in"] + } + }, + "additionalProperties": false, + "required": ["type", "config"] + }, + "selfServiceWebHook": { + "type": "object", + "properties": { + "hook": { + "const": "web_hook" + }, + "config": { + "type": "object", + "title": "Web-Hook Configuration", + "description": "Define what the hook should do", + "properties": { + "id": { + "type": "string", + "description": "The ID of the hook. Used to identify the hook in logs and errors. For debugging purposes only." + }, + "response": { + "title": "Response Handling", + "description": "How the web hook should handle the response", + "type": "object", + "additionalProperties": false, + "properties": { + "ignore": { + "type": "boolean", + "description": "Ignore the response from the web hook. If enabled the request will be made asynchronously which can be useful if you only wish to notify another system but do not parse the response.", + "default": false + }, + "parse": { + "type": "boolean", + "default": false, + "description": "If enabled parses the response before saving the flow result. Set this value to true if you would like to modify the identity, for example identity metadata, before saving it during registration. When enabled, you may also abort the registration, verification, login or settings flow due to, for example, a validation flow. Head over to the [web hook documentation](https://www.ory.sh/docs/kratos/hooks/configure-hooks) for more information." + } + }, + "not": { + "properties": { + "ignore": { + "const": true + }, + "parse": { + "const": true + } + }, + "required": ["ignore", "parse"] + } + }, + "url": { + "type": "string", + "description": "The URL the Web-Hook should call", + "format": "uri" + }, + "method": { + "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": [ + { + "format": "uri", + "pattern": "^(http|https|file|base64)://", + "description": "URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads", + "examples": [ + "file:///path/to/body.jsonnet", + "file://./body.jsonnet", + "base64://ZnVuY3Rpb24oY3R4KSB7CiAgaWRlbnRpdHlfaWQ6IGlmIGN0eFsiaWRlbnRpdHkiXSAhPSBudWxsIHRoZW4gY3R4LmlkZW50aXR5LmlkLAp9=", + "https://oryapis.com/default_body.jsonnet" + ] + }, + { + "description": "DEPRECATED: please use a URI instead (i.e. prefix your filepath with 'file://')", + "not": { + "pattern": "^(http|https|file|base64)://" + } + } + ] + }, + "can_interrupt": { + "type": "boolean", + "default": false, + "description": "Deprecated, please use `response.parse` instead. If enabled allows the web hook to interrupt / abort the self-service flow. It only applies to certain flows (registration/verification/login/settings) and requires a valid response format." + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this webhook 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 + }, + "anyOf": [ + { + "not": { + "properties": { + "response": { + "properties": { + "ignore": { + "const": true + } + }, + "required": ["ignore"] + } + }, + "required": ["response"] + } + }, + { + "properties": { + "can_interrupt": { + "const": false + } + }, + "require": ["can_interrupt"] + } + ], + "additionalProperties": false, + "required": ["url", "method"] + } + }, + "additionalProperties": false, + "required": ["hook", "config"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, amazon.", + "type": "string", + "enum": [ + "github", + "github-app", + "gitlab", + "generic", + "google", + "microsoft", + "discord", + "salesforce", + "slack", + "facebook", + "auth0", + "vk", + "yandex", + "apple", + "spotify", + "netid", + "dingtalk", + "patreon", + "line", + "linkedin", + "linkedin_v2", + "lark", + "x", + "fedcm-test", + "amazon", + "uaepass" + ], + "examples": ["google"] + }, + "label": { + "title": "Optional string which will be used when generating labels for UI buttons.", + "type": "string" + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to Ory Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "microsoft_tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "subject_source": { + "title": "Microsoft subject source", + "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier. If the value is `oid` then the the oid (Object ID) is taken to identify users across different services.", + "type": "string", + "enum": ["userinfo", "me", "oid"], + "default": "userinfo", + "examples": ["userinfo"] + }, + "apple_team_id": { + "title": "Apple Developer Team ID", + "description": "Apple Developer Team ID needed for generating a JWT token for client secret", + "type": "string", + "examples": ["KP76DQS54M"] + }, + "apple_private_key_id": { + "title": "Apple Private Key Identifier", + "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", + "type": "string", + "examples": ["UX56C66723"] + }, + "apple_private_key": { + "title": "Apple Private Key", + "description": "Sign In with Apple Private Key needed for generating a JWT token for client secret", + "type": "string", + "examples": [ + "-----BEGIN PRIVATE KEY-----\n........\n-----END PRIVATE KEY-----" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + }, + "organization_id": { + "title": "Organization ID", + "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.", + "type": "string", + "examples": ["12345678-1234-1234-1234-123456789012"] + }, + "additional_id_token_audiences": { + "title": "Additional client ids allowed when using ID token submission", + "type": "array", + "items": { + "type": "string", + "examples": ["12345678-1234-1234-1234-123456789012"] + } + }, + "claims_source": { + "title": "Claims source", + "description": "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`", + "type": "string", + "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" + }, + "fedcm_config_url": { + "title": "Federation Configuration URL", + "description": "The URL where the FedCM IdP configuration is located for the provider. This is only effective in the Ory Network.", + "type": "string", + "format": "uri", + "examples": ["https://example.com/config.json"] + }, + "net_id_token_origin_header": { + "title": "NetID Token Origin Header", + "description": "Contains the orgin header to be used when exchanging a NetID FedCM token for an ID token", + "type": "string", + "examples": ["https://example.com"] + }, + "account_linking_mode": { + "title": "Account linking mode", + "description": "Controls how account conflicts are resolved for this provider. `confirm_with_existing_credential` (default) requires the user to verify their identity with an existing credential. `automatic` silently links accounts if the provider verifies email ownership. This is only effective in the Ory Network.", + "type": "string", + "enum": ["confirm_with_existing_credential", "automatic"], + "default": "confirm_with_existing_credential" + } + }, + "additionalProperties": false, + "required": ["id", "provider", "client_id", "mapper_url"], + "allOf": [ + { + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["microsoft_tenant"] + }, + "else": { + "not": { + "properties": { + "microsoft_tenant": {} + }, + "required": ["microsoft_tenant"] + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "apple" + } + }, + "required": ["provider"] + }, + "then": { + "not": { + "properties": { + "client_secret": { + "type": "string", + "minLength": 1 + } + }, + "required": ["client_secret"] + }, + "required": [ + "apple_private_key_id", + "apple_private_key", + "apple_team_id" + ] + }, + "else": { + "required": ["client_secret"], + "allOf": [ + { + "not": { + "properties": { + "apple_team_id": { + "type": "string", + "minLength": 1 + } + }, + "required": ["apple_team_id"] + } + }, + { + "not": { + "properties": { + "apple_private_key_id": { + "type": "string", + "minLength": 1 + } + }, + "required": ["apple_private_key_id"] + } + }, + { + "not": { + "properties": { + "apple_private_key": { + "type": "string", + "minLength": 1 + } + }, + "required": ["apple_private_key"] + } + } + ] + } + } + ] + }, + "selfServiceHooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/b2bSSOHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + }, + "selfServiceAfterRecoveryHooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + }, + "selfServiceAfterSettingsProfileMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, + { + "$ref": "#/definitions/b2bSSOHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettingsAuthMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterDefaultLoginMethodHooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + }, + { + "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" + }, + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, + { + "$ref": "#/definitions/b2bSSOHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + }, + "selfServiceAfterDefaultLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethodHooks" + } + } + }, + "selfServiceAfterOIDCLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + }, + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" + }, + { + "$ref": "#/definitions/b2bSSOHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + }, + { + "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, + { + "$ref": "#/definitions/b2bSSOHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "featureRequiredAal": { + "title": "Required Authenticator Assurance Level", + "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", + "type": "string", + "enum": ["aal1", "highest_available"], + "default": "highest_available" + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "totp": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "webauthn": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "lookup_secret": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsProfileMethod" + }, + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceBeforeLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "webauthn": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" + }, + "code": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "totp": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "lookup_secret": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, + "hooks": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethodHooks" + } + } + }, + "selfServiceBeforeRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceBeforeSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceBeforeRecovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceBeforeVerification": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "webauthn": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "code": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceAfterVerification": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, + "selfServiceAfterRecovery": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "$ref": "#/definitions/selfServiceAfterRecoveryHooks" + } + }, + "additionalProperties": false + }, + "tlsxSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + }, + "tlsx": { + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + } + } + }, + "courierTemplates": { + "additionalProperties": false, + "type": "object", + "properties": { + "invalid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": ["email"] + }, + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + }, + "sms": { + "$ref": "#/definitions/smsCourierTemplate" + } + }, + "required": ["email"] + } + } + }, + "smsCourierTemplate": { + "additionalProperties": false, + "type": "object", + "properties": { + "body": { + "additionalProperties": false, + "type": "object", + "properties": { + "plaintext": { + "type": "string", + "description": "A template send to the SMS provider.", + "format": "uri", + "examples": [ + "file://path/to/body.plaintext.gotmpl", + "https://foo.bar.com/path/to/body.plaintext.gotmpl" + ] + } + } + } + } + }, + "emailCourierTemplate": { + "additionalProperties": false, + "type": "object", + "properties": { + "body": { + "additionalProperties": false, + "type": "object", + "properties": { + "plaintext": { + "type": "string", + "description": "The fallback template for email clients that do not support html.", + "format": "uri", + "examples": [ + "file://path/to/body.plaintext.gotmpl", + "https://foo.bar.com/path/to/body.plaintext.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K" + ] + }, + "html": { + "type": "string", + "description": "The default template used for sending out emails. The template can contain HTML ", + "format": "uri", + "examples": [ + "file://path/to/body.html.gotmpl", + "https://foo.bar.com/path/to/body.html.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4" + ] + } + } + }, + "subject": { + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/subject.gotmpl", + "https://foo.bar.com/path/to/subject.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4" + ] + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "allowed_return_urls": { + "title": "Allowed Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/", + "https://*.my-app.com/" + ] + ] + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "required_aal": { + "$ref": "#/definitions/featureRequiredAal" + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + }, + "before": { + "$ref": "#/definitions/selfServiceBeforeSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable User Registration", + "description": "If set to true will enable [User Registration](https://www.ory.sh/kratos/docs/self-service/flows/user-registration/).", + "default": true + }, + "login_hints": { + "type": "boolean", + "title": "Provide Login Hints on Failed Registration", + "description": "When registration fails because an account with the given credentials or addresses previously signed up, provide login hints about available methods to sign in to the user.", + "default": false + }, + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "before": { + "$ref": "#/definitions/selfServiceBeforeRegistration" + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + }, + "enable_legacy_one_step": { + "type": "boolean", + "title": "Disable two-step registration", + "description": "Deprecated, please use `style` instead.", + "deprecationMessage": "Deprecated, please use `style` instead.", + "default": false + }, + "style": { + "title": "Registration Flow Style", + "description": "The style of the registration flow. If set to `unified` the login flow will be a one-step process. If set to `profile_first` the registration flow will first ask for the profile information first, and then the credentials.", + "type": "string", + "enum": ["unified", "profile_first"], + "default": "profile_first" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "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" + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "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/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "$ref": "#/definitions/selfServiceAfterVerification" + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "before": { + "$ref": "#/definitions/selfServiceBeforeVerification" + }, + "use": { + "title": "Verification Strategy", + "description": "The strategy to use for verification requests", + "type": "string", + "enum": ["link", "code"], + "default": "code" + }, + "notify_unknown_recipients": { + "title": "Notify unknown recipients", + "description": "Whether to notify recipients, if verification was requested for their address.", + "type": "boolean", + "default": false + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRecovery" + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "before": { + "$ref": "#/definitions/selfServiceBeforeRecovery" + }, + "use": { + "title": "Recovery Strategy", + "description": "The strategy to use for recovery requests", + "type": "string", + "enum": ["link", "code"], + "default": "code" + }, + "notify_unknown_recipients": { + "title": "Notify unknown recipients", + "description": "Whether to notify recipients, if recovery was requested for their account.", + "type": "boolean", + "default": false + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Ory Kratos Error UI URL", + "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "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, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": false + }, + "config": { + "type": "object", + "title": "Link Configuration", + "description": "Additional configuration for the link strategy.", + "properties": { + "base_url": { + "title": "Override the base URL which should be used as the base for recovery and verification links.", + "type": "string", + "deprecationMessage": "This option has no effect, because the request URL is now used as the base URL.", + "examples": ["https://my-app.com"] + }, + "lifespan": { + "title": "How long a link is valid for", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + } + } + }, + "code": { + "type": "object", + "additionalProperties": true, + "anyOf": [ + { + "properties": { + "passwordless_enabled": { + "const": true + }, + "mfa_enabled": { + "const": false + } + } + }, + { + "properties": { + "mfa_enabled": { + "const": true + }, + "passwordless_enabled": { + "const": false + } + } + }, + { + "properties": { + "mfa_enabled": { + "const": false + }, + "passwordless_enabled": { + "const": false + } + } + } + ], + "properties": { + "passwordless_enabled": { + "type": "boolean", + "title": "Enables login and registration with the code method.", + "description": "If set to true, code.enabled will be set to true as well.", + "default": false + }, + "mfa_enabled": { + "type": "boolean", + "title": "Enables login flows code method to fulfil MFA requests", + "default": false + }, + "enabled": { + "type": "boolean", + "title": "Enables Code Method", + "default": true + }, + "config": { + "type": "object", + "title": "Code Configuration", + "description": "Additional configuration for the code strategy.", + "properties": { + "lifespan": { + "title": "How long a code is valid for", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "max_submissions": { + "type": "integer", + "title": "Maximum number of times the code can be submitted before a flow is invalidated", + "minimum": 1, + "maximum": 255, + "default": 5 + }, + "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 + } + } + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + }, + "config": { + "type": "object", + "title": "Password Configuration", + "description": "Define how passwords are validated.", + "properties": { + "haveibeenpwned_host": { + "title": "Custom haveibeenpwned host", + "description": "Allows changing the default HIBP host to a self hosted version.", + "type": "string", + "default": "api.pwnedpasswords.com" + }, + "haveibeenpwned_enabled": { + "title": "Enable the HaveIBeenPwned API", + "description": "If set to false the password validation does not utilize the Have I Been Pwnd API.", + "type": "boolean", + "default": true + }, + "max_breaches": { + "title": "Allow Password Breaches", + "description": "Defines how often a password may have been breached before it is rejected.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 0 + }, + "ignore_network_errors": { + "title": "Ignore Lookup Network Errors", + "description": "If set to false the password validation fails when the network or the Have I Been Pwnd API is down.", + "type": "boolean", + "default": true + }, + "min_password_length": { + "title": "Minimum Password Length", + "description": "Defines the minimum length of the password.", + "type": "integer", + "default": 8, + "minimum": 6 + }, + "identifier_similarity_check_enabled": { + "title": "Enable password-identifier similarity check", + "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" + } + ] + }, + "body": { + "type": "string", + "format": "uri", + "pattern": "^(http|https|file|base64)://", + "description": "URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads", + "examples": [ + "file:///path/to/body.jsonnet", + "file://./body.jsonnet", + "base64://ZnVuY3Rpb24oY3R4KSB7CiAgaWRlbnRpdHlfaWQ6IGlmIGN0eFsiaWRlbnRpdHkiXSAhPSBudWxsIHRoZW4gY3R4LmlkZW50aXR5LmlkLAp9=", + "https://oryapis.com/default_body.jsonnet" + ] + }, + "additionalProperties": false + } + } + } + } + }, + "additionalProperties": false + } + } + }, + "totp": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the TOTP method", + "default": false + }, + "config": { + "type": "object", + "title": "TOTP Configuration", + "properties": { + "issuer": { + "title": "TOTP Issuer", + "description": "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.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "lookup_secret": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the lookup secret method", + "default": false + } + } + }, + "webauthn": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the WebAuthn method", + "default": false + }, + "config": { + "type": "object", + "title": "WebAuthn Configuration", + "properties": { + "passwordless": { + "type": "boolean", + "title": "Use For Passwordless Flows", + "description": "If enabled will have the effect that WebAuthn is used for passwordless flows (as a first factor) and not for multi-factor set ups. With this set to true, users will see an option to sign up with WebAuthn on the registration screen." + }, + "rp": { + "title": "Relying Party (RP) Config", + "properties": { + "display_name": { + "type": "string", + "title": "Relying Party Display Name", + "description": "An 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"] + }, + "origin": { + "type": "string", + "title": "Relying Party Origin", + "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", + "format": "uri", + "deprecationMessage": "This field is deprecated. Use `origins` instead.", + "examples": ["https://www.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", + "format": "uri", + "examples": [ + "https://www.ory.sh", + "https://auth.ory.sh" + ] + } + }, + "icon": { + "type": "string", + "title": "Relying Party Icon", + "description": "An icon to help the user identify this RP.", + "format": "uri", + "deprecationMessage": "This field is deprecated and ignored due to security considerations.", + "examples": ["https://www.ory.sh/an-icon.png"] + } + }, + "type": "object", + "oneOf": [ + { + "required": ["id", "display_name"], + "properties": { + "origin": { + "not": {} + }, + "origins": { + "not": {} + } + } + }, + { + "required": ["id", "display_name", "origin"], + "properties": { + "origin": { + "type": "string" + }, + "origins": { + "not": {} + } + } + }, + { + "required": ["id", "display_name", "origins"], + "properties": { + "origin": { + "not": {} + }, + "origins": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "additionalProperties": false + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + }, + "then": { + "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", + "showEnvVarBlockForObject": true, + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "base_redirect_uri": { + "type": "string", + "title": "Base URL for OAuth2 Redirect URIs", + "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", + "format": "uri", + "examples": ["https://auth.myexample.org/"] + }, + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "database": { + "type": "object", + "title": "Database related configuration", + "description": "Miscellaneous settings used in database related tasks (cleanup, etc.)", + "properties": { + "cleanup": { + "type": "object", + "title": "Database cleanup settings", + "description": "Settings that controls how the database cleanup process is configured (delays, batch size, etc.)", + "properties": { + "batch_size": { + "type": "integer", + "title": "Number of records to clean in one iteration", + "description": "Controls how many records should be purged from one table during database cleanup task", + "minimum": 1, + "default": 100 + }, + "sleep": { + "type": "object", + "title": "Delays between various database cleanup phases", + "description": "Configures delays between each step of the cleanup process. It is useful to tune the process so it will be efficient and performant.", + "properties": { + "tables": { + "type": "string", + "title": "Delay between each table cleanups", + "description": "Controls the delay time between cleaning each table in one cleanup iteration", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1m" + } + } + }, + "older_than": { + "type": "string", + "title": "Remove records older than", + "description": "Controls how old records do we want to leave", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "0s" + } + } + } + }, + "additionalProperties": false + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "templates": { + "additionalProperties": false, + "type": "object", + "properties": { + "recovery": { + "$ref": "#/definitions/courierTemplates" + }, + "recovery_code": { + "$ref": "#/definitions/courierTemplates" + }, + "verification": { + "$ref": "#/definitions/courierTemplates" + }, + "verification_code": { + "$ref": "#/definitions/courierTemplates" + }, + "registration_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + }, + "sms": { + "$ref": "#/definitions/smsCourierTemplate" + } + }, + "required": ["email"] + } + } + }, + "login_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + }, + "sms": { + "$ref": "#/definitions/smsCourierTemplate" + } + }, + "required": ["email"] + } + } + } + } + }, + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "message_retries": { + "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", + "type": "integer", + "default": 5, + "examples": [10, 60] + }, + "worker": { + "description": "Configures the dispatch worker.", + "type": "object", + "properties": { + "pull_count": { + "description": "Defines how many messages are pulled from the queue at once.", + "type": "integer", + "default": 10 + }, + "pull_wait": { + "description": "Defines how long the worker waits before pulling messages from the queue again.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1s" + } + } + }, + "delivery_strategy": { + "title": "Delivery Strategy", + "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", + "type": "string", + "enum": ["smtp", "http"], + "default": "smtp" + }, + "http": { + "title": "HTTP Configuration", + "description": "Configures outgoing emails using HTTP.", + "type": "object", + "properties": { + "request_config": { + "$ref": "#/definitions/httpRequestConfig" + } + }, + "additionalProperties": false + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the scheme smtps for implicit TLS sessions or smtp for explicit StartTLS/cleartext sessions. Please note that TLS is always enforced with certificate trust verification by default for security reasons on both schemes. With the smtp scheme you can use the query parameter (`?disable_starttls=true`) to allow cleartext sessions or (`?disable_starttls=false`) to enforce StartTLS (default behaviour). Additionally, use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates (default behaviour) on both implicit and explicit TLS sessions.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false", + "smtp://foo:bar@my-mailserver:1234/?disable_starttls=true (NOT RECOMMENDED: Cleartext smtp for devel and legacy infrastructure only)", + "smtp://foo:bar@my-mailserver:1234/ (Explicit StartTLS with certificate trust verification)", + "smtp://foo:bar@my-mailserver:1234/?skip_ssl_verify=true (NOT RECOMMENDED: Explicit StartTLS without certificate trust verification)", + "smtps://foo:bar@my-mailserver:1234/ (Implicit TLS with certificate trust verification)", + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=true (NOT RECOMMENDED: Implicit TLS without certificate trust verification)", + "smtps://subdomain.my-mailserver:1234/?server_name=my-mailserver (allows TLS to work if the server is hosted on a sudomain that uses a non-wildcard domain certificate)" + ], + "type": "string", + "pattern": "^smtps?://.*" + }, + "client_cert_path": { + "title": "SMTP Client certificate path", + "description": "Path of the client X.509 certificate, in case of certificate based client authentication to the SMTP server.", + "type": "string", + "default": "" + }, + "client_key_path": { + "title": "SMTP Client private key path", + "description": "Path of the client certificate private key, in case of certificate based client authentication to the SMTP server", + "type": "string", + "default": "" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + }, + "from_name": { + "title": "SMTP Sender Name", + "description": "The recipient of an email will see this as the sender name.", + "type": "string", + "examples": ["Bob"] + }, + "headers": { + "title": "SMTP Headers", + "description": "These headers will be passed in the SMTP conversation -- e.g. when using the AWS SES SMTP interface for cross-account sending.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "X-SES-SOURCE-ARN": "arn:aws:ses:us-west-2:123456789012:identity/example.com", + "X-SES-FROM-ARN": "arn:aws:ses:us-west-2:123456789012:identity/example.com", + "X-SES-RETURN-PATH-ARN": "arn:aws:ses:us-west-2:123456789012:identity/example.com" + } + ] + }, + "local_name": { + "title": "SMTP HELO/EHLO name", + "description": "Identifier used in the SMTP HELO/EHLO command. Some SMTP relays require a unique identifier.", + "type": "string", + "default": "localhost" + } + }, + "additionalProperties": false + }, + "channels": { + "type": "array", + "items": { + "title": "Courier channel configuration", + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Channel id", + "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only sms is supported.", + "maxLength": 32, + "enum": ["sms"] + }, + "type": { + "type": "string", + "title": "Channel type", + "description": "The channel type. Currently only http is supported.", + "enum": ["http"] + }, + "request_config": { + "$ref": "#/definitions/httpRequestConfig" + } + }, + "required": ["id", "request_config"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "oauth2_provider": { + "title": "OAuth2 Provider Configuration", + "type": "object", + "properties": { + "url": { + "title": "OAuth 2.0 Provider URL.", + "description": "If set, the login and registration flows will handle the Ory OAuth 2.0 & OpenID `login_challenge` query parameter to serve as an OpenID Connect Provider. This URL should point to Ory Hydra when you are not running on the Ory Network and be left untouched otherwise.", + "type": "string", + "format": "uri", + "examples": [ + "https://some-slug.projects.oryapis.com", + "https://domain-of-ory-hydra:4445" + ] + }, + "headers": { + "title": "HTTP Request Headers", + "description": "These headers will be passed in HTTP request to the OAuth2 Provider.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "Authorization": "Bearer some-token" + } + ] + }, + "override_return_to": { + "title": "Persist OAuth2 request between flows", + "type": "boolean", + "default": false, + "description": "Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." + } + }, + "additionalProperties": false + }, + "preview": { + "title": "Configure Preview Features", + "type": "object", + "properties": { + "default_read_consistency_level": { + "type": "string", + "title": "Default Read Consistency Level", + "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.", + "enum": ["strong", "eventual"], + "default": "strong" + } + } + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "request_log": { + "type": "object", + "properties": { + "disable_for_health": { + "title": "Disable health endpoints request logging", + "description": "Disable request logging for /health/alive and /health/ready endpoints", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "request_log": { + "type": "object", + "properties": { + "disable_for_health": { + "title": "Disable health endpoints request logging", + "description": "Disable request logging for /health/alive and /health/ready endpoints", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "Max-Age", + "X-Session-Token", + "X-XSRF-TOKEN", + "X-CSRF-TOKEN" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "$ref": "#/definitions/baseUrl" + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "tracing": { + "$ref": "https://raw.githubusercontent.com/ory/kratos/c651ecf2e/oryx/otelx/config.schema.json" + }, + "log": { + "title": "Log", + "description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL.", + "type": "string", + "default": "info", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "description": "The log format can either be text or JSON.", + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_id": { + "title": "The default Identity Schema", + "description": "This Identity Schema will be used as the default for self-service flows. Its ID needs to exist in the \"schemas\" list.", + "type": "string", + "default": "default" + }, + "schemas": { + "type": "array", + "title": "All JSON Schemas for Identity Traits", + "description": "Note that identities that used the \"default_schema_url\" field in older kratos versions will be corrupted unless you specify their schema url with the id \"default\" in this list.", + "examples": [ + [ + { + "id": "customer", + "url": "base64://ewogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAicHJvcGVydGllcyI6IHsKICAgICJiYXIiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJiYXIiCiAgXQp9" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "file://path/to/employee.v2.traits.schema.json" + } + ] + ], + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "JSON Schema URL for identity traits schema", + "description": "URL for JSON Schema which describes a identity's traits. Can be a file path, a https URL, or a base64 encoded string.", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json", + "base64://ewogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAicHJvcGVydGllcyI6IHsKICAgICJiYXIiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJiYXIiCiAgXQp9" + ] + }, + "selfservice_selectable": { + "type": "boolean", + "title": "Is the schema enabled in self-service flows", + "description": "If set to true, this schema can be used explicity in self-service flows by setting `identity_schema` query parameter to the schema's ID.", + "default": false + } + }, + "required": ["id", "url"] + } + } + }, + "required": ["schemas"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for signing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Signing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "pagination": { + "type": "array", + "title": "Secrets to encrypt the pagination token", + "description": "To avoid clients reverse-engineering and relying on the implementation details of the pagination token, it is encrypted with these keys", + "items": { + "type": "string", + "minLength": 16 + }, + "minItems": 1, + "examples": [ + [ + "secret used for encryption", + "old secret kept for decryption", + "another old secret kept for decryption" + ] + ] + }, + "cipher": { + "type": "array", + "title": "Secrets to use for encryption by cipher", + "description": "The first secret in the array is used for encryption data while all other keys are used to decrypt older data that were signed with.", + "items": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "minItems": 1 + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "algorithm": { + "title": "Password hashing algorithm", + "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", + "type": "string", + "default": "bcrypt", + "enum": ["argon2", "bcrypt"] + }, + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "string", + "pattern": "^[0-9]+(B|KB|MB|GB|TB|PB|EB)", + "default": "128MB" + }, + "iterations": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1, + "description": "Number of parallel workers, defaults to 2*runtime.NumCPU()." + }, + "salt_length": { + "type": "integer", + "minimum": 16, + "default": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16, + "default": 32 + }, + "expected_duration": { + "description": "The time a hashing operation (~login latency) should take.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "500ms" + }, + "expected_deviation": { + "description": "The standard deviation expected for hashing operations. If this value is exceeded you will be warned in the logs to adjust the parameters.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "500ms" + }, + "dedicated_memory": { + "description": "The memory dedicated for Kratos. As password hashing is very resource intense, Kratos will monitor the memory consumption and warn about high values.", + "type": "string", + "pattern": "^[0-9]+(B|KB|MB|GB|TB|PB|EB)", + "default": "1GB" + } + }, + "additionalProperties": false + }, + "bcrypt": { + "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", + "type": "object", + "additionalProperties": false, + "required": ["cost"], + "properties": { + "cost": { + "type": "integer", + "minimum": 4, + "maximum": 31, + "default": 12 + } + } + } + }, + "additionalProperties": false + }, + "ciphers": { + "title": "Cipher Algorithm Configuration", + "type": "object", + "properties": { + "algorithm": { + "title": "ciphering algorithm", + "description": "One of the values: noop, aes, xchacha20-poly1305", + "type": "string", + "default": "noop", + "enum": ["noop", "aes", "xchacha20-poly1305"] + } + } + }, + "cookies": { + "type": "object", + "title": "HTTP Cookie Configuration", + "description": "Configure the HTTP Cookies. Applies to both CSRF and session cookies.", + "properties": { + "domain": { + "title": "HTTP Cookie Domain", + "description": "Sets the cookie domain for session and CSRF cookies. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "path": { + "title": "HTTP Cookie Path", + "description": "Sets the session and CSRF cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "secure": { + "title": "Session Cookie Secure Flag", + "description": "Sets the session secure flag. If unset, defaults to !dev mode.", + "type": "string" + }, + "same_site": { + "title": "HTTP Cookie Same Site Configuration", + "description": "Sets the session and CSRF cookie SameSite.", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "whoami": { + "title": "WhoAmI / ToSession Settings", + "description": "Control how the `/sessions/whoami` endpoint is behaving.", + "type": "object", + "properties": { + "required_aal": { + "$ref": "#/definitions/featureRequiredAal" + }, + "tokenizer": { + "title": "Tokenizer configuration", + "description": "Configure the tokenizer, responsible for converting a session into a token format such as JWT.", + "type": "object", + "properties": { + "templates": { + "title": "Tokenizer templates", + "description": "A list of different templates that govern how a session is converted to a token format.", + "type": "object", + "patternProperties": { + "[a-zA-Z0-9-_.]+": { + "type": "object", + "required": ["jwks_url"], + "properties": { + "ttl": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1m", + "title": "Token time to live" + }, + "claims_mapper_url": { + "type": "string", + "format": "uri", + "title": "Jsonnet mapper URL" + }, + "jwks_url": { + "type": "string", + "format": "uri", + "title": "JSON Web Key Set URL" + }, + "subject_source": { + "type": "string", + "title": "Subject source", + "description": "The source of the subject claim in the token. Can be one of: `id`, or `external_id`.", + "enum": ["id", "external_id"], + "default": "id" + } + } + } + } + } + } + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care! Overrides `cookies.domain`.", + "type": "string" + }, + "name": { + "title": "Session Cookie Name", + "description": "Sets the session cookie name. Use with care!", + "type": "string", + "default": "ory_kratos_session" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4). This option affects the Ory OAuth2 and OpenID Provider's remember feature as well.", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care! Overrides `cookies.path`.", + "type": "string" + }, + "secure": { + "title": "Session Cookie Secure Flag", + "description": "Sets the session secure flag. If unset, defaults to !dev mode.", + "type": "string" + }, + "same_site": { + "title": "Session Cookie SameSite Configuration", + "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", + "type": "string", + "enum": ["Strict", "Lax", "None"] + } + }, + "additionalProperties": false + }, + "earliest_possible_extend": { + "title": "Earliest Possible Session Extension", + "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "examples": ["1h", "1m", "1s"] + } + } + }, + "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.", + "type": "string", + "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", + "examples": ["v0.5.0-alpha.1"] + }, + "dev": { + "type": "boolean" + }, + "help": { + "type": "boolean" + }, + "sqa-opt-out": { + "type": "boolean", + "default": false, + "description": "This is a CLI flag and environment variable and can not be set using the config file." + }, + "watch-courier": { + "type": "boolean", + "default": false, + "description": "This is a CLI flag and environment variable and can not be set using the config file." + }, + "expose-metrics-port": { + "title": "Metrics port", + "description": "The port the courier's metrics endpoint listens on (0/disabled by default). This is a CLI flag and environment variable and can not be set using the config file.", + "type": "integer", + "minimum": 0, + "maximum": 65535, + "examples": [4434], + "default": 0 + }, + "config": { + "type": "array", + "items": { + "type": "string" + }, + "description": "This is a CLI flag and environment variable and can not be set using the config file." + }, + "clients": { + "title": "Global outgoing network settings", + "description": "Configure how outgoing network calls behave.", + "type": "object", + "properties": { + "http": { + "title": "Global HTTP client configuration", + "description": "Configure how outgoing HTTP calls behave.", + "type": "object", + "properties": { + "disallow_private_ip_ranges": { + "title": "Disallow private IP ranges", + "description": "Disallow all outgoing HTTP calls to private IP ranges. This feature can help protect against SSRF attacks.", + "type": "boolean", + "default": false + }, + "private_ip_exception_urls": { + "title": "Add exempt URLs to private IP ranges", + "description": "Allows the given URLs to be called despite them being in the private IP range. URLs need to have an exact and case-sensitive match to be excempt.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "default": [] + } + } + }, + "web_hook": { + "title": "Global web_hook HTTP client configuration", + "description": "Configure the global HTTP client of the web_hook action.", + "type": "object", + "properties": { + "header_allowlist": { + "title": "Allowed request headers", + "description": "List of request headers that are forwarded to the web hook target in canonical form.", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "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" + ] + } + } + } + } + }, + "feature_flags": { + "title": "Feature flags", + "properties": { + "cacheable_sessions": { + "type": "boolean", + "title": "Enable Ory Sessions caching", + "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 + }, + "choose_recovery_address": { + "type": "boolean", + "title": "Enable new recovery screens to pick which address to send a recovery code/link to", + "description": "If enabled, enable new recovery screens to pick which address to send a recovery code to, and can send a code via SMS. It is safe to toggle it back and forth, existing recovery flows will be handled with their respective logic. That is because it is decided at creation time whether a recovery flow is V1 or V2 and this cannot be changed afterwards. Thus, if a recovery flow is created with this flag enabled, it will be created as a recovery v2 flow. If this flag is disabled while this flow is still active, this flow will still be handled with the correct logic (v2).", + "default": false + }, + "legacy_continue_with_verification_ui": { + "type": "boolean", + "title": "Always include show_verification_ui in continue_with", + "description": "If true, restores the legacy behavior of always including `show_verification_ui` in the registration flow's `continue_with` when verification is enabled. If set to false, `show_verification_ui` is only set in `continue_with` if the `show_verification_ui` hook is used. This flag will be removed in the future.", + "deprecationMessage": "This behavior is deprecated and will be removed in the future. Use the `show_verification_hook` in the post-registration hook instead.", + "default": false + }, + "legacy_require_verified_login_error": { + "type": "boolean", + "title": "Return a form error if the login identifier is not verified", + "description": "If true, the login flow will return a form error if the login identifier is not verified, which restores legacy behavior. If this value is false, the `continue_with` array will contain a `show_verification_ui` hook instead.", + "deprecationMessage": "This behavior is deprecated and will be removed in the future. Please upgrade your SDKs.", + "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 + }, + "password_profile_registration_node_group": { + "title": "Registration node group", + "description": "The node group to use for registration flows. Previously, the node group for the password method's profile fields was `password`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility.", + "enum": ["password", "default"], + "default": "default" + }, + "legacy_oidc_registration_node_group": { + "title": "Registration node group for OIDC", + "description": "The node group to use for registration flows. Previously, the node group for the oidc method's profile fields was `oidc`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility and will be removed in the future.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "organizations": { + "title": "Organizations", + "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 + }, + "revision": { + "title": "Config revision", + "description": "Set a recognizable revision. This could be the commit time or a random value. This value is exposed at the `/health/config` endpoint and allows you to ensure that the correct config is loaded.", + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + }, + { + "if": { + "properties": { + "ciphers": { + "properties": { + "algorithm": { + "oneOf": [ + { + "const": "aes" + }, + { + "const": "xchacha20-poly1305" + } + ] + } + }, + "required": ["algorithm"] + } + }, + "required": ["ciphers"] + }, + "then": { + "required": ["secrets"], + "properties": { + "secrets": { + "required": ["cipher"] + } + } + } + } + ], + "required": ["identity", "dsn", "selfservice"], + "additionalProperties": false +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2960772 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Launch configuration to build and launch Kratos + // It uses a barebones + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Kratos", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/main.go", + "buildFlags": "", + "preLaunchTask": "Kratos: setup", + "postDebugTask": "close tasks", // stops mailhog. Needed, because VSCode does not re-use existing isBackground tasks + "args": [ + "serve", + "--dev", + "--watch-courier", + "-c=${workspaceFolder}/contrib/quickstart/kratos/email-password/kratos.yml" + ], + "internalConsoleOptions": "openOnSessionStart", + "env": { + "IDENTITY_SCHEMAS_0_URL": "file://${workspaceFolder}/contrib/quickstart/kratos/email-password/identity.schema.json", + "DSN": "sqlite://${workspaceFolder}/.debug.sqlite.db?_fk=true", + "COURIER_SMTP_CONNECTION_URI": "smtp://localhost:8026/?disable_starttls=true", + "DEV_DISABLE_API_FLOW_ENFORCEMENT": "true", + "SELFSERVICE_METHODS_PASSWORD_CONFIG_HAVEIBEENPWNED_ENABLED": "false", // disable locally, as the integration hangs requests, if internet is slow + "TRACING_PROVIDER": "jaeger", + "TRACING_PROVIDERS_JAEGER_SAMPLING_SERVER_URL": "http://127.0.0.1:5778/sampling", + "TRACING_PROVIDERS_JAEGER_LOCAL_AGENT_ADDRESS": "127.0.0.1:6831" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b853f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "gopls": { + "formatting.gofumpt": true, + "formatting.local": "github.com/ory" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..a0278c7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,58 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Kratos: build", + "type": "shell", + "command": "go", + "args": [ + "build", + "-tags", + "sqlite", + "-o", + "${workspaceFolder}/kratos", + "${workspaceFolder}/main.go" + ] + }, + { + "label": "Kratos debug: setup sqlite", + "type": "shell", + "presentation": { + "echo": false, + "reveal": "always" + }, + "dependsOn": ["Kratos: build"], + "command": "rm -f ${workspaceFolder}/.debug.sqlite.db && ${workspaceFolder}/kratos migrate sql up -e --yes", + "options": { + "env": { + "DSN": "sqlite://${workspaceFolder}/.debug.sqlite.db?_fk=true" + } + } + }, + { + "label": "Kratos: setup", + "type": "shell", + "presentation": { + "reveal": "silent" + }, + "dependsOn": ["Kratos debug: setup sqlite"], + "problemMatcher": [] + }, + { + "label": "close tasks", + "command": "echo ${input:terminate}", + "type": "shell", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "terminate", + "type": "command", + "command": "workbench.action.tasks.terminate", + "args": "terminateAll" + } + ] +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..704329d --- /dev/null +++ b/AUTHORS @@ -0,0 +1,198 @@ +# This is the official list of Kratos authors. +# If you don't want to be on this list, please contact Ory. + +Adam Schepis +Addison Snelling +adrian5 +Aeneas Rekkas +aenoralanar <38026322+aenoralanar@users.noreply.github.com> +Aidan Holland +Ajay Kelkar +Alano Terblanche +Alexander Probst +Alexandre Burgoni +Alexandre Gagneux <46828169+alexGNX@users.noreply.github.com> +Alexey Reshetnik +Andreas +Andreas Bucksteeg +Andrew Banchich <13824577+andrewbanchich@users.noreply.github.com> +Andrew Minkin +Andrew Tan +Andrey +Andy Steele +angryPopcorn +Anirudh Oppiliappan +Antoine Beyet +Anuar Ustayev +Arne +Arne +aspeteRakete +Atreya <44151328+atreya2011@users.noreply.github.com> +AVA Monitoring <56961522+avamonitoring@users.noreply.github.com> +Avinash Dwarapu +b3j0f +Bengt Hagemeister <30391176+BengtHagemeister@users.noreply.github.com> +Bengt Hagemeister +Benjamin Freeman +BIKI DAS +Bill Monkman +Bipin Paul Bedi +Brahm Lower +Brahm Lower +BrentChesny +CaptainStandby <18215579+CaptainStandby@users.noreply.github.com> +Carlos Chida +chlasch <72044019+chlasch@users.noreply.github.com> +cwei-wish +Cλctysman +Daniel Hobbs +David ALEXANDRE <9482408+david972@users.noreply.github.com> +David Cheung +David Laban +David Laban +David San +David van der Sluijs +debrutal +Dejan Filipovic +Dejan Filipovic +Denis Palnitsky +Dibyajyoti Behera +Dimitrij Drus +Dimitrij Drus +Eifoen <35534229+Eifoen@users.noreply.github.com> +emrah +emtammaru +Eric B +ernax78 <65963237+ernax78@users.noreply.github.com> +Erol Keskin +Erol Keskin +Fabian Meyer <3982806+meyfa@users.noreply.github.com> +Felix Beuke +Ferdi +Florian Kramer +Florian Nagel +Florian Wiech +frederikhors <41120635+frederikhors@users.noreply.github.com> +Gajewski Dmitriy +Geoff Lawler +Giacomo Mazzamuto +Gibheer +goughjo02 +Grant Zvolský +Harsimran Singh Maan +Henning Perl +Jacob Lehr +Jakob Høgenes <1014990+jakhog@users.noreply.github.com> +Jakob Sinclair +Jakub Błaszczyk +James D. Nurmi +James D. Nurmi +Jason Ertel +JeffreyThijs +Jelle Besseling +Jhonatan Hulse +JiggyDown <84430646+JiggyDown@users.noreply.github.com> +jingkai +jld3103 +Joe Krill +Johan Forssell +John Binstead <67282336+Bin-fluence@users.noreply.github.com> +John +Jonas Hungershausen +Jonathon Sheffield +Jordan +Jordan May +Juri <1773075+2mol@users.noreply.github.com> +Kevin Goslar +Kim Neunert +Klaus Herrmann <106238709+kmherrmann@users.noreply.github.com> +koenmtb1 +Koen van Marrewijk +kszafran +Kun Chong +Landon Pattison <67596936+LandonPattison@users.noreply.github.com> +Lan Phan +Li Ming +Lorenzo Baldassarri +Łukasz Harasimowicz +Łukasz Szcześniak +Márk Bartos +Mart Aarma +Martin Eigenbrodt +Martin Eigenbrodt +Martin +Mathias Polligkeit <13847569+woylie@users.noreply.github.com> +Matt Bonnell +Matthieu Jacquot +Matúš Múčka +Maurice Freitag +Mike Milano +Mike +Mikołaj Meller <52668809+mmeller-wikia@users.noreply.github.com> +Miłosz <12242002+mszekiel@users.noreply.github.com> +MiniDigger | Martin +Mitar +Mitsuo Heijo <25817501+johejo@users.noreply.github.com> +Nanik +Natanael Oliva +Neil Rutherford <374275+nrutherford@users.noreply.github.com> +Nichlas Lendal <62498670+pr1ze@users.noreply.github.com> +Nicholas Bush +Nick Campbell +Nick Malcev <41836660+nmlc@users.noreply.github.com> +Nick Ufer +Nikita Puzankov +nipsufn <17983323+nipsufn@users.noreply.github.com> +Ole Petersen <56505957+peteole@users.noreply.github.com> +oliverpool <3864879+oliverpool@users.noreply.github.com> +Omar A. Hachach +Patrik Neu +Paweł Hemperek +Piotr Mścichowski +Przemysław Czaus +Quentin Gliech +qvamatel +Radek Gruchalski +RamiBerm <54766858+RamiBerm@users.noreply.github.com> +Rauno Viskus +reshetnik-alexey +Rodrigo Queiro +rvo +sawadashota +sawadashota +Seb +seremenko-wish <60801091+seremenko-wish@users.noreply.github.com> +Sergey Plaunov +sherbang +shivam <86538330+shivam-909@users.noreply.github.com> +Simon Lipp +Simon-Pierre Gingras <892367+spg@users.noreply.github.com> +Steffen Heidel +TannerGabriel <40315960+TannerGabriel@users.noreply.github.com> +Theodor Brandt +theotherian +Thomas Aidan Curran <2030403+tacurran@users.noreply.github.com> +Thomas Guillet +Thomas Ruiz +Tomasz Tomalak <12939493+t-tomalak@users.noreply.github.com> +Tom Fenech +Toon van Strijp +Tsirkin Evgeny +ttimonen +Tyler Battle +VeenaInd <104088519+VeenaInd@users.noreply.github.com> +Victor Duarte +Vincent +vinckr +Viz +WeiQi <17816875364@163.com> +wezzle +Yagiz Nizipli +yon <38630464+yonbh@users.noreply.github.com> +Yorick Holkamp +you1996 <45292366+you1996@users.noreply.github.com> +Yuvraj <10830562+evalsocket@users.noreply.github.com> +Zageron +Zhiming Guo +znerol +好风 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fc7d9bc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,145 @@ + + + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Open Source Community Support + +Ory Open source software is collaborative and based on contributions by +developers in the Ory community. There is no obligation from Ory to help with +individual problems. If Ory open source software is used in production in a +for-profit company or enterprise environment, we mandate a paid support contract +where Ory is obligated under their service level agreements (SLAs) to offer a +defined level of availability and responsibility. For more information about +paid support please contact us at sales@ory.com. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[office@ory.com](mailto:office@ory.com). All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6302de7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,293 @@ + + + +# Contribute to Ory Kratos + + + + +- [Introduction](#introduction) +- [FAQ](#faq) +- [How can I contribute?](#how-can-i-contribute) +- [Communication](#communication) +- [Contribute examples or community projects](#contribute-examples-or-community-projects) +- [Contribute code](#contribute-code) +- [Contribute documentation](#contribute-documentation) +- [Disclosing vulnerabilities](#disclosing-vulnerabilities) +- [Code style](#code-style) + - [Working with forks](#working-with-forks) +- [Conduct](#conduct) + + + +## Introduction + +_Please note_: We take Ory Kratos's security and our users' trust very +seriously. If you believe you have found a security issue in Ory Kratos, please +disclose it by contacting us at security@ory.com. + +There are many ways in which you can contribute. The goal of this document is to +provide a high-level overview of how you can get involved in Ory. + +As a potential contributor, your changes and ideas are welcome at any hour of +the day or night, on weekdays, weekends, and holidays. Please do not ever +hesitate to ask a question or send a pull request. + +If you are unsure, just ask or submit the issue or pull request anyways. You +won't be yelled at for giving it your best effort. The worst that can happen is +that you'll be politely asked to change something. We appreciate any sort of +contributions and don't want a wall of rules to get in the way of that. + +That said, if you want to ensure that a pull request is likely to be merged, +talk to us! You can find out our thoughts and ensure that your contribution +won't clash with Ory Kratos's direction. A great way to do this is via +[Ory Kratos Discussions](https://github.com/ory/kratos/discussions) or the +[Ory Chat](https://www.ory.com/chat). + +## FAQ + +- I am new to the community. Where can I find the + [Ory Community Code of Conduct?](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md) + +- I have a question. Where can I get + [answers to questions regarding Ory Kratos?](#communication) + +- I would like to contribute but I am not sure how. Are there + [easy ways to contribute?](#how-can-i-contribute) + [Or good first issues?](https://github.com/search?l=&o=desc&q=label%3A%22help+wanted%22+label%3A%22good+first+issue%22+is%3Aopen+user%3Aory+user%3Aory-corp&s=updated&type=Issues) + +- I want to talk to other Ory Kratos users. + [How can I become a part of the community?](#communication) + +- I would like to know what I am agreeing to when I contribute to Ory Kratos. + Does Ory have + [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://www.ory.com/l/sign-up-newsletter) + +## How can I contribute? + +If you want to start to contribute code right away, take a look at the +[list of good first issues](https://github.com/ory/kratos/labels/good%20first%20issue). + +There are many other ways you can contribute. Here are a few things you can do +to help out: + +- **Give us a star.** It may not seem like much, but it really makes a + difference. This is something that everyone can do to help out Ory Kratos. + Github stars help the project gain visibility and stand out. + +- **Join the community.** Sometimes helping people can be as easy as listening + to their problems and offering a different perspective. Join our Slack, have a + look at discussions in the forum and take part in community events. More info + on this in [Communication](#communication). + +- **Answer discussions.** At all times, there are several unanswered discussions + on GitHub. You can see an + [overview here](https://github.com/discussions?discussions_q=is%3Aunanswered+org%3Aory+sort%3Aupdated-desc). + If you think you know an answer or can provide some information that might + help, please share it! Bonus: You get GitHub achievements for answered + discussions. + +- **Help with open issues.** We have a lot of open issues for Ory Kratos and + some of them may lack necessary information, some are duplicates of older + issues. You can help out by guiding people through the process of filling out + the issue template, asking for clarifying information or pointing them to + existing issues that match their description of the problem. + +- **Review documentation changes.** Most documentation just needs a review for + proper spelling and grammar. If you think a document can be improved in any + way, feel free to hit the `edit` button at the top of the page. More info on + contributing to the documentation [here](#contribute-documentation). + +- **Help with tests.** Pull requests may lack proper tests or test plans. These + are needed for the change to be implemented safely. + +## Communication + +We use [Slack](https://www.ory.com/chat). You are welcome to drop in and ask +questions, discuss bugs and feature requests, talk to other users of Ory, etc. + +Check out [Ory Kratos Discussions](https://github.com/ory/kratos/discussions). +This is a great place for in-depth discussions and lots of code examples, logs +and similar data. + +You can also join our community calls if you want to speak to the Ory team +directly or ask some questions. You can find more info and participate in +[Slack](https://www.ory.com/chat) in the #community-call channel. + +If you want to receive regular notifications about updates to Ory Kratos, +consider joining the mailing list. We will _only_ send you vital information on +the projects that you are interested in. + +Also, [follow us on Twitter](https://twitter.com/orycorp). + +## Contribute examples or community projects + +One of the most impactful ways to contribute is by adding code examples or other +Ory-related code. You can find an overview of community code in the +[awesome-ory](https://github.com/ory/awesome-ory) repository. + +_If you would like to contribute a new example, we would love to hear from you!_ + +Please [open a pull request at awesome-ory](https://github.com/ory/awesome-ory/) +to add your example or Ory-related project to the awesome-ory README. + +## Contribute code + +**All code contributions require prior discussion and agreement with maintainers +before opening a pull request.** + +This applies to bug fixes, new features, and refactoring. Before writing code: + +1. Open a [Discussion](https://github.com/ory/kratos/discussions/new/choose) or + [Issue](https://github.com/ory/kratos/issues/new/choose) describing the + problem and your proposed solution. +2. Wait for maintainer feedback and explicit agreement on the implementation + approach. +3. Only then begin writing code. + +Pull requests without prior discussion may be closed without review. This policy +exists to protect your time and maintainer time. + +"Drive-by" PRs, even well-intentioned ones, often conflict with the project +roadmap, duplicate ongoing work, or introduce architectural inconsistencies. A +quick conversation can help avoid these problems. + +All contributions are made via pull requests. To make a pull request, you will +need a GitHub account; if you are unclear on this process, see GitHub's +documentation on [forking](https://help.github.com/articles/fork-a-repo) and +[pull requests](https://help.github.com/articles/using-pull-requests). Pull +requests should be targeted at the `master` branch. Before creating a pull +request, go through this checklist: + +1. Create a feature branch off of `master` so that changes do not get mixed up. +1. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local + changes against the `master` branch. +1. Run the full project test suite with the `go test ./...` (or + equivalent) command and confirm that it passes. +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 + [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). + +Before your contributions can be reviewed you need to sign our +[Contributor License Agreement](https://cla-assistant.io/ory/kratos). + +This agreement defines the terms under which your code is contributed to Ory. +More specifically it declares that you have the right to, and actually do, grant +us the rights to use your contribution. You can see the Apache 2.0 license under +which our projects are published +[here](https://github.com/ory/meta/blob/master/LICENSE). + +When pull requests fail the automated testing stages (for example unit or E2E +tests), authors are expected to update their pull requests to address the +failures until the tests pass. + +Pull requests eligible for review + +1. follow the repository's code formatting conventions; +2. include tests that prove that the change works as intended and does not add + regressions; +3. document the changes in the code and/or the project's documentation; +4. pass the CI pipeline; +5. have signed our + [Contributor License Agreement](https://cla-assistant.io/ory/kratos); +6. include a proper git commit message following the + [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/). + +If all of these items are checked, the pull request is ready to be reviewed and +you should change the status to "Ready for review" and +[request review from a maintainer](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review). + +Reviewers will approve the pull request once they are satisfied with the patch. + +### AI-assisted contributions + +AI can be a valuable aid for writing code, documentation, and tests. However, to +maintain the quality and reliability of Ory Kratos, please follow these +guidelines: + +- When submitting a pull request or issue that involved AI assistance, mention + the tools you used and the extent of their involvement. This helps reviewers + understand the context of your contribution. + +- Pull requests created with AI assistance should address an existing, accepted + issue. If you have an idea for a new feature or improvement, please open a + discussion or issue first to ensure alignment with the project's direction + before investing time in implementation. + +- You must use AI responsibly when writing code. All contributions must be + tested and verified before submission. Unreviewed AI-generated code will not + be accepted, and repeated submissions of this nature may result in restricted + contribution privileges. + +- When using AI to help draft issues, discussions, or documentation, review and + edit the output before submitting. AI tends to be verbose. Trim unnecessary + content and ensure your submission is clear and focused. + +Contributors must use AI responsibly. These guidelines exist to ensure that +every contribution meets the high standards our community expects, while still +embracing the productivity benefits that AI tools can provide. + +## Contribute documentation + +Please provide documentation when changing, removing, or adding features. All +Ory Documentation resides in the +[Ory documentation repository](https://github.com/ory/docs/). For further +instructions please head over to the Ory Documentation +[README.md](https://github.com/ory/docs/blob/master/README.md). + +## Disclosing vulnerabilities + +Please disclose vulnerabilities exclusively to +[security@ory.com](mailto:security@ory.com). Do not use GitHub issues. + +## Code style + +Please run `make format` to format all source code following the Ory standard. + +### Working with forks + +```bash +# First you clone the original repository +git clone git@github.com:ory/ory/kratos.git + +# Next you add a git remote that is your fork: +git remote add fork git@github.com:/ory/kratos.git + +# Next you fetch the latest changes from origin for master: +git fetch origin +git checkout master +git pull --rebase + +# Next you create a new feature branch off of master: +git checkout my-feature-branch + +# Now you do your work and commit your changes: +git add -A +git commit -a -m "fix: this is the subject line" -m "This is the body line. Closes #123" + +# And the last step is pushing this to your fork +git push -u fork my-feature-branch +``` + +Now go to the project's GitHub Pull Request page and click "New pull request" + +## Conduct + +Whether you are a regular contributor or a newcomer, we care about making this +community a safe place for you and we've got your back. + +[Ory Community Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md) + +We welcome discussion about creating a welcoming, safe, and productive +environment for the community. If you have any questions, feedback, or concerns +[please let us know](https://www.ory.com/chat). diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..1cc859c --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,215 @@ +# Development + +This document explains how to develop Ory Kratos, run tests, and work with the tooling around it. + +## Upgrading and changelog + +Check [releases tab](https://github.com/ory/kratos/releases) for updates and changelogs when using the open source license. + +## Command line documentation + +To see available commands and flags, run: + +```bash +kratos -h +# or +kratos help +``` + +## Contribution guidelines + +We encourage all contributions. Before opening a pull request, read the +[contribution guidelines](./CONTRIBUTING.md). + +## Prerequisites + +You need Go 1.16+ and, for the test suites: + +* Docker and Docker Compose +* `make` +* Node.js and npm + +You can develop Ory Kratos on Windows, but most guides assume a Unix shell such as `bash` or `zsh`. + +## Install from source + +To install Kratos from source: + +```make +make install +``` + +## Formatting code + +Format all code using: + +```make +make format +``` + +The continuous integration pipeline checks code formatting. + +## Running tests + +There are three types of tests: + +* Short tests that do not require a SQL database +* Regular tests that require PostgreSQL, MySQL, and CockroachDB +* End to end tests that use real databases and a test browser + +### Short tests + +Short tests run quickly and use SQLite. + +Run all short tests: + +```bash +go test -short ./... +``` + +Run short tests in a specific module: + +```bash +cd client +go test -short . +``` + +### Regular tests + +Regular tests require a database setup. + +The test suite can start databases using +[ory/dockertest](https://github.com/ory/dockertest). In practice, it is usually +easier and faster to use the Makefile targets. + +Run the full test suite: + +```make +make test +``` + +> Note: `make test` recreates the databases every time. This can be slow if you +> are iterating frequently on a specific test. + +If you want to reuse databases across test runs, initialize them once: + +```bash +make test-resetdb +export TEST_DATABASE_MYSQL='mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true' +export TEST_DATABASE_POSTGRESQL='postgres://postgres:secret@127.0.0.1:3445/kratos?sslmode=disable' +export TEST_DATABASE_COCKROACHDB='cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable' +``` + +Then you can run Go tests directly as often as needed: + +```bash +go test ./... + +# or in a module: +cd client +go test . +``` + +### Updating test fixtures + +Some tests use snapshot fixtures. + +Update snapshots for short tests: + +```bash +make test-update-snapshots +``` + +Update all snapshots: + +```bash +UPDATE_SNAPSHOTS=true go test -p 4 ./... +``` + +You can run this from the repository root or from subdirectories. + +### End-to-end tests + +End to end tests are implemented with [Cypress](https://www.cypress.io). + +> On ARM based Macs you may need to install Rosetta 2 to run Cypress. +> See the Cypress documentation: +> [https://www.cypress.io/blog/2021/01/20/running-cypress-on-the-apple-m1-silicon-arm-architecture-using-rosetta-2/](https://www.cypress.io/blog/2021/01/20/running-cypress-on-the-apple-m1-silicon-arm-architecture-using-rosetta-2/) + +To install Rosetta 2: + +```bash +softwareupdate --install-rosetta --agree-to-license +``` + +Run e2e tests in development mode: + +```bash +./test/e2e/run.sh --dev sqlite +``` + +Run all e2e tests with databases: + +```make +make test-e2e +``` + +For more options: + +```bash +./test/e2e/run.sh +``` + +#### Run a single test + +Add `.only` to the test you want to run, for example: + +```ts +it.only('invalid remote recovery email template', () => { + // ... +}) +``` + +#### Run a subset of tests + +To run a subset of e2e tests: + +1. Edit `cypress.json` in `test/e2e/`. + +2. Add the `testFiles` option and point it to the specs you want, for example: + + ```json + "testFiles": ["profiles/network/*"] + ``` + +3. Start the tests again using the run script or Makefile. + +## Build Docker image + +To build a development Docker image: + +```make +make docker +``` + +## Preview API documentation + +To work on and preview the generated API documentation: + +1. Update the SDK including the OpenAPI specification: + + ```make + make sdk + ``` + +2. Run the preview server for API documentation: + + ```make + make docs/api + ``` + +3. Run the preview server for Swagger documentation: + + ```make + make docs/swagger + ``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a33b129 --- /dev/null +++ b/Makefile @@ -0,0 +1,214 @@ +SHELL=/usr/bin/env bash -o pipefail + +# EXECUTABLES = docker-compose docker node npm go +# K := $(foreach exec,$(EXECUTABLES),\ +# $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH"))) + +export PATH := .bin:${PATH} +export PWD := $(shell pwd) +export BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +export VCS_REF := $(shell git rev-parse HEAD) +export QUICKSTART_OPTIONS ?= +export IMAGE_TAG := $(if $(IMAGE_TAG),$(IMAGE_TAG),latest) + +.bin/clidoc: + echo "deprecated usage, use docs/cli instead" + go build -o .bin/clidoc ./cmd/clidoc/. + +.PHONY: docs/cli +docs/cli: + go run ./cmd/clidoc/. . + +.PHONY: docs/api +docs/api: + npx @redocly/openapi-cli preview-docs spec/api.json + +.PHONY: docs/swagger +docs/swagger: + npx @redocly/openapi-cli preview-docs spec/swagger.json + +.bin/golangci-lint: Makefile + curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v2.10.1 + +.bin/hydra: Makefile + bash <(curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0 + +.bin/ory: Makefile + curl --retry 7 --retry-connrefused 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 .bin/buf + .bin/golangci-lint run -v --timeout 10m ./... + .bin/buf lint + +.PHONY: mocks +mocks: + go tool mockgen -mock_names Manager=MockLoginExecutorDependencies -package pkg -destination pkg/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/buf generate + go tool goimports -w gen/ + +.PHONY: install +install: + go install . + +.PHONY: test-resetdb +test-resetdb: + script/testenv.sh + +.PHONY: test +test: + docker pull oryd/hydra:v2.2.0 + go test -p 1 -count=1 -failfast $(if $(TAGS),-tags $(TAGS)) ./... + +test-short: + go test -count=1 -failfast -short $(if $(TAGS),-tags $(TAGS)) ./... + +.PHONY: test-coverage +test-coverage: + go test -coverprofile=coverage.out -failfast -timeout=20m ./... + +.PHONY: test-coverage-next +test-coverage-next: + go test -short -failfast -timeout=20m -cover ./... --args test.gocoverdir="$$PWD/coverage" + go tool covdata percent -i=coverage + go tool covdata textfmt -i=./coverage -o coverage.new.out + +# Generates the SDK +.PHONY: sdk +sdk: .bin/ory node_modules + ../../.bin/swagger generate spec -m -o spec/swagger.json \ + -c github.com/ory/kratos \ + -c github.com/ory/x/healthx \ + -c github.com/ory/x/crdbx \ + -c github.com/ory/x/openapix \ + -c github.com/ory/x/region + ory dev swagger sanitize ./spec/swagger.json + ../../.bin/swagger validate ./spec/swagger.json + CIRCLE_PROJECT_USERNAME=ory CIRCLE_PROJECT_REPONAME=kratos \ + ory dev openapi migrate \ + --health-path-tags metadata \ + -p https://raw.githubusercontent.com/ory/x/master/healthx/openapi/patch.yaml \ + -p file://.schema/openapi/patches/meta.yaml \ + -p file://.schema/openapi/patches/schema.yaml \ + -p file://.schema/openapi/patches/selfservice.yaml \ + -p file://.schema/openapi/patches/security.yaml \ + -p file://.schema/openapi/patches/session.yaml \ + -p file://.schema/openapi/patches/identity.yaml \ + -p file://.schema/openapi/patches/courier.yaml \ + -p file://.schema/openapi/patches/generic_error.yaml \ + -p file://.schema/openapi/patches/nulls.yaml \ + -p file://.schema/openapi/patches/common.yaml \ + spec/swagger.json spec/api.json + + rm -rf pkg/httpclient + mkdir -p pkg/httpclient/ + npm run openapi-generator-cli -- generate -i "spec/api.json" \ + -g go \ + -o "pkg/httpclient" \ + --git-user-id ory \ + --git-repo-id client-go \ + --git-host github.com \ + --api-name-suffix "API" \ + -c .schema/openapi/gen.go.yml + + (cd pkg/httpclient; rm -rf go.mod go.sum test api docs) + + rm -rf pkg/client-go + mkdir -p pkg/client-go/ + npm run openapi-generator-cli -- generate -i "spec/api.json" \ + -g go \ + -o "pkg/client-go" \ + --git-user-id ory \ + --git-repo-id client-go \ + --git-host github.com \ + --api-name-suffix "API" \ + -c .schema/openapi/gen.go.yml + + (cd pkg/client-go; go mod edit -module github.com/ory/client-go go.mod; rm -rf test api docs; go mod tidy) + + make format + +.PHONY: quickstart +quickstart: + docker pull oryd/kratos:latest + docker pull oryd/kratos-selfservice-ui-node:latest + docker-compose -f quickstart.yml -f quickstart-standalone.yml up --build --force-recreate + +.PHONY: quickstart-dev +quickstart-dev: + docker build -f .docker/Dockerfile-build -t oryd/kratos:latest . + docker-compose -f quickstart.yml -f quickstart-standalone.yml -f quickstart-latest.yml $(QUICKSTART_OPTIONS) up --build --force-recreate + +authors: # updates the AUTHORS file + curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/ci/master/authors/authors.sh | env PRODUCT="Ory Kratos" bash + +PRETTIER_VERSION=$(shell cat package.json | jq -r '.devDependencies["prettier"] // .dependencies["prettier"]') + +# Formats the code +.PHONY: format +format: .bin/ory .bin/buf + .bin/ory dev headers copyright --exclude=gen --exclude=pkg/httpclient --exclude=pkg/client-go --exclude test/e2e/proxy/node_modules --exclude test/e2e/node_modules --exclude node_modules --exclude=oryx + go tool goimports -w -local github.com/ory . + @echo "Prettier Version: $(PRETTIER_VERSION)" + npx --package=prettier@$(PRETTIER_VERSION) prettier 'test/e2e/**/*{.ts,.js}' --write + npx --package=prettier@$(PRETTIER_VERSION) prettier '.github' --write + .bin/buf format --write + +# Build local docker image +.PHONY: docker +docker: + DOCKER_BUILDKIT=1 DOCKER_CONTENT_TRUST=1 docker build -f .docker/Dockerfile-build --build-arg=COMMIT=$(VCS_REF) --build-arg=BUILD_DATE=$(BUILD_DATE) -t oryd/kratos:${IMAGE_TAG} . + +.PHONY: test-e2e +test-e2e: node_modules test-resetdb kratos-config-e2e + source script/test-envs.sh + test/e2e/run.sh sqlite + test/e2e/run.sh postgres + test/e2e/run.sh cockroach + test/e2e/run.sh mysql + +.PHONY: test-e2e-playwright +test-e2e-playwright: node_modules test-resetdb kratos-config-e2e + source script/test-envs.sh + test/e2e/run.sh --only-setup + (cd test/e2e; DB=memory npm run playwright) + +.PHONY: test-refresh +test-refresh: + UPDATE_SNAPSHOTS=true go test -tags refresh -short ./... + +.PHONY: pre-release +pre-release: + go tool yq '.services.kratos.image = "oryd/kratos:'$$DOCKER_TAG'"' -i quickstart.yml + go tool yq '.services.kratos-migrate.image = "oryd/kratos:'$$DOCKER_TAG'"' -i quickstart.yml + go tool yq '.services.kratos-selfservice-ui-node.image = "oryd/kratos-selfservice-ui-node:'$$DOCKER_TAG'"' -i quickstart.yml + +.PHONY: post-release +post-release: + echo "nothing to do" + +licenses: .bin/licenses node_modules # checks open-source licenses + .bin/licenses + +.bin/licenses: Makefile + curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/ci/master/licenses/install | sh + +node_modules: package-lock.json + npm ci + touch node_modules + +.PHONY: kratos-config-e2e +kratos-config-e2e: + sh ./test/e2e/render-kratos-config.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..837e3c6 --- /dev/null +++ b/README.md @@ -0,0 +1,517 @@ +

+ Ory Kratos - Cloud native identity and user management +

+ +

+ Chat · + Discussions · + Newsletter · + Docs · + Try Ory Network · + Jobs +

+ +Ory Kratos is an API first identity and user management system for cloud native +applications. It centralizes login, registration, recovery, verification, and +profile management flows so your services consume them instead of reimplementing +them. + + + + +**Table of contents** + +- [What is Ory Kratos?](#what-is-ory-kratos) + - [Why Ory Kratos](#why-ory-kratos) +- [Migrating from Auth0, Okta, and similar providers](#migrating-from-auth0-okta-and-similar-providers) +- [Deployment options](#deployment-options) + - [Use Ory Kratos on the Ory Network](#use-ory-kratos-on-the-ory-network) + - [Self-host Ory Kratos](#self-host-ory-kratos) +- [Quickstart](#quickstart) + - [Who is using it?](#who-is-using-it) + + + +## What is Ory Kratos? + +Ory Kratos is an API first identity and user management system that follows +[cloud architecture best practices](https://www.ory.com/docs/ecosystem/software-architecture-philosophy). +It focuses on core identity workflows that almost every application needs: + +- Self service login and registration +- Account verification and recovery +- Multi factor authentication +- Profile and account management +- Identity schemas and traits +- Admin APIs for lifecycle management + +We recommend starting with the +[Ory Kratos introduction docs](https://www.ory.com/kratos/docs/) to learn more +about its architecture, feature set, and how it compares to other systems. + +### Why Ory Kratos + +Ory Kratos is designed to: + +- Remove identity logic from your application code and expose it over HTTP APIs +- Work well with any UI framework through browser based and native app flows +- Scale to large numbers of identities and devices +- Integrate with the rest of the Ory stack for OAuth2, OpenID Connect, and + access control +- Fit into modern cloud native environments such as Kubernetes and managed + platforms + +## Migrating from Auth0, Okta, and similar providers + +If you are migrating from Auth0, Okta, or another identity provider that uses +OAuth2 / OpenID Connect based login, consider using **Ory Hydra + Ory Kratos** +together: + +- **Ory Hydra** acts as the OAuth2 and OpenID Connect provider and can replace + most authorization server and token issuing capabilities of your existing IdP. +- **Ory Kratos** provides identity, credentials, and user-facing flows (login, + registration, recovery, verification, profile management). + +This combination is often a drop-in replacement for OAuth2 and OpenID Connect +capabilities at the protocol level. In practice, you update client configuration +and endpoints to point to Hydra, migrate identities into Kratos, and keep your +applications speaking the same OAuth2 / OIDC protocols they already use. + +## Deployment options + +You can run Ory Kratos in two main ways: + +- As a managed service on the Ory Network +- As a self hosted service under your own control, with or without the Ory + Enterprise License + +### Use Ory Kratos on the Ory Network + +The [Ory Network](https://www.ory.com/cloud) is the fastest way to use Ory +services in production. **Ory Identities** is powered by the open source Ory +Kratos server and is API compatible. + +The Ory Network provides: + +- Identity and credential management that scales to billions of users and + devices +- Registration, login, and account management flows for passkeys, biometrics, + social login, SSO, and multi factor authentication +- Prebuilt login, registration, and account management pages and components +- OAuth2 and OpenID Connect for single sign on, API access, and machine to + machine authorization +- Low latency permission checks based on the Zanzibar model with the Ory + Permission Language +- GDPR friendly storage with data locality and compliance in mind +- Web based Ory Console and Ory CLI for administration and operations +- Cloud native APIs compatible with the open source servers +- Fair, usage based [pricing](https://www.ory.com/pricing) + +Sign up for a +[free developer account](https://console.ory.sh/registration?utm_source=github&utm_medium=banner&utm_campaign=kratos-readme) +to get started. + +### Self-host Ory Kratos + +You can run Ory Kratos yourself for full control over infrastructure, +deployment, and customization. + +The [install guide](https://www.ory.com/kratos/docs/install) explains how to: + +- Install Kratos on Linux, macOS, Windows, and Docker +- Configure databases such as PostgreSQL, MySQL, and CockroachDB +- Deploy to Kubernetes and other orchestration systems +- Build Kratos from source + +This guide uses the open source distribution to get you started without license +requirements. It is a great fit for individuals, researchers, hackers, and +companies that want to experiment, prototype, or run unimportant workloads +without SLAs. You get the full core engine, and you are free to inspect, extend, +and build it from source. + +If you run Kratos as part of a business-critical system, for example login and +account recovery for all your users, you should use a commercial agreement to +reduce operational and security risk. The **Ory Enterprise License (OEL)** +layers on top of self-hosted Kratos and provides: + +- Additional enterprise features that are not available in the open source + version such as SCIM, SAML, organization login ("SSO"), CAPTCHAs and more +- Regular security releases, including CVE patches, with service level + agreements +- Support for advanced scaling, multi-tenancy, and complex deployments +- Premium support options with SLAs, direct access to engineers, and onboarding + help +- Access to a private Docker registry with frequent and vetted, up-to-date + enterprise builds + +For guaranteed CVE fixes, current enterprise builds, advanced features, and +support in production, you need a valid +[Ory Enterprise License](https://www.ory.com/ory-enterprise-license) and access +to the Ory Enterprise Docker registry. To learn more, +[contact the Ory team](https://www.ory.com/contact/). + +## Quickstart + +Install the [Ory CLI](https://www.ory.com/docs/guides/cli/installation) and +create a new project to try Ory Identities. + +```bash +# Install the Ory CLI if you do not have it yet: +bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b . ory +sudo mv ./ory /usr/local/bin/ + +# Sign in or sign up +ory auth + +# Create a new project +ory create project --create-workspace "Ory Open Source" --name "GitHub Quickstart" --use-project +ory open ax login +``` + +### Who is using it? + + + +The Ory community stands on the shoulders of individuals, companies, and +maintainers. The Ory team thanks everyone involved - from submitting bug reports +and feature requests, to contributing patches and documentation. The Ory +community counts more than 50.000 members and is growing. The Ory stack protects +7.000.000.000+ API requests every day across thousands of companies. None of +this would have been possible without each and everyone of you! + +The following list represents companies that have accompanied us along the way +and that have made outstanding contributions to our ecosystem. _If you think +that your company deserves a spot here, reach out to +office@ory.com now_! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLogoWebsiteCase Study
OpenAI + + + OpenAI + + openai.comOpenAI Case Study
Fandom + + + Fandom + + fandom.comFandom Case Study
Lumin + + + Lumin + + luminpdf.comLumin Case Study
Sencrop + + + Sencrop + + sencrop.comSencrop Case Study
OSINT Industries + + + OSINT Industries + + osint.industriesOSINT Industries Case Study
HGV + + + HGV + + hgv.itHGV Case Study
Maxroll + + + Maxroll + + maxroll.ggMaxroll Case Study
Zezam + + + Zezam + + zezam.ioZezam Case Study
T.RowePrice + + + T.RowePrice + + troweprice.com
Mistral + + + Mistral + + mistral.ai
Axel Springer + + + Axel Springer + + axelspringer.com
Hemnet + + + Hemnet + + hemnet.se
Cisco + + + Cisco + + cisco.com
Presidencia de la República Dominicana + + + Presidencia de la República Dominicana + + presidencia.gob.do
Moonpig + + + Moonpig + + moonpig.com
Booster + + + Booster + + choosebooster.com
Zaptec + + + Zaptec + + zaptec.com
Klarna + + + Klarna + + klarna.com
Raspberry PI Foundation + + + Raspberry PI Foundation + + raspberrypi.org
Tulip + + + Tulip Retail + + tulip.com
Hootsuite + + + Hootsuite + + hootsuite.com
Segment + + + Segment + + segment.com
Arduino + + + Arduino + + arduino.cc
Sainsbury's + + + Sainsbury's + + sainsburys.co.uk
Contraste + + + Contraste + + contraste.com
inMusic + + + InMusic + + inmusicbrands.com
Buhta + + + Buhta + + buhta.com
Amplitude + + + amplitude.com + + amplitude.com
TIER IVKyma ProjectSerloPadis
CloudbearSecurity Onion SolutionsFactlyAll My Funds
NortalOrderMyGearR2DevopsParalus
dyrector.iopinniped.devpvotal.tech
+ +Many thanks to all individual contributors + + + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d1764fc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ + + + +# Ory Security Policy + +This policy outlines Ory's security commitments and practices for users across +different licensing and deployment models. + +To learn more about Ory's security service level agreements (SLAs) and +processes, please [contact us](https://www.ory.com/contact/). + +## Ory Network Users + +- **Security SLA:** Ory addresses vulnerabilities in the Ory Network according + to the following guidelines: + - Critical: Typically addressed within 14 days. + - High: Typically addressed within 30 days. + - Medium: Typically addressed within 90 days. + - Low: Typically addressed within 180 days. + - Informational: Addressed as necessary. + These timelines are targets and may vary based on specific circumstances. +- **Release Schedule:** Updates are deployed to the Ory Network as + vulnerabilities are resolved. +- **Version Support:** The Ory Network always runs the latest version, ensuring + up-to-date security fixes. + +## Ory Enterprise License Customers + +- **Security SLA:** Ory addresses vulnerabilities based on their severity: + - Critical: Typically addressed within 14 days. + - High: Typically addressed within 30 days. + - Medium: Typically addressed within 90 days. + - Low: Typically addressed within 180 days. + - Informational: Addressed as necessary. + These timelines are targets and may vary based on specific circumstances. +- **Release Schedule:** Updates are made available as vulnerabilities are + resolved. Ory works closely with enterprise customers to ensure timely updates + that align with their operational needs. +- **Version Support:** Ory may provide security support for multiple versions, + depending on the terms of the enterprise agreement. + +## Apache 2.0 License Users + +- **Security SLA:** Ory does not provide a formal SLA for security issues under + the Apache 2.0 License. +- **Release Schedule:** Releases prioritize new functionality and include fixes + for known security vulnerabilities at the time of release. While major + releases typically occur one to two times per year, Ory does not guarantee a + fixed release schedule. +- **Version Support:** Security patches are only provided for the latest release + version. + +## Reporting a Vulnerability + +For details on how to report security vulnerabilities, visit our +[security policy documentation](https://www.ory.com/docs/ecosystem/security). diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..bcc94c8 --- /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 0000000..227c4a6 --- /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 new file mode 100644 index 0000000..a089e71 --- /dev/null +++ b/cipher/aes.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cipher + +import ( + "context" + "crypto/aes" + stdcipher "crypto/cipher" + "encoding/hex" + + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +type AES struct { + c SecretsProvider +} + +func NewCryptAES(c SecretsProvider) *AES { + return &AES{c: c} +} + +// aesGCM returns an AES-256-GCM AEAD that generates a random nonce internally +// and prepends it to the ciphertext (nonce | ciphertext | tag). +func aesGCM(key *[32]byte) (stdcipher.AEAD, error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, errors.WithStack(err) + } + gcm, err := stdcipher.NewGCMWithRandomNonce(block) + if err != nil { + return nil, errors.WithStack(err) + } + return gcm, nil +} + +// Encrypt returns the AES-256-GCM encryption of plaintext. +func (a *AES) Encrypt(ctx context.Context, message []byte) (string, error) { + if len(message) == 0 { + return "", nil + } + + if len(a.c.SecretsCipher(ctx)) == 0 { + return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason("Unable to encrypt message because no cipher secrets were configured.")) + } + + gcm, err := aesGCM(&a.c.SecretsCipher(ctx)[0]) + if err != nil { + return "", errors.WithStack(herodot.ErrForbidden().WithWrap(err)) + } + + return hex.EncodeToString(gcm.Seal(nil, nil, message, nil)), nil +} + +// Decrypt returns the decrypted AES data. +func (a *AES) Decrypt(ctx context.Context, ciphertext string) ([]byte, error) { + if len(ciphertext) == 0 { + return nil, nil + } + + secrets := a.c.SecretsCipher(ctx) + if len(secrets) == 0 { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReason("Unable to decipher the encrypted message because no AES secrets were configured.")) + } + + decode, err := hex.DecodeString(ciphertext) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err)) + } + + for i := range secrets { + gcm, err := aesGCM(&secrets[i]) + if err != nil { + continue + } + if plaintext, err := gcm.Open(nil, nil, decode, nil); err == nil { + return plaintext, nil + } + } + + return nil, errors.WithStack(herodot.ErrForbidden().WithReason("Unable to decipher the encrypted message.")) +} diff --git a/cipher/chacha20.go b/cipher/chacha20.go new file mode 100644 index 0000000..139012a --- /dev/null +++ b/cipher/chacha20.go @@ -0,0 +1,94 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cipher + +import ( + "context" + "crypto/rand" + "encoding/hex" + "io" + "math" + + "github.com/pkg/errors" + "golang.org/x/crypto/chacha20poly1305" + + "github.com/ory/herodot" +) + +type XChaCha20Poly1305 struct { + c SecretsProvider +} + +func NewCryptChaCha20(c SecretsProvider) *XChaCha20Poly1305 { + return &XChaCha20Poly1305{c: c} +} + +// Encrypt returns a ChaCha encryption of plaintext +func (c *XChaCha20Poly1305) Encrypt(ctx context.Context, message []byte) (string, error) { + if len(message) == 0 { + return "", nil + } + + if len(c.c.SecretsCipher(ctx)) == 0 { + return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason("Unable to encrypt message because no cipher secrets were configured.")) + } + + 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 { + return "", errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReason("Unable to generate nonce")) + } + + encryptedMsg := aead.Seal(nonce, nonce, message, nil) + return hex.EncodeToString(encryptedMsg), nil +} + +// Decrypt decrypts data using 256 bit key +func (c *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string) ([]byte, error) { + if len(ciphertext) == 0 { + return nil, nil + } + + secrets := c.c.SecretsCipher(ctx) + if len(secrets) == 0 { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReason("Unable to decipher the encrypted message because no cipher secrets were configured.")) + } + + rawCiphertext, err := hex.DecodeString(ciphertext) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason("Unable to decode hex encrypted string")) + } + + for i := range secrets { + aead, err := chacha20poly1305.NewX(secrets[i][:]) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReason("Unable to instantiate chacha20")) + } + + // Guard the DECODED length, not the hex string length: a hex string of + // 24+ chars passes a hex-length check while decoding to fewer than + // NonceSize bytes, and the slice below would then panic. + if len(rawCiphertext) < aead.NonceSize() { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("cipher text too short")) + } + + nonce, ciphertext := rawCiphertext[:aead.NonceSize()], rawCiphertext[aead.NonceSize():] + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err == nil { + return plaintext, nil + } + } + + return nil, errors.WithStack(herodot.ErrForbidden().WithReason("Unable to decrypt string")) +} diff --git a/cipher/cipher.go b/cipher/cipher.go new file mode 100644 index 0000000..73c2a0b --- /dev/null +++ b/cipher/cipher.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cipher + +import "context" + +// Cipher provides methods for encrypt and decrypt string +type Cipher interface { + // Encrypt encrypts the (binary) message and returns a hex-encoded binary ciphertext + // or an error if the encryption failed. + // + // If the message is empty, the ciphertext is also empty and no error is returned. + Encrypt(ctx context.Context, message []byte) (string, error) + + // Decrypt takes a hex-encoded binary ciphertext and decrypts it or returns an error if the decryption + // failed. + // + // If the ciphertext is empty a nil byte slice is returned. + Decrypt(ctx context.Context, encrypted string) ([]byte, error) +} + +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 new file mode 100644 index 0000000..4ccc374 --- /dev/null +++ b/cipher/cipher_test.go @@ -0,0 +1,136 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cipher_test + +import ( + "context" + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +var goodSecret = []string{"secret-thirty-two-character-long"} + +func TestCipher(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsDefault, goodSecret)) + + ciphers := []cipher.Cipher{ + 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) { + t.Parallel() + + testAllWork(ctx, t, c) + }) + + t.Run("case=encryption_failed", func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) + + // secret have to be set + _, 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()) + + ctx = contextx.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) + + // bad secret length + _, 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) { + t.Parallel() + + _, err := c.Decrypt(ctx, hex.EncodeToString([]byte("bad-data"))) + require.Error(t, err) + + _, err = c.Decrypt(ctx, "not-empty") + require.Error(t, err) + + _, err = c.Decrypt(contextx.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") + require.Error(t, err) + + // Valid hex that decodes to fewer bytes than the AEAD nonce must be + // a clean error, not a panic: a corrupt or truncated stored + // ciphertext must never crash the caller. 16 bytes (32 hex chars) + // clears the hex-length check but is shorter than XChaCha20's + // 24-byte nonce. + _, err = c.Decrypt(ctx, hex.EncodeToString([]byte("sixteen-bytes!!!"))) + require.Error(t, err) + }) + }) + } + + c := cipher.NewNoop() + t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { + t.Parallel() + testAllWork(ctx, t, c) + }) +} + +func TestAESBackwardsCompatibility(t *testing.T) { + t.Parallel() + // Ciphertext produced by the previous implementation + // (github.com/gtank/cryptopasta: caller-generated GCM nonce). The new + // stdlib NewGCMWithRandomNonce implementation must keep decrypting it. + // Key (32 bytes): the literal config secret below. + // Plaintext: "fixture: encrypted with caller-supplied nonce". + const legacyCiphertext = "74756223e46170ef0579f207af41b7ed435d649ae94f22bd57934398e9861870e58feba1e949d28f18dfdda6395eabf4d2d31fdeb2b07c4c428e9829509470f6d8e23c17708f1368b6" + + ctx := t.Context() + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsCipher, []string{"change this password to a secret"})) + c := cipher.NewCryptAES(reg.Config()) + + plain, err := c.Decrypt(ctx, legacyCiphertext) + require.NoError(t, err) + assert.Equal(t, "fixture: encrypted with caller-supplied nonce", string(plain)) + + // Round-trip with the current implementation. + out, err := c.Encrypt(ctx, []byte("fixture: encrypted with caller-supplied nonce")) + require.NoError(t, err) + plain2, err := c.Decrypt(ctx, out) + require.NoError(t, err) + assert.Equal(t, "fixture: encrypted with caller-supplied nonce", string(plain2)) +} + +func testAllWork(ctx context.Context, t *testing.T, c cipher.Cipher) { + message := "my secret message!" + + encryptedSecret, err := c.Encrypt(ctx, []byte(message)) + require.NoError(t, err) + + 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(ctx, []byte("")) + require.NoError(t, err) + + // empty encrypted data return blank + _, err = c.Decrypt(ctx, "") + require.NoError(t, err) +} diff --git a/cipher/noop.go b/cipher/noop.go new file mode 100644 index 0000000..481d4c8 --- /dev/null +++ b/cipher/noop.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cipher + +import ( + "context" + "encoding/hex" +) + +// Noop is default cipher implementation witch does not do encryption +type Noop struct{} + +func NewNoop() *Noop { + return &Noop{} +} + +// Encrypt encode message to hex +func (*Noop) Encrypt(_ context.Context, message []byte) (string, error) { + return hex.EncodeToString(message), nil +} + +// Decrypt decode the hex message +func (*Noop) Decrypt(_ context.Context, ciphertext string) ([]byte, error) { + return hex.DecodeString(ciphertext) +} diff --git a/cmd/cleanup/handler.go b/cmd/cleanup/handler.go new file mode 100644 index 0000000..7c5dc3e --- /dev/null +++ b/cmd/cleanup/handler.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cleanup + +import ( + "github.com/pkg/errors" + + "github.com/ory/x/contextx" + + "github.com/ory/x/configx" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/flagx" +) + +type CleanupHandler struct{} + +func NewCleanupHandler() *CleanupHandler { + return &CleanupHandler{} +} + +func (h *CleanupHandler) CleanupSQL(cmd *cobra.Command, args []string) error { + opts := []configx.OptionModifier{ + configx.WithFlags(cmd.Flags()), + configx.SkipValidation(), + } + + if !flagx.MustGetBool(cmd, "read-from-env") { + if len(args) != 1 { + return errors.New(`expected to get the DSN as an argument, or the "read-from-env" flag`) + } + opts = append(opts, configx.WithValue(config.ViperKeyDSN, args[0])) + } + + d, err := driver.NewWithoutInit( + cmd.Context(), + cmd.ErrOrStderr(), + driver.WithConfigOptions(opts...), + ) + if len(d.Config().DSN(cmd.Context())) == 0 { + return errors.New(`required config value "dsn" was not set`) + } else if err != nil { + return errors.Wrap(err, "An error occurred initializing cleanup") + } + + err = d.Init(cmd.Context(), &contextx.Default{}) + if err != nil { + return errors.Wrap(err, "An error occurred initializing cleanup") + } + + keepLast := flagx.MustGetDuration(cmd, "keep-last") + + err = d.Persister().CleanupDatabase( + cmd.Context(), + d.Config().DatabaseCleanupSleepTables(cmd.Context()), + keepLast, + d.Config().DatabaseCleanupBatchSize(cmd.Context())) + if err != nil { + return errors.Wrap(err, "An error occurred while cleaning up expired data") + } + + return nil +} diff --git a/cmd/cleanup/root.go b/cmd/cleanup/root.go new file mode 100644 index 0000000..0afa640 --- /dev/null +++ b/cmd/cleanup/root.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cleanup + +import ( + "github.com/spf13/cobra" + + "github.com/ory/x/configx" +) + +func NewCleanupCmd() *cobra.Command { + c := &cobra.Command{ + Use: "cleanup", + Short: "Various cleanup helpers", + } + configx.RegisterFlags(c.PersistentFlags()) + return c +} + +func RegisterCommandRecursive(parent *cobra.Command) { + c := NewCleanupCmd() + parent.AddCommand(c) + c.AddCommand(NewCleanupSQLCmd()) +} diff --git a/cmd/cleanup/sql.go b/cmd/cleanup/sql.go new file mode 100644 index 0000000..97afa53 --- /dev/null +++ b/cmd/cleanup/sql.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cleanup + +import ( + "fmt" + "time" + + "github.com/ory/x/cmdx" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver/config" + + "github.com/ory/x/configx" +) + +// cleanupSqlCmd represents the sql command +func NewCleanupSQLCmd() *cobra.Command { + c := &cobra.Command{ + Use: "sql ", + Short: "Cleanup sql database from expired flows and sessions", + Long: `Run this command as frequently as you need. +It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet. +This decreases risk of failure and decreases time required. +You can read in the database URL using the -e flag, for example: + export DSN=... + kratos cleanup sql -e +### WARNING ### +Before running this command on an existing database, create a back up! +`, + RunE: func(cmd *cobra.Command, args []string) error { + err := NewCleanupHandler().CleanupSQL(cmd, args) + if err != nil { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), err) + return cmdx.FailSilently(cmd) + } + return nil + }, + } + + configx.RegisterFlags(c.PersistentFlags()) + c.Flags().BoolP("read-from-env", "e", true, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") + c.Flags().Duration(config.ViperKeyDatabaseCleanupSleepTables, time.Minute, "How long to wait between each table cleanup") + c.Flags().IntP(config.ViperKeyDatabaseCleanupBatchSize, "b", 100, "Set the number of records to be cleaned per run") + c.Flags().Duration("keep-last", 0, "Don't remove records younger than") + return c +} diff --git a/cmd/cleanup/sql_test.go b/cmd/cleanup/sql_test.go new file mode 100644 index 0000000..d0f925a --- /dev/null +++ b/cmd/cleanup/sql_test.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cleanup + +import ( + "bytes" + "io" + "strings" + "testing" +) + +func Test_ExecuteCleanupFailedDSN(t *testing.T) { + cmd := NewCleanupSQLCmd() + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetArgs([]string{"--read-from-env=false"}) + _ = cmd.Execute() + out, err := io.ReadAll(b) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(out), "expected to get the DSN as an argument") { + t.Fatalf("expected \"%s\" got \"%s\"", "expected to get the DSN as an argument", string(out)) + } + _ = cmd.Execute() +} diff --git a/cmd/cliclient/client.go b/cmd/cliclient/client.go new file mode 100644 index 0000000..5e3aaf7 --- /dev/null +++ b/cmd/cliclient/client.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cliclient + +import ( + "fmt" + "net/http" + "net/url" + "os" + "time" + + "github.com/pkg/errors" + + "github.com/spf13/cobra" + + "github.com/spf13/pflag" + + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/x/httpx" +) + +const ( + envKeyEndpoint = "KRATOS_ADMIN_URL" + FlagEndpoint = "endpoint" +) + +type ContextKey int + +const ( + ClientContextKey ContextKey = iota + 1 +) + +type ClientContext struct { + Endpoint string + HTTPClient *http.Client +} + +func NewClient(cmd *cobra.Command) (*kratos.APIClient, error) { + if f, ok := cmd.Context().Value(ClientContextKey).(func(cmd *cobra.Command) (*ClientContext, error)); ok { + cc, err := f(cmd) + if err != nil { + return nil, err + } + + conf := kratos.NewConfiguration() + conf.HTTPClient = cc.HTTPClient + conf.Servers = kratos.ServerConfigurations{{URL: cc.Endpoint}} + return kratos.NewAPIClient(conf), nil + } else if f != nil { + return nil, errors.Errorf("ClientContextKey was expected to be *client.OryKratos but it contained an invalid type %T ", f) + } + + endpoint, err := cmd.Flags().GetString(FlagEndpoint) + if err != nil { + return nil, errors.WithStack(err) + } + + if endpoint == "" { + endpoint = os.Getenv(envKeyEndpoint) + } + + if endpoint == "" { + return nil, errors.Errorf("you have to set the remote endpoint, try --help for details") + } + + u, err := url.Parse(endpoint) + if err != nil { + return nil, errors.Wrapf(err, `could not parse the endpoint URL "%s"`, endpoint) + } + + conf := kratos.NewConfiguration() + conf.HTTPClient = httpx.NewResilientClient( + httpx.ResilientClientWithConnectionTimeout(10 * time.Second), + ).StandardClient() + conf.Servers = kratos.ServerConfigurations{{URL: u.String()}} + return kratos.NewAPIClient(conf), nil +} + +func RegisterClientFlags(flags *pflag.FlagSet) { + flags.StringP(FlagEndpoint, FlagEndpoint[:1], "", fmt.Sprintf("The URL of Ory Kratos' Admin API. Alternatively set using the %s environmental variable.", envKeyEndpoint)) +} diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go new file mode 100644 index 0000000..98ea8fd --- /dev/null +++ b/cmd/clidoc/main.go @@ -0,0 +1,431 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/ory/kratos/cmd" + "github.com/ory/kratos/text" + + "github.com/ory/x/clidoc" + "github.com/ory/x/clock" +) + +var ( + aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) + inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) + + // Fixed clocks make the rendered "expired N minutes ago" / "within the next + // N minutes" example messages deterministic for doc generation. They are + // anchored so the durations come out as exactly one minute. + docExpiredClock = clock.NewMock(aSecondAgo.Add(time.Minute)) + docUntilClock = clock.NewMock(inAMinute.Add(-time.Minute)) +) + +var messages map[string]*text.Message + +func init() { + messages = map[string]*text.Message{ + "NewInfoNodeLabelVerifyOTP": text.NewInfoNodeLabelVerifyOTP(), + "NewInfoNodeLabelVerificationCode": text.NewInfoNodeLabelVerificationCode(), + "NewInfoNodeLabelRecoveryCode": text.NewInfoNodeLabelRecoveryCode(), + "NewInfoNodeInputPassword": text.NewInfoNodeInputPassword(), + "NewInfoNodeInputPhoneNumber": text.NewInfoNodeInputPhoneNumber(), + "NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}", "{name}"), + "NewInfoNodeLabelSave": text.NewInfoNodeLabelSave(), + "NewInfoNodeLabelSubmit": text.NewInfoNodeLabelSubmit(), + "NewInfoNodeLabelID": text.NewInfoNodeLabelID(), + "NewErrorValidationSettingsFlowExpired": text.NewErrorValidationSettingsFlowExpired(docExpiredClock, aSecondAgo), + "NewInfoSelfServiceSettingsTOTPQRCode": text.NewInfoSelfServiceSettingsTOTPQRCode(), + "NewInfoSelfServiceSettingsTOTPSecret": text.NewInfoSelfServiceSettingsTOTPSecret("{secret}"), + "NewInfoSelfServiceSettingsTOTPSecretLabel": text.NewInfoSelfServiceSettingsTOTPSecretLabel(), + "NewInfoSelfServiceSettingsUpdateSuccess": text.NewInfoSelfServiceSettingsUpdateSuccess(), + "NewInfoSelfServiceSettingsUpdateUnlinkTOTP": text.NewInfoSelfServiceSettingsUpdateUnlinkTOTP(), + "NewInfoSelfServiceSettingsRevealLookup": text.NewInfoSelfServiceSettingsRevealLookup(), + "NewInfoSelfServiceSettingsRegenerateLookup": text.NewInfoSelfServiceSettingsRegenerateLookup(), + "NewInfoSelfServiceSettingsDisableLookup": text.NewInfoSelfServiceSettingsDisableLookup(), + "NewInfoSelfServiceSettingsLookupConfirm": text.NewInfoSelfServiceSettingsLookupConfirm(), + "NewInfoSelfServiceSettingsManagedByOrganization": text.NewInfoSelfServiceSettingsManagedByOrganization(), + "NewInfoSelfServiceSettingsLookupSecretList": text.NewInfoSelfServiceSettingsLookupSecretList([]string{"{secrets_list}"}, []interface{}{ + text.NewInfoSelfServiceSettingsLookupSecret("{secret}"), + text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo), + }), + "NewInfoSelfServiceSettingsLookupSecret": text.NewInfoSelfServiceSettingsLookupSecret("{secret}"), + "NewInfoSelfServiceSettingsLookupSecretUsed": text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo), + "NewInfoSelfServiceSettingsLookupSecretsLabel": text.NewInfoSelfServiceSettingsLookupSecretsLabel(), + "NewInfoSelfServiceSettingsUpdateLinkOIDC": text.NewInfoSelfServiceSettingsUpdateLinkOIDC("{provider}"), + "NewInfoSelfServiceSettingsUpdateUnlinkOIDC": text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC("{provider}"), + "NewInfoSelfServiceRegisterWebAuthnDisplayName": text.NewInfoSelfServiceRegisterWebAuthnDisplayName(), + "NewInfoSelfServiceRemoveWebAuthn": text.NewInfoSelfServiceRemoveWebAuthn("{display_name}", aSecondAgo), + "NewInfoSelfServiceRemovePasskey": text.NewInfoSelfServiceRemovePasskey("{display_name}", aSecondAgo), + "NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(docExpiredClock, aSecondAgo), + "NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(), + "NewVerificationEmailSent": text.NewVerificationEmailSent(), + "NewVerificationEmailWithCodeSent": text.NewVerificationEmailWithCodeSent(), + "NewVerificationPhoneWithCodeSent": text.NewVerificationPhoneWithCodeSent(), + "NewInfoSelfServiceVerificationPhoneSuccessful": text.NewInfoSelfServiceVerificationPhoneSuccessful(), + "NewErrorValidationVerificationTokenInvalidOrAlreadyUsed": text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed(), + "NewErrorValidationVerificationRetrySuccess": text.NewErrorValidationVerificationRetrySuccess(), + "NewErrorValidationVerificationStateFailure": text.NewErrorValidationVerificationStateFailure(), + "NewErrorValidationVerificationCodeInvalidOrAlreadyUsed": text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), + "NewErrorSystemGeneric": text.NewErrorSystemGeneric("{reason}"), + "NewErrorSystemNoAuthenticationMethodsAvailable": text.NewErrorSystemNoAuthenticationMethodsAvailable(), + "NewErrorSystemOrganizationNoSSOProvidersAvailable": text.NewErrorSystemOrganizationNoSSOProvidersAvailable(), + "NewValidationErrorGeneric": text.NewValidationErrorGeneric("{reason}"), + "NewValidationErrorRequired": text.NewValidationErrorRequired("{property}"), + "NewErrorValidationMinLength": text.NewErrorValidationMinLength(5, 3), + "NewErrorValidationMaxLength": text.NewErrorValidationMaxLength(5, 6), + "NewErrorValidationInvalidFormat": text.NewErrorValidationInvalidFormat("{pattern}"), + "NewErrorValidationMinimum": text.NewErrorValidationMinimum(5, 3), + "NewErrorValidationExclusiveMinimum": text.NewErrorValidationExclusiveMinimum(5, 5), + "NewErrorValidationMaximum": text.NewErrorValidationMaximum(5, 6), + "NewErrorValidationExclusiveMaximum": text.NewErrorValidationExclusiveMaximum(5, 5), + "NewErrorValidationMultipleOf": text.NewErrorValidationMultipleOf(7, 3), + "NewErrorValidationMaxItems": text.NewErrorValidationMaxItems(3, 4), + "NewErrorValidationMinItems": text.NewErrorValidationMinItems(3, 2), + "NewErrorValidationUniqueItems": text.NewErrorValidationUniqueItems(0, 2), + "NewErrorValidationWrongType": text.NewErrorValidationWrongType([]string{"{allowed_types_list}"}, "{actual_type}"), + "NewErrorValidationConst": text.NewErrorValidationConst("{expected}"), + "NewErrorValidationConstGeneric": text.NewErrorValidationConstGeneric(), + "NewErrorValidationPasswordPolicyViolationGeneric": text.NewErrorValidationPasswordPolicyViolationGeneric("{reason}"), + "NewErrorValidationPasswordIdentifierTooSimilar": text.NewErrorValidationPasswordIdentifierTooSimilar(), + "NewErrorValidationPasswordMinLength": text.NewErrorValidationPasswordMinLength(6, 5), + "NewErrorValidationPasswordMaxLength": text.NewErrorValidationPasswordMaxLength(72, 80), + "NewErrorValidationPasswordTooManyBreaches": text.NewErrorValidationPasswordTooManyBreaches(101), + "NewErrorValidationPasswordNewSameAsOld": text.NewErrorValidationPasswordNewSameAsOld(), + "NewErrorValidationInvalidCredentials": text.NewErrorValidationInvalidCredentials(), + "NewErrorValidationDuplicateCredentials": text.NewErrorValidationDuplicateCredentials(), + "NewErrorValidationDuplicateCredentialsWithHints": text.NewErrorValidationDuplicateCredentialsWithHints([]string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}, "{credential_identifier_hint}"), + "NewErrorValidationDuplicateCredentialsOnOIDCLink": text.NewErrorValidationDuplicateCredentialsOnOIDCLink(), + "NewErrorValidationTOTPVerifierWrong": text.NewErrorValidationTOTPVerifierWrong(), + "NewErrorValidationWebAuthnVerifierWrong": text.NewErrorValidationWebAuthnVerifierWrong(), + "NewErrorValidationDeviceAuthnVerifierWrong": text.NewErrorValidationDeviceAuthnVerifierWrong(), + "NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid": text.NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid(), + "NewErrorValidationDeviceAuthnKeyReenrollmentRequired": text.NewErrorValidationDeviceAuthnKeyReenrollmentRequired(), + "NewErrorValidationLookupAlreadyUsed": text.NewErrorValidationLookupAlreadyUsed(), + "NewErrorValidationLookupInvalid": text.NewErrorValidationLookupInvalid(), + "NewErrorValidationIdentifierMissing": text.NewErrorValidationIdentifierMissing(), + "NewErrorValidationAddressNotVerified": text.NewErrorValidationAddressNotVerified(), + "NewErrorValidationNoTOTPDevice": text.NewErrorValidationNoTOTPDevice(), + "NewErrorValidationNoLookup": text.NewErrorValidationNoLookup(), + "NewErrorValidationNoWebAuthnDevice": text.NewErrorValidationNoWebAuthnDevice(), + "NewInfoLoginReAuth": text.NewInfoLoginReAuth(), + "NewInfoLoginMFA": text.NewInfoLoginMFA(), + "NewInfoLoginTOTPLabel": text.NewInfoLoginTOTPLabel(), + "NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(), + "NewInfoLogin": text.NewInfoLogin(), + "NewInfoLoginAndLink": text.NewInfoLoginAndLink(), + "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}", "{providerID}"), + "NewInfoLoginWithAndLink": text.NewInfoLoginWithAndLink("{provider}"), + "NewErrorValidationNoDeviceAuthnDevice": text.NewErrorValidationNoDeviceAuthnDevice(), + "NewErrorValidationLoginFlowExpired": text.NewErrorValidationLoginFlowExpired(docExpiredClock, aSecondAgo), + "NewErrorValidationLoginNoStrategyFound": text.NewErrorValidationLoginNoStrategyFound(), + "NewErrorValidationRegistrationNoStrategyFound": text.NewErrorValidationRegistrationNoStrategyFound(), + "NewErrorValidationSettingsNoStrategyFound": text.NewErrorValidationSettingsNoStrategyFound(), + "NewErrorValidationRecoveryNoStrategyFound": text.NewErrorValidationRecoveryNoStrategyFound(), + "NewErrorValidationVerificationNoStrategyFound": text.NewErrorValidationVerificationNoStrategyFound(), + "NewInfoSelfServiceLoginWebAuthn": text.NewInfoSelfServiceLoginWebAuthn(), + "NewInfoSelfServiceLoginDeviceAuthn": text.NewInfoSelfServiceLoginDeviceAuthn(), + "NewInfoRegistration": text.NewInfoRegistration(), + "NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}", "{providerID}"), + "NewInfoRegistrationContinue": text.NewInfoRegistrationContinue(), + "NewInfoRegistrationBack": text.NewInfoRegistrationBack(), + "NewInfoSelfServiceChooseCredentials": text.NewInfoSelfServiceChooseCredentials(), + "NewErrorValidationRegistrationFlowExpired": text.NewErrorValidationRegistrationFlowExpired(docExpiredClock, aSecondAgo), + "NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(docExpiredClock, aSecondAgo), + "NewRecoverySuccessful": text.NewRecoverySuccessful(docUntilClock, inAMinute), + "NewRecoveryEmailSent": text.NewRecoveryEmailSent(), + "NewRecoveryEmailWithCodeSent": text.NewRecoveryEmailWithCodeSent(), + "NewRecoveryCodeRecoverySelectAddressSent": text.NewRecoveryCodeRecoverySelectAddressSent("{masked_address}"), + "NewRecoveryAskAnyRecoveryAddress": text.NewRecoveryAskAnyRecoveryAddress(), + "NewRecoveryAskForFullAddress": text.NewRecoveryAskForFullAddress(), + "NewRecoveryAskToChooseAddress": text.NewRecoveryAskToChooseAddress(), + "NewRecoveryBack": text.NewRecoveryBack(), + "NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed(), + "NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed(), + "NewErrorValidationRecoveryRetrySuccess": text.NewErrorValidationRecoveryRetrySuccess(), + "NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(), + "NewInfoNodeInputEmail": text.NewInfoNodeInputEmail(), + "NewInfoNodeInputEmailOrPhone": text.NewInfoNodeInputEmailOrPhone(), + "NewInfoNodeResendOTP": text.NewInfoNodeResendOTP(), + "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(), + "NewLoginCodeSent": text.NewLoginCodeSent(), + "NewLoginCodeSentForAuthenticatedUser": text.NewLoginCodeSentForAuthenticatedUser(), + "NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(), + "NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(), + "NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(), + "NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(), + "NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(), + "NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(), + "NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(), + "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), + "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), + "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), + "NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(), + "NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(), + "NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(), + "NewInfoLoginPassword": text.NewInfoLoginPassword(), + "NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(), + "NewInfoSelfServiceLoginAAL2CodeAddress": text.NewInfoSelfServiceLoginAAL2CodeAddress("{channel}", "{address}"), + "NewErrorCaptchaFailed": text.NewErrorCaptchaFailed(), + "NewCaptchaContainerMessage": text.NewCaptchaContainerMessage(), + "NewErrorValidationEmail": text.NewErrorValidationEmail("{value}"), + "NewErrorValidationPhone": text.NewErrorValidationPhone("{value}"), + "NewErrorValidationIdentityDisabled": text.NewErrorValidationIdentityDisabled(), + "NewErrorValidationSettingsTooManyAddressChanges": text.NewErrorValidationSettingsTooManyAddressChanges(), + } +} + +func main() { + if os.Args[1] == "elements" { + path, err := filepath.Abs(os.Args[2]) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to determine absolute path for elements generation: %+v", err) + os.Exit(1) + } + + if err := generateElements(path); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to generate locales for elements: %+v", err) + os.Exit(1) + } + return + } + + if err := clidoc.Generate(cmd.NewRootCmd(nil, nil), []string{filepath.Join(os.Args[2], "cli")}); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to generate CLI docs: %+v", err) + os.Exit(1) + } + + if err := validateAllMessages(filepath.Join(os.Args[1], "text")); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to validate messages: %+v\n", err) + os.Exit(1) + } + + sortedMessages := sortMessages() + for i := 1; i < len(sortedMessages); i++ { + if sortedMessages[i].ID == sortedMessages[i-1].ID { + _, _ = fmt.Fprintf(os.Stderr, "Message ID %d is used more than once: %q %q\n", sortedMessages[i].ID, sortedMessages[i].Text, sortedMessages[i-1].Text) + os.Exit(1) + } + } + + 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) + } + + if err := writeMessagesJson(filepath.Join(os.Args[2], "concepts/messages.json"), sortedMessages); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to generate messages.json: %+v\n", err) + os.Exit(1) + } + + fmt.Println("All files have been generated and updated.") +} + +func codeEncode(in interface{}) string { + out, err := json.MarshalIndent(in, "", " ") + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to encode to JSON: %+v", err) + os.Exit(1) + } + + return string(out) +} + +func sortMessages() []*text.Message { + var toSort []*text.Message + for _, m := range messages { + toSort = append(toSort, m) + } + + sort.Slice(toSort, func(i, j int) bool { + if toSort[i].ID == toSort[j].ID { + return toSort[i].Text < toSort[j].Text + } + return toSort[i].ID < toSort[j].ID + }) + + return toSort +} + +func writeMessages(path string, sortedMessages []*text.Message) error { + //nolint:gosec + content, err := os.ReadFile(path) // #nosec + if err != nil { + return err + } + + var w bytes.Buffer + for _, m := range sortedMessages { + fmt.Fprintf(&w, `###### %s (%d) + +%s + +`, m.Text, m.ID, "```json\n"+codeEncode(m)+"\n```") + } + + r := regexp.MustCompile(`(?s)(.*?)`) + result := r.ReplaceAllString(string(content), "\n"+w.String()+"\n") + + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) // #nosec + if err != nil { + return err + } + + if _, err := f.WriteString(result); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + return nil +} + +func writeMessagesJson(path string, sortedMessages []*text.Message) error { + result := codeEncode(sortedMessages) + + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) // #nosec + if err != nil { + return err + } + + if _, err := f.WriteString(result); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + return nil +} + +func validateAllMessages(path string) error { + type message struct { + ID, Name string + } + + usedIDs := make([]message, 0, len(messages)) + set := token.NewFileSet() + packs, err := parser.ParseDir(set, path, nil, 0) + if err != nil { + return errors.Wrapf(err, "unable to parse text directory") + } + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + } + + //nolint:staticcheck + var pack *ast.Package + for _, p := range packs { + if p.Name == "text" { + pack = p + break + } + } + allFiles := make([]*ast.File, 0) + for fn, f := range pack.Files { + if strings.HasSuffix(fn, "/id.go") { + allFiles = append(allFiles, f) + } + } + _, err = (&types.Config{Importer: importer.Default()}).Check("text", set, allFiles, info) + if err != nil { + return err // type error + } + + for _, f := range pack.Files { + for _, d := range f.Decls { + switch decl := d.(type) { + case *ast.FuncDecl: + if name := decl.Name.String(); decl.Name.IsExported() && strings.HasPrefix(name, "New") { + if _, ok := messages[name]; !ok { + return errors.Errorf("expected to find message %s in the list for the documentation generation but could not", name) + } + } + case *ast.GenDecl: + if decl.Tok == token.CONST { + for _, spec := range decl.Specs { + value := spec.(*ast.ValueSpec) // safe because decl.Tok is token.CONST + if t, ok := value.Type.(*ast.Ident); ok { + if t.Name == "ID" { + for _, name := range value.Names { + c := info.ObjectOf(name) + if c == nil { + return errors.Errorf("expected to find const %s in text/id.go", name.Name) + } + usedIDs = append(usedIDs, message{ + ID: c.(*types.Const).Val().ExactString(), + Name: name.Name, + }) + } + } + } + } + } + } + } + } + + sort.Slice(usedIDs, func(i, j int) bool { + return usedIDs[i].ID < usedIDs[j].ID + }) + for i := 1; i < len(usedIDs); i++ { + if usedIDs[i].ID == usedIDs[i-1].ID { + return errors.Errorf("message ID %s is used more than once: %s %s", usedIDs[i].ID, usedIDs[i].Name, usedIDs[i-1].Name) + } + } + + return nil +} + +func generateElements(messageFilePath string) error { + // If the file at messageFilePath does not exist, create it + f, err := os.OpenFile(messageFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) // #nosec + if err != nil { + return errors.Wrapf(err, "unable to open or create message file at %s", messageFilePath) + } + if err := f.Close(); err != nil { + return errors.Wrapf(err, "unable to close message file at %s", messageFilePath) + } + + sortedMessages := sortMessages() + if err := writeMessagesJson(messageFilePath, sortedMessages); err != nil { + return errors.Wrapf(err, "unable to write messages json to %s", messageFilePath) + } + return nil +} diff --git a/cmd/clidoc/main_test.go b/cmd/clidoc/main_test.go new file mode 100644 index 0000000..4e75ce3 --- /dev/null +++ b/cmd/clidoc/main_test.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMessages(t *testing.T) { + require.NoError(t, validateAllMessages("../../text")) +} diff --git a/cmd/courier/root.go b/cmd/courier/root.go new file mode 100644 index 0000000..4e33242 --- /dev/null +++ b/cmd/courier/root.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver" + "github.com/ory/x/configx" +) + +// NewCourierCmd creates a new courier command +func NewCourierCmd() *cobra.Command { + c := &cobra.Command{ + Use: "courier", + Short: "Commands related to the Ory Kratos message courier", + } + configx.RegisterFlags(c.PersistentFlags()) + return c +} + +func RegisterCommandRecursive(parent *cobra.Command, dOpts []driver.RegistryOption) { + c := NewCourierCmd() + parent.AddCommand(c) + c.AddCommand(NewWatchCmd(dOpts)) +} diff --git a/cmd/courier/watch.go b/cmd/courier/watch.go new file mode 100644 index 0000000..e15e701 --- /dev/null +++ b/cmd/courier/watch.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "net/http" + + "github.com/spf13/cobra" + "github.com/urfave/negroni" + "golang.org/x/sync/errgroup" + + "github.com/ory/graceful" + "github.com/ory/kratos/driver" + "github.com/ory/x/configx" + "github.com/ory/x/prometheusx" + "github.com/ory/x/reqlog" +) + +func NewWatchCmd(dOpts []driver.RegistryOption) *cobra.Command { + c := &cobra.Command{ + Use: "watch", + Short: "Starts the Ory Kratos message courier", + RunE: func(cmd *cobra.Command, args []string) error { + r, err := driver.New(cmd.Context(), cmd.ErrOrStderr(), append(dOpts, driver.WithConfigOptions(configx.WithFlags(cmd.Flags())))...) + if err != nil { + return err + } + + return StartCourier(cmd.Context(), r) + }, + } + c.PersistentFlags().Int("expose-metrics-port", 0, "The port to expose the metrics endpoint on (not exposed by default)") + return c +} + +func StartCourier(ctx context.Context, r driver.Registry) error { + eg, ctx := errgroup.WithContext(ctx) + + if port := r.Config().CourierExposeMetricsPort(ctx); port != 0 { + eg.Go(func() error { + return ServeMetrics(ctx, r, port) + }) + } + + eg.Go(func() error { + return Watch(ctx, r) + }) + + return eg.Wait() +} + +func ServeMetrics(ctx context.Context, r driver.Registry, port int) error { + cfg := r.Config().ServeAdmin(ctx) + l := r.Logger() + + recovery := negroni.NewRecovery() + recovery.Logger = l + + n := negroni.New( + recovery, + reqlog.NewMiddlewareFromLogger(l, "admin#"+cfg.BaseURL.String()), + ) + + router := http.NewServeMux() + router.Handle(prometheusx.MetricsPrometheusPath, prometheusx.Handler()) + n.UseHandler(router) + + var handler http.Handler = n + + //#nosec G112 -- the correct settings are set by graceful.WithDefaults + server := graceful.WithDefaults(&http.Server{ + Addr: configx.GetAddress(cfg.Host, port), + Handler: handler, + }) + + l.WithField("addr", server.Addr).Info("Starting the metrics httpd") + if err := graceful.GracefulContext(ctx, server.ListenAndServe, server.Shutdown); err != nil { + l.Errorln("Failed to gracefully shutdown metrics httpd") + return err + } + l.Println("Metrics httpd was shutdown gracefully") + return nil +} + +func Watch(ctx context.Context, r driver.Registry) error { + ctx, cancel := context.WithCancel(ctx) + + r.Logger().Println("Courier worker started.") + if err := graceful.Graceful(func() error { + c, err := r.Courier(ctx) + if err != nil { + return err + } + + return c.Work(ctx) + }, func(_ context.Context) error { + cancel() + return nil + }); err != nil { + r.Logger().WithError(err).Error("Failed to run courier worker.") + return err + } + + r.Logger().Println("Courier worker was shutdown gracefully.") + return nil +} diff --git a/cmd/courier/watch_test.go b/cmd/courier/watch_test.go new file mode 100644 index 0000000..183d383 --- /dev/null +++ b/cmd/courier/watch_test.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/x/configx" +) + +func TestStartCourier(t *testing.T) { + t.Run("case=without metrics", func(t *testing.T) { + _, r := pkg.NewFastRegistryWithMocks(t) + go func() { _ = StartCourier(t.Context(), r) }() + require.Equal(t, r.Config().CourierExposeMetricsPort(t.Context()), 0) + }) + + t.Run("case=with metrics", func(t *testing.T) { + port, err := freeport.GetFreePort() + require.NoError(t, err) + _, r := pkg.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port)) + go func() { _ = StartCourier(t.Context(), r) }() + transport := http.DefaultTransport.(*http.Transport).Clone() + t.Cleanup(transport.CloseIdleConnections) + client := &http.Client{Transport: transport} + url := fmt.Sprintf("http://127.0.0.1:%d/metrics/prometheus", port) + require.EventuallyWithT(t, func(t *assert.CollectT) { + res, err := client.Get(url) + if !assert.NoError(t, err) { + return + } + defer func() { + assert.NoError(t, res.Body.Close()) + }() + assert.Equal(t, 200, res.StatusCode) + }, 10*time.Second, 10*time.Millisecond) + }) +} diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go new file mode 100644 index 0000000..573ca60 --- /dev/null +++ b/cmd/daemon/serve.go @@ -0,0 +1,371 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package daemon + +import ( + "context" + "crypto/tls" + "net/http" + "time" + + "github.com/pkg/errors" + "github.com/rs/cors" + "github.com/spf13/cobra" + "github.com/urfave/negroni" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "golang.org/x/sync/errgroup" + + "github.com/ory/analytics-go/v5" + "github.com/ory/graceful" + "github.com/ory/kratos/cmd/courier" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/logout" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/strategy/link" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/healthx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/metricsx" + "github.com/ory/x/networkx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/popx" + "github.com/ory/x/prometheusx" + "github.com/ory/x/reqlog" + "github.com/ory/x/urlx" +) + +func init() { + graceful.DefaultShutdownTimeout = 120 * time.Second +} + +var httpMetrics = prometheusx.NewHTTPMetrics("kratos", prometheusx.HTTPPrefix, config.Version, config.Commit, config.Date) + +func servePublic(ctx context.Context, r *driver.RegistryDefault, cmd *cobra.Command) (func() error, error) { + cfg := r.Config().ServePublic(ctx) + l := r.Logger() + + rec := negroni.NewRecovery() + rec.Logger = l + + router := httprouterx.NewRouterPublic() + n := negroni.New( + rec, + httpMetrics, + ) + + for _, mw := range r.HTTPMiddlewares() { + n.Use(mw) + } + + publicLogger := reqlog.NewMiddlewareFromLogger(l, "public#"+cfg.BaseURL.String()) + + if cfg.RequestLog.DisableHealth { + publicLogger.ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath) + } + + n.UseFunc(semconv.Middleware) + n.Use(publicLogger) + n.Use(x.HTTPLoaderContextMiddleware(r)) + n.UseFunc(httprouterx.NoCacheNegroni) + n.Use(sqa(ctx, cmd, r)) + + csrf := nosurfx.NewCSRFHandler(router, r) + + // we need to always load the CORS middleware even if it is disabled, to allow hot-enabling CORS + n.UseFunc(func(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) { + cfg, enabled := r.Config().CORSPublic(req.Context()) + if !enabled { + next(w, req) + return + } + cors.New(cfg).ServeHTTP(w, req, next) + }) + + r.WithCSRFHandler(csrf) + n.UseHandler(r.CSRFHandler()) + + // Disable CSRF for these endpoints + csrf.DisablePath(healthx.AliveCheckPath) + csrf.DisablePath(healthx.ReadyCheckPath) + csrf.DisablePath(healthx.VersionPath) + csrf.DisablePath(prometheusx.MetricsPrometheusPath) + + r.RegisterPublicRoutes(ctx, router) + + var handler http.Handler = n + if tracer := r.Tracer(ctx); tracer.IsLoaded() { + handler = otelx.NewMiddleware(handler, "servePublic", + otelhttp.WithTracerProvider(tracer.Provider()), + ) + } + + handler = http.MaxBytesHandler(handler, 5*1024*1024 /* 5 MB */) // Important: this must be the outermost handler or our tracing breaks + + certFunc, err := cfg.TLS.GetCertFunc(ctx, l, "public") + if err != nil { + return nil, err + } + + //#nosec G112 -- the correct settings are set by graceful.WithDefaults + server := graceful.WithDefaults(&http.Server{ + Handler: handler, + TLSConfig: &tls.Config{GetCertificate: certFunc, MinVersion: tls.VersionTLS12}, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 15 * time.Second, + WriteTimeout: 60 * time.Second, + IdleTimeout: 120 * time.Second, + }) + addr := cfg.GetAddress() + + return func() error { + l.WithField("addr", addr).Info("Starting the public httpd") + if err := graceful.GracefulContext(ctx, func() error { + listener, err := networkx.MakeListener(addr, &cfg.Socket) + if err != nil { + return err + } + + if certFunc == nil { + return server.Serve(listener) + } + return server.ServeTLS(listener, "", "") + }, server.Shutdown); err != nil { + if !errors.Is(err, context.Canceled) { + l.Errorf("Failed to gracefully shutdown public httpd: %s", err) + return err + } + } + l.Println("Public httpd was shutdown gracefully") + return nil + }, nil +} + +func serveAdmin(ctx context.Context, r *driver.RegistryDefault, cmd *cobra.Command) (func() error, error) { + cfg := r.Config().ServeAdmin(ctx) + l := r.Logger() + + rec := negroni.NewRecovery() + rec.Logger = l + + router := httprouterx.NewRouterAdminWithPrefix() + n := negroni.New( + rec, + httpMetrics, + ) + + for _, mw := range r.HTTPMiddlewares() { + n.Use(mw) + } + + adminLogger := reqlog.NewMiddlewareFromLogger(l, "admin#"+cfg.BaseURL.String()) + + if cfg.RequestLog.DisableHealth { + adminLogger.ExcludePaths( + httprouterx.AdminPrefix+healthx.AliveCheckPath, + httprouterx.AdminPrefix+healthx.ReadyCheckPath, + httprouterx.AdminPrefix+prometheusx.MetricsPrometheusPath, + ) + } + n.UseFunc(semconv.Middleware) + n.Use(adminLogger) + n.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni) + n.UseFunc(httprouterx.NoCacheNegroni) + n.Use(x.HTTPLoaderContextMiddleware(r)) + n.Use(sqa(ctx, cmd, r)) + + r.RegisterAdminRoutes(ctx, router) + + n.UseHandler(router) + + var handler http.Handler = n + if tracer := r.Tracer(ctx); tracer.IsLoaded() { + handler = otelx.NewMiddleware(handler, "serveAdmin", + otelhttp.WithTracerProvider(tracer.Provider()), + ) + } + + handler = http.MaxBytesHandler(handler, 5*1024*1024 /* 5 MB */) // Important: this must be the outermost handler or our tracing breaks + + certFunc, err := cfg.TLS.GetCertFunc(ctx, l, "admin") + if err != nil { + return nil, err + } + + //#nosec G112 -- the correct settings are set by graceful.WithDefaults + server := graceful.WithDefaults(&http.Server{ + Handler: handler, + TLSConfig: &tls.Config{GetCertificate: certFunc, MinVersion: tls.VersionTLS12}, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 600 * time.Second, + }) + + addr := cfg.GetAddress() + + return func() error { + l.WithField("addr", addr).Info("Starting the admin httpd") + if err := graceful.GracefulContext(ctx, func() error { + listener, err := networkx.MakeListener(addr, &cfg.Socket) + if err != nil { + return err + } + + if certFunc == nil { + return server.Serve(listener) + } + return server.ServeTLS(listener, "", "") + }, server.Shutdown); err != nil { + if !errors.Is(err, context.Canceled) { + l.Errorf("Failed to gracefully shutdown admin httpd: %s", err) + return err + } + } + l.Println("Admin httpd was shutdown gracefully") + return nil + }, nil +} + +func sqa(ctx context.Context, cmd *cobra.Command, d driver.Registry) *metricsx.Service { + urls := []string{ + d.Config().ServePublic(ctx).BaseURL.Host, + d.Config().ServeAdmin(ctx).BaseURL.Host, + d.Config().SelfServiceFlowLoginUI(ctx).Host, + d.Config().SelfServiceFlowSettingsUI(ctx).Host, + d.Config().SelfServiceFlowErrorURL(ctx).Host, + d.Config().SelfServiceFlowRegistrationUI(ctx).Host, + d.Config().SelfServiceFlowRecoveryUI(ctx).Host, + d.Config().ServePublic(ctx).Host, + d.Config().ServeAdmin(ctx).Host, + } + if c, y := d.Config().CORSPublic(ctx); y { + urls = append(urls, c.AllowedOrigins...) + } + host := urlx.ExtractPublicAddress(urls...) + + // Creates only ones + // instance + return metricsx.New( + cmd, + d.Logger(), + d.Config().GetProvider(ctx), + &metricsx.Options{ + Service: "kratos", + DeploymentId: metricsx.Hash(d.Persister().NetworkID(ctx).String()), + DBDialect: d.Persister().GetConnection(ctx).Dialect.Name(), + IsDevelopment: d.Config().IsInsecureDevMode(ctx), + WriteKey: "qQlI6q8Q4WvkzTjKQSor4sHYOikHIvvi", + WhitelistedPaths: []string{ + "/", + healthx.AliveCheckPath, + healthx.ReadyCheckPath, + healthx.VersionPath, + + oidc.RouteBase, + + login.RouteInitBrowserFlow, + login.RouteInitAPIFlow, + login.RouteGetFlow, + login.RouteSubmitFlow, + + logout.RouteInitBrowserFlow, + logout.RouteSubmitFlow, + logout.RouteAPIFlow, + + registration.RouteInitBrowserFlow, + registration.RouteInitAPIFlow, + registration.RouteGetFlow, + registration.RouteSubmitFlow, + + session.RouteWhoami, + + httprouterx.AdminPrefix + "/" + schema.SchemasPath, + httprouterx.AdminPrefix + identity.RouteCollection, + + settings.RouteInitBrowserFlow, + settings.RouteInitAPIFlow, + settings.RouteGetFlow, + settings.RouteSubmitFlow, + + verification.RouteInitAPIFlow, + verification.RouteInitBrowserFlow, + verification.RouteGetFlow, + verification.RouteSubmitFlow, + + recovery.RouteInitAPIFlow, + recovery.RouteInitBrowserFlow, + recovery.RouteGetFlow, + recovery.RouteSubmitFlow, + + link.RouteAdminCreateRecoveryLink, + + errorx.RouteGet, + prometheusx.MetricsPrometheusPath, + }, + BuildVersion: config.Version, + BuildHash: config.Commit, + BuildTime: config.Date, + Config: &analytics.Config{ + Endpoint: "https://sqa.ory.sh", + GzipCompressionLevel: 6, + BatchMaxSize: 500 * 1000, + BatchSize: 1000, + Interval: time.Hour * 6, + }, + Hostname: host, + }, + ) +} + +func courierTask(ctx context.Context, d driver.Registry) func() error { + return func() error { + if d.Config().IsBackgroundCourierEnabled(ctx) { + return courier.Watch(ctx, d) + } + return nil + } +} + +func ServeAll(d *driver.RegistryDefault) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + g, ctx := errgroup.WithContext(ctx) + cmd.SetContext(ctx) + + if err := popx.VerifyDialect(ctx, d.Persister().GetConnection(ctx)); err != nil { + return errors.WithStack(err) + } + + // construct all tasks upfront to avoid race conditions + publicSrv, err := servePublic(ctx, d, cmd) + if err != nil { + return errors.WithStack(err) + } + adminSrv, err := serveAdmin(ctx, d, cmd) + if err != nil { + return errors.WithStack(err) + } + tasks := []func() error{ + publicSrv, + adminSrv, + courierTask(ctx, d), + } + for _, task := range tasks { + g.Go(task) + } + return g.Wait() + } +} diff --git a/cmd/daemon/serve_test.go b/cmd/daemon/serve_test.go new file mode 100644 index 0000000..b67333d --- /dev/null +++ b/cmd/daemon/serve_test.go @@ -0,0 +1,76 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package daemon + +import ( + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/phayes/freeport" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/pkg" +) + +func TestMetricsRouterPaths(t *testing.T) { + // TODO refactor this test to be parallelizable once we rewrite the server setup to be properly testable + ports, err := freeport.GetFreePorts(2) + require.NoError(t, err) + adminPort, publicPort := ports[0], ports[1] + cmd := &cobra.Command{} + cmd.Flags().Bool("sqa-opt-out", true, "") + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + "serve.admin.port": adminPort, + "serve.public.port": publicPort, + })) + + startAdmin, err := serveAdmin(t.Context(), reg, cmd) + require.NoError(t, err) + startPublic, err := servePublic(t.Context(), reg, cmd) + require.NoError(t, err) + eg, _ := errgroup.WithContext(t.Context()) + eg.Go(startAdmin) + eg.Go(startPublic) + + transport := http.DefaultTransport.(*http.Transport).Clone() + t.Cleanup(transport.CloseIdleConnections) + client := &http.Client{Transport: transport} + + require.EventuallyWithT(t, func(t *assert.CollectT) { + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/health/ready", publicPort)) + require.NoError(t, err) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equalf(t, http.StatusOK, resp.StatusCode, "%s", body) + }, 2*time.Second, 10*time.Millisecond) + + // Make some requests that should be recorded in the metrics + req, _ := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://127.0.0.1:%d/sessions/session-id", publicPort), nil) + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + resp, err = client.Get(fmt.Sprintf("http://127.0.0.1:%d/admin/identities/8e5f29ec-db29-4e56-8517-75f7d4e2f1bc/sessions", adminPort)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + assert.EventuallyWithT(t, func(t *assert.CollectT) { + res, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/admin/metrics/prometheus", adminPort)) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + respBody, err := io.ReadAll(res.Body) + body := string(respBody) + + require.NoError(t, err) + assert.Contains(t, body, `endpoint="/sessions/{param}"`) + assert.Contains(t, body, `endpoint="/admin/identities/{param}/sessions"`) + }, 5*time.Second, 100*time.Millisecond) +} diff --git a/cmd/hashers/argon2/calibrate.go b/cmd/hashers/argon2/calibrate.go new file mode 100644 index 0000000..4469176 --- /dev/null +++ b/cmd/hashers/argon2/calibrate.go @@ -0,0 +1,332 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package argon2 + +import ( + "fmt" + "io" + "runtime" + "strconv" + "time" + + "github.com/ory/x/configx" + + "github.com/ory/x/cmdx" + + "github.com/fatih/color" + "github.com/inhies/go-bytesize" + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" +) + +const ( + FlagStartMemory = "start-memory" + FlagMaxMemory = "max-memory" + FlagAdjustMemory = "adjust-memory-by" + FlagStartIterations = "start-iterations" + FlagMaxConcurrent = "max-concurrent" + + FlagRuns = "probe-runs" +) + +type ( + colorWriter struct { + c *color.Color + w io.Writer + } + loadResult struct { + r *resultTable + v *config.Argon2 + } + loadResults []*loadResult +) + +var _ cmdx.Table = loadResults{} + +func (l loadResults) Header() []string { + return append((&resultTable{}).Header(), "MEMOry PARAM", "ITERATIONS PARAM") +} + +func (l loadResults) Table() [][]string { + t := make([][]string, len(l)) + + for i, r := range l { + t[i] = append(r.r.Columns(), fmt.Sprintf("%d", r.v.Memory), fmt.Sprintf("%d", r.v.Iterations)) + } + + return t +} + +func (l loadResults) Interface() interface{} { + return l +} + +func (l loadResults) Len() int { + return len(l) +} + +func (c *colorWriter) Write(o []byte) (int, error) { + return c.c.Fprintf(c.w, "%s", o) +} + +func newCalibrateCmd() *cobra.Command { + var ( + maxMemory, adjustMemory bytesize.ByteSize = 0, 512 * bytesize.MB + runs int + ) + + flagConfig := &argon2Config{ + localConfig: config.Argon2{}, + } + + cmd := &cobra.Command{ + Use: "calibrate ", + Args: cobra.ExactArgs(1), + Short: "Computes Optimal Argon2 Parameters", + Long: `This command helps you calibrate the configuration parameters for Argon2. Password hashing is a trade-off between security, resource consumption, and user experience. Resource consumption should not be too high and the login should not take too long. + +We recommend that the login process takes between half a second and one second for password hashing, giving a good balance between security and user experience. + +Please note that the values depend on the machine you run the hashing on. If you have RAM constraints, please set the memory dedicated to Ory Kratos to avoid out of memory panics.`, + RunE: func(cmd *cobra.Command, args []string) error { + progressPrinter := cmdx.NewLoudErrPrinter(cmd) + resultPrinter := cmdx.NewLoudPrinter(cmd, &colorWriter{c: color.New(color.FgGreen), w: cmd.ErrOrStderr()}) + + conf, err := configProvider(cmd, flagConfig) + if err != nil { + return err + } + + // always take start flags, or their default + conf.localConfig.Memory = flagConfig.localConfig.Memory + conf.localConfig.Iterations = flagConfig.localConfig.Iterations + + desiredDuration := conf.localConfig.ExpectedDuration + + reqPerMin, err := strconv.ParseInt(args[0], 0, 0) + if err != nil { + // we want the error and usage string printed so just return + return err + } + + hasher := hash.NewHasherArgon2(conf) + + var currentDuration time.Duration + + _, _ = progressPrinter.Printf("Increasing memory to get over %s:\n", desiredDuration) + + for { + if maxMemory != 0 && conf.localConfig.Memory > maxMemory { + // don't further increase memory + _, _ = progressPrinter.Println(" ouch, hit the memory limit there") + conf.localConfig.Memory = maxMemory + break + } + + currentDuration, err = probe(cmd, hasher, runs, progressPrinter) + if err != nil { + return err + } + + _, _ = progressPrinter.Printf(" took %s with %s of memory\n", currentDuration, conf.localConfig.Memory) + + if currentDuration > desiredDuration { + if conf.localConfig.Memory <= adjustMemory { + // adjusting the memory would now result in <= 0B + adjustMemory = adjustMemory >> 1 + } + conf.localConfig.Memory -= adjustMemory + break + } + + // adjust config + conf.localConfig.Memory += adjustMemory + } + + _, _ = progressPrinter.Printf("Decreasing memory to get under %s:\n", desiredDuration) + + for { + currentDuration, err = probe(cmd, hasher, runs, progressPrinter) + if err != nil { + return err + } + + _, _ = progressPrinter.Printf(" took %s with %s of memory\n", currentDuration, conf.localConfig.Memory) + + if currentDuration < desiredDuration { + break + } + + for conf.localConfig.Memory <= adjustMemory { + // adjusting the memory would now result in <= 0B + adjustMemory = adjustMemory >> 1 + } + + // adjust config + conf.localConfig.Memory -= adjustMemory + } + + _, _ = resultPrinter.Printf("Settled on %s of memory.\n", conf.localConfig.Memory) + _, _ = progressPrinter.Printf("Increasing iterations to get over %s:\n", desiredDuration) + + for { + currentDuration, err = probe(cmd, hasher, runs, progressPrinter) + if err != nil { + return err + } + + _, _ = progressPrinter.Printf(" took %s with %d iterations\n", currentDuration, conf.localConfig.Iterations) + + if currentDuration > desiredDuration { + if conf.localConfig.Iterations > 1 { + conf.localConfig.Iterations -= 1 + } + break + } + + // adjust config + conf.localConfig.Iterations += 1 + } + + _, _ = progressPrinter.Printf("Decreasing iterations to get under %s:\n", desiredDuration) + + for { + currentDuration, err = probe(cmd, hasher, runs, progressPrinter) + if err != nil { + return err + } + + _, _ = progressPrinter.Printf(" took %s with %d iterations\n", currentDuration, conf.localConfig.Iterations) + + // break also when iterations is 1; this catches the case where 1 was only slightly under the desired time and took longer a bit longer on another run + if currentDuration < desiredDuration || conf.localConfig.Iterations == 1 { + break + } + + // adjust config + conf.localConfig.Iterations -= 1 + } + _, _ = resultPrinter.Printf("Settled on %d iterations.\n", conf.localConfig.Iterations) + + results := make(loadResults, 5) + for i := 0; i < len(results); i++ { + _, _ = progressPrinter.Printf("\nRunning load test to simulate %d login requests per minute.\n", reqPerMin) + + res, err := runLoadTest(cmd, conf, int(reqPerMin)) + if err != nil { + return err + } + cv, _ := conf.HasherArgon2() + ccv := *cv + results[i] = &loadResult{r: res, v: &ccv} + + _, _ = progressPrinter.Println("The load test result is:") + _, _ = progressPrinter.Println() + cmdx.PrintRow(cmd, res) + + switch { + // too fast + case res.MedianTime < conf.localConfig.ExpectedDuration: + _, _ = progressPrinter.Printf("The median was %s under the minimal duration of %s, going to increase the hash cost.\n", conf.localConfig.ExpectedDuration-res.MedianTime, conf.localConfig.ExpectedDuration) + + // try to increase memory first + if res.MaxMem+64*bytesize.MB < maxMemory { + // only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request + conf.localConfig.Memory += 64 * bytesize.MB + _, _ = progressPrinter.Printf("Increasing memory to %s\n", conf.localConfig.Memory) + } else { + // increasing memory is not allowed by maxMemory, therefore increase CPU load + conf.localConfig.Iterations++ + _, _ = progressPrinter.Printf("Increasing iterations to %d\n", conf.localConfig.Iterations) + } + // too much memory + case res.MaxMem > conf.localConfig.DedicatedMemory: + _, _ = progressPrinter.Printf("The required memory was %s more than the maximum allowed of %s.\n", res.MaxMem-maxMemory, conf.localConfig.DedicatedMemory) + + //nolint:gosec // disable G115 + conf.localConfig.Memory -= (res.MaxMem - conf.localConfig.DedicatedMemory) / bytesize.ByteSize(reqPerMin) + _, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory) + // too slow + case res.MaxTime > conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration: + _, _ = progressPrinter.Printf("The longest request took %s longer than the longest acceptable time of %s, going to decrease the hash cost.\n", res.MaxTime-conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration, conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration) + + // try to decrease iterations first + if conf.localConfig.Iterations > 1 { + conf.localConfig.Iterations-- + _, _ = progressPrinter.Printf("Decreasing iterations to %d\n", conf.localConfig.Iterations) + } else { + // decreasing iterations is not possible anymore, decreasing memory + // only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request + conf.localConfig.Memory -= 64 * bytesize.MB + _, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory) + } + // too high deviation + case res.StdDev > conf.localConfig.ExpectedDeviation: + _, _ = progressPrinter.Printf("The deviation was %s more than the expected deviation of %s.\n", res.StdDev-conf.localConfig.ExpectedDeviation, conf.localConfig.ExpectedDeviation) + + // try to decrease iterations first + if conf.localConfig.Iterations > 1 { + conf.localConfig.Iterations-- + _, _ = progressPrinter.Printf("Decreasing iterations to %d\n", conf.localConfig.Iterations) + } else { + // decreasing iterations is not possible anymore, decreasing memory + // only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request + conf.localConfig.Memory -= 64 * bytesize.MB + _, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory) + } + // all values seem reasonable + default: + _, _ = progressPrinter.Println("These values look good to me.") + _, _ = progressPrinter.Println() + cmdx.PrintRow(cmd, conf) + return nil + } + } + + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Could not automatically determine good parameters. Have a look at all the measurements taken and select acceptable values yourself. Have a look in the docs for more information: https://www.ory.com/kratos/docs/debug/performance-out-of-memory-password-hashing-argon2") + cmdx.PrintTable(cmd, results) + return nil + }, + } + + flags := cmd.Flags() + + flags.IntVarP(&runs, FlagRuns, "r", 2, "Runs per probe, median of all runs is taken as the result.") + + flags.VarP(&flagConfig.localConfig.Memory, FlagStartMemory, "m", "Amount of memory to start probing at.") + flags.Var(&maxMemory, FlagMaxMemory, "Maximum memory allowed (0 means no limit).") + flags.Var(&adjustMemory, FlagAdjustMemory, "Amount by which the memory is adjusted in every step while probing.") + + flags.Uint32VarP(&flagConfig.localConfig.Iterations, FlagStartIterations, "i", 1, "Number of iterations to start probing at.") + + flags.Uint8(FlagMaxConcurrent, 16, "Maximum number of concurrent hashing operations.") + + registerArgon2ConstantConfigFlags(flags, flagConfig) + cmdx.RegisterFormatFlags(flags) + configx.RegisterFlags(flags) + + return cmd +} + +func probe(cmd *cobra.Command, hasher hash.Hasher, runs int, progressPrinter *cmdx.ConditionalPrinter) (time.Duration, error) { + // force GC at the start of the experiment + runtime.GC() + + start := time.Now() + + var mid time.Time + for i := 0; i < runs; i++ { + mid = time.Now() + _, err := hasher.Generate(cmd.Context(), []byte("password")) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate a hash: %s\n", err) + return 0, cmdx.FailSilently(cmd) + } + + _, _ = progressPrinter.Printf(" took %s in try %d\n", time.Since(mid), i) + } + return time.Duration(int64(time.Since(start)) / int64(runs)), nil +} diff --git a/cmd/hashers/argon2/hash.go b/cmd/hashers/argon2/hash.go new file mode 100644 index 0000000..6b654dc --- /dev/null +++ b/cmd/hashers/argon2/hash.go @@ -0,0 +1,83 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package argon2 + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/hash" + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/flagx" +) + +const ( + FlagParallel = "parallel" +) + +func newHashCmd() *cobra.Command { + flagConfig := &argon2Config{} + + cmd := &cobra.Command{ + Use: "hash [ ...]", + Short: "Hash a list of passwords for benchmarking the hashing parameters", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + conf, err := configProvider(cmd, flagConfig) + if err != nil { + return err + } + + flagConfig.ctx = cmd.Context() + + hasher := hash.NewHasherArgon2(conf) + hashes := make([][]byte, len(args)) + errs := make(chan error, len(args)) + + start := time.Now() + + for i, pw := range args { + go func(i int, pw string) { + start := time.Now() + h, err := hasher.Generate(cmd.Context(), []byte(pw)) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "password %d: %s\n", i, time.Since(start)) + + hashes[i] = h + errs <- err + }(i, pw) + + if !flagx.MustGetBool(cmd, FlagParallel) { + if err := <-errs; err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate hash: %s\n", err.Error()) + return cmdx.FailSilently(cmd) + } + } + } + + if flagx.MustGetBool(cmd, FlagParallel) { + for i := 0; i < len(args); i++ { + if err := <-errs; err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate hash: %s\n", err.Error()) + return cmdx.FailSilently(cmd) + } + } + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "total: %s\n", time.Since(start)) + return nil + }, + } + + flags := cmd.Flags() + + flags.Bool(FlagParallel, false, "Run all hashing operations in parallel.") + + registerArgon2ConfigFlags(flags, flagConfig) + configx.RegisterFlags(flags) + + return cmd +} diff --git a/cmd/hashers/argon2/loadtest.go b/cmd/hashers/argon2/loadtest.go new file mode 100644 index 0000000..3018ef6 --- /dev/null +++ b/cmd/hashers/argon2/loadtest.go @@ -0,0 +1,269 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package argon2 + +import ( + "context" + "fmt" + "math/rand" + "runtime" + "strconv" + "time" + + "github.com/ory/x/flagx" + + "github.com/fatih/color" + "github.com/inhies/go-bytesize" + "github.com/montanaflynn/stats" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" + + "github.com/ory/kratos/hash" + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" +) + +type resultTable struct { + TotalTime time.Duration `json:"total_time"` + MedianTime time.Duration `json:"median_request_time"` + StdDev time.Duration `json:"std_deviation"` + MinTime time.Duration `json:"min_request_time"` + MaxTime time.Duration `json:"max_request_time"` + MaxMem bytesize.ByteSize `json:"mem_used"` +} + +var ( + ErrSampleTimeExceeded = fmt.Errorf("the sample time was exceeded") + ErrMemoryConsumptionExceeded = fmt.Errorf("the memory consumption was exceeded") + + _ cmdx.TableRow = &resultTable{} +) + +func (r *resultTable) Header() []string { + return []string{"TOTAL SAMPLE TIME", "MEDIAN REQUEST TIME", "STANDARD DEVIATION", "MIN REQUEST TIME", "MAX REQUEST TIME", "MEMOry USED"} +} + +func (r *resultTable) Columns() []string { + return []string{ + r.TotalTime.String(), + r.MedianTime.String(), + r.StdDev.String(), + r.MinTime.String(), + r.MaxTime.String(), + r.MaxMem.String(), + } +} + +func (r *resultTable) Interface() interface{} { + return r +} + +func newLoadTestCmd() *cobra.Command { + flagConf := &argon2Config{} + + cmd := &cobra.Command{ + Use: "load-test ", + Short: "Simulate the password hashing with a number of concurrent requests/minute.", + Long: "Simulates a number of concurrent authentication requests per minute. Gives statistical data about the measured performance and resource consumption. Can be used to tune and test the hashing parameters for peak demand situations.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + perMinute, err := strconv.ParseInt(args[0], 0, 0) + if err != nil { + return err + } + + flagConf.ctx = cmd.Context() + + conf, err := configProvider(cmd, flagConf) + if err != nil { + return err + } + + if !flagx.MustGetBool(cmd, cmdx.FlagQuiet) { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "The hashing configuration used is:") + cmdx.PrintRow(cmd, conf) + } + + res, err := runLoadTest(cmd, conf, int(perMinute)) + if err != nil { + return err + } + + cmdx.PrintRow(cmd, res) + return nil + }, + } + + registerArgon2ConfigFlags(cmd.Flags(), flagConf) + configx.RegisterFlags(cmd.Flags()) + cmdx.RegisterFormatFlags(cmd.Flags()) + + return cmd +} + +func runLoadTest(cmd *cobra.Command, conf *argon2Config, reqPerMin int) (*resultTable, error) { + // force GC at the start of the experiment + runtime.GC() + + sampleTime := time.Minute / 3 + reqNum := reqPerMin / int(time.Minute/sampleTime) + + progressPrinter := cmdx.NewLoudErrPrinter(cmd) + _, _ = progressPrinter.Printf("It takes about %s to collect all necessary data, please be patient.\n", sampleTime) + + ctx, cancel := context.WithCancel(cmd.Context()) + hasher := hash.NewHasherArgon2(conf) + allDone := make(chan struct{}) + startAll := time.Now() + var cancelReason error + + var memStats []uint64 + go func() { + clock := time.NewTicker(time.Second) + defer func() { + clock.Stop() + }() + + for { + select { + case <-cmd.Context().Done(): + return + case <-allDone: + return + case <-clock.C: + // cancel if the allowed time is exceeded by 110% + if time.Since(startAll) > ((sampleTime+conf.localConfig.ExpectedDuration+conf.localConfig.ExpectedDeviation)/100)*110 { + cancelReason = ErrSampleTimeExceeded + cancel() + return + } + + ms := runtime.MemStats{} + runtime.ReadMemStats(&ms) + + // cancel if memory is exceeded by 110% + if ms.HeapAlloc > (uint64(conf.localConfig.DedicatedMemory)/100)*110 { + cancelReason = ErrMemoryConsumptionExceeded + cancel() + return + } + + memStats = append(memStats, ms.HeapAlloc) + } + } + }() + + go func() { + // don't read std_in when quiet + if flagx.MustGetBool(cmd, cmdx.FlagQuiet) { + return + } + + input := make([]byte, 1) + for { + n, err := cmd.InOrStdin().Read(input) + if err != nil { + return + } + if n != 0 { + _, _ = color.New(color.FgRed).Fprintln(cmd.ErrOrStderr(), "I SAID BE PATIENT!!!") + return + } + + select { + case <-allDone: + return + case <-cmd.Context().Done(): + return + case <-time.After(time.Millisecond): + } + } + }() + + calcTimes := make([]time.Duration, reqNum) + eg, _ := errgroup.WithContext(ctx) + + for i := 0; i < reqNum; i++ { + eg.Go(func(i int) func() error { + return func() error { + // wait randomly before starting, max. sample time + //#nosec G404 -- just a timeout to collect statistical data + t := time.Duration(rand.Intn(int(sampleTime))) + timer := time.NewTimer(t) + defer timer.Stop() + + select { + case <-ctx.Done(): + return nil + case <-timer.C: + } + + start := time.Now() + done := make(chan struct{}) + var err error + + go func() { + _, err = hasher.Generate(ctx, []byte("password")) + close(done) + }() + + select { + case <-ctx.Done(): + return nil + case <-done: + if err != nil { + return err + } + + calcTimes[i] = time.Since(start) + return nil + } + } + }(i)) + } + + if err := eg.Wait(); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Error during hashing: %+v\n", err) + return nil, cmdx.FailSilently(cmd) + } + switch cancelReason { + case ErrSampleTimeExceeded: + memUsed, err2 := stats.LoadRawData(memStats).Max() + if err2 != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", cancelReason, memStats) + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "The hashing load test took too long. This indicates that you don't have enough resources to handle %d login requests per minute with the desired minimal time of %s. The memory used was %s. Either dedicate more CPU/memory, or decrease the hashing cost (memory and iterations parameters).\n", reqPerMin, conf.localConfig.ExpectedDuration, bytesize.ByteSize(memUsed)) + return nil, cmdx.FailSilently(cmd) + case ErrMemoryConsumptionExceeded: + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "The hashing load test exceeded the memory limit of %s. This indicates that you don't have enough resources to handle %d login requests per minute with the desired minimal time of %s. Either dedicate more memory, or decrease the hashing cost (memory and iterations parameters).\n", conf.localConfig.DedicatedMemory, reqPerMin, conf.localConfig.ExpectedDuration) + return nil, cmdx.FailSilently(cmd) + } + + totalTime := time.Since(startAll) + close(allDone) + + calcData := stats.LoadRawData(calcTimes) + + duration := func(f func() (float64, error)) time.Duration { + v, err := f() + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", err, calcTimes) + } + return time.Duration(int64(v)) + } + + memUsed, err := stats.LoadRawData(memStats).Max() + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", err, memStats) + } + + return &resultTable{ + TotalTime: totalTime, + MedianTime: duration(calcData.Mean), + StdDev: duration(calcData.StandardDeviation), + MinTime: duration(calcData.Min), + MaxTime: duration(calcData.Max), + MaxMem: bytesize.ByteSize(memUsed), + }, nil +} diff --git a/cmd/hashers/argon2/root.go b/cmd/hashers/argon2/root.go new file mode 100644 index 0000000..2282f04 --- /dev/null +++ b/cmd/hashers/argon2/root.go @@ -0,0 +1,177 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package argon2 + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/ory/x/contextx" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/logrusx" +) + +const ( + FlagIterations = "iterations" + FlagParallelism = "parallelism" + FlagSaltLength = "salt-length" + FlagKeyLength = "key-length" + FlagMemory = "memory" + FlagDedicatedMemory = "dedicated-memory" + FlagMinimalDuration = "min-duration" + FlagExpectedDeviation = "expected-deviation" +) + +var rootCmd = &cobra.Command{ + Use: "argon2", +} + +func RegisterCommandRecursive(parent *cobra.Command) { + parent.AddCommand(rootCmd) + + rootCmd.AddCommand(newCalibrateCmd(), newHashCmd(), newLoadTestCmd()) +} + +func registerArgon2ConstantConfigFlags(flags *pflag.FlagSet, c *argon2Config) { + // set default value first + c.localConfig.DedicatedMemory = config.Argon2DefaultDedicatedMemory + flags.Var(&c.localConfig.DedicatedMemory, FlagDedicatedMemory, "Amount of memory dedicated for password hashing. Kratos will try to not consume more memory.") + flags.DurationVar(&c.localConfig.ExpectedDuration, FlagMinimalDuration, config.Argon2DefaultDuration, "Minimal duration a hashing operation (~login request) takes.") + flags.DurationVar(&c.localConfig.ExpectedDeviation, FlagExpectedDeviation, config.Argon2DefaultDeviation, "Expected deviation of the time a hashing operation (~login request) takes.") + + flags.Uint8Var(&c.localConfig.Parallelism, FlagParallelism, config.Argon2DefaultParallelism, "Number of threads to use.") + + flags.Uint32Var(&c.localConfig.SaltLength, FlagSaltLength, config.Argon2DefaultSaltLength, "Length of the salt in bytes.") + flags.Uint32Var(&c.localConfig.KeyLength, FlagKeyLength, config.Argon2DefaultKeyLength, "Length of the key in bytes.") +} + +func registerArgon2ConfigFlags(flags *pflag.FlagSet, c *argon2Config) { + flags.Uint32Var(&c.localConfig.Iterations, FlagIterations, 1, "Number of iterations to start probing at.") + + // set default value first + c.localConfig.Memory = config.Argon2DefaultMemory + flags.Var(&c.localConfig.Memory, FlagMemory, "Memory to use.") + + registerArgon2ConstantConfigFlags(flags, c) +} + +func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config, error) { + l := logrusx.New("Ory Kratos", config.Version) + conf := &argon2Config{} + var err error + conf.config, err = config.New( + cmd.Context(), + l, + cmd.ErrOrStderr(), + &contextx.Default{}, + configx.WithFlags(cmd.Flags()), + configx.SkipValidation(), + configx.WithContext(cmd.Context()), + configx.WithImmutables("hashers"), + ) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to initialize the config provider: %s\n", err) + return nil, cmdx.FailSilently(cmd) + } + conf.localConfig = *conf.config.HasherArgon2(cmd.Context()) + + if cmd.Flags().Changed(FlagIterations) { + conf.localConfig.Iterations = flagConf.localConfig.Iterations + } + if cmd.Flags().Changed(FlagParallelism) { + conf.localConfig.Parallelism = flagConf.localConfig.Parallelism + } + if cmd.Flags().Changed(FlagMemory) { + conf.localConfig.Memory = flagConf.localConfig.Memory + } + if cmd.Flags().Changed(FlagDedicatedMemory) { + conf.localConfig.DedicatedMemory = flagConf.localConfig.DedicatedMemory + } + if cmd.Flags().Changed(FlagKeyLength) { + conf.localConfig.KeyLength = flagConf.localConfig.KeyLength + } + if cmd.Flags().Changed(FlagSaltLength) { + conf.localConfig.SaltLength = flagConf.localConfig.SaltLength + } + if cmd.Flags().Changed(FlagExpectedDeviation) { + conf.localConfig.ExpectedDeviation = flagConf.localConfig.ExpectedDeviation + } + if cmd.Flags().Changed(FlagMinimalDuration) { + conf.localConfig.ExpectedDuration = flagConf.localConfig.ExpectedDuration + } + + return conf, nil +} + +type ( + argon2Config struct { + localConfig config.Argon2 + config *config.Config + ctx context.Context + } +) + +var _ cmdx.TableRow = (*argon2Config)(nil) + +func (c *argon2Config) Header() []string { + var header []string + + t := reflect.TypeOf(c.localConfig) + for i := 0; i < t.NumField(); i++ { + header = append(header, strings.ReplaceAll(strings.ToUpper(t.Field(i).Tag.Get("json")), "_", " ")) + } + + return header +} + +func (c *argon2Config) Columns() []string { + conf, _ := c.HasherArgon2() + return []string{ + conf.Memory.String(), + fmt.Sprintf("%d", conf.Iterations), + fmt.Sprintf("%d", conf.Parallelism), + fmt.Sprintf("%d", conf.SaltLength), + fmt.Sprintf("%d", conf.KeyLength), + conf.ExpectedDuration.String(), + conf.ExpectedDeviation.String(), + conf.DedicatedMemory.String(), + } +} + +func (c *argon2Config) Interface() interface{} { + i, _ := c.HasherArgon2() + return i +} + +func (c *argon2Config) Config() *config.Config { + ac, _ := c.HasherArgon2() + for k, v := range map[string]interface{}{ + config.ViperKeyHasherArgon2ConfigIterations: ac.Iterations, + config.ViperKeyHasherArgon2ConfigMemory: ac.Memory, + config.ViperKeyHasherArgon2ConfigParallelism: ac.Parallelism, + config.ViperKeyHasherArgon2ConfigDedicatedMemory: ac.DedicatedMemory, + config.ViperKeyHasherArgon2ConfigKeyLength: ac.KeyLength, + config.ViperKeyHasherArgon2ConfigSaltLength: ac.SaltLength, + config.ViperKeyHasherArgon2ConfigExpectedDuration: ac.ExpectedDuration, + config.ViperKeyHasherArgon2ConfigExpectedDeviation: ac.ExpectedDeviation, + } { + _ = c.config.Set(c.ctx, k, v) + } + return c.config +} + +func (c *argon2Config) HasherArgon2() (*config.Argon2, error) { + if c.localConfig.Memory == 0 { + c.localConfig.Memory = config.Argon2DefaultMemory + } + return &c.localConfig, nil +} diff --git a/cmd/hashers/root.go b/cmd/hashers/root.go new file mode 100644 index 0000000..e7b57d2 --- /dev/null +++ b/cmd/hashers/root.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hashers + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/hashers/argon2" +) + +func NewRootCmd() *cobra.Command { + c := &cobra.Command{ + Use: "hashers", + Short: "This command contains helpers around hashing", + } + return c +} + +func RegisterCommandRecursive(parent *cobra.Command) { + rootCmd := NewRootCmd() + parent.AddCommand(rootCmd) + + argon2.RegisterCommandRecursive(rootCmd) +} diff --git a/cmd/identities/definitions.go b/cmd/identities/definitions.go new file mode 100644 index 0000000..881d94a --- /dev/null +++ b/cmd/identities/definitions.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities + +import ( + "strings" + + kratos "github.com/ory/kratos/pkg/httpclient" + + "github.com/ory/x/cmdx" +) + +type ( + outputIdentity kratos.Identity + outputIdentityCollection struct { + Identities []kratos.Identity `json:"identities"` + NextPageToken string `json:"next_page_token"` + includePageToken bool + } +) + +func (outputIdentity) Header() []string { + return []string{"ID", "EXTERNAL ID", "VERIFIED ADDRESSES", "RECOVERY ADDRESSES", "SCHEMA ID", "SCHEMA URL"} +} + +func (i outputIdentity) Columns() []string { + data := [6]string{ + i.Id, + cmdx.None, + cmdx.None, + cmdx.None, + cmdx.None, + cmdx.None, + } + + if i.ExternalId != nil && len(*i.ExternalId) > 0 { + data[1] = *i.ExternalId + } + + addresses := make([]string, 0, len(i.VerifiableAddresses)) + for _, a := range i.VerifiableAddresses { + if len(a.Value) > 0 { + addresses = append(addresses, a.Value) + } + } + data[2] = strings.Join(addresses, ", ") + + addresses = addresses[:0] + for _, a := range i.RecoveryAddresses { + if len(a.Value) > 0 { + addresses = append(addresses, a.Value) + } + } + data[3] = strings.Join(addresses, ", ") + data[4] = i.SchemaId + data[5] = i.SchemaUrl + + return data[:] +} + +func (i outputIdentity) Interface() interface{} { + return i +} + +func (outputIdentityCollection) Header() []string { + return outputIdentity{}.Header() +} + +func (c outputIdentityCollection) Table() [][]string { + rows := make([][]string, len(c.Identities)) + for i, ident := range c.Identities { + rows[i] = outputIdentity(ident).Columns() + } + return append(rows, + []string{""}, + []string{"NEXT PAGE TOKEN", c.NextPageToken}, + ) +} + +func (c outputIdentityCollection) Interface() interface{} { + if c.includePageToken { + return c + } + return c.Identities +} + +func (c *outputIdentityCollection) Len() int { + return len(c.Identities) +} diff --git a/cmd/identities/delete.go b/cmd/identities/delete.go new file mode 100644 index 0000000..e92e7c7 --- /dev/null +++ b/cmd/identities/delete.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cliclient" + "github.com/ory/kratos/pkg/clihelpers" + "github.com/ory/x/cmdx" +) + +func NewDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete resources", + } + cmd.AddCommand(NewDeleteIdentityCmd()) + cliclient.RegisterClientFlags(cmd.PersistentFlags()) + cmdx.RegisterFormatFlags(cmd.PersistentFlags()) + return cmd +} + +func NewDeleteIdentityCmd() *cobra.Command { + return &cobra.Command{ + Use: "identity id-0 [id-1] [id-2] [id-n]", + Short: "Delete one or more identities by their ID(s)", + Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq. + +%s`, clihelpers.WarningJQIsComplicated), + Example: `To delete the identity with the recovery email address "foo@bar.com", run: + + {{ .CommandPath }} $({{ .Root.Name }} list identities --format json | jq -r 'map(select(.recovery_addresses[].value == "foo@bar.com")) | .[].id')`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := cliclient.NewClient(cmd) + if err != nil { + return err + } + + var ( + deleted = make([]cmdx.OutputIder, 0, len(args)) + failed = make(map[string]error) + ) + + for _, a := range args { + _, err := c.IdentityAPI.DeleteIdentity(cmd.Context(), a).Execute() + if err != nil { + failed[a] = cmdx.PrintOpenAPIError(cmd, err) + continue + } + deleted = append(deleted, cmdx.OutputIder(a)) + } + + if len(deleted) == 1 { + cmdx.PrintRow(cmd, &deleted[0]) + } else if len(deleted) > 1 { + cmdx.PrintTable(cmd, &cmdx.OutputIderCollection{Items: deleted}) + } + + cmdx.PrintErrors(cmd, failed) + if len(failed) != 0 { + return cmdx.FailSilently(cmd) + } + + return nil + }, + } +} diff --git a/cmd/identities/delete_test.go b/cmd/identities/delete_test.go new file mode 100644 index 0000000..6b3ac2b --- /dev/null +++ b/cmd/identities/delete_test.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities_test + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/cmd/identities" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlcon" +) + +func TestDeleteCmd(t *testing.T) { + reg, cmd := setup(t, identities.NewDeleteIdentityCmd) + + t.Run("case=deletes successfully", func(t *testing.T) { + // create identity to delete + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) + + stdOut := cmd.ExecNoErr(t, i.ID.String()) + + // expect ID and no error + assert.Equal(t, i.ID.String(), gjson.Parse(stdOut).String()) + + // expect identity to be deleted + _, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing) + assert.True(t, errors.Is(err, sqlcon.ErrNoRows())) + }) + + t.Run("case=deletes three identities", func(t *testing.T) { + is, ids := makeIdentities(t, reg, 3) + + stdOut := cmd.ExecNoErr(t, ids...) + + assert.Equal(t, `["`+strings.Join(ids, "\",\"")+"\"]\n", stdOut) + + for _, i := range is { + _, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing) + assert.Error(t, err) + } + }) + + t.Run("case=fails with unknown ID", func(t *testing.T) { + stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String()) + + assert.Contains(t, stdErr, "Unable to locate the resource", stdErr) + }) +} diff --git a/cmd/identities/get.go b/cmd/identities/get.go new file mode 100644 index 0000000..36ed6e2 --- /dev/null +++ b/cmd/identities/get.go @@ -0,0 +1,116 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities + +import ( + "fmt" + + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/x/cmdx" + "github.com/ory/x/stringsx" + + "github.com/ory/kratos/pkg/clihelpers" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cliclient" +) + +const ( + FlagIncludeCreds = "include-credentials" + FlagExternalID = "external-id" +) + +func NewGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get resources", + } + cmd.AddCommand(NewGetIdentityCmd()) + cliclient.RegisterClientFlags(cmd.PersistentFlags()) + cmdx.RegisterFormatFlags(cmd.PersistentFlags()) + return cmd +} + +func NewGetIdentityCmd() *cobra.Command { + var ( + includeCreds []string + byExternalID bool + ) + + cmd := &cobra.Command{ + Use: "identity [id-1] [id-2] [id-n]", + Short: "Get one or more identities by their ID(s)", + Long: fmt.Sprintf(`This command gets all the details about an identity. To get an identity by some selector, e.g. the recovery email address, use the list command in combination with jq. + +Use the --%s flag to look up identities by their external ID instead of their Ory-internal ID. + +%s`, FlagExternalID, clihelpers.WarningJQIsComplicated), + Example: `To get the identities with the recovery email address at the domain "ory.sh", run: + + {{ .CommandPath }} $({{ .Root.Name }} ls identities --format json | jq -r 'map(select(.recovery_addresses[].value | endswith("@ory.sh"))) | .[].id') + +To get an identity by its external ID, run: + + {{ .CommandPath }} --external-id customer-12345`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := cliclient.NewClient(cmd) + if err != nil { + return err + } + + // we check includeCreds argument is valid + for _, opt := range includeCreds { + e := stringsx.SwitchExact(opt) + if !e.AddCase("oidc") { + cmd.PrintErrln(`You have to put a valid value of credentials type to be included, try --help for details.`) + return cmdx.FailSilently(cmd) + } + } + + identities := make([]kratos.Identity, 0, len(args)) + failed := make(map[string]error) + for _, id := range args { + var identity *kratos.Identity + if byExternalID { + identity, _, err = c.IdentityAPI. + GetIdentityByExternalID(cmd.Context(), id). + IncludeCredential(includeCreds). + Execute() + } else { + identity, _, err = c.IdentityAPI. + GetIdentity(cmd.Context(), id). + IncludeCredential(includeCreds). + Execute() + } + + if clihelpers.SDKError(err) != nil { + failed[id] = cmdx.PrintOpenAPIError(cmd, err) + continue + } + + identities = append(identities, *identity) + } + + if len(identities) == 1 { + cmdx.PrintRow(cmd, (*outputIdentity)(&identities[0])) + } else if len(identities) > 1 { + cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: identities, includePageToken: false}) + } + cmdx.PrintErrors(cmd, failed) + + if len(failed) != 0 { + return cmdx.FailSilently(cmd) + } + return nil + }, + } + + flags := cmd.Flags() + // include credential flag to add third party tokens in returned data + flags.StringArrayVarP(&includeCreds, FlagIncludeCreds, "i", []string{}, `Include third party tokens (only "oidc" supported) `) + flags.BoolVar(&byExternalID, FlagExternalID, false, "Treat the provided ID(s) as external IDs and look up the identities by their external ID instead of their Ory-internal ID.") + return cmd +} diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go new file mode 100644 index 0000000..fab576c --- /dev/null +++ b/cmd/identities/get_test.go @@ -0,0 +1,132 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/ory/kratos/cmd/identities" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/x" + + "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" +) + +func TestGetCmd(t *testing.T) { + reg, cmd := setup(t, identities.NewGetIdentityCmd) + + t.Run("case=gets a single identity", func(t *testing.T) { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.MetadataPublic = []byte(`"public"`) + i.MetadataAdmin = []byte(`"admin"`) + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) + + stdOut := cmd.ExecNoErr(t, i.ID.String()) + + ij, err := json.Marshal(identity.WithCredentialsNoConfigAndAdminMetadataInJSON(*i)) + require.NoError(t, err) + + assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"}) + }) + + t.Run("case=gets three identities", func(t *testing.T) { + is, ids := makeIdentities(t, reg, 3) + + stdOut := cmd.ExecNoErr(t, ids...) + + isj, err := json.Marshal(is) + require.NoError(t, err) + + assertx.EqualAsJSONExcept(t, json.RawMessage(isj), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"}) + }) + + t.Run("case=fails with unknown ID", func(t *testing.T) { + stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String()) + + assert.Contains(t, stdErr, "Unable to locate the resource", stdErr) + }) + + t.Run("case=gets a single identity by its external ID", func(t *testing.T) { + externalID := "customer-" + x.NewUUID().String() + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.ExternalID = sqlxx.NullString(externalID) + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) + + stdOut := cmd.ExecNoErr(t, "--"+identities.FlagExternalID, externalID) + + assert.Equal(t, i.ID.String(), gjson.Get(stdOut, "id").String()) + assert.Equal(t, externalID, gjson.Get(stdOut, "external_id").String()) + }) + + t.Run("case=fails with unknown external ID", func(t *testing.T) { + stdErr := cmd.ExecExpectedErr(t, "--"+identities.FlagExternalID, "does-not-exist") + + assert.Contains(t, stdErr, "Unable to locate the resource", stdErr) + }) + + t.Run("case=gets a single identity with oidc credentials", func(t *testing.T) { + applyCredentials := func(identifier, accessToken, refreshToken, idToken string, encrypt bool) identity.Credentials { + toJson := func(c identity.CredentialsOIDC) []byte { + out, err := json.Marshal(&c) + require.NoError(t, err) + return out + } + transform := func(token string) string { + if encrypt { + s, err := reg.Cipher(context.Background()).Encrypt(context.Background(), []byte(token)) + require.NoError(t, err) + return s + } + return token + } + return identity.Credentials{ + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"bar:" + identifier}, + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + { + Subject: "foo", + Provider: "bar", + InitialAccessToken: transform(accessToken + "0"), + InitialRefreshToken: transform(refreshToken + "0"), + InitialIDToken: transform(idToken + "0"), + Organization: "foo-org-id", + }, + { + Subject: "baz", + Provider: "zab", + InitialAccessToken: transform(accessToken + "1"), + InitialRefreshToken: transform(refreshToken + "1"), + InitialIDToken: transform(idToken + "1"), + Organization: "bar-org-id", + }, + }}), + } + } + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.MetadataPublic = []byte(`"public"`) + i.MetadataAdmin = []byte(`"admin"`) + i.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", true)) + // duplicate identity with decrypted tokens + di := i.CopyWithoutCredentials() + di.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", false)) + + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) + + stdOut := cmd.ExecNoErr(t, "--"+identities.FlagIncludeCreds, "oidc", i.ID.String()) + ij, err := json.Marshal(identity.WithCredentialsAndAdminMetadataInJSON(*di)) + require.NoError(t, err) + + ii := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at", "credentials.oidc.created_at", "credentials.oidc.updated_at", "credentials.oidc.version", "AdditionalProperties"} + assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), ii) + }) +} diff --git a/cmd/identities/helpers.go b/cmd/identities/helpers.go new file mode 100644 index 0000000..4ed93e6 --- /dev/null +++ b/cmd/identities/helpers.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/tidwall/gjson" + + "github.com/ory/x/cmdx" +) + +func parseIdentities(raw []byte) (rawIdentities []string) { + res := gjson.ParseBytes(raw) + if !res.IsArray() { + return []string{res.Raw} + } + res.ForEach(func(_, v gjson.Result) bool { + rawIdentities = append(rawIdentities, v.Raw) + return true + }) + return +} + +func readIdentities(cmd *cobra.Command, args []string) (map[string]string, error) { + rawIdentities := make(map[string]string) + if len(args) == 0 { + fc, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "STD_IN: Could not read: %s\n", err) + return nil, cmdx.FailSilently(cmd) + } + for i, id := range parseIdentities(fc) { + rawIdentities[fmt.Sprintf("STD_IN[%d]", i)] = id + } + return rawIdentities, nil + } + for _, fn := range args { + fc, err := os.ReadFile(fn) // #nosec G304 -- file is supplied by user + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not open identity file: %s\n", fn, err) + return nil, cmdx.FailSilently(cmd) + } + for i, id := range parseIdentities(fc) { + rawIdentities[fmt.Sprintf("%s[%d]", fn, i)] = id + } + } + return rawIdentities, nil +} diff --git a/cmd/identities/helpers_test.go b/cmd/identities/helpers_test.go new file mode 100644 index 0000000..59c9758 --- /dev/null +++ b/cmd/identities/helpers_test.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities_test + +import ( + "context" + "testing" + + "github.com/ory/x/cmdx" + + "github.com/ory/kratos/identity" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cmd/cliclient" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" +) + +func setup(t *testing.T, newCmd func() *cobra.Command) (*driver.RegistryDefault, *cmdx.CommandExecuter) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + _, admin := testhelpers.NewKratosServerWithCSRF(t, reg) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stubs/identity.schema.json") + // setup command + return reg, &cmdx.CommandExecuter{ + New: func() *cobra.Command { + cmd := newCmd() + cliclient.RegisterClientFlags(cmd.Flags()) + cmdx.RegisterFormatFlags(cmd.Flags()) + return cmd + }, + PersistentArgs: []string{"--" + cliclient.FlagEndpoint, admin.URL, "--" + cmdx.FlagFormat, string(cmdx.FormatJSON)}, + } +} + +func makeIdentities(t *testing.T, reg driver.Registry, n int) (is []*identity.Identity, ids []string) { + for j := 0; j < n; j++ { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.MetadataPublic = []byte(`{"foo":"bar"}`) + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) + is = append(is, i) + ids = append(ids, i.ID.String()) + } + return +} diff --git a/cmd/identities/import.go b/cmd/identities/import.go new file mode 100644 index 0000000..eae37e0 --- /dev/null +++ b/cmd/identities/import.go @@ -0,0 +1,98 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identities + +import ( + "encoding/json" + "fmt" + + kratos "github.com/ory/kratos/pkg/httpclient" + + "github.com/ory/x/cmdx" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cliclient" +) + +func NewImportCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "import", + Short: "Import resources", + } + cmd.AddCommand(NewImportIdentitiesCmd()) + cliclient.RegisterClientFlags(cmd.PersistentFlags()) + cmdx.RegisterFormatFlags(cmd.PersistentFlags()) + return cmd +} + +// NewImportIdentitiesCmd represents the import command +func NewImportIdentitiesCmd() *cobra.Command { + return &cobra.Command{ + Use: "identities file-1.json [file-2.json] [file-3.json] [file-n.json]", + Short: "Import one or more identities from files or STD_IN", + Example: `Create an example identity: + + cat > ./file.json < --start-after verifiable=") + + return c +} diff --git a/cmd/migrate/normalize_phone_handler.go b/cmd/migrate/normalize_phone_handler.go new file mode 100644 index 0000000..5a15eac --- /dev/null +++ b/cmd/migrate/normalize_phone_handler.go @@ -0,0 +1,221 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "fmt" + "regexp" + "strings" + "text/tabwriter" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" + "github.com/ory/x/flagx" +) + +// phonePattern matches values that look like phone numbers: + followed by at least one digit, +// possibly with spaces, dashes, or parens. This filters out OIDC subjects and other +// identifiers that happen to start with +. +var phonePattern = regexp.MustCompile(`^\+[\d\s\-().]+$`) + +type NormalizePhoneHandler struct{} + +func NewNormalizePhoneHandler() *NormalizePhoneHandler { + return &NormalizePhoneHandler{} +} + +type normalizeStats struct { + scanned int + updated int + skipped int + errors int +} + +func (h *NormalizePhoneHandler) NormalizePhoneNumbers(cmd *cobra.Command, args []string, opts ...driver.RegistryOption) error { + d, err := getPersister(cmd, args, opts) + if err != nil { + return err + } + + conn := d.Persister().GetConnection(cmd.Context()) + batchSize := flagx.MustGetInt(cmd, "batch-size") + dryRun := flagx.MustGetBool(cmd, "dry-run") + batchDelay := flagx.MustGetDuration(cmd, "batch-delay") + + if dryRun { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Dry run mode enabled. No changes will be written.") + } + + tables := []tableConfig{ + { + key: "ci", + name: "Credential identifiers", + selectQuery: ` + SELECT id, identifier AS value + FROM identity_credential_identifiers + WHERE identifier LIKE '+%' + AND id > ? + ORDER BY id ASC LIMIT ?`, + updateQuery: `UPDATE identity_credential_identifiers SET identifier = ?, updated_at = ? WHERE id = ? AND identifier = ?`, + }, + { + key: "va", + name: "Verifiable addresses", + selectQuery: ` + SELECT id, value + FROM identity_verifiable_addresses + WHERE via = 'sms' + AND id > ? + AND value LIKE '+%' + ORDER BY id ASC LIMIT ?`, + updateQuery: `UPDATE identity_verifiable_addresses SET value = ?, updated_at = ? WHERE id = ? AND value = ?`, + }, + { + key: "ra", + name: "Recovery addresses", + selectQuery: ` + SELECT id, value + FROM identity_recovery_addresses + WHERE via = 'sms' + AND id > ? + AND value LIKE '+%' + ORDER BY id ASC LIMIT ?`, + updateQuery: `UPDATE identity_recovery_addresses SET value = ?, updated_at = ? WHERE id = ? AND value = ?`, + }, + } + + startAfterMap, err := parseStartAfter(flagx.MustGetStringSlice(cmd, "start-after")) + if err != nil { + return err + } + + allStats := make([]normalizeStats, len(tables)) + for i, table := range tables { + startAfter := startAfterMap[table.key] + if startAfter != uuid.Nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: resuming after ID %s\n", table.name, startAfter) + } + stats, err := normalizeTable(conn, batchSize, batchDelay, startAfter, dryRun, cmd, table) + if err != nil { + return errors.Wrapf(err, "normalizing %s", table.name) + } + allStats[i] = stats + } + + printSummary(cmd, tables, allStats) + + return nil +} + +type tableConfig struct { + key string + name string + selectQuery string + updateQuery string +} + +// parseStartAfter parses --start-after flags of the form "key=uuid". +func parseStartAfter(args []string) (map[string]uuid.UUID, error) { + result := make(map[string]uuid.UUID) + for _, arg := range args { + key, val, ok := strings.Cut(arg, "=") + if !ok { + return nil, errors.Errorf("invalid --start-after format %q: expected key=uuid (e.g. credentials=)", arg) + } + id, err := uuid.FromString(val) + if err != nil { + return nil, errors.Wrapf(err, "invalid UUID in --start-after %q", arg) + } + result[key] = id + } + return result, nil +} + +func printSummary(cmd *cobra.Command, tables []tableConfig, allStats []normalizeStats) { + out := cmd.OutOrStdout() + + _, _ = fmt.Fprintln(out) + + tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(tw, "\tSCANNED\tUPDATED\tSKIPPED\tERRORS") + + var total normalizeStats + for i, table := range tables { + s := allStats[i] + _, _ = fmt.Fprintf(tw, "%s\t%d\t%d\t%d\t%d\n", table.name, s.scanned, s.updated, s.skipped, s.errors) + total.scanned += s.scanned + total.updated += s.updated + total.skipped += s.skipped + total.errors += s.errors + } + _, _ = fmt.Fprintf(tw, "TOTAL\t%d\t%d\t%d\t%d\n", total.scanned, total.updated, total.skipped, total.errors) + _ = tw.Flush() +} + +func normalizeTable(conn *pop.Connection, batchSize int, batchDelay time.Duration, startAfter uuid.UUID, dryRun bool, cmd *cobra.Command, table tableConfig) (normalizeStats, error) { + var stats normalizeStats + lastID := startAfter + + for { + var rows []struct { + ID uuid.UUID `db:"id"` + Value string `db:"value"` + } + + if err := conn.RawQuery(table.selectQuery, lastID, batchSize).All(&rows); err != nil { + return stats, errors.Wrapf(err, "querying %s", table.name) + } + + if len(rows) == 0 { + break + } + + for _, row := range rows { + lastID = row.ID + stats.scanned++ + + // The SQL query pre-filters with LIKE '+%'. This Go-side regex additionally + // rejects non-phone identifiers (e.g. OIDC subjects) that happen to start + // with '+'. We cannot use SQL regex because SQLite does not support it. + if !phonePattern.MatchString(row.Value) { + stats.skipped++ + continue + } + + normalized := x.NormalizePhoneIdentifier(row.Value) + if normalized == row.Value { + stats.skipped++ + continue + } + + if dryRun { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "[dry-run] %s %s: %q -> %q\n", table.name, row.ID, row.Value, normalized) + stats.updated++ + continue + } + + now := time.Now().UTC().Truncate(time.Second) + if err := conn.RawQuery(table.updateQuery, normalized, now, row.ID, row.Value).Exec(); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "error updating %s %s (%q -> %q): %v\n", table.name, row.ID, row.Value, normalized, err) + stats.errors++ + continue + } + stats.updated++ + } + + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\tprocessed %d rows so far (--start-after %s=%s)\n", table.name, stats.scanned, table.key, lastID) + + if batchDelay > 0 { + time.Sleep(batchDelay) + } + } + + return stats, nil +} diff --git a/cmd/migrate/normalize_phone_handler_test.go b/cmd/migrate/normalize_phone_handler_test.go new file mode 100644 index 0000000..c8b7657 --- /dev/null +++ b/cmd/migrate/normalize_phone_handler_test.go @@ -0,0 +1,70 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/x" +) + +func TestNormalizePhoneIdentifier(t *testing.T) { + for _, tc := range []struct { + name string + input string + expected string + shouldSkip bool + }{ + { + name: "German phone with spaces", + input: "+49 176 671 11 638", + expected: "+4917667111638", + }, + { + name: "German phone with dashes", + input: "+49-176-671-11-638", + expected: "+4917667111638", + }, + { + name: "Already normalized E.164", + input: "+4917667111638", + expected: "+4917667111638", + shouldSkip: true, + }, + { + name: "US phone with parens", + input: "+1 (415) 555-2671", + expected: "+14155552671", + }, + { + name: "Malaysian phone with extra zero", + input: "+600136866982", + expected: "+60136866982", + }, + { + name: "Not a valid phone number", + input: "+invalid", + expected: "+invalid", + shouldSkip: true, + }, + { + name: "Email address (not a phone)", + input: "+foo@bar.com", + expected: "+foo@bar.com", + shouldSkip: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + normalized := x.NormalizePhoneIdentifier(tc.input) + assert.Equal(t, tc.expected, normalized) + if tc.shouldSkip { + assert.Equal(t, tc.input, normalized, "should be unchanged (skip)") + } else { + assert.NotEqual(t, tc.input, normalized, "should be changed (update)") + } + }) + } +} diff --git a/cmd/migrate/root.go b/cmd/migrate/root.go new file mode 100644 index 0000000..32f3c5f --- /dev/null +++ b/cmd/migrate/root.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver" + "github.com/ory/x/configx" + "github.com/ory/x/popx" +) + +func NewMigrateCmd() *cobra.Command { + return &cobra.Command{ + Use: "migrate", + Short: "Various migration helpers", + } +} + +func RegisterCommandRecursive(parent *cobra.Command) { + c := NewMigrateCmd() + + configx.RegisterFlags(c.PersistentFlags()) + c.AddCommand(NewMigrateSQLCmd()) + c.AddCommand(NewNormalizePhoneCmd()) + + parent.AddCommand(c) +} + +func NewMigrateSQLDownCmd(opts ...driver.RegistryOption) *cobra.Command { + return popx.NewMigrateSQLDownCmd(func(cmd *cobra.Command, args []string) error { + return NewMigrateHandler().MigrateSQLDown(cmd, args, opts...) + }) +} + +func NewMigrateSQLUpCmd(opts ...driver.RegistryOption) *cobra.Command { + return popx.NewMigrateSQLUpCmd(func(cmd *cobra.Command, args []string) error { + return NewMigrateHandler().MigrateSQLUp(cmd, args, opts...) + }) +} + +func NewMigrateSQLStatusCmd(opts ...driver.RegistryOption) *cobra.Command { + return popx.NewMigrateSQLStatusCmd(func(cmd *cobra.Command, args []string) error { + return NewMigrateHandler().MigrateSQLStatus(cmd, args, opts...) + }) +} diff --git a/cmd/migrate/sql.go b/cmd/migrate/sql.go new file mode 100644 index 0000000..cd1d24f --- /dev/null +++ b/cmd/migrate/sql.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/driver" + "github.com/ory/x/configx" +) + +func NewMigrateSQLCmd(opts ...driver.RegistryOption) *cobra.Command { + c := &cobra.Command{ + Use: "sql [database-url]", + Deprecated: "Please use `kratos migrate sql up|down|status` instead.", + Short: "Create SQL schemas and apply migration plans", + Long: `Run this command on a fresh SQL installation and when you upgrade Ory Kratos to a new minor version. + +It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet. +This decreases risk of failure and decreases time required. + +You can read in the database URL using the -e flag, for example: + export DSN=... + kratos migrate sql -e + +### WARNING ### + +Before running this command on an existing database, create a back up! +`, + RunE: func(cmd *cobra.Command, args []string) error { + return NewMigrateHandler().MigrateSQLUp(cmd, args, opts...) + }, + } + + configx.RegisterFlags(c.PersistentFlags()) + c.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") + c.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + + c.AddCommand(NewMigrateSQLStatusCmd(opts...)) + c.AddCommand(NewMigrateSQLUpCmd(opts...)) + c.AddCommand(NewMigrateSQLDownCmd(opts...)) + + return c +} diff --git a/cmd/remote/root.go b/cmd/remote/root.go new file mode 100644 index 0000000..77496c1 --- /dev/null +++ b/cmd/remote/root.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package remote + +import ( + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + + "github.com/ory/kratos/cmd/cliclient" +) + +var remoteCmd = &cobra.Command{ + Use: "remote", + Short: "Helpers and management for remote Ory Kratos instances", +} + +func RegisterCommandRecursive(parent *cobra.Command) { + parent.AddCommand(remoteCmd) + + remoteCmd.AddCommand(versionCmd) + remoteCmd.AddCommand(statusCmd) +} + +func init() { + cliclient.RegisterClientFlags(remoteCmd.PersistentFlags()) + cmdx.RegisterFormatFlags(remoteCmd.PersistentFlags()) +} diff --git a/cmd/remote/status.go b/cmd/remote/status.go new file mode 100644 index 0000000..7421eef --- /dev/null +++ b/cmd/remote/status.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package remote + +import ( + "github.com/ory/x/cmdx" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cliclient" +) + +type statusState struct { + Alive bool `json:"alive"` + Ready bool `json:"ready"` +} + +func (s *statusState) Header() []string { + return []string{"ALIVE", "READY"} +} + +func (s *statusState) Columns() []string { + f := [2]string{ + "false", + "false", + } + if s.Alive { + f[0] = "true" + } + if s.Ready { + f[1] = "true" + } + return f[:] +} + +func (s *statusState) Interface() interface{} { + return s +} + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Print the alive and readiness status of a Ory Kratos instance", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := cliclient.NewClient(cmd) + if err != nil { + return err + } + + state := &statusState{} + defer cmdx.PrintRow(cmd, state) + + 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() + if err != nil { + return err + } + + state.Ready = ready.Status == "ok" + return nil + }, +} diff --git a/cmd/remote/version.go b/cmd/remote/version.go new file mode 100644 index 0000000..23bf102 --- /dev/null +++ b/cmd/remote/version.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package remote + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cliclient" + "github.com/ory/x/cmdx" +) + +type versionValue struct { + Version string `json:"version"` +} + +func (v *versionValue) Header() []string { + return []string{"VERSION"} +} + +func (v *versionValue) Columns() []string { + return []string{v.Version} +} + +func (v *versionValue) Interface() interface{} { + return v +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version of an Ory Kratos instance", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := cliclient.NewClient(cmd) + if err != nil { + return err + } + + resp, _, err := c.MetadataAPI.GetVersion(cmd.Context()).Execute() + if err != nil { + return err + } + + cmdx.PrintRow(cmd, &versionValue{Version: resp.Version}) + return nil + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e3e1127 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "errors" + "fmt" + "runtime" + + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/cleanup" + "github.com/ory/kratos/cmd/courier" + "github.com/ory/kratos/cmd/hashers" + "github.com/ory/kratos/cmd/identities" + "github.com/ory/kratos/cmd/jsonnet" + "github.com/ory/kratos/cmd/migrate" + "github.com/ory/kratos/cmd/remote" + "github.com/ory/kratos/cmd/serve" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/cmdx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/profilex" +) + +func NewRootCmd(driverOpts ...driver.RegistryOption) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "kratos", + } + cmdx.EnableUsageTemplating(cmd) + + courier.RegisterCommandRecursive(cmd, driverOpts) + cmd.AddCommand(identities.NewGetCmd()) + cmd.AddCommand(identities.NewDeleteCmd()) + cmd.AddCommand(jsonnet.NewFormatCmd()) + hashers.RegisterCommandRecursive(cmd) + cmd.AddCommand(identities.NewImportCmd()) + cmd.AddCommand(jsonnet.NewLintCmd()) + cmd.AddCommand(identities.NewListCmd()) + migrate.RegisterCommandRecursive(cmd) + serve.RegisterCommandRecursive(cmd, driverOpts) + cleanup.RegisterCommandRecursive(cmd) + remote.RegisterCommandRecursive(cmd) + cmd.AddCommand(identities.NewValidateCmd()) + cmd.AddCommand(cmdx.Version(&config.Version, &config.Commit, &config.Date)) + + // Registers a hidden "jsonnet" subcommand for process-isolated Jsonnet VMs. + cmd.AddCommand(jsonnetsecure.NewJsonnetCmd()) + + return cmd +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the RootCmd. +func Execute() int { + defer profilex.Profile().Stop() + + jsonnetPool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) + defer jsonnetPool.Close() + + c := NewRootCmd(driver.WithJsonnetPool(jsonnetPool)) + + if err := c.Execute(); err != nil { + if !errors.Is(err, cmdx.ErrNoPrintButFail) { + _, _ = fmt.Fprintln(c.ErrOrStderr(), err) + } + return 1 + } + return 0 +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..df00363 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "testing" + + "github.com/ory/x/cmdx" +) + +func TestUsageStrings(t *testing.T) { + cmdx.AssertUsageTemplates(t, NewRootCmd()) +} diff --git a/cmd/serve/root.go b/cmd/serve/root.go new file mode 100644 index 0000000..c6ca8c7 --- /dev/null +++ b/cmd/serve/root.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package serve + +import ( + "github.com/spf13/cobra" + + "github.com/ory/kratos/cmd/daemon" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/configx" +) + +// NewServeCmd returns the serve command +func NewServeCmd(dOpts ...driver.RegistryOption) (serveCmd *cobra.Command) { + serveCmd = &cobra.Command{ + Use: "serve", + Short: "Run the Ory Kratos server", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + d, err := driver.New(ctx, cmd.ErrOrStderr(), append(dOpts, driver.WithConfigOptions(configx.WithFlags(cmd.Flags())))...) + if err != nil { + return err + } + + if d.Config().IsInsecureDevMode(ctx) { + d.Logger().Warn(` + +YOU ARE RUNNING Ory KRATOS IN DEV MODE. +SECURITY IS DISABLED. +DON'T DO THIS IN PRODUCTION! + +`) + } + + configVersion := d.Config().ConfigVersion(ctx) + if configVersion == config.UnknownVersion { + d.Logger().Warn("The config has no version specified. Add the version to improve your development experience.") + } else if config.Version != "" && + configVersion != config.Version { + d.Logger().Warnf("Config version is '%s' but kratos runs on version '%s'", configVersion, config.Version) + } + + return daemon.ServeAll(d)(cmd, args) + }, + } + configx.RegisterFlags(serveCmd.PersistentFlags()) + + serveCmd.PersistentFlags().Bool("sqa-opt-out", false, "Disable anonymized telemetry reports - for more information please visit https://www.ory.com/docs/ecosystem/sqa") + serveCmd.PersistentFlags().Bool("dev", false, "Disables critical security features to make development easier") + serveCmd.PersistentFlags().Bool("watch-courier", false, "Run the message courier as a background task, to simplify single-instance setup") + return serveCmd +} + +func RegisterCommandRecursive(parent *cobra.Command, dOpts []driver.RegistryOption) { + parent.AddCommand(NewServeCmd(dOpts...)) +} diff --git a/cmd/serve/root_test.go b/cmd/serve/root_test.go new file mode 100644 index 0000000..e686936 --- /dev/null +++ b/cmd/serve/root_test.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package serve_test + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" +) + +func TestServe(t *testing.T) { + _, _ = testhelpers.StartE2EServer(t, "./stub/kratos.yml", nil) +} + +func TestServeTLSBase64(t *testing.T) { + _, _, certBase64, keyBase64 := testhelpers.GenerateTLSCertificateFilesForTests(t) + publicPort, adminPort := testhelpers.StartE2EServerOnly(t, + "./stub/kratos.yml", + true, + testhelpers.ConfigOptions{ + "serve.public.tls.key.base64": keyBase64, + "serve.public.tls.cert.base64": certBase64, + "serve.admin.tls.key.base64": keyBase64, + "serve.admin.tls.cert.base64": certBase64, + }, + ) + testhelpers.CheckE2EServerOnHTTPS(t, publicPort, adminPort) +} + +func TestServeTLSPaths(t *testing.T) { + certPath, keyPath, _, _ := testhelpers.GenerateTLSCertificateFilesForTests(t) + + publicPort, adminPort := testhelpers.StartE2EServerOnly(t, + "./stub/kratos.yml", + true, + testhelpers.ConfigOptions{ + "serve.public.tls.key.path": keyPath, + "serve.public.tls.cert.path": certPath, + "serve.admin.tls.key.path": keyPath, + "serve.admin.tls.cert.path": certPath, + }, + ) + testhelpers.CheckE2EServerOnHTTPS(t, publicPort, adminPort) +} diff --git a/cmd/serve/stub/identity.schema.json b/cmd/serve/stub/identity.schema.json new file mode 100644 index 0000000..1a13787 --- /dev/null +++ b/cmd/serve/stub/identity.schema.json @@ -0,0 +1,49 @@ +{ + "$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 + } + }, + "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/cmd/serve/stub/kratos.yml b/cmd/serve/stub/kratos.yml new file mode 100644 index 0000000..7301a2f --- /dev/null +++ b/cmd/serve/stub/kratos.yml @@ -0,0 +1,81 @@ +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + allowed_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - + hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + pagination: + - "test pagination secret" + +hashers: + argon2: + parallelism: 1 + memory: 128MB + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + schemas: + - id: default + url: file://stub/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&legacy_ssl=true diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..ced260f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,17 @@ +coverage: + status: + project: + default: + target: 60% + threshold: 10% + only_pulls: true +ignore: + - "test" + - "pkg" + - "docs" + - "proto" + - "gen" + - "examples" + - "contrib" + - "selfservice/strategy/oidc/provider_netid.go" # No way to test this provider automatically + - "**/*_test.go" diff --git a/continuity/container.go b/continuity/container.go new file mode 100644 index 0000000..c37fa54 --- /dev/null +++ b/continuity/container.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/pointerx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/x" +) + +type Container struct { + ID uuid.UUID `json:"id" db:"id" rw:"r"` + NID uuid.UUID `json:"-" db:"nid"` + Name string `json:"name" db:"name"` + IdentityID *uuid.UUID `json:"identity_id" db:"identity_id"` + + // ExpiresAt defines when this container expires. + ExpiresAt time.Time `json:"expires_at" db:"expires_at"` + + // Payload is the container's payload. + Payload sqlxx.NullJSONRawMessage `json:"payload" db:"payload"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +func (c *Container) UTC() *Container { + c.CreatedAt = c.CreatedAt.UTC() + c.UpdatedAt = c.UpdatedAt.UTC() + c.ExpiresAt = c.ExpiresAt.UTC() + return c +} + +func (Container) TableName() string { return "continuity_containers" } + +func NewContainer(name string, o managerOptions) *Container { + return &Container{ + ID: uuid.Nil, + Name: name, + IdentityID: x.PointToUUID(o.iid), + ExpiresAt: time.Now().Add(o.ttl).UTC().Truncate(time.Second), + Payload: sqlxx.NullJSONRawMessage(o.payload), + } +} + +func (c *Container) Valid(identity uuid.UUID) error { + if c.ExpiresAt.Before(time.Now()) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("You must restart the flow because the resumable session has expired.")) + } + + if identity != uuid.Nil && pointerx.Deref(c.IdentityID) != identity { + 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/container_reference_store.go b/continuity/container_reference_store.go new file mode 100644 index 0000000..f755bf2 --- /dev/null +++ b/continuity/container_reference_store.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "context" + "net/http" + + "github.com/gofrs/uuid" + "github.com/gorilla/sessions" + "github.com/pkg/errors" + + "github.com/ory/kratos/x" +) + +// ContainerReferenceStore abstracts where a continuity container's ID is +// stored between Pause and Continue/Abort calls. +type ContainerReferenceStore interface { + Store(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, id uuid.UUID) error + Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, error) + Clear(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) error +} + +// CookieReferenceStore stores the container ID in an HTTP cookie. +type CookieReferenceStore struct { + cs sessions.StoreExact +} + +func NewCookieReferenceStore(cs sessions.StoreExact) *CookieReferenceStore { + return &CookieReferenceStore{cs: cs} +} + +func (s *CookieReferenceStore) Store(_ context.Context, w http.ResponseWriter, r *http.Request, name string, id uuid.UUID) error { + return x.SessionPersistValues(w, r, s.cs, CookieName, map[string]any{ + name: id.String(), + }) +} + +func (s *CookieReferenceStore) Retrieve(_ context.Context, w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, error) { + str, err := x.SessionGetString(r, s.cs, CookieName, name) + if err != nil { + _ = x.SessionUnsetKey(w, r, s.cs, CookieName, name) + return uuid.Nil, errors.WithStack(ErrNotResumable().WithDebugf("%+v", err)) + } + + sid, err := uuid.FromString(str) + if err != nil { + _ = x.SessionUnsetKey(w, r, s.cs, CookieName, name) + return uuid.Nil, errors.WithStack(ErrNotResumable().WithDebug("session id is not a valid uuid")) + } + + return sid, nil +} + +func (s *CookieReferenceStore) Clear(_ context.Context, w http.ResponseWriter, r *http.Request, name string) error { + return x.SessionUnsetKey(w, r, s.cs, CookieName, name) +} diff --git a/continuity/container_test.go b/continuity/container_test.go new file mode 100644 index 0000000..0769f9d --- /dev/null +++ b/continuity/container_test.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "fmt" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestContainer(t *testing.T) { + id := x.NewUUID() + for k, tc := range []struct { + c *Container + i uuid.UUID + pass bool + }{ + { + c: &Container{ + ExpiresAt: time.Now().Add(-time.Minute), + }, + pass: false, + }, + { + c: &Container{ + ExpiresAt: time.Now().Add(-time.Minute), + }, + i: x.NewUUID(), + pass: false, + }, + { + c: &Container{ + IdentityID: x.PointToUUID(x.NewUUID()), + ExpiresAt: time.Now().Add(-time.Minute), + }, + i: x.NewUUID(), + pass: false, + }, + { + c: &Container{ + ExpiresAt: time.Now().Add(time.Minute), + }, + pass: true, + }, + { + c: &Container{ + IdentityID: x.PointToUUID(id), + ExpiresAt: time.Now().Add(time.Minute), + }, + i: id, + pass: true, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + err := tc.c.Valid(tc.i) + if tc.pass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/continuity/internal_context_reference_store.go b/continuity/internal_context_reference_store.go new file mode 100644 index 0000000..7f8421d --- /dev/null +++ b/continuity/internal_context_reference_store.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "context" + "net/http" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/flow" +) + +const InternalContextKey = "continuity_container_id" + +// InternalContextReferenceStore stores the container ID in a flow's +// InternalContext. Used for native/API flows where cookies are not available. +// This store only modifies the in-memory InternalContext — the caller is +// responsible for persisting the flow to the database. +type InternalContextReferenceStore struct { + ic flow.InternalContexter +} + +func NewInternalContextReferenceStore(ic flow.InternalContexter) *InternalContextReferenceStore { + return &InternalContextReferenceStore{ic: ic} +} + +func (s *InternalContextReferenceStore) Store(_ context.Context, _ http.ResponseWriter, _ *http.Request, _ string, id uuid.UUID) error { + s.ic.EnsureInternalContext() + raw, err := sjson.SetBytes(s.ic.GetInternalContext(), InternalContextKey, id.String()) + if err != nil { + return errors.WithStack(err) + } + s.ic.SetInternalContext(raw) + return nil +} + +func (s *InternalContextReferenceStore) Retrieve(_ context.Context, _ http.ResponseWriter, _ *http.Request, _ string) (uuid.UUID, error) { + cidStr := gjson.GetBytes(s.ic.GetInternalContext(), InternalContextKey).String() + if cidStr == "" { + return uuid.Nil, errors.WithStack(ErrNotResumable().WithDebug("no continuity container ID in flow internal context")) + } + cid, err := uuid.FromString(cidStr) + if err != nil { + return uuid.Nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Invalid continuity container ID in flow context.")) + } + return cid, nil +} + +// Clear is a no-op: InternalContext entries are cleaned up when the flow +// expires, so there is no separate reference to remove. +func (s *InternalContextReferenceStore) Clear(_ context.Context, _ http.ResponseWriter, _ *http.Request, _ string) error { + return nil +} diff --git a/continuity/manager.go b/continuity/manager.go new file mode 100644 index 0000000..28f7823 --- /dev/null +++ b/continuity/manager.go @@ -0,0 +1,197 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func ErrNotResumable() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("no resumable session found").WithReasonf("The browser does not contain the necessary cookie to resume the session. This is a security violation and was blocked. Please clear your browser's cookies and cache and try again!") +} + +const CookieName = "ory_kratos_continuity" + +type ( + ManagementProvider interface { + ContinuityManager() *Manager + } + managerDependencies interface { + PersistenceProvider + otelx.Provider + } + Manager struct { + d managerDependencies + } +) + +func NewManager(d managerDependencies) *Manager { + return &Manager{d: d} +} + +type managerOptions struct { + iid uuid.UUID + ttl time.Duration + setExpiresIn time.Duration + payload json.RawMessage + payloadRaw any +} + +type ManagerOption func(*managerOptions) error + +func newManagerOptions(opts []ManagerOption) (*managerOptions, error) { + var o = &managerOptions{ + ttl: time.Minute * 10, + } + for _, opt := range opts { + if err := opt(o); err != nil { + return nil, err + } + } + return o, nil +} + +func WithIdentity(i *identity.Identity) ManagerOption { + return func(o *managerOptions) error { + if i != nil { + o.iid = i.ID + } + return nil + } +} + +func WithLifespan(ttl time.Duration) ManagerOption { + return func(o *managerOptions) error { + o.ttl = ttl + return nil + } +} + +func WithPayload(payload any) ManagerOption { + return func(o *managerOptions) error { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(payload); err != nil { + return errors.WithStack(err) + } + o.payload = b.Bytes() + o.payloadRaw = payload + return nil + } +} + +func WithExpireInsteadOfDelete(duration time.Duration) ManagerOption { + return func(o *managerOptions) error { + o.setExpiresIn = duration + return nil + } +} + +func (m *Manager) Pause(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, store ContainerReferenceStore, opts ...ManagerOption) (containerID uuid.UUID, err error) { + ctx, span := m.d.Tracer(ctx).Tracer().Start(ctx, "continuity.ManagerDefault.Pause") + defer otelx.End(span, &err) + if len(name) == 0 { + return uuid.Nil, errors.Errorf("continuity container name must be set") + } + + o, err := newManagerOptions(opts) + if err != nil { + return uuid.Nil, err + } + c := NewContainer(name, *o) + + if err := m.d.ContinuityPersister().SaveContinuitySession(ctx, c); err != nil { + return uuid.Nil, errors.WithStack(err) + } + + if err := store.Store(ctx, w, r, name, c.ID); err != nil { + return uuid.Nil, err + } + + return c.ID, nil +} + +func (m *Manager) Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, store ContainerReferenceStore, opts ...ManagerOption) (container *Container, err error) { + ctx, span := m.d.Tracer(ctx).Tracer().Start(ctx, "continuity.ManagerDefault.Continue") + defer otelx.End(span, &err) + + o, err := newManagerOptions(opts) + if err != nil { + return nil, err + } + + sid, err := store.Retrieve(ctx, w, r, name) + if err != nil { + return nil, err + } + + container, err = m.d.ContinuityPersister().GetContinuitySession(ctx, sid) + if errors.Is(err, sqlcon.ErrNoRows()) { + _ = store.Clear(ctx, w, r, name) + return nil, errors.WithStack(ErrNotResumable().WithDebugf("Resumable ID could not be found in the datastore: %+v", err)) + } else if err != nil { + _ = store.Clear(ctx, w, r, name) + return nil, err + } else if container.ExpiresAt.Before(time.Now()) { + _ = store.Clear(ctx, w, r, name) + return nil, errors.WithStack(ErrNotResumable().WithDebugf("Resumable session has expired")) + } + + if err := container.Valid(o.iid); err != nil { + return nil, err + } + + if o.payloadRaw != nil && container.Payload != nil { + if err := json.NewDecoder(bytes.NewBuffer(container.Payload)).Decode(o.payloadRaw); err != nil { + return nil, errors.WithStack(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 { + _ = store.Clear(ctx, w, r, name) + if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return nil, err + } + } + + return container, nil +} + +func (m Manager) Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, store ContainerReferenceStore) (err error) { + ctx, span := m.d.Tracer(ctx).Tracer().Start(ctx, "continuity.ManagerDefault.Abort") + defer otelx.End(span, &err) + + sid, err := store.Retrieve(ctx, w, r, name) + if errors.Is(err, ErrNotResumable()) { + return nil + } else if err != nil { + return err + } + + _ = store.Clear(ctx, w, r, name) + + if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, sid); err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return errors.WithStack(err) + } + + return nil +} diff --git a/continuity/manager_options_test.go b/continuity/manager_options_test.go new file mode 100644 index 0000000..8286f12 --- /dev/null +++ b/continuity/manager_options_test.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestManagerOptions(t *testing.T) { + for k, tc := range []struct { + err bool + e func(t *testing.T, actual *managerOptions) + opts []ManagerOption + }{ + { + e: func(t *testing.T, actual *managerOptions) { + assert.EqualValues(t, time.Minute*10, actual.ttl) + }, + }, + { + opts: []ManagerOption{WithLifespan(time.Minute * 5)}, + e: func(t *testing.T, actual *managerOptions) { + assert.EqualValues(t, time.Minute*5, actual.ttl) + }, + }, + { + opts: []ManagerOption{WithPayload(map[string]interface{}{"foo": "bar"})}, + e: func(t *testing.T, actual *managerOptions) { + assert.EqualValues(t, map[string]interface{}{"foo": "bar"}, actual.payloadRaw) + assert.JSONEq(t, `{"foo":"bar"}`, string(actual.payload)) + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + o, err := newManagerOptions(tc.opts) + if tc.err { + require.Error(t, err) + return + } + require.NoError(t, err) + tc.e(t, o) + }) + } +} diff --git a/continuity/manager_test.go b/continuity/manager_test.go new file mode 100644 index 0000000..e3fe28a --- /dev/null +++ b/continuity/manager_test.go @@ -0,0 +1,335 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/logrusx" +) + +type persisterTestCase struct { + ro []continuity.ManagerOption + wo []continuity.ManagerOption + expected *persisterTestPayload + expectedErr error +} + +type persisterTestPayload struct { + Foo string `json:"foo"` +} + +func TestManager(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://../test/stub/identity/empty.schema.json")), + ) + + i := identity.NewIdentity("") + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + + newServer := func(t *testing.T, p *continuity.Manager, tc *persisterTestCase) *httptest.Server { + writer := herodot.NewJSONWriter(logrusx.New("", "")) + router := http.NewServeMux() + router.HandleFunc("PUT /{name}", func(w http.ResponseWriter, r *http.Request) { + store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context())) + if _, err := p.Pause(r.Context(), w, r, r.PathValue("name"), store, tc.ro...); err != nil { + writer.WriteError(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) + }) + + router.HandleFunc("POST /{name}", func(w http.ResponseWriter, r *http.Request) { + store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context())) + if _, err := p.Pause(r.Context(), w, r, r.PathValue("name"), store, tc.ro...); err != nil { + writer.WriteError(w, r, err) + return + } + + c, err := p.Continue(r.Context(), w, r, r.PathValue("name"), store, tc.wo...) + if err != nil { + writer.WriteError(w, r, err) + return + } + writer.Write(w, r, c) + }) + + router.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) { + store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context())) + c, err := p.Continue(r.Context(), w, r, r.PathValue("name"), store, tc.ro...) + if err != nil { + writer.WriteError(w, r, err) + return + } + writer.Write(w, r, c) + }) + + router.HandleFunc("DELETE /{name}", func(w http.ResponseWriter, r *http.Request) { + store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context())) + err := p.Abort(r.Context(), w, r, r.PathValue("name"), store) + if err != nil { + writer.WriteError(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + return ts + } + + newClient := func() *http.Client { + return &http.Client{Jar: testhelpers.EasyCookieJar(t, nil), Transport: testhelpers.NewTestTransport(t)} + } + + p := reg.ContinuityManager() + cl := newClient() + + t.Run("case=continue cookie resets when signature is invalid", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + href := ts.URL + "/" + x.NewUUID().String() + + 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) + + req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil) + require.Len(t, res.Cookies(), 1) + for _, c := range res.Cookies() { + // Change something in the string + c.Value = strings.Replace(c.Value, "a", "b", 1) + req.AddCookie(c) + } + res, err = testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Equal(t, http.StatusBadRequest, res.StatusCode) + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField) + + require.Len(t, res.Cookies(), 1, "continuing the flow with a broken cookie should instruct the browser to forget it") + assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) + }) + + t.Run("case=can deal with duplicate cookies", func(t *testing.T) { + tc := &persisterTestCase{expected: &persisterTestPayload{"bar"}} + ts := newServer(t, p, tc) + href := ts.URL + "/" + x.NewUUID().String() + + res, err := testhelpers.NewTestClient(t).Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + // We change the key to another one + href = ts.URL + "/" + x.NewUUID().String() + req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil) + require.Len(t, res.Cookies(), 1) + for _, c := range res.Cookies() { + req.AddCookie(c) + } + + tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"})} + res, err = testhelpers.NewTestClient(t).Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + require.Len(t, res.Cookies(), 1) + for _, c := range res.Cookies() { + req.AddCookie(c) + } + + res, err = testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 1, "continuing the flow with a broken cookie should instruct the browser to forget it") + assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) + + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(tc.expected)) + body := ioutilx.MustReadAll(res.Body) + assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body) + 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{ + {}, + { + ro: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"})}, + wo: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{})}, + expected: &persisterTestPayload{"bar"}, + }, + { + ro: []continuity.ManagerOption{continuity.WithIdentity(i)}, + wo: []continuity.ManagerOption{continuity.WithIdentity(i)}, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + cl := newClient() + ts := newServer(t, p, &tc) + genid := func() string { + return ts.URL + "/" + x.NewUUID().String() + } + + t.Run("case=resume non-existing session", func(t *testing.T) { + href := genid() + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + body := ioutilx.MustReadAll(res.Body) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField) + }) + + t.Run("case=pause and resume session", func(t *testing.T) { + 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) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + body := ioutilx.MustReadAll(res.Body) + if tc.expectedErr != nil { + require.Equal(t, http.StatusGone, res.StatusCode, "%s", body) + return + } + + require.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(tc.expected)) + assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body) + assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body) + }) + + t.Run("case=pause and retry session", func(t *testing.T) { + 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) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + 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) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField) + }) + + t.Run("case=pause and resume session in the same request", func(t *testing.T) { + href := genid() + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "POST", href, nil)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(tc.expected)) + + body := ioutilx.MustReadAll(res.Body) + assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body) + assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body) + }) + + t.Run("case=pause, abort, and continue session with failure", func(t *testing.T) { + 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, "DELETE", href, nil)) + require.NoError(t, err) + t.Cleanup(func() { 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) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Equal(t, http.StatusBadRequest, res.StatusCode) + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField) + }) + }) + } +} diff --git a/continuity/persistence.go b/continuity/persistence.go new file mode 100644 index 0000000..cc91273 --- /dev/null +++ b/continuity/persistence.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type PersistenceProvider interface { + ContinuityPersister() Persister +} + +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 new file mode 100644 index 0000000..04fd4ac --- /dev/null +++ b/continuity/test/persistence.go @@ -0,0 +1,151 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func TestPersister(ctx context.Context, p interface { + persistence.Persister + continuity.Persister + identity.PrivilegedPool +}) func(t *testing.T) { + var createIdentity = func(t *testing.T) *identity.Identity { + id := identity.Identity{} + require.NoError(t, p.CreateIdentity(ctx, &id)) + return &id + } + + var createContainer = func(t *testing.T) continuity.Container { + m := sqlxx.NullJSONRawMessage(`{"foo": "bar"}`) + return continuity.Container{Name: "foo", IdentityID: x.PointToUUID(createIdentity(t).ID), + ExpiresAt: time.Now().Add(time.Hour).UTC().Truncate(time.Second), + Payload: m, + } + } + + return func(t *testing.T) { + nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=not found", func(t *testing.T) { + _, err := p.GetContinuitySession(ctx, x.NewUUID()) + require.EqualError(t, err, sqlcon.ErrNoRows().Error()) + }) + + t.Run("case=save and find", func(t *testing.T) { + expected := createContainer(t) + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + + actual, err := p.GetContinuitySession(ctx, expected.ID) + require.NoError(t, err) + actual.UpdatedAt, actual.CreatedAt, expected.UpdatedAt, expected.CreatedAt = time.Time{}, time.Time{}, time.Time{}, time.Time{} + assert.EqualValues(t, expected.UTC(), actual.UTC()) + }) + + t.Run("case=save and delete", func(t *testing.T) { + expected := createContainer(t) + + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + require.NotEqual(t, uuid.Nil, expected.ID) + require.NoError(t, p.DeleteContinuitySession(ctx, expected.ID)) + + _, err := p.GetContinuitySession(ctx, expected.ID) + require.EqualError(t, err, sqlcon.ErrNoRows().Error()) + }) + + t.Run("case=network", func(t *testing.T) { + var id uuid.UUID + + t.Run("sets id on creation", func(t *testing.T) { + expected := createContainer(t) + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + + // The persister assigns the ID; callers must not choose it. + require.NotEqual(t, uuid.Nil, expected.ID) + id = expected.ID + assert.EqualValues(t, nid, expected.NID) + + actual, err := p.GetContinuitySession(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetLoginFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + t.Run("can not delete on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + err := p.DeleteContinuitySession(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + 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) { + yesterday := time.Now().Add(-24 * time.Hour).UTC().Truncate(time.Second) + m := sqlxx.NullJSONRawMessage(`{"foo": "bar"}`) + expected := continuity.Container{Name: "foo", IdentityID: x.PointToUUID(createIdentity(t).ID), + ExpiresAt: yesterday, + Payload: m, + } + + t.Run("can cleanup", func(t *testing.T) { + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + + // The persister assigns the ID; callers must not choose it. + require.NotEqual(t, uuid.Nil, expected.ID) + assert.EqualValues(t, nid, expected.NID) + + require.NoError(t, p.DeleteExpiredContinuitySessions(ctx, time.Now(), 5)) + + _, err := p.GetContinuitySession(ctx, expected.ID) + require.Error(t, err) + }) + }) + } +} diff --git a/contrib/quickstart/.dockerignore b/contrib/quickstart/.dockerignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/contrib/quickstart/.dockerignore @@ -0,0 +1 @@ +* \ No newline at end of file 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 0000000..6915fbe --- /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 0000000..7aa978e --- /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/cloud/Caddyfile b/contrib/quickstart/kratos/cloud/Caddyfile new file mode 100644 index 0000000..96cb4c4 --- /dev/null +++ b/contrib/quickstart/kratos/cloud/Caddyfile @@ -0,0 +1,17 @@ +{ + http_port 4455 + auto_https off + +} + +:4455 { + route /ui/* { + uri strip_prefix /ui + reverse_proxy kratos-selfservice-ui-node:4438 { + header_up Host {http.request.hostport} + } + } + reverse_proxy /* kratos:4433 { + header_up Host {http.request.hostport} + } +} diff --git a/contrib/quickstart/kratos/cloud/identity.schema.json b/contrib/quickstart/kratos/cloud/identity.schema.json new file mode 100644 index 0000000..1a13787 --- /dev/null +++ b/contrib/quickstart/kratos/cloud/identity.schema.json @@ -0,0 +1,49 @@ +{ + "$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 + } + }, + "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/cloud/kratos.yml b/contrib/quickstart/kratos/cloud/kratos.yml new file mode 100644 index 0000000..d6d8628 --- /dev/null +++ b/contrib/quickstart/kratos/cloud/kratos.yml @@ -0,0 +1,76 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4455/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://localhost:4455/ui/welcome + allowed_return_urls: + - http://localhost:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://localhost:4455/ui/error + + settings: + ui_url: http://localhost:4455/ui/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://localhost:4455/ui/recovery + + verification: + enabled: true + ui_url: http://localhost:4455/ui/verification + after: + default_browser_return_url: http://localhost:4455/ui/welcome + + logout: + after: + default_browser_return_url: http://localhost:4455/ui/login + + login: + ui_url: http://localhost:4455/ui/login + + registration: + ui_url: http://localhost:4455/ui/registration + after: + password: + hooks: + - + hook: session + +log: + level: info + format: text + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: preset://email + schemas: + - id: preset://email + url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/kratos/cloud/quickstart.yml b/contrib/quickstart/kratos/cloud/quickstart.yml new file mode 100644 index 0000000..703ae0f --- /dev/null +++ b/contrib/quickstart/kratos/cloud/quickstart.yml @@ -0,0 +1,42 @@ +version: '3.7' + +services: + kratos: + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/cloud + target: /etc/config/kratos + kratos-migrate: + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/cloud + target: /etc/config/kratos + + kratos-selfservice-ui-node: + ports: + - "4438:4438" + environment: + - PORT=4438 + - KRATOS_BROWSER_URL=http://localhost:4455/ + + kratos-caddy: + image: caddy:2.4.5-alpine + ports: + - "4455:4455" + volumes: + - type: bind + source: ./contrib/quickstart/kratos/cloud/Caddyfile + target: /etc/caddy/Caddyfile + command: caddy run -watch -config /etc/caddy/Caddyfile + restart: on-failure + networks: + - intranet + diff --git a/contrib/quickstart/kratos/email-password/identity.schema.json b/contrib/quickstart/kratos/email-password/identity.schema.json new file mode 100644 index 0000000..1a13787 --- /dev/null +++ b/contrib/quickstart/kratos/email-password/identity.schema.json @@ -0,0 +1,49 @@ +{ + "$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 + } + }, + "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/email-password/kratos.yml b/contrib/quickstart/kratos/email-password/kratos.yml new file mode 100644 index 0000000..5597a1a --- /dev/null +++ b/contrib/quickstart/kratos/email-password/kratos.yml @@ -0,0 +1,102 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/welcome + allowed_return_urls: + - http://127.0.0.1:4455 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verification + use: code + after: + default_browser_return_url: http://127.0.0.1:4455/welcome + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/login + + login: + ui_url: http://127.0.0.1:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/registration + after: + 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:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true + +feature_flags: + use_continue_with_transitions: true diff --git a/contrib/quickstart/kratos/passkey/identity.schema.json b/contrib/quickstart/kratos/passkey/identity.schema.json new file mode 100644 index 0000000..5fabe0c --- /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 0000000..776b17e --- /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/identity.schema.json b/contrib/quickstart/kratos/phone-password/identity.schema.json new file mode 100644 index 0000000..e3a013f --- /dev/null +++ b/contrib/quickstart/kratos/phone-password/identity.schema.json @@ -0,0 +1,51 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/phone-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 + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "phone": { + "type": "string", + "format": "tel", + "title": "Phone number", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "sms" + } + } + } + }, + "required": ["email", "phone"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/phone-password/kratos.yml b/contrib/quickstart/kratos/phone-password/kratos.yml new file mode 100644 index 0000000..5826e4a --- /dev/null +++ b/contrib/quickstart/kratos/phone-password/kratos.yml @@ -0,0 +1,114 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/welcome + allowed_return_urls: + - http://127.0.0.1:4455 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verification + use: code + after: + default_browser_return_url: http://127.0.0.1:4455/welcome + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/login + + login: + ui_url: http://127.0.0.1:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/registration + after: + 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:///etc/config/kratos/identity.schema.json + +courier: + channels: + - id: sms + type: http + request_config: + url: https://api.twilio.com/2010-04-01/Accounts/AXXXXXXXXXXXXXX/Messages.json + method: POST + body: base64://ZnVuY3Rpb24oY3R4KSB7ClRvOiBjdHguUmVjaXBpZW50LApCb2R5OiBjdHguQm9keSwKfQ== + headers: + Content-Type: application/x-www-form-urlencoded + auth: + type: basic_auth + config: + user: AXXXXXXX + password: XXXX + +feature_flags: + use_continue_with_transitions: true diff --git a/contrib/quickstart/kratos/webauthn/identity.schema.json b/contrib/quickstart/kratos/webauthn/identity.schema.json new file mode 100644 index 0000000..2734e5c --- /dev/null +++ b/contrib/quickstart/kratos/webauthn/identity.schema.json @@ -0,0 +1,50 @@ +{ + "$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 + } + }, + "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/webauthn/kratos.yml b/contrib/quickstart/kratos/webauthn/kratos.yml new file mode 100644 index 0000000..6216885 --- /dev/null +++ b/contrib/quickstart/kratos/webauthn/kratos.yml @@ -0,0 +1,107 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://localhost:4455/welcome + allowed_return_urls: + - http://localhost:4455 + + methods: + password: + enabled: false + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + webauthn: + 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 + enabled: true + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://localhost:4455/verification + use: code + after: + default_browser_return_url: http://localhost:4455/welcome + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + 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:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/oathkeeper/access-rules.yml b/contrib/quickstart/oathkeeper/access-rules.yml new file mode 100644 index 0000000..fb6e613 --- /dev/null +++ b/contrib/quickstart/oathkeeper/access-rules.yml @@ -0,0 +1,60 @@ +- + id: "ory:kratos:public" + upstream: + preserve_host: true + url: "http://kratos:4433" + strip_path: /.ory/kratos/public + match: + url: "http://127.0.0.1:4455/.ory/kratos/public/<**>" + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + authenticators: + - + handler: noop + authorizer: + handler: allow + mutators: + - handler: noop + +- + id: "ory:kratos-selfservice-ui-node:anonymous" + upstream: + preserve_host: true + url: "http://kratos-selfservice-ui-node:4435" + match: + url: "http://127.0.0.1:4455/<{registration,welcome,recovery,verification,login,error,health/{alive,ready},**.css,**.js,**.png,**.svg,**.woff*}>" + methods: + - GET + authenticators: + - + handler: anonymous + authorizer: + handler: allow + mutators: + - + handler: noop + +- + id: "ory:kratos-selfservice-ui-node:protected" + upstream: + preserve_host: true + url: "http://kratos-selfservice-ui-node:4435" + match: + url: "http://127.0.0.1:4455/<{sessions,settings}>" + methods: + - GET + authenticators: + - + handler: cookie_session + authorizer: + handler: allow + mutators: + - handler: id_token + errors: + - handler: redirect + config: + to: http://127.0.0.1:4455/login diff --git a/contrib/quickstart/oathkeeper/id_token.jwks.json b/contrib/quickstart/oathkeeper/id_token.jwks.json new file mode 100644 index 0000000..5bc1ec1 --- /dev/null +++ b/contrib/quickstart/oathkeeper/id_token.jwks.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "a2aa9739-d753-4a0d-87ee-61f101050277", + "alg": "RS256", + "n": "zpjSl0ySsdk_YC4ZJYYV-cSznWkzndTo0lyvkYmeBkW60YHuHzXaviHqonY_DjFBdnZC0Vs_QTWmBlZvPzTp4Oni-eOetP-Ce3-B8jkGWpKFOjTLw7uwR3b3jm_mFNiz1dV_utWiweqx62Se0SyYaAXrgStU8-3P2Us7_kz5NnBVL1E7aEP40aB7nytLvPhXau-YhFmUfgykAcov0QrnNY0DH0eTcwL19UysvlKx6Uiu6mnbaFE1qx8X2m2xuLpErfiqj6wLCdCYMWdRTHiVsQMtTzSwuPuXfH7J06GTo3I1cEWN8Mb-RJxlosJA_q7hEd43yYisCO-8szX0lgCasw", + "e": "AQAB", + "d": "x3dfY_rna1UQTmFToBoMn6Edte47irhkra4VSNPwwaeTTvI-oN2TO51td7vo91_xD1nw-0c5FFGi4V2UfRcudBv9LD1rHt_O8EPUh7QtAUeT3_XXgjx1Xxpqu5goMZpkTyGZ-B6JzOY3L8lvWQ_Qeia1EXpvxC-oTOjJnKZeuwIPlcoNKMRU-mIYOnkRFfnUvrDm7N9UZEp3PfI3vhE9AquP1PEvz5KTUYkubsfmupqqR6FmMUm6ulGT7guhBw9A3vxIYbYGKvXLdBvn68mENrEYxXrwmu6ITMh_y208M5rC-hgEHIAIvMu1aVW6jNgyQTunsGST3UyrSbwjI0K9UQ", + "p": "77fDvnfHRFEgyi7mh0c6fAdtMEMJ05W8NwTG_D-cSwfWipfTwJJrroWoRwEgdAg5AWGq-MNUzrubTVXoJdC2T4g1o-VRZkKKYoMvav3CvOIMzCBxBs9I_GAKr5NCSk7maksMqiCTMhmkoZ5RPuMYMY_YzxKNAbjBd9qFLfaVAqs", + "q": "3KEmPA2XQkf7dvtpY1Xkp1IfMV_UBdmYk7J6dB5BYqzviQWdEFvWaSATJ_7qV1dw0JDZynOgipp8gvoL-RepfjtArhPz41wB3J2xmBYrBr1sJ-x5eqAvMkQk2bd5KTor44e79TRIkmkFYAIdUQ5JdVXPA13S8WUZfb_bAbwaCBk", + "dp": "5uyy32AJkNFKchqeLsE6INMSp0RdSftbtfCfM86fZFQno5lA_qjOnO_avJPkTILDT4ZjqoKYxxJJOEXCffNCPPltGvbE5GrDXsUbP8k2-LgWNeoml7XFjIGEqcCFQoohQ1IK4DTDN6cmRh76C0e_Pbdh15D6TydJEIlsdGuu_kM", + "dq": "aegFNYCEojFxeTzX6vIZL2RRSt8oJKK-Be__reu0EUzYMtr5-RdMhev6phFMph54LfXKRc9ZOg9MQ4cJ5klAeDKzKpyzTukkj6U20b2aa8LTvxpZec6YuTVSxxu2Ul71IGRQijTNvVIiXWLGddk409Ub6Q7JqkyQfvdwhpWnnUk", + "qi": "P68-EwgcRy9ce_PZ75c909cU7dzCiaGcTX1psJiXmQAFBcG0msWfsyHGbllOZG27pKde78ORGJDYDNk1FqTwsogZyCP87EiBmOoqXWnMvKYfJ1DOx7x42LMAGwMD3bgQj9jgRACxFJG4n3NI6uFlFruyl_CLQzwW_rQFHshLK7Q" + } + ] +} diff --git a/contrib/quickstart/oathkeeper/oathkeeper.yml b/contrib/quickstart/oathkeeper/oathkeeper.yml new file mode 100644 index 0000000..ff8ec39 --- /dev/null +++ b/contrib/quickstart/oathkeeper/oathkeeper.yml @@ -0,0 +1,88 @@ +log: + level: debug + format: json + +serve: + proxy: + cors: + enabled: true + allowed_origins: + - "*" + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + debug: true + +errors: + fallback: + - json + + handlers: + redirect: + enabled: true + config: + to: http://127.0.0.1:4455/login + when: + - + error: + - unauthorized + - forbidden + request: + header: + accept: + - text/html + json: + enabled: true + config: + verbose: true + +access_rules: + matching_strategy: glob + repositories: + - file:///etc/config/oathkeeper/access-rules.yml + +authenticators: + anonymous: + enabled: true + config: + subject: guest + + cookie_session: + enabled: true + config: + check_session_url: http://kratos:4433/sessions/whoami + preserve_path: true + extra_from: "@this" + subject_from: "identity.id" + only: + - ory_kratos_session + + noop: + enabled: true + +authorizers: + allow: + enabled: true + +mutators: + noop: + enabled: true + + id_token: + enabled: true + config: + issuer_url: http://127.0.0.1:4455/ + jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json + claims: | + { + "session": {{ .Extra | toJson }} + } diff --git a/corpx/faker.go b/corpx/faker.go new file mode 100644 index 0000000..147e844 --- /dev/null +++ b/corpx/faker.go @@ -0,0 +1,157 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// #nosec G404 -- used in tests only +package corpx + +import ( + "math/rand" + "net/http" + "reflect" + "sync" + "time" + + "github.com/go-faker/faker/v4" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/randx" +) + +var setup sync.Once + +func RegisterFakes() { + setup.Do(registerFakes) +} + +func registerFakes() { + _ = faker.SetRandomMapAndSliceSize(4) + + if err := faker.AddProvider("ptr_geo_location", func(v reflect.Value) (interface{}, error) { + return new("Munich, Germany"), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("ptr_ipv4", func(v reflect.Value) (interface{}, error) { + return new(faker.IPv4()), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("birthdate", func(v reflect.Value) (interface{}, error) { + return time.Now().Add(time.Duration(rand.Int())).Round(time.Second).UTC(), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("time_types", func(v reflect.Value) (interface{}, error) { + es := make([]time.Time, rand.Intn(5)) + for k := range es { + es[k] = time.Now().Add(time.Duration(rand.Int())).Round(time.Second).UTC() + } + return es, nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("http_header", func(v reflect.Value) (interface{}, error) { + headers := http.Header{} + for i := 0; i <= rand.Intn(5); i++ { + values := make([]string, rand.Intn(4)+1) + for k := range values { + values[k] = randx.MustString(8, randx.AlphaNum) + } + headers[randx.MustString(8, randx.AlphaNum)] = values + } + + return headers, nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("http_method", func(v reflect.Value) (interface{}, error) { + methods := []string{"POST", "PUT", "GET", "PATCH"} + return methods[rand.Intn(len(methods))], nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("identity_credentials_type", func(v reflect.Value) (interface{}, error) { + methods := []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypePassword} + return string(methods[rand.Intn(len(methods))]), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("string", func(v reflect.Value) (interface{}, error) { + return randx.MustString(25, randx.AlphaNum), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("time_type", func(v reflect.Value) (interface{}, error) { + return time.Now().Add(time.Duration(rand.Int())).Round(time.Second).UTC(), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("ui_node_attributes", func(v reflect.Value) (interface{}, error) { + var a node.Attributes + switch rand.Intn(4) { + case 0: + a = new(node.InputAttributes) + case 1: + a = new(node.ImageAttributes) + case 2: + a = new(node.AnchorAttributes) + case 3: + a = new(node.TextAttributes) + } + + if err := faker.FakeData(a); err != nil { + return nil, err + } + return a, nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("uuid", func(v reflect.Value) (interface{}, error) { + return x.NewUUID(), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("identity", func(v reflect.Value) (interface{}, error) { + var i identity.Identity + return &i, faker.FakeData(&i) + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("flow_type", func(v reflect.Value) (interface{}, error) { + if rand.Intn(2) == 0 { + return flow.TypeAPI, nil + } + return flow.TypeBrowser, nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("session_device", func(v reflect.Value) (interface{}, error) { + var d session.Device + return &d, faker.FakeData(&d) + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("aal_type", func(v reflect.Value) (interface{}, error) { + return "aal1", nil + }); err != nil { + panic(err) + } +} diff --git a/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=admin.json b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=admin.json new file mode 100644 index 0000000..274cc9f --- /dev/null +++ b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=admin.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 404, + "status": "Not Found", + "message": "Unable to locate the resource" + } +} diff --git a/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=public.json b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=public.json new file mode 100644 index 0000000..274cc9f --- /dev/null +++ b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_no_message_is_found-endpoint=public.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 404, + "status": "Not Found", + "message": "Unable to locate the resource" + } +} diff --git a/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=admin.json b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=admin.json new file mode 100644 index 0000000..7009aa9 --- /dev/null +++ b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=admin.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 400, + "status": "Bad Request", + "message": "uuid: incorrect UUID length 10 in string \"not-a-uuid\"" + } +} diff --git a/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=public.json b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=public.json new file mode 100644 index 0000000..7009aa9 --- /dev/null +++ b/courier/.snapshots/TestHandler-handler=getCourierMessage-case=returns_an_error_if_parameter_is_malformed-endpoint=public.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 400, + "status": "Bad Request", + "message": "uuid: incorrect UUID length 10 in string \"not-a-uuid\"" + } +} diff --git a/courier/channel.go b/courier/channel.go new file mode 100644 index 0000000..12697a6 --- /dev/null +++ b/courier/channel.go @@ -0,0 +1,13 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" +) + +type Channel interface { + ID() string + Dispatch(ctx context.Context, msg Message) error +} diff --git a/courier/courier.go b/courier/courier.go new file mode 100644 index 0000000..f43b292 --- /dev/null +++ b/courier/courier.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "time" + + "github.com/cenkalti/backoff" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" +) + +type ( + Dependencies interface { + PersistenceProvider + otelx.Provider + logrusx.Provider + ConfigProvider + httpx.ClientProvider + jsonnetsecure.VMProvider + } + + Courier interface { + Work(ctx context.Context) error + QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, error) + QueueSMS(ctx context.Context, t SMSTemplate) (uuid.UUID, error) + DispatchQueue(ctx context.Context) error + DispatchMessage(ctx context.Context, msg Message) error + UseBackoff(b backoff.BackOff) + FailOnDispatchError() + } + + Provider interface { + Courier(ctx context.Context) (Courier, error) + } + + ConfigProvider interface { + CourierConfig() config.CourierConfigs + } + + courier struct { + deps Dependencies + failOnDispatchError bool + backoff backoff.BackOff + newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error) + } +) + +func NewCourier(ctx context.Context, deps Dependencies) (Courier, error) { + return NewCourierWithCustomTemplates(ctx, deps, NewEmailTemplateFromMessage) +} + +func NewCourierWithCustomTemplates(_ context.Context, deps Dependencies, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (Courier, error) { + return &courier{ + deps: deps, + backoff: backoff.NewExponentialBackOff(), + newEmailTemplateFromMessage: newEmailTemplateFromMessage, + }, nil +} + +func (c *courier) FailOnDispatchError() { + c.failOnDispatchError = true +} + +func (c *courier) Work(ctx context.Context) error { + errChan := make(chan error) + defer close(errChan) + + go c.watchMessages(ctx, errChan) + + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.Canceled) { + return nil + } + return ctx.Err() + case err := <-errChan: + return errors.WithStack(err) + } +} + +func (c *courier) UseBackoff(b backoff.BackOff) { + c.backoff = b +} + +func (c *courier) watchMessages(ctx context.Context, errChan chan error) { + wait := c.deps.CourierConfig().CourierWorkerPullWait(ctx) + c.backoff.Reset() + for { + if err := backoff.Retry(func() error { + return c.DispatchQueue(ctx) + }, c.backoff); err != nil { + errChan <- errors.WithStack(err) + return + } + time.Sleep(wait) + } +} diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go new file mode 100644 index 0000000..288d1f3 --- /dev/null +++ b/courier/courier_dispatcher.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "time" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" +) + +func (c *courier) channels(ctx context.Context, id string) (Channel, error) { + cs, err := c.deps.CourierConfig().CourierChannels(ctx) + if err != nil { + return nil, err + } + + for _, channel := range cs { + if channel.ID != id { + continue + } + switch channel.Type { + case "smtp": + courierChannel, err := NewSMTPChannelWithCustomTemplates(c.deps, channel.SMTPConfig, c.newEmailTemplateFromMessage) + if err != nil { + return nil, err + } + return courierChannel, nil + case "http": + return newHttpChannel(channel.ID, &channel.RequestConfig, c.deps), nil + default: + return nil, errors.Errorf("unknown courier channel type: %s", channel.Type) + } + } + + return nil, errors.Errorf("no courier channels configured for: %s", id) +} + +func (c *courier) DispatchMessage(ctx context.Context, msg Message) (err error) { + ctx, span := c.deps.Tracer(ctx).Tracer().Start(ctx, "courier.DispatchMessage", trace.WithAttributes( + attribute.Stringer("message.id", msg.ID), + attribute.Stringer("message.nid", msg.NID), + attribute.Stringer("message.type", msg.Type), + attribute.String("message.template_type", string(msg.TemplateType)), + attribute.Int("message.send_count", msg.SendCount), + )) + defer otelx.End(span, &err) + ctx = semconv.ContextWithAttributes(ctx, semconv.AttrNID(msg.NID)) + + 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). + WithField("trace_id", span.SpanContext().TraceID()) + + if err := c.deps.CourierPersister().IncrementMessageSendCount(ctx, msg.ID); err != nil { + logger. + WithError(err). + Error(`Unable to increment the message's "send_count" field`) + return err + } + + channel, err := c.channels(ctx, msg.Channel.String()) + if err != nil { + return err + } + + span.SetAttributes(attribute.String("channel.id", channel.ID())) + logger = logger. + WithField("channel", channel.ID()) + + if err := channel.Dispatch(ctx, msg); err != nil { + return err + } + span.AddEvent(events.NewCourierMessageDispatched(ctx, msg.ID, msg.Channel.String(), string(msg.TemplateType))) + + if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusSent); err != nil { + logger. + WithError(err). + Error(`Unable to set the message status to "sent".`) + return err + } + + dispatchDuration := time.Since(msg.CreatedAt).Milliseconds() + logger.WithField("dispatch_duration_ms", dispatchDuration).Debug("Courier sent out message.") + + return nil +} + +func (c *courier) DispatchQueue(ctx context.Context) (err error) { + ctx, span := c.deps.Tracer(ctx).Tracer().Start(ctx, "courier.DispatchQueue") + defer otelx.End(span, &err) + maxRetries := c.deps.CourierConfig().CourierMessageRetries(ctx) + pullCount := c.deps.CourierConfig().CourierWorkerPullCount(ctx) + + //nolint:gosec // disable G115 + messages, err := c.deps.CourierPersister().NextMessages(ctx, uint8(pullCount)) + if err != nil { + if errors.Is(err, ErrQueueEmpty) { + return nil + } + return err + } + span.SetAttributes(attribute.Int("messages_count", len(messages))) + + 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 { + logger. + WithError(err). + Error(`Unable to set the retried message's status to "abandoned".`) + return err + } + + msgCtx := semconv.ContextWithAttributes(ctx, semconv.AttrNID(msg.NID)) + span.AddEvent(events.NewCourierMessageAbandoned(msgCtx, msg.ID, msg.Channel.String(), string(msg.TemplateType))) + + // Skip the message + logger. + Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount) + } else if err := c.DispatchMessage(ctx, msg); err != nil { + logger. + WithError(err). + Warn(`Unable to dispatch message.`) + if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil { + logger. + WithError(err). + Error(`Unable to record failure log entry.`) + if c.failOnDispatchError { + return err + } + } + + for _, replace := range messages[k:] { + if err := c.deps.CourierPersister().SetMessageStatus(ctx, replace.ID, MessageStatusQueued); err != nil { + logger. + WithError(err). + Error(`Unable to reset the failed message's status to "queued".`) + if c.failOnDispatchError { + return err + } + } + } + + if c.failOnDispatchError { + return err + } + } else if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusSuccess, nil); err != nil { + logger. + WithError(err). + Error(`Unable to record success log entry.`) + // continue with execution, as the message was successfully dispatched + } + } + + return nil +} diff --git a/courier/courier_dispatcher_test.go b/courier/courier_dispatcher_test.go new file mode 100644 index 0000000..107a2c4 --- /dev/null +++ b/courier/courier_dispatcher_test.go @@ -0,0 +1,176 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + + "github.com/ory/kratos/courier" + templates "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x/events" + "github.com/ory/x/configx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" +) + +func queueNewMessage(t *testing.T, c courier.Courier) uuid.UUID { + t.Helper() + id, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "test-recipient-1@example.org", + Subject: "test-subject-1", + Body: "test-body-1", + })) + require.NoError(t, err) + return id +} + +func TestDispatchMessageWithInvalidSMTP(t *testing.T) { + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{ + config.ViperKeyCourierMessageRetries: 5, + config.ViperKeyCourierSMTPURL: "http://foo.url", + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + + t.Run("case=failed sending", func(t *testing.T) { + id := queueNewMessage(t, c) + message, err := reg.CourierPersister().LatestQueuedMessage(t.Context()) + require.NoError(t, err) + require.Equal(t, id, message.ID) + + err = c.DispatchMessage(t.Context(), *message) + // sending the email fails, because there is no SMTP server at foo.url + require.Error(t, err) + + messages, err := reg.CourierPersister().NextMessages(t.Context(), 10) + require.NoError(t, err) + require.Len(t, messages, 1) + }) +} + +func TestDispatchMessage(t *testing.T) { + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{ + config.ViperKeyCourierMessageRetries: 5, + config.ViperKeyCourierSMTPURL: "http://foo.url", + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + t.Run("case=invalid channel", func(t *testing.T) { + message := courier.Message{ + Channel: "invalid-channel", + Status: courier.MessageStatusQueued, + Type: courier.MessageTypeEmail, + Recipient: testhelpers.RandomEmail(), + Subject: "test-subject-1", + Body: "test-body-1", + TemplateType: "stub", + } + require.NoError(t, reg.CourierPersister().AddMessage(t.Context(), &message)) + assert.ErrorContains(t, c.DispatchMessage(t.Context(), message), "no courier channels configured for: invalid-channel") + }) +} + +func TestDispatchQueue(t *testing.T) { + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValue(config.ViperKeyCourierMessageRetries, 1)) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + c.FailOnDispatchError() + + id := queueNewMessage(t, c) + require.NotEqual(t, uuid.Nil, id) + + // Fails to deliver the first time + err = c.DispatchQueue(t.Context()) + require.Error(t, err) + + // Retry once, as we set above - still fails + err = c.DispatchQueue(t.Context()) + require.Error(t, err) + + // Now it has been retried once, which means 2 > 1 is true and it is no longer tried + err = c.DispatchQueue(t.Context()) + require.NoError(t, err) + + var message courier.Message + err = reg.Persister().GetConnection(t.Context()). + Where("status = ?", courier.MessageStatusAbandoned). + Eager("Dispatches"). + First(&message) + + require.NoError(t, err) + require.Equal(t, id, message.ID) + + require.Len(t, message.Dispatches, 2) + require.Contains(t, gjson.GetBytes(message.Dispatches[0].Error, "reason").String(), "failed to send email via smtp") + require.Contains(t, gjson.GetBytes(message.Dispatches[1].Error, "reason").String(), "failed to send email via smtp") +} + +func TestDispatchMessageEmitsEventWithNID(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + requestConfig := fmt.Sprintf(`{"url": "%s", "method": "POST", "body": "file://./stub/request.config.mailer.jsonnet"}`, srv.URL) + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyCourierDeliveryStrategy: "http", + config.ViperKeyCourierHTTPRequestConfig: requestConfig, + })) + + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + reg.SetTracer(otelx.NewNoop().WithOTLP(provider.Tracer("test"))) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + + _, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: testhelpers.RandomEmail(), + Subject: "test-subject", + Body: "test-body", + })) + require.NoError(t, err) + + require.NoError(t, c.DispatchQueue(t.Context())) + + ended := recorder.Ended() + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { + return sp.Name() == "courier.DispatchMessage" + }) + require.GreaterOrEqual(t, i, 0, "courier.DispatchMessage span not found") + + evs := ended[i].Events() + j := slices.IndexFunc(evs, func(ev sdktrace.Event) bool { + return ev.Name == events.CourierMessageDispatched.String() + }) + require.GreaterOrEqual(t, j, 0, "CourierMessageDispatched event not found on span") + + attrs := evs[j].Attributes + k := slices.IndexFunc([]attribute.KeyValue(attrs), func(a attribute.KeyValue) bool { + return string(a.Key) == semconv.AttributeKeyNID.String() + }) + require.GreaterOrEqual(t, k, 0, "NID attribute not found on event") + assert.NotEmpty(t, attrs[k].Value.AsString()) +} diff --git a/courier/email_templates.go b/courier/email_templates.go new file mode 100644 index 0000000..5f7e37f --- /dev/null +++ b/courier/email_templates.go @@ -0,0 +1,120 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/ory/kratos/courier/template" + + "github.com/pkg/errors" + + "github.com/ory/kratos/courier/template/email" +) + +type ( + Template interface { + json.Marshaler + TemplateType() template.TemplateType + } + + EmailTemplate interface { + Template + EmailSubject(context.Context) (string, error) + EmailBody(context.Context) (string, error) + EmailBodyPlaintext(context.Context) (string, error) + EmailRecipient() (string, error) + } + + RequestHeadersCarrier interface { + RequestHeaders() http.Header + } +) + +func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTemplate, error) { + switch msg.TemplateType { + case template.TypeRecoveryInvalid: + var t email.RecoveryInvalidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRecoveryInvalid(d, &t), nil + case template.TypeRecoveryValid: + var t email.RecoveryValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRecoveryValid(d, &t), nil + case template.TypeRecoveryCodeInvalid: + var t email.RecoveryCodeInvalidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRecoveryCodeInvalid(d, &t), nil + case template.TypeRecoveryCodeValid: + var t email.RecoveryCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRecoveryCodeValid(d, &t), nil + case template.TypeVerificationInvalid: + var t email.VerificationInvalidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewVerificationInvalid(d, &t), nil + case template.TypeVerificationValid: + var t email.VerificationValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewVerificationValid(d, &t), nil + case template.TypeVerificationCodeInvalid: + var t email.VerificationCodeInvalidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewVerificationCodeInvalid(d, &t), nil + case template.TypeVerificationCodeValid: + var t email.VerificationCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewVerificationCodeValid(d, &t), nil + case template.TypeTestStub: + var t email.TestStubModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewTestStub(&t), nil + case template.TypeLoginCodeValid: + var t email.LoginCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewLoginCodeValid(d, &t), nil + case template.TypeRegistrationCodeValid: + var t email.RegistrationCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRegistrationCodeValid(d, &t), nil + case template.TypeVerifiableAddressChanged: + var t email.VerifiableAddressChangedModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewVerifiableAddressChanged(d, &t), nil + case template.TypeAuthenticatorKeyAdded: + var t email.AuthenticatorKeyAddedModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewAuthenticatorKeyAdded(d, &t), nil + default: + return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType) + } +} diff --git a/courier/email_templates_test.go b/courier/email_templates_test.go new file mode 100644 index 0000000..0cddf9d --- /dev/null +++ b/courier/email_templates_test.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/pkg" +) + +func TestNewEmailTemplateFromMessage(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + ctx := context.Background() + + for tmplType, expectedTmpl := range map[template.TemplateType]courier.EmailTemplate{ + template.TypeRecoveryInvalid: email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{To: "foo"}), + template.TypeRecoveryValid: email.NewRecoveryValid(reg, &email.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}), + template.TypeRecoveryCodeValid: email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{To: "bar", RecoveryCode: "12345678"}), + template.TypeRecoveryCodeInvalid: email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{To: "bar"}), + template.TypeVerificationInvalid: email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{To: "baz"}), + template.TypeVerificationValid: email.NewVerificationValid(reg, &email.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}), + template.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), + template.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), + template.TypeTestStub: email.NewTestStub(&email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + template.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), + template.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), + template.TypeVerifiableAddressChanged: email.NewVerifiableAddressChanged(reg, &email.VerifiableAddressChangedModel{To: "far", ChangedAt: "2026-04-21T12:00:00Z", Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}}), + template.TypeAuthenticatorKeyAdded: email.NewAuthenticatorKeyAdded(reg, &email.AuthenticatorKeyAddedModel{To: "far", AddedAt: "2026-04-21T12:00:00Z", Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}}), + } { + t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { + tmplData, err := json.Marshal(expectedTmpl) + require.NoError(t, err) + + m := courier.Message{TemplateType: tmplType, TemplateData: tmplData} + actualTmpl, err := courier.NewEmailTemplateFromMessage(reg, m) + require.NoError(t, err) + + require.IsType(t, expectedTmpl, actualTmpl) + + expectedRecipient, err := expectedTmpl.EmailRecipient() + require.NoError(t, err) + actualRecipient, err := actualTmpl.EmailRecipient() + require.NoError(t, err) + require.Equal(t, expectedRecipient, actualRecipient) + + expectedSubject, err := expectedTmpl.EmailSubject(ctx) + require.NoError(t, err) + actualSubject, err := actualTmpl.EmailSubject(ctx) + require.NoError(t, err) + require.Equal(t, expectedSubject, actualSubject) + + expectedBody, err := expectedTmpl.EmailBody(ctx) + require.NoError(t, err) + actualBody, err := actualTmpl.EmailBody(ctx) + require.NoError(t, err) + require.Equal(t, expectedBody, actualBody) + + expectedBodyPlaintext, err := expectedTmpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) + }) + } +} diff --git a/courier/handler.go b/courier/handler.go new file mode 100644 index 0000000..de9b945 --- /dev/null +++ b/courier/handler.go @@ -0,0 +1,219 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "net/http" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" +) + +const ( + AdminRouteCourier = "/courier" + AdminRouteListMessages = AdminRouteCourier + "/messages" + AdminRouteGetMessage = AdminRouteCourier + "/messages/{msgID}" +) + +type ( + handlerDependencies interface { + httpx.WriterProvider + logrusx.Provider + nosurfx.CSRFProvider + PersistenceProvider + config.Provider + } + Handler struct { + r handlerDependencies + } + HandlerProvider interface { + CourierHandler() *Handler + } +) + +func NewHandler(r handlerDependencies) *Handler { + return &Handler{r: r} +} + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.r.CSRFHandler().IgnoreGlobs(httprouterx.AdminPrefix+AdminRouteListMessages, AdminRouteListMessages) + public.GET(httprouterx.AdminPrefix+AdminRouteListMessages, redir.RedirectToAdminRoute(h.r)) + public.GET(httprouterx.AdminPrefix+AdminRouteGetMessage, redir.RedirectToAdminRoute(h.r)) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(AdminRouteListMessages, h.listCourierMessages) + admin.GET(AdminRouteGetMessage, h.getCourierMessage) +} + +// Paginated Courier Message List Response +// +// swagger:response listCourierMessages +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listCourierMessagesResponse struct { + keysetpagination.ResponseHeaders + + // List of identities + // + // in:body + Body []Message +} + +// Paginated List Courier Message Parameters +// +// swagger:parameters listCourierMessages +type ListCourierMessagesParameters struct { + keysetpagination.RequestParameters + + // Status filters out messages based on status. + // If no value is provided, it doesn't take effect on filter. + // + // required: false + // in: query + Status *MessageStatus `json:"status"` + + // Recipient filters out messages based on recipient. + // If no value is provided, it doesn't take effect on filter. + // + // required: false + // in: query + Recipient string `json:"recipient"` +} + +// swagger:route GET /admin/courier/messages courier listCourierMessages +// +// # List Messages +// +// Lists all messages by given status and recipient. +// +// Produces: +// - application/json +// +// Security: +// oryAccessToken: +// +// Schemes: http, https +// +// Responses: +// 200: listCourierMessages +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) listCourierMessages(w http.ResponseWriter, r *http.Request) { + keys := h.r.Config().SecretsPagination(r.Context()) + filter, paginator, err := parseMessagesFilter(r, keys) + if err != nil { + h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, err) + return + } + + messages, nextPage, err := h.r.CourierPersister().ListMessages(r.Context(), filter, paginator) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + if !h.r.Config().IsInsecureDevMode(r.Context()) { + for i := range messages { + messages[i].Body = "" + messages[i].Subject = "" + } + } + + u := *r.URL + keysetpagination.SetLinkHeader(w, keys, &u, nextPage) + h.r.Writer().Write(w, r, messages) +} + +func parseMessagesFilter(r *http.Request, keys [][32]byte) (ListCourierMessagesParameters, []keysetpagination.Option, error) { + var status *MessageStatus + + if r.URL.Query().Has("status") { + ms, err := ToMessageStatus(r.URL.Query().Get("status")) + if err != nil { + return ListCourierMessagesParameters{}, nil, errors.WithStack(err) + } + + status = &ms + } + + opts, err := keysetpagination.ParseQueryParams(keys, r.URL.Query()) + if err != nil { + return ListCourierMessagesParameters{}, nil, errors.WithStack(err) + } + + return ListCourierMessagesParameters{ + Status: status, + Recipient: r.URL.Query().Get("recipient"), + }, opts, nil +} + +// Get Courier Message Parameters +// +// swagger:parameters getCourierMessage +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getCourierMessage struct { + // MessageID is the ID of the message. + // + // required: true + // in: path + MessageID string `json:"id"` +} + +// swagger:route GET /admin/courier/messages/{id} courier getCourierMessage +// +// # Get a Message +// +// Gets a specific messages by the given ID. +// +// Produces: +// - application/json +// +// Security: +// oryAccessToken: +// +// Schemes: http, https +// +// Responses: +// 200: message +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) getCourierMessage(w http.ResponseWriter, r *http.Request) { + msgID, err := uuid.FromString(r.PathValue("msgID")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebugf("could not parse parameter {id} as UUID, got %s", r.PathValue("id"))) + return + } + + message, err := h.r.CourierPersister().FetchMessage(r.Context(), msgID) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + if !h.r.Config().IsInsecureDevMode(r.Context()) { + message.Body = "" + message.Subject = "" + } + + h.r.Writer().Write(w, r, message) +} diff --git a/courier/handler_test.go b/courier/handler_test.go new file mode 100644 index 0000000..5c8624a --- /dev/null +++ b/courier/handler_test.go @@ -0,0 +1,285 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/x/httprouterx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/urlx" + "github.com/ory/x/uuidx" +) + +var defaultPageToken = courier.Message{}.DefaultPageToken().Encrypt(nil) + +func TestHandler(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + // Start kratos server + publicTS, adminTS := testhelpers.NewKratosServerWithCSRF(t, reg) + + tss := []struct { + name string + s *httptest.Server + }{ + { + name: "public", + s: publicTS, + }, + { + name: "admin", + s: adminTS, + }, + } + + mockServerURL := urlx.ParseOrPanic(publicTS.URL) + conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, mockServerURL.String()) + + 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) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + assert.EqualValuesf(t, expectCode, res.StatusCode, "%s", body) + return gjson.ParseBytes(body) + } + + getList := func(t *testing.T, tsName string, qs string) gjson.Result { + t.Helper() + href := courier.AdminRouteListMessages + qs + ts := adminTS + + if tsName == "public" { + href = httprouterx.AdminPrefix + href + ts = publicTS + } + + parsed := get(t, ts, href, http.StatusOK) + require.Truef(t, parsed.IsArray(), "%s", parsed.Raw) + return parsed + } + + t.Run("case=should return an empty list of messages", func(t *testing.T) { + for _, name := range []string{"public", "admin"} { + t.Run("endpoint="+name, func(t *testing.T) { + parsed := getList(t, name, "") + assert.Len(t, parsed.Array(), 0) + }) + } + }) + + t.Run("case=list messages", func(t *testing.T) { + // Arrange test data + const msgCount = 10 // total message count + const procCount = 5 // how many messages' status should be equal to `processing` + const rcptOryCount = 2 // how many messages' recipient should be equal to `noreply@ory.sh` + messages := make([]courier.Message, msgCount) + + for i := range messages { + require.NoError(t, faker.FakeData(&messages[i])) + messages[i].Type = courier.MessageTypeEmail + messages[i].Body = "body content" + if i < rcptOryCount { + messages[i].Recipient = "noreply@ory.sh" + } + require.NoError(t, reg.CourierPersister().AddMessage(context.Background(), &messages[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 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 { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, qs) + assert.Len(t, parsed.Array(), msgCount/2) + }) + } + }) + t.Run("case=should error with random page token", func(t *testing.T) { + qs := fmt.Sprintf(`?page_token=%s&page_size=%s`, uuidx.NewV4().String(), "250") + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + path := courier.AdminRouteListMessages + qs + if tc.name == "public" { + path = httprouterx.AdminPrefix + path + } + resp, err := tc.s.Client().Get(tc.s.URL + path) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + } + }) + }) + t.Run("filtering", func(t *testing.T) { + t.Run("case=should return all queued messages", func(t *testing.T) { + qs := fmt.Sprintf(`?page_token=%s&page_size=250&status=queued`, defaultPageToken) + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, qs) + assert.Len(t, parsed.Array(), msgCount-procCount) + + for _, item := range parsed.Array() { + assert.Equal(t, "queued", item.Get("status").String()) + } + }) + } + }) + t.Run("case=should return all processing messages", func(t *testing.T) { + qs := fmt.Sprintf(`?page_token=%s&page_size=250&status=processing`, defaultPageToken) + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, qs) + assert.Len(t, parsed.Array(), procCount) + + for _, item := range parsed.Array() { + assert.Equal(t, "processing", item.Get("status").String()) + } + }) + } + }) + t.Run("case=should return all messages with recipient equals to noreply@ory.sh", func(t *testing.T) { + qs := fmt.Sprintf(`?page_token=%s&page_size=250&recipient=noreply@ory.sh`, defaultPageToken) + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, qs) + assert.Len(t, parsed.Array(), rcptOryCount) + + for _, item := range parsed.Array() { + assert.Equal(t, "noreply@ory.sh", item.Get("recipient").String()) + } + }) + } + }) + }) + t.Run("case=body should be redacted if kratos is not in dev mode", func(t *testing.T) { + conf.MustSet(ctx, "dev", false) + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, "") + require.Lenf(t, parsed.Array(), msgCount, "%s", parsed.Raw) + + for _, item := range parsed.Array() { + assert.Equal(t, "", item.Get("body").String()) + assert.Equal(t, "", item.Get("subject").String()) + } + }) + } + }) + t.Run("case=body should not be redacted if kratos is in dev mode", func(t *testing.T) { + conf.MustSet(ctx, "dev", true) + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + parsed := getList(t, tc.name, "") + require.Lenf(t, parsed.Array(), msgCount, "%s", parsed.Raw) + + for _, item := range parsed.Array() { + assert.Equal(t, "body content", item.Get("body").String()) + assert.NotEqual(t, "", item.Get("subject").String()) + } + }) + } + }) + t.Run("case=should return with http status BadRequest when given status is invalid", func(t *testing.T) { + qs := fmt.Sprintf(`?page_token=%s&page_size=250&status=invalid_status`, defaultPageToken) + + res, err := adminTS.Client().Get(adminTS.URL + courier.AdminRouteListMessages + qs) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "status code should be equal to StatusBadRequest") + }) + + }) + t.Run("handler=getCourierMessage", func(t *testing.T) { + + message := courier.Message{} + require.NoError(t, faker.FakeData(&message)) + message.Type = courier.MessageTypeEmail + message.Body = "body content" + require.NoError(t, reg.CourierPersister().AddMessage(context.Background(), &message)) + require.NoError(t, reg.CourierPersister().RecordDispatch(ctx, message.ID, courier.CourierMessageDispatchStatusSuccess, errors.New("some error"))) + + getCourierMessag := func(s *httptest.Server, id string) gjson.Result { + + r, err := s.Client().Get(s.URL + "/admin/courier/messages/" + id) + require.NoError(t, err) + return gjson.ParseBytes(ioutilx.MustReadAll(r.Body)) + } + + t.Run("case=should return a message by id", func(t *testing.T) { + conf.MustSet(ctx, "dev", true) + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + body := getCourierMessag(tc.s, message.ID.String()) + assert.Equal(t, message.ID.String(), body.Get("id").String()) + assert.Equal(t, message.Recipient, body.Get("recipient").String()) + assert.Equal(t, message.Body, body.Get("body").String()) + assert.Equal(t, message.Subject, body.Get("subject").String()) + + // assert Eager works + assert.NotEmpty(t, body.Get("dispatches").Array()) + }) + } + }) + t.Run("case=does not contain body if not in production", func(t *testing.T) { + conf.MustSet(ctx, "dev", false) + + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + body := getCourierMessag(tc.s, message.ID.String()) + assert.Equal(t, message.ID.String(), body.Get("id").String()) + assert.Equal(t, "", body.Get("body").String()) + assert.Equal(t, "", body.Get("subject").String()) + }) + } + }) + t.Run("case=returns an error if parameter is malformed", func(t *testing.T) { + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + body := getCourierMessag(tc.s, "not-a-uuid") + + snapshotx.SnapshotTJSON(t, body.Raw) + }) + } + }) + t.Run("case=returns an error if no message is found", func(t *testing.T) { + for _, tc := range tss { + t.Run("endpoint="+tc.name, func(t *testing.T) { + body := getCourierMessag(tc.s, uuid.Nil.String()) + snapshotx.SnapshotTJSON(t, body.Raw) + }) + } + }) + }) +} diff --git a/courier/http_channel.go b/courier/http_channel.go new file mode 100644 index 0000000..9032d17 --- /dev/null +++ b/courier/http_channel.go @@ -0,0 +1,157 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "encoding/json" + "fmt" + "io" + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/request" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/otelx" +) + +type ( + httpChannel struct { + id string + requestConfig *request.Config + d channelDependencies + } + channelDependencies interface { + otelx.Provider + logrusx.Provider + httpx.ClientProvider + jsonnetsecure.VMProvider + ConfigProvider + } +) + +var _ Channel = new(httpChannel) + +func newHttpChannel(id string, requestConfig *request.Config, d channelDependencies) *httpChannel { + return &httpChannel{ + id: id, + requestConfig: requestConfig, + d: d, + } +} + +func (c *httpChannel) ID() string { + return c.id +} + +type httpDataModel struct { + Recipient string `json:"recipient"` + Subject string `json:"subject"` + Body string `json:"body"` + // HTMLBody optionally contains the HTML version of an email template when available. + HTMLBody string `json:"html_body,omitempty"` + TemplateType template.TemplateType `json:"template_type"` + TemplateData Template `json:"template_data"` + MessageType string `json:"message_type"` + RequestHeaders json.RawMessage `json:"request_headers"` +} + +func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { + ctx, span := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.httpChannel.Dispatch") + defer otelx.End(span, &err) + + builder, err := request.NewBuilder(c.requestConfig, c.d) + if err != nil { + return errors.WithStack(err) + } + + tmpl, err := newTemplate(c.d, msg) + if err != nil { + return errors.WithStack(err) + } + + td := httpDataModel{ + Recipient: msg.Recipient, + Subject: msg.Subject, + Body: msg.Body, + TemplateType: msg.TemplateType, + TemplateData: tmpl, + RequestHeaders: msg.RequestHeaders, + MessageType: msg.Type.String(), + } + + c.tryPopulateHTMLBody(ctx, tmpl, &td) + + req, err := builder.BuildRequest(ctx, td) + if err != nil { + return errors.WithStack(err) + } + + res, err := c.d.HTTPClient(ctx, + // fail fast and let the courier retry if needed instead of blocking the queue + httpx.ResilientClientWithMaxRetry(0), + httpx.ResilientClientWithConnectionTimeout(10*time.Second), + ).Do(req) + if err != nil { + return errors.WithStack(err) + } + // Close the original body, not the NopCloser below: closing it returns + // the connection to the pool and ends the otelhttp client span. + defer func(body io.ReadCloser) { _ = body.Close() }(res.Body) + res.Body = io.NopCloser(io.LimitReader(res.Body, 1024)) + + logger := c.d.Logger(). + WithField("http_server", c.requestConfig.URL). + 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 { + logger.Debug("Courier sent out mailer.") + return nil + } + + err = errors.Errorf( + "unable to dispatch mail delivery because upstream server replied with status code %d", + res.StatusCode, + ) + + body, _ := io.ReadAll(res.Body) + logger. + WithError(err). + WithField("http_response_body", string(body)). + Error("sending mail via HTTP failed.") + + return errors.WithStack(err) +} + +func (c *httpChannel) tryPopulateHTMLBody(ctx context.Context, tmpl Template, td *httpDataModel) { + if emailTmpl, ok := tmpl.(EmailTemplate); ok { + // Only get the HTML body from the template; plaintext body comes from msg.Body + // to maintain backward compatibility with existing behavior + if htmlBody, err := emailTmpl.EmailBody(ctx); err != nil { + c.d.Logger().WithError(err).Error("Unable to get email HTML body from template.") + } else { + td.HTMLBody = htmlBody + } + } +} + +func newTemplate(d template.Dependencies, msg Message) (Template, error) { + switch msg.Type { + case MessageTypeEmail: + return NewEmailTemplateFromMessage(d, msg) + case MessageTypeSMS: + return NewSMSTemplateFromMessage(d, msg) + default: + return nil, fmt.Errorf("received unexpected message type: %s", msg.Type) + } +} diff --git a/courier/http_test.go b/courier/http_test.go new file mode 100644 index 0000000..fd3472e --- /dev/null +++ b/courier/http_test.go @@ -0,0 +1,169 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/configx" +) + +func TestQueueHTTPEmail(t *testing.T) { + type sendEmailRequestBody struct { + IdentityID string `json:"identity_id"` + IdentityEmail string `json:"identity_email"` + Recipient string `json:"recipient"` + TemplateType string `json:"template_type"` + To string `json:"to"` + RecoveryCode string `json:"recovery_code"` + RecoveryURL string `json:"recovery_url"` + VerificationURL string `json:"verification_url"` + VerificationCode string `json:"verification_code"` + Body string `json:"body"` + HTMLBody string `json:"html_body"` + Subject string `json:"subject"` + } + + expectedEmail := []*email.TestStubModel{ + { + To: "test-2@test.com", + Subject: "test-mailer-subject-1", + Body: "test-mailer-body-1", + HTMLBody: "test-mailer-body-html-1", + }, + { + To: "test-2@test.com", + Subject: "test-mailer-subject-2", + Body: "test-mailer-body-2", + }, + } + + actual := make(chan sendEmailRequestBody, len(expectedEmail)) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rb, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var body sendEmailRequestBody + + err = json.Unmarshal(rb, &body) + require.NoError(t, err) + + assert.NotEmpty(t, r.Header["Authorization"]) + assert.Equal(t, "Basic bWU6MTIzNDU=", r.Header["Authorization"][0]) + + actual <- body + })) + t.Cleanup(srv.Close) + + requestConfig := fmt.Sprintf(`{ + "url": "%s", + "method": "POST", + "auth": { + "type": "basic_auth", + "config": { + "user": "me", + "password": "12345" + } + }, + "body": "file://./stub/request.config.mailer.jsonnet" + }`, srv.URL) + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyCourierDeliveryStrategy: "http", + config.ViperKeyCourierHTTPRequestConfig: requestConfig, + config.ViperKeyCourierSMTPURL: "http://foo.url", + })) + + courier, err := reg.Courier(t.Context()) + require.NoError(t, err) + + for _, message := range expectedEmail { + id, err := courier.QueueEmail(t.Context(), email.NewTestStub(message)) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, id) + } + + require.NoError(t, courier.DispatchQueue(t.Context())) + close(actual) + + require.Len(t, actual, len(expectedEmail)) + + i := 0 + for message := range actual { + expected := expectedEmail[i] + + assert.Equal(t, expected.To, message.To) + assert.Equal(t, expected.Body, message.Body) + assert.Equal(t, expected.HTMLBody, message.HTMLBody) + assert.Equal(t, expected.Subject, message.Subject) + + i++ + } +} + +func TestQueueHTTPEmailWithOAuth2LoginRequest(t *testing.T) { + actual := make(chan []byte, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rb, err := io.ReadAll(r.Body) + require.NoError(t, err) + actual <- rb + })) + t.Cleanup(srv.Close) + + requestConfig := fmt.Sprintf(`{ + "url": "%s", + "method": "POST", + "body": "base64://%s" + }`, srv.URL, base64.StdEncoding.EncodeToString([]byte("function(ctx) ctx"))) + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyCourierDeliveryStrategy: "http", + config.ViperKeyCourierHTTPRequestConfig: requestConfig, + config.ViperKeyCourierSMTPURL: "http://foo.url", + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + + _, err = c.QueueEmail(t.Context(), email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{ + To: "test@test.com", + LoginCode: "123456", + OAuth2LoginRequest: &template.OAuth2LoginRequest{ + Challenge: "test-challenge", + Client: template.OAuth2Client{ + ClientID: "test-client-id", + ClientName: "Test Client", + ClientURI: "https://client.example.com", + LogoURI: "https://client.example.com/logo.png", + Metadata: map[string]any{"brand": "acme"}, + }, + }, + })) + require.NoError(t, err) + + require.NoError(t, c.DispatchQueue(t.Context())) + + body := <-actual + assert.Equal(t, "test-challenge", gjson.GetBytes(body, "template_data.oauth2_login_request.challenge").String()) + assert.Equal(t, "test-client-id", gjson.GetBytes(body, "template_data.oauth2_login_request.client.client_id").String()) + assert.Equal(t, "Test Client", gjson.GetBytes(body, "template_data.oauth2_login_request.client.client_name").String()) + assert.Equal(t, "https://client.example.com", gjson.GetBytes(body, "template_data.oauth2_login_request.client.client_uri").String()) + assert.Equal(t, "https://client.example.com/logo.png", gjson.GetBytes(body, "template_data.oauth2_login_request.client.logo_uri").String()) + assert.Equal(t, "acme", gjson.GetBytes(body, "template_data.oauth2_login_request.client.metadata.brand").String()) +} diff --git a/courier/message.go b/courier/message.go new file mode 100644 index 0000000..c987726 --- /dev/null +++ b/courier/message.go @@ -0,0 +1,226 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "encoding/json" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier/template" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" +) + +// A Message's Status +// +// swagger:model courierMessageStatus +type MessageStatus int + +const ( + MessageStatusQueued MessageStatus = iota + 1 + MessageStatusSent + MessageStatusProcessing + MessageStatusAbandoned +) + +const ( + messageStatusQueuedText = "queued" + messageStatusSentText = "sent" + messageStatusProcessingText = "processing" + messageStatusAbandonedText = "abandoned" +) + +func ToMessageStatus(str string) (MessageStatus, error) { + switch s := stringsx.SwitchExact(str); { + case s.AddCase(MessageStatusQueued.String()): + return MessageStatusQueued, nil + case s.AddCase(MessageStatusSent.String()): + return MessageStatusSent, nil + case s.AddCase(MessageStatusProcessing.String()): + return MessageStatusProcessing, nil + case s.AddCase(MessageStatusAbandoned.String()): + return MessageStatusAbandoned, nil + default: + return 0, errors.WithStack(herodot.ErrBadRequest().WithWrap(s.ToUnknownCaseErr()).WithReason("Message status is not valid")) + } +} + +func (ms MessageStatus) String() string { + switch ms { + case MessageStatusQueued: + return messageStatusQueuedText + case MessageStatusSent: + return messageStatusSentText + case MessageStatusProcessing: + return messageStatusProcessingText + case MessageStatusAbandoned: + return messageStatusAbandonedText + default: + return "" + } +} + +func (ms MessageStatus) IsValid() error { + switch ms { + case MessageStatusQueued, MessageStatusSent, MessageStatusProcessing, MessageStatusAbandoned: + return nil + default: + return errors.WithStack(herodot.ErrBadRequest().WithReason("Message status is not valid")) + } +} + +func (ms MessageStatus) MarshalJSON() ([]byte, error) { + if err := ms.IsValid(); err != nil { + return nil, err + } + return json.Marshal(ms.String()) +} + +func (ms *MessageStatus) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + + s, err := ToMessageStatus(str) + if err != nil { + return err + } + + *ms = s + return nil +} + +// A Message's Type +// +// It can either be `email` or `phone` +// +// swagger:model courierMessageType +type MessageType int + +const ( + MessageTypeEmail MessageType = iota + 1 + MessageTypeSMS +) + +const ( + messageTypeEmailText = "email" + messageTypeSMSText = "sms" +) + +func ToMessageType(str string) (MessageType, error) { + switch s := stringsx.SwitchExact(str); { + case s.AddCase(messageTypeEmailText): + return MessageTypeEmail, nil + case s.AddCase(messageTypeSMSText): + return MessageTypeSMS, nil + default: + return 0, errors.WithStack(herodot.ErrBadRequest().WithWrap(s.ToUnknownCaseErr()).WithReason("Message type is not valid")) + } +} + +func (mt MessageType) String() string { + switch mt { + case MessageTypeEmail: + return messageTypeEmailText + case MessageTypeSMS: + return messageTypeSMSText + default: + return "" + } +} + +func (mt MessageType) IsValid() error { + switch mt { + case MessageTypeEmail, MessageTypeSMS: + return nil + default: + return errors.WithStack(herodot.ErrBadRequest().WithReason("Message type is not valid")) + } +} + +func (mt MessageType) MarshalJSON() ([]byte, error) { + if err := mt.IsValid(); err != nil { + return nil, err + } + return json.Marshal(mt.String()) +} + +func (mt *MessageType) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + + t, err := ToMessageType(str) + if err != nil { + return err + } + + *mt = t + return nil +} + +// swagger:model message +type Message struct { + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + // required: true + Status MessageStatus `json:"status" db:"status"` + // required: true + Type MessageType `json:"type" db:"type"` + // required: true + Recipient string `json:"recipient" db:"recipient"` + // required: true + Body string `json:"body" db:"body"` + // required: true + Subject string `json:"subject" db:"subject"` + // required: true + TemplateType template.TemplateType `json:"template_type" db:"template_type"` + + Channel sqlxx.NullString `json:"channel" db:"channel"` + + TemplateData []byte `json:"-" db:"template_data"` + RequestHeaders []byte `json:"-" faker:"-" db:"request_headers"` + // required: true + SendCount int `json:"send_count" db:"send_count"` + + // Dispatches store information about the attempts of delivering a message + // May contain an error if any happened, or just the `success` state. + Dispatches []MessageDispatch `json:"dispatches,omitempty" has_many:"courier_message_dispatches" order_by:"created_at desc" faker:"-"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + // required: true + CreatedAt time.Time `json:"created_at" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + // required: true + UpdatedAt time.Time `json:"updated_at" faker:"-" db:"updated_at"` +} + +func (m Message) PageToken() keysetpagination.PageToken { + return keysetpagination.NewPageToken( + keysetpagination.Column{ + Name: "created_at", + Order: keysetpagination.OrderDescending, + Value: m.CreatedAt, + }, keysetpagination.Column{ + Name: "id", + Value: m.ID, + }, + ) +} + +func (m Message) DefaultPageToken() keysetpagination.PageToken { + return Message{ID: uuid.Nil, CreatedAt: time.Date(2200, 12, 31, 23, 59, 59, 0, time.UTC)}.PageToken() +} + +func (m Message) TableName() string { return "courier_messages" } +func (m *Message) GetID() uuid.UUID { return m.ID } diff --git a/courier/message_dispatch.go b/courier/message_dispatch.go new file mode 100644 index 0000000..a7e94f7 --- /dev/null +++ b/courier/message_dispatch.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlxx" +) + +// swagger:enum CourierMessageDispatchStatus +type CourierMessageDispatchStatus string + +const ( + CourierMessageDispatchStatusFailed CourierMessageDispatchStatus = "failed" + CourierMessageDispatchStatusSuccess CourierMessageDispatchStatus = "success" +) + +// MessageDispatch represents an attempt of sending a courier message +// It contains the status of the attempt (failed or successful) and the error if any occured +// +// swagger:model messageDispatch +type MessageDispatch struct { + // The ID of this message dispatch + // required: true + ID uuid.UUID `json:"id" db:"id"` + + // The ID of the message being dispatched + // required: true + MessageID uuid.UUID `json:"message_id" db:"message_id"` + + // The status of this dispatch + // Either "failed" or "success" + // required: true + Status CourierMessageDispatchStatus `json:"status" db:"status"` + + // An optional error + Error sqlxx.JSONRawMessage `json:"error,omitempty" db:"error"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + // required: true + CreatedAt time.Time `json:"created_at" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + // required: true + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + + NID uuid.UUID `json:"-" db:"nid"` +} + +func (MessageDispatch) TableName() string { + return "courier_message_dispatches" +} diff --git a/courier/message_test.go b/courier/message_test.go new file mode 100644 index 0000000..5d2e328 --- /dev/null +++ b/courier/message_test.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier" +) + +func TestMessageStatusValidity(t *testing.T) { + invalid := courier.MessageStatus(0) + require.ErrorIs(t, invalid.IsValid(), herodot.ErrBadRequest(), "IsValid() should return an error when message status is invalid") +} + +func TestToMessageStatus(t *testing.T) { + t.Run("case=should return corresponding MessageStatus for given str", func(t *testing.T) { + for str, exp := range map[string]courier.MessageStatus{ + "queued": courier.MessageStatusQueued, + "sent": courier.MessageStatusSent, + "processing": courier.MessageStatusProcessing, + "abandoned": courier.MessageStatusAbandoned, + } { + result, err := courier.ToMessageStatus(str) + require.NoError(t, err) + require.Equal(t, exp, result) + } + }) + t.Run("case=should return error for invalid message status str", func(t *testing.T) { + result, err := courier.ToMessageStatus("invalid") + require.Error(t, err, herodot.ErrBadRequest()) + require.Error(t, result.IsValid(), herodot.ErrBadRequest()) + }) +} + +func TestMessageTypeValidity(t *testing.T) { + invalid := courier.MessageType(0) + require.ErrorIs(t, invalid.IsValid(), herodot.ErrBadRequest(), "IsValid() should return an error when message type is invalid") +} + +func TestToMessageType(t *testing.T) { + t.Run("case=should return corresponding MessageType for given str", func(t *testing.T) { + for str, exp := range map[string]courier.MessageType{ + "email": courier.MessageTypeEmail, + "sms": courier.MessageTypeSMS, + } { + result, err := courier.ToMessageType(str) + require.NoError(t, err) + require.Equal(t, exp, result) + } + }) + t.Run("case=should return error for invalid message type str", func(t *testing.T) { + result, err := courier.ToMessageType("invalid") + require.ErrorIs(t, err, herodot.ErrBadRequest()) + require.ErrorIs(t, result.IsValid(), herodot.ErrBadRequest()) + }) +} diff --git a/courier/persistence.go b/courier/persistence.go new file mode 100644 index 0000000..2789d1d --- /dev/null +++ b/courier/persistence.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" +) + +var ErrQueueEmpty = errors.New("queue is empty") + +type ( + Persister interface { + AddMessage(context.Context, *Message) error + + NextMessages(context.Context, uint8) ([]Message, error) + + SetMessageStatus(context.Context, uuid.UUID, MessageStatus) error + + LatestQueuedMessage(ctx context.Context) (*Message, error) + + IncrementMessageSendCount(context.Context, uuid.UUID) error + + // ListMessages lists all messages in the store given the page, itemsPerPage, status and recipient. + // Returns list of messages, total count of messages satisfied by given filter, and error if any + ListMessages(context.Context, ListCourierMessagesParameters, []keysetpagination.Option) ([]Message, *keysetpagination.Paginator, error) + + // FetchMessage returns a message with the id or nil and an error if not found + FetchMessage(context.Context, uuid.UUID) (*Message, error) + + // Records an attempt of sending out a courier message + // Returns an error if it fails + RecordDispatch(ctx context.Context, msgID uuid.UUID, status CourierMessageDispatchStatus, err error) error + } + PersistenceProvider interface { + CourierPersister() Persister + } +) diff --git a/courier/sms.go b/courier/sms.go new file mode 100644 index 0000000..f23ef86 --- /dev/null +++ b/courier/sms.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "encoding/json" + + "github.com/gofrs/uuid" +) + +func (c *courier) QueueSMS(ctx context.Context, t SMSTemplate) (uuid.UUID, error) { + recipient, err := t.PhoneNumber() + if err != nil { + return uuid.Nil, err + } + + templateData, err := json.Marshal(t) + if err != nil { + return uuid.Nil, err + } + + body, err := t.SMSBody(ctx) + if err != nil { + return uuid.Nil, err + } + + requestHeaders := []byte(`{}`) + if t, ok := t.(RequestHeadersCarrier); ok { + requestHeaders, err = json.Marshal(t.RequestHeaders()) + if err != nil { + return uuid.Nil, err + } + } + + message := &Message{ + Status: MessageStatusQueued, + Type: MessageTypeSMS, + Channel: "sms", + Recipient: recipient, + TemplateType: t.TemplateType(), + TemplateData: templateData, + RequestHeaders: requestHeaders, + Body: body, + } + if err := c.deps.CourierPersister().AddMessage(ctx, message); err != nil { + return uuid.Nil, err + } + + return message.ID, nil +} diff --git a/courier/sms_templates.go b/courier/sms_templates.go new file mode 100644 index 0000000..325ac5c --- /dev/null +++ b/courier/sms_templates.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/sms" +) + +type SMSTemplate interface { + Template + SMSBody(context.Context) (string, error) + PhoneNumber() (string, error) +} + +func NewSMSTemplateFromMessage(d template.Dependencies, m Message) (SMSTemplate, error) { + switch m.TemplateType { + case template.TypeVerificationCodeValid: + var t sms.VerificationCodeValidModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewVerificationCodeValid(d, &t), nil + case template.TypeRecoveryCodeValid: + var t sms.RecoveryCodeValidModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewRecoveryCodeValid(d, &t), nil + case template.TypeTestStub: + var t sms.TestStubModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewTestStub(&t), nil + case template.TypeLoginCodeValid: + var t sms.LoginCodeValidModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewLoginCodeValid(d, &t), nil + case template.TypeRegistrationCodeValid: + var t sms.RegistrationCodeValidModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewRegistrationCodeValid(d, &t), nil + case template.TypeVerifiableAddressChanged: + var t sms.VerifiableAddressChangedModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewVerifiableAddressChanged(d, &t), nil + case template.TypeAuthenticatorKeyAdded: + var t sms.AuthenticatorKeyAddedModel + if err := json.Unmarshal(m.TemplateData, &t); err != nil { + return nil, err + } + return sms.NewAuthenticatorKeyAdded(d, &t), nil + default: + return nil, errors.Errorf("received unexpected message template type: %s", m.TemplateType) + } +} diff --git a/courier/sms_templates_test.go b/courier/sms_templates_test.go new file mode 100644 index 0000000..a0f204b --- /dev/null +++ b/courier/sms_templates_test.go @@ -0,0 +1,64 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestSMSTemplateType(t *testing.T) { + for expectedType, tmpl := range map[template.TemplateType]courier.SMSTemplate{ + template.TypeVerificationCodeValid: &sms.VerificationCodeValid{}, + template.TypeTestStub: &sms.TestStub{}, + } { + t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { + require.Equal(t, expectedType, tmpl.TemplateType()) + }) + } +} + +func TestNewSMSTemplateFromMessage(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + ctx := context.Background() + + for tmplType, expectedTmpl := range map[template.TemplateType]courier.SMSTemplate{ + template.TypeVerificationCodeValid: sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{To: "+12345678901"}), + template.TypeTestStub: sms.NewTestStub(&sms.TestStubModel{To: "+12345678901", Body: "test body"}), + template.TypeVerifiableAddressChanged: sms.NewVerifiableAddressChanged(reg, &sms.VerifiableAddressChangedModel{To: "+12345678901", ChangedAt: "2026-04-21T12:00:00Z", Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}}), + template.TypeAuthenticatorKeyAdded: sms.NewAuthenticatorKeyAdded(reg, &sms.AuthenticatorKeyAddedModel{To: "+12345678901", AddedAt: "2026-04-21T12:00:00Z", Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}}), + } { + t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { + tmplData, err := json.Marshal(expectedTmpl) + require.NoError(t, err) + + m := courier.Message{TemplateType: tmplType, TemplateData: tmplData} + actualTmpl, err := courier.NewSMSTemplateFromMessage(reg, m) + require.NoError(t, err) + + require.IsType(t, expectedTmpl, actualTmpl) + + expectedRecipient, err := expectedTmpl.PhoneNumber() + require.NoError(t, err) + actualRecipient, err := actualTmpl.PhoneNumber() + require.NoError(t, err) + require.Equal(t, expectedRecipient, actualRecipient) + + expectedBody, err := expectedTmpl.SMSBody(ctx) + require.NoError(t, err) + actualBody, err := actualTmpl.SMSBody(ctx) + require.NoError(t, err) + require.Equal(t, expectedBody, actualBody) + }) + } +} diff --git a/courier/sms_test.go b/courier/sms_test.go new file mode 100644 index 0000000..4c3dd2b --- /dev/null +++ b/courier/sms_test.go @@ -0,0 +1,140 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/configx" +) + +func TestQueueSMS(t *testing.T) { + expectedSender := "Kratos Test" + expectedSMS := []*sms.TestStubModel{ + { + To: "+12065550101", + Body: "test-sms-body-1", + }, + { + To: "+12065550102", + Body: "test-sms-body-2", + }, + } + + actual := make(chan *sms.TestStubModel, len(expectedSMS)) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + type sendSMSRequestBody struct { + To string + From string + Body string + } + + rb, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var body sendSMSRequestBody + + err = json.Unmarshal(rb, &body) + require.NoError(t, err) + + assert.NotEmpty(t, r.Header["Authorization"]) + assert.Equal(t, "Basic bWU6MTIzNDU=", r.Header["Authorization"][0]) + + assert.Equal(t, body.From, expectedSender) + actual <- &sms.TestStubModel{ + To: body.To, + Body: body.Body, + } + })) + t.Cleanup(srv.Close) + + requestConfig := fmt.Sprintf(`{ + "url": "%s", + "method": "POST", + "body": "file://./stub/request.config.twilio.jsonnet", + "auth": { + "type": "basic_auth", + "config": { + "user": "me", + "password": "12345" + } + } + }`, srv.URL) + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyCourierChannels: fmt.Sprintf(`[{ + "id": "sms", + "type": "http", + "request_config": %s + }]`, requestConfig), + config.ViperKeyCourierSMTPURL: "http://foo.url", + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + + for _, message := range expectedSMS { + id, err := c.QueueSMS(t.Context(), sms.NewTestStub(message)) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, id) + } + + require.NoError(t, c.DispatchQueue(t.Context())) + close(actual) + + require.Len(t, actual, len(expectedSMS)) + + i := 0 + for message := range actual { + expected := expectedSMS[i] + + assert.Equal(t, expected.To, message.To) + assert.Equal(t, expected.Body, message.Body) + i++ + } +} + +func TestDisallowedInternalNetwork(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyCourierChannels: `[ + { + "id": "sms", + "type": "http", + "request_config": { + "url": "http://127.0.0.1/", + "method": "GET", + "body": "file://./stub/request.config.twilio.jsonnet" + } + } + ]`, + config.ViperKeyCourierSMTPURL: "http://foo.url", + config.ViperKeyClientHTTPNoPrivateIPRanges: true, + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + c.(interface { + FailOnDispatchError() + }).FailOnDispatchError() + _, err = c.QueueSMS(t.Context(), sms.NewTestStub(&sms.TestStubModel{ + To: "+12065550101", + Body: "test-sms-body-1", + })) + require.NoError(t, err) + + err = c.DispatchQueue(t.Context()) + assert.ErrorContains(t, err, "no route to host") +} diff --git a/courier/smtp.go b/courier/smtp.go new file mode 100644 index 0000000..340f557 --- /dev/null +++ b/courier/smtp.go @@ -0,0 +1,159 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "crypto/tls" + "encoding/json" + stdmail "net/mail" + "net/url" + "strconv" + "time" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/x/contextx" + "github.com/ory/x/ipx" + + "github.com/gofrs/uuid" + + gomail "github.com/ory/mail/v3" +) + +type SMTPClient struct { + *gomail.Dialer +} + +func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, error) { + uri, err := url.Parse(cfg.ConnectionURI) + if err != nil { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("The SMTP connection URI is malformed. Please contact a system administrator.")) + } + + var tlsCertificates []tls.Certificate + + if cfg.ClientCertPath != "" && cfg.ClientKeyPath != "" { + clientCert, err := tls.LoadX509KeyPair(cfg.ClientCertPath, cfg.ClientKeyPath) + if err == nil { + tlsCertificates = append(tlsCertificates, clientCert) + } else { + deps.Logger(). + WithError(err). + Error("Unable to load tls certificate and private key for smtp client.") + } + } + + password, _ := uri.User.Password() + port, _ := strconv.ParseInt(uri.Port(), 10, 0) + + dial := ipx.AllowInternalDialFunc + if deps.CourierConfig().ClientSMTPNoPrivateIPRanges(contextx.RootContext) { + dial = ipx.ProhibitInternalDialFunc + } + + dialer := &gomail.Dialer{ + Host: uri.Hostname(), + Port: int(port), + Username: uri.User.Username(), + Password: password, + LocalName: cfg.LocalName, + DialFunc: dial, + + Timeout: time.Second * 10, + RetryFailure: true, + } + + sslSkipVerify, _ := strconv.ParseBool(uri.Query().Get("skip_ssl_verify")) + + serverName := uri.Query().Get("server_name") + if serverName == "" { + 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 + // and defaulting to fully-encrypted protocols https://datatracker.ietf.org/doc/html/rfc8314) + switch uri.Scheme { + case "smtp": + // Enforcing StartTLS by default for security best practices (config review, etc.) + skipStartTLS, _ := strconv.ParseBool(uri.Query().Get("disable_starttls")) + if !skipStartTLS { + dialer.TLSConfig = tlsConfig + // Enforcing StartTLS + dialer.StartTLSPolicy = gomail.MandatoryStartTLS + } + case "smtps": + dialer.TLSConfig = tlsConfig + dialer.SSL = true + } + + return &SMTPClient{ + Dialer: dialer, + }, nil +} + +func (c *courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, error) { + recipient, err := t.EmailRecipient() + if err != nil { + return uuid.Nil, errors.WithStack(err) + } + // The RFC 5322 fallback keeps previously-valid addresses working; the + // address is not included in the error because it is identity PII. + if _, err := stdmail.ParseAddress(recipient); err != nil && !x.IsEmailAddress(recipient) { + return uuid.Nil, errors.New("courier: email recipient is not a valid email address") + } + + subject, err := t.EmailSubject(ctx) + if err != nil { + return uuid.Nil, errors.WithStack(err) + } + + bodyPlaintext, err := t.EmailBodyPlaintext(ctx) + if err != nil { + return uuid.Nil, errors.WithStack(err) + } + + templateData, err := json.Marshal(t) + if err != nil { + return uuid.Nil, errors.WithStack(err) + } + + requestHeaders := []byte(`{}`) + if t, ok := t.(RequestHeadersCarrier); ok { + requestHeaders, err = json.Marshal(t.RequestHeaders()) + if err != nil { + return uuid.Nil, err + } + } + + message := &Message{ + Status: MessageStatusQueued, + Type: MessageTypeEmail, + Channel: "email", + Recipient: recipient, + Body: bodyPlaintext, + Subject: subject, + TemplateType: t.TemplateType(), + TemplateData: templateData, + RequestHeaders: requestHeaders, + } + + if err := c.deps.CourierPersister().AddMessage(ctx, message); err != nil { + return uuid.Nil, errors.WithStack(err) + } + + return message.ID, nil +} diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go new file mode 100644 index 0000000..4ca4d85 --- /dev/null +++ b/courier/smtp_channel.go @@ -0,0 +1,201 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "net" + stdmail "net/mail" + "net/textproto" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/mail/v3" + "github.com/ory/x/otelx" +) + +type ( + SMTPChannel struct { + smtpClient *SMTPClient + d Dependencies + + newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error) + } +) + +var _ Channel = new(SMTPChannel) + +func NewSMTPChannel(deps Dependencies, cfg *config.SMTPConfig) (*SMTPChannel, error) { + return NewSMTPChannelWithCustomTemplates(deps, cfg, NewEmailTemplateFromMessage) +} + +func NewSMTPChannelWithCustomTemplates(deps Dependencies, cfg *config.SMTPConfig, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (*SMTPChannel, error) { + smtpClient, err := NewSMTPClient(deps, cfg) + if err != nil { + return nil, err + } + return &SMTPChannel{ + smtpClient: smtpClient, + d: deps, + newEmailTemplateFromMessage: newEmailTemplateFromMessage, + }, nil +} + +func (c *SMTPChannel) ID() string { + return "email" +} + +func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) (err error) { + ctx, span := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.SMTPChannel.Dispatch") + defer otelx.End(span, &err) + + if c.smtpClient.Host == "" { + return errors.WithStack(herodot.ErrInternalServerError().WithErrorf("Courier tried to deliver an email but %s is not set!", config.ViperKeyCourierSMTPURL)) + } + + channels, err := c.d.CourierConfig().CourierChannels(ctx) + if err != nil { + return err + } + + var cfg *config.SMTPConfig + for _, channel := range channels { + if channel.ID == "email" && channel.SMTPConfig != nil { + cfg = channel.SMTPConfig + break + } + } + + if cfg == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithErrorf("Courier tried to deliver an email but SMTP channel is misconfigured.")) + } + + // Re-validate the recipient read from the database: a value that is neither + // RFC 5322-valid nor injection-safe per x.IsEmailAddress is abandoned + // rather than placed in an SMTP command or header. + _, rfcErr := stdmail.ParseAddress(msg.Recipient) + if rfcErr != nil && !x.IsEmailAddress(msg.Recipient) { + if err := c.d.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { + c.d.Logger().WithError(err).Error(`Unable to set the message status to "abandoned".`) + return errors.WithStack(err) + } + return errors.WithStack(herodot.ErrInternalServerError(). + WithReasonf("courier: email recipient for message %s is not a valid email address", msg.ID)) + } + + gm := mail.NewMessage() + if cfg.FromName == "" { + gm.SetHeader("From", cfg.FromAddress) + } else { + gm.SetAddressHeader("From", cfg.FromAddress, cfg.FromName) + } + + gm.SetHeader("To", msg.Recipient) + gm.SetHeader("Subject", msg.Subject) + + headers := cfg.Headers + for k, v := range headers { + gm.SetHeader(k, v) + } + + gm.SetBody("text/plain", msg.Body) + + logger := c.d.Logger(). + WithField("smtp_server", net.JoinHostPort(c.smtpClient.Host, strconv.Itoa(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). + WithField("trace_id", span.SpanContext().TraceID()) + + tmpl, err := c.newEmailTemplateFromMessage(c.d, msg) + if err != nil { + logger. + WithError(err).Error(`Unable to get email template from message.`) + } else if htmlBody, err := tmpl.EmailBody(ctx); err != nil { + logger. + WithError(err).Error(`Unable to get email body from template.`) + } else { + gm.AddAlternative("text/html", htmlBody) + } + + dialCtx, dialSpan := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.SMTPChannel.Dispatch.Dial", trace.WithAttributes( + semconv.NetPeerName(c.smtpClient.Host), + semconv.NetPeerPort(c.smtpClient.Port), + semconv.NetProtocolName("smtp"), + )) + snd, err := c.smtpClient.Dial(dialCtx) + otelx.End(dialSpan, &err) + + if err != nil { + logger. + WithError(err). + WithField("smtp_host", c.smtpClient.Host). + WithField("smtp_port", c.smtpClient.Port). + Error("Unable to dial SMTP connection.") + return errors.WithStack(herodot.ErrInternalServerError(). + WithError(err.Error()).WithReason("failed to send email via smtp")) + } + defer func() { _ = snd.Close() }() + + sendCtx, sendSpan := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.SMTPChannel.Dispatch.Send") + if rfcErr == nil { + // mail.Send derives the envelope from the To/Cc/Bcc headers, so Cc/Bcc + // configured via courier.smtp.headers are delivered. + err = mail.Send(sendCtx, snd, gm) + } else { + // A non-RFC recipient bypasses mail.Send's strict header re-parse with + // an explicit envelope; Cc/Bcc headers are not delivered on this path. + for k := range headers { + if strings.EqualFold(k, "Cc") || strings.EqualFold(k, "Bcc") { + logger.Warnf("Configured %q recipients are not delivered for a non-RFC email address.", k) + } + } + err = snd.Send(sendCtx, cfg.FromAddress, []string{msg.Recipient}, gm) + } + otelx.End(sendSpan, &err) + + if err != nil { + logger. + WithError(err). + Error("Unable to send email using SMTP connection.") + + var protoErr *textproto.Error + var mailErr *mail.SendError + + switch { + case errors.As(err, &mailErr) && errors.As(mailErr.Cause, &protoErr) && protoErr.Code >= 500: + fallthrough + case errors.As(err, &protoErr) && protoErr.Code >= 500: + // 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 { + logger. + WithError(err). + Error(`Unable to reset the retried message's status to "abandoned".`) + return errors.WithStack(err) + } + } + + return errors.WithStack(herodot.ErrInternalServerError(). + WithError(err.Error()).WithReason("failed to send email via smtp")) + } + + dispatchDuration := time.Since(msg.CreatedAt).Milliseconds() + logger.WithField("dispatch_duration_ms", dispatchDuration).Debug("Courier sent out message.") + + return nil +} diff --git a/courier/smtp_test.go b/courier/smtp_test.go new file mode 100644 index 0000000..89ed969 --- /dev/null +++ b/courier/smtp_test.go @@ -0,0 +1,249 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "io" + "math/big" + "net/http" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier" + templates "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + gomail "github.com/ory/mail/v3" + "github.com/ory/x/configx" +) + +func TestNewSMTPClientPreventLeak(t *testing.T) { + // Test for https://hackerone.com/reports/2384028 + + invalidURL := "sm<>t>p://f%oo::bar:baz@my-server:1234:122/" + conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyCourierSMTPURL, invalidURL)) + + channels, err := conf.CourierChannels(t.Context()) + 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) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + + setupSMTPClient := func(stringURL string) *courier.SMTPClient { + conf.MustSet(t.Context(), config.ViperKeyCourierSMTPURL, stringURL) + + channels, err := conf.CourierChannels(t.Context()) + require.NoError(t, err) + require.Len(t, channels, 1) + c, err := courier.NewSMTPClient(reg, channels[0].SMTPConfig) + require.NoError(t, err) + return c + } + + // Should enforce StartTLS => dialer.StartTLSPolicy = gomail.MandatoryStartTLS and dialer.SSL = false + smtp := setupSMTPClient("smtp://foo:bar@my-server:1234/") + assert.Equal(t, smtp.StartTLSPolicy, gomail.MandatoryStartTLS, "StartTLS not enforced") + assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled") + + // Should enforce TLS => dialer.SSL = true + smtp = setupSMTPClient("smtps://foo:bar@my-server:1234/") + assert.Equal(t, smtp.SSL, true, "Implicit TLS should be enabled") + + // Should allow cleartext => dialer.StartTLSPolicy = gomail.OpportunisticStartTLS and dialer.SSL = false + smtp = setupSMTPClient("smtp://foo:bar@my-server:1234/?disable_starttls=true") + assert.Equal(t, smtp.StartTLSPolicy, gomail.OpportunisticStartTLS, "StartTLS is enforced") + assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled") + + // Test cert based SMTP client auth + clientCert, clientKey, err := generateTestClientCert(t) + require.NoError(t, err) + t.Cleanup(func() { _ = os.Remove(clientCert.Name()) }) //nolint:gosec + t.Cleanup(func() { _ = os.Remove(clientKey.Name()) }) //nolint:gosec + + conf.MustSet(t.Context(), config.ViperKeyCourierSMTPClientCertPath, clientCert.Name()) + conf.MustSet(t.Context(), config.ViperKeyCourierSMTPClientKeyPath, clientKey.Name()) + + clientPEM, err := tls.LoadX509KeyPair(clientCert.Name(), clientKey.Name()) + require.NoError(t, err) + + smtpWithCert := setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server") + assert.Equal(t, smtpWithCert.SSL, true, "Implicit TLS should be enabled") + assert.Equal(t, smtpWithCert.Host, "subdomain.my-server", "SMTP Dialer host should match") + assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match") + assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match") + assert.Contains(t, smtpWithCert.TLSConfig.Certificates, clientPEM, "TLS config should contain client pem") + + // error case: invalid client key + require.NoError(t, conf.Set(t.Context(), config.ViperKeyCourierSMTPClientKeyPath, clientCert.Name())) // mixup client key and client cert + smtpWithCert = setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server") + assert.Equal(t, len(smtpWithCert.TLSConfig.Certificates), 0, "TLS config certificates should be empty") +} + +func TestNotAllowedPrivateIPs(t *testing.T) { + smtp, _ := testhelpers.StartMailhog(t, false) + + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{ + config.ViperKeyCourierSMTPURL: smtp, + config.ViperKeyCourierSMTPFrom: "test-stub@ory.sh", + config.ViperKeyCourierSMTPFromName: "Bob", + config.ViperKeyCourierSMTPHeaders + ".test-stub-header1": "foo", + config.ViperKeyCourierSMTPHeaders + ".test-stub-header2": "bar", + config.ViperKeyCourierMessageRetries: 50, + config.ViperKeyClientSMTPNoPrivateIPRanges: true, + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + c.FailOnDispatchError() + + id, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "test-recipient-1@example.org", + Subject: "test-subject-1", + Body: "test-body-1", + })) + require.NoError(t, err) + require.NotZero(t, id) + + err = c.DispatchQueue(t.Context()) + require.ErrorContains(t, err, "no route to host") +} + +func TestQueueEmail(t *testing.T) { + smtp, api := testhelpers.StartMailhog(t, true) + + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{ + config.ViperKeyCourierSMTPURL: smtp, + config.ViperKeyCourierSMTPFrom: "test-stub@ory.sh", + config.ViperKeyCourierSMTPFromName: "Bob", + config.ViperKeyCourierSMTPHeaders + ".test-stub-header1": "foo", + config.ViperKeyCourierSMTPHeaders + ".test-stub-header2": "bar", + config.ViperKeyCourierMessageRetries: 50, + config.ViperKeyClientSMTPNoPrivateIPRanges: false, + })) + + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + c.FailOnDispatchError() + + _, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "invalid-email", + Subject: "test-subject-1", + Body: "test-body-1", + })) + require.Error(t, err) + + id, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "test-recipient-1@example.org", + Subject: "test-subject-1", + Body: "test-body-1", + })) + require.NoError(t, err) + require.NotZero(t, id) + + id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "test-recipient-2@example.org", + Subject: "test-subject-2", + Body: "test-body-2", + })) + require.NoError(t, err) + require.NotZero(t, id) + + id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "test-recipient-3@example.org", + Subject: "test-subject-3", + Body: "test-body-3", + })) + require.NoError(t, err) + require.NotZero(t, id) + + // A schema-approved non-RFC carrier address must queue and be delivered. + id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "foo.@docomo.ne.jp", + Subject: "test-subject-docomo", + Body: "test-body-docomo", + })) + require.NoError(t, err) + require.NotZero(t, id) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + require.NoError(t, c.DispatchQueue(context.Background())) + }, time.Second, 10*time.Millisecond) + + res, err := testhelpers.NewTestClient(t).Get(api + "/api/v2/messages") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + require.EqualValues(t, 4, gjson.GetBytes(body, "total").Int()) + + for k := 1; k <= 3; k++ { + assert.Contains(t, string(body), fmt.Sprintf("test-subject-%d", k)) + assert.Contains(t, string(body), fmt.Sprintf("test-body-%d", k)) + assert.Contains(t, string(body), fmt.Sprintf("test-recipient-%d@example.org", k)) + assert.Contains(t, string(body), "test-stub@ory.sh") + } + + assert.Contains(t, string(body), "test-subject-docomo") + assert.Contains(t, string(body), "test-body-docomo") + assert.Contains(t, string(body), "foo.@docomo.ne.jp") + + assert.Contains(t, string(body), "Bob") + assert.Contains(t, string(body), `"test-stub-header1":["foo"]`) + assert.Contains(t, string(body), `"test-stub-header2":["bar"]`) +} + +func generateTestClientCert(t *testing.T) (clientCert *os.File, clientKey *os.File, err error) { + hostName := flag.String("host", "127.0.0.1", "Hostname to certify") + priv, err := rsa.GenerateKey(rand.Reader, 1024) // #nosec G403 -- test code + require.NoError(t, err) + now := time.Now() + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{ + CommonName: *hostName, + Organization: []string{"myorg"}, + }, + NotBefore: now.Add(-300 * time.Second), + NotAfter: now.Add(24 * time.Hour), + SubjectKeyId: []byte{1, 2, 3, 4}, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + } + cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &priv.PublicKey, priv) + require.NoError(t, err) + clientCert, err = os.CreateTemp("./test", "testCert") + require.NoError(t, err) + defer func() { _ = clientCert.Close() }() + + require.NoError(t, pem.Encode(clientCert, &pem.Block{Type: "CERTIFICATE", Bytes: cert})) + + clientKey, err = os.CreateTemp("./test", "testKey") + require.NoError(t, err) + defer func() { _ = clientKey.Close() }() + require.NoError(t, pem.Encode(clientKey, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})) + + return clientCert, clientKey, nil +} diff --git a/courier/smtp_wire_test.go b/courier/smtp_wire_test.go new file mode 100644 index 0000000..fa69e52 --- /dev/null +++ b/courier/smtp_wire_test.go @@ -0,0 +1,216 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "bufio" + "fmt" + "net" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + templates "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/configx" + "github.com/ory/x/sqlxx" +) + +// smtpRecorder captures the raw SMTP commands and DATA lines a client sends. +type smtpRecorder struct { + mu sync.Mutex + cmds []string + data []string +} + +func (r *smtpRecorder) snapshot() (cmds, data []string) { + r.mu.Lock() + defer r.mu.Unlock() + return append([]string(nil), r.cmds...), append([]string(nil), r.data...) +} + +// newRecordingSMTPServer starts a minimal SMTP server that accepts everything +// and records the raw protocol. It returns a courier SMTP connection URI. +func newRecordingSMTPServer(t *testing.T) (string, *smtpRecorder) { + t.Helper() + rec := &smtpRecorder{} + + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { _ = l.Close() }) + + go func() { + for { + conn, err := l.Accept() + if err != nil { + return + } + go func(conn net.Conn) { + defer func() { _ = conn.Close() }() + _, _ = fmt.Fprintf(conn, "220 fake ESMTP\r\n") + sc := bufio.NewScanner(conn) + inData := false + for sc.Scan() { + line := sc.Text() + if inData { + if line == "." { + inData = false + _, _ = fmt.Fprintf(conn, "250 ok\r\n") + continue + } + rec.mu.Lock() + rec.data = append(rec.data, line) + rec.mu.Unlock() + continue + } + rec.mu.Lock() + rec.cmds = append(rec.cmds, line) + rec.mu.Unlock() + switch verb := strings.ToUpper(line); { + case strings.HasPrefix(verb, "EHLO"), strings.HasPrefix(verb, "HELO"): + _, _ = fmt.Fprintf(conn, "250-fake\r\n250 8BITMIME\r\n") + case strings.HasPrefix(verb, "DATA"): + inData = true + _, _ = fmt.Fprintf(conn, "354 go ahead\r\n") + case strings.HasPrefix(verb, "QUIT"): + _, _ = fmt.Fprintf(conn, "221 bye\r\n") + return + default: + _, _ = fmt.Fprintf(conn, "250 ok\r\n") + } + } + }(conn) + } + }() + + return fmt.Sprintf("smtp://%s/?disable_starttls=true", l.Addr().String()), rec +} + +func rcptLines(cmds []string) []string { + var out []string + for _, c := range cmds { + if strings.HasPrefix(strings.ToUpper(c), "RCPT TO:") { + out = append(out, c) + } + } + return out +} + +func headerLines(data []string, prefix string) []string { + var out []string + for _, d := range data { + if d == "" { + break // End of headers. + } + if strings.HasPrefix(d, prefix) { + out = append(out, d) + } + } + return out +} + +func newWireCourier(t *testing.T, smtpURL string, extra map[string]any) (courier.Courier, *config.Config) { + t.Helper() + values := map[string]any{ + config.ViperKeyCourierSMTPURL: smtpURL, + config.ViperKeyCourierSMTPFrom: "from@ory.sh", + config.ViperKeyCourierMessageRetries: 5, + config.ViperKeyClientSMTPNoPrivateIPRanges: false, + } + for k, v := range extra { + values[k] = v + } + conf, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(values)) + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + c.FailOnDispatchError() + return c, conf +} + +func TestDispatchSMTPWireProtocol(t *testing.T) { + t.Run("case=rfc recipient keeps Cc and Bcc header recipients in the envelope", func(t *testing.T) { + smtpURL, rec := newRecordingSMTPServer(t) + c, _ := newWireCourier(t, smtpURL, map[string]any{ + config.ViperKeyCourierSMTPHeaders + ".Bcc": "archive@ory.sh", + }) + + _, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "user@example.org", Subject: "s", Body: "b", + })) + require.NoError(t, err) + require.NoError(t, c.DispatchQueue(t.Context())) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + cmds, _ := rec.snapshot() + rcpts := rcptLines(cmds) + assert.Contains(t, rcpts, "RCPT TO:") + assert.Contains(t, rcpts, "RCPT TO:") + }, 5*time.Second, 50*time.Millisecond) + }) + + t.Run("case=non-rfc recipient produces exactly one RCPT and one To header", func(t *testing.T) { + smtpURL, rec := newRecordingSMTPServer(t) + c, _ := newWireCourier(t, smtpURL, nil) + + _, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{ + To: "foo.@docomo.ne.jp", Subject: "s", Body: "b", + })) + require.NoError(t, err) + require.NoError(t, c.DispatchQueue(t.Context())) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + cmds, data := rec.snapshot() + assert.Equal(t, []string{"RCPT TO:"}, rcptLines(cmds)) + assert.Equal(t, []string{"To: foo.@docomo.ne.jp"}, headerLines(data, "To:")) + }, 5*time.Second, 50*time.Millisecond) + }) + + t.Run("case=poisoned recipient is abandoned at dispatch, nothing hits the wire", func(t *testing.T) { + smtpURL, rec := newRecordingSMTPServer(t) + _, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{ + config.ViperKeyCourierSMTPURL: smtpURL, + config.ViperKeyCourierSMTPFrom: "from@ory.sh", + config.ViperKeyCourierMessageRetries: 5, + config.ViperKeyClientSMTPNoPrivateIPRanges: false, + })) + c, err := reg.Courier(t.Context()) + require.NoError(t, err) + + // Insert a row with a smuggled header directly, bypassing QueueEmail's + // validation, to simulate a poisoned message from another path. + require.NoError(t, reg.CourierPersister().AddMessage(t.Context(), &courier.Message{ + Type: courier.MessageTypeEmail, + Recipient: "victim@example.org\r\nBcc: attacker@evil.com", + Body: "b", + Subject: "s", + TemplateType: template.TypeTestStub, + Channel: sqlxx.NullString("email"), + })) + + // Dispatch the message directly. DispatchMessage (unlike DispatchQueue) + // does not reset a failed message back to "queued", so the abandonment + // is observable. + queued, err := reg.CourierPersister().LatestQueuedMessage(t.Context()) + require.NoError(t, err) + require.Error(t, c.DispatchMessage(t.Context(), *queued)) + + var got courier.Message + require.NoError(t, reg.Persister().GetConnection(t.Context()).Where("id = ?", queued.ID).First(&got)) + assert.Equal(t, courier.MessageStatusAbandoned, got.Status) + + cmds, data := rec.snapshot() + assert.Empty(t, rcptLines(cmds), "poisoned recipient must not produce a RCPT") + assert.Empty(t, headerLines(data, "Bcc:"), "poisoned recipient must not smuggle a Bcc header") + for _, line := range data { + assert.NotContains(t, line, "attacker@evil.com", "attacker address must never reach the wire") + } + }) +} diff --git a/courier/stub/request.config.mailer.jsonnet b/courier/stub/request.config.mailer.jsonnet new file mode 100644 index 0000000..fc56e71 --- /dev/null +++ b/courier/stub/request.config.mailer.jsonnet @@ -0,0 +1,12 @@ +function(ctx) { + recipient: ctx.recipient, + template_type: ctx.template_type, + to: if "template_data" in ctx && "to" in ctx.template_data then ctx.template_data.to else null, + recovery_code: if "template_data" in ctx && "recovery_code" in ctx.template_data then ctx.template_data.recovery_code else null, + recovery_url: if "template_data" in ctx && "recovery_url" in ctx.template_data then ctx.template_data.recovery_url else null, + verification_url: if "template_data" in ctx && "verification_url" in ctx.template_data then ctx.template_data.verification_url else null, + verification_code: if "template_data" in ctx && "verification_code" in ctx.template_data then ctx.template_data.verification_code else null, + subject: if "template_data" in ctx && "subject" in ctx.template_data then ctx.template_data.subject else null, + body: if "template_data" in ctx && "body" in ctx.template_data then ctx.template_data.body else null, + html_body: if "template_data" in ctx && "html_body" in ctx.template_data then ctx.template_data.html_body else null +} diff --git a/courier/stub/request.config.twilio.jsonnet b/courier/stub/request.config.twilio.jsonnet new file mode 100644 index 0000000..b849766 --- /dev/null +++ b/courier/stub/request.config.twilio.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + from: "Kratos Test", + to: ctx.recipient, + body: ctx.body +} diff --git a/courier/template/courier/builtin/templates/authenticator_key_added/email.body.gotmpl b/courier/template/courier/builtin/templates/authenticator_key_added/email.body.gotmpl new file mode 100644 index 0000000..0d1a449 --- /dev/null +++ b/courier/template/courier/builtin/templates/authenticator_key_added/email.body.gotmpl @@ -0,0 +1,5 @@ +

Hello,

+

A new sign-in method (device key) was added to your account. If you did this, no further action is needed.

+

If this wasn't you, secure your account immediately and contact support.

+

Account ID: {{ index .Identity "id" }}
+Added at: {{ .AddedAt }}

diff --git a/courier/template/courier/builtin/templates/authenticator_key_added/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/authenticator_key_added/email.body.plaintext.gotmpl new file mode 100644 index 0000000..4dfb5fc --- /dev/null +++ b/courier/template/courier/builtin/templates/authenticator_key_added/email.body.plaintext.gotmpl @@ -0,0 +1,9 @@ +Hello, + +A new sign-in method (device key) was added to your account. If you did +this, no further action is needed. + +If this wasn't you, secure your account immediately and contact support. + +Account ID: {{ index .Identity "id" }} +Added at: {{ .AddedAt }} diff --git a/courier/template/courier/builtin/templates/authenticator_key_added/email.subject.gotmpl b/courier/template/courier/builtin/templates/authenticator_key_added/email.subject.gotmpl new file mode 100644 index 0000000..4828eb2 --- /dev/null +++ b/courier/template/courier/builtin/templates/authenticator_key_added/email.subject.gotmpl @@ -0,0 +1 @@ +A new sign-in method was added to your account diff --git a/courier/template/courier/builtin/templates/authenticator_key_added/sms.body.gotmpl b/courier/template/courier/builtin/templates/authenticator_key_added/sms.body.gotmpl new file mode 100644 index 0000000..9f3a77a --- /dev/null +++ b/courier/template/courier/builtin/templates/authenticator_key_added/sms.body.gotmpl @@ -0,0 +1 @@ +A new sign-in method (device key) was added to your account. If this wasn't you, secure your account. diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl new file mode 100644 index 0000000..0b1b3ab --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Login to your account with the following code: + +{{ .LoginCode }} + +It expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..0b1b3ab --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Login to your account with the following code: + +{{ .LoginCode }} + +It expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl new file mode 100644 index 0000000..0015e66 --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Use code {{ .LoginCode }} to log in diff --git a/courier/template/courier/builtin/templates/login_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/sms.body.gotmpl new file mode 100644 index 0000000..20de927 --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/sms.body.gotmpl @@ -0,0 +1,6 @@ +Your login code is: {{ .LoginCode }} + +It expires in {{ .ExpiresInMinutes }} minutes. +{{ if .RequestURLDomain }} +@{{ .RequestURLDomain }} #{{ .LoginCode }} +{{ end -}} diff --git a/courier/template/courier/builtin/templates/otp/sms.body.gotmpl b/courier/template/courier/builtin/templates/otp/sms.body.gotmpl new file mode 100644 index 0000000..ff95187 --- /dev/null +++ b/courier/template/courier/builtin/templates/otp/sms.body.gotmpl @@ -0,0 +1 @@ +Your verification code is: {{ .Code }} diff --git a/courier/template/courier/builtin/templates/recovery/invalid/email.body.gotmpl b/courier/template/courier/builtin/templates/recovery/invalid/email.body.gotmpl new file mode 100644 index 0000000..b8d9188 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/invalid/email.body.gotmpl @@ -0,0 +1,9 @@ +Hi, + +you (or someone else) entered this email address when trying to recover access to an account. + +However, this email address is not on our database of registered users and therefore the attempt has failed. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/recovery/invalid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/recovery/invalid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..b8d9188 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/invalid/email.body.plaintext.gotmpl @@ -0,0 +1,9 @@ +Hi, + +you (or someone else) entered this email address when trying to recover access to an account. + +However, this email address is not on our database of registered users and therefore the attempt has failed. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/recovery/invalid/email.subject.gotmpl b/courier/template/courier/builtin/templates/recovery/invalid/email.subject.gotmpl new file mode 100644 index 0000000..403d0dd --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/invalid/email.subject.gotmpl @@ -0,0 +1 @@ +Account access attempted diff --git a/courier/template/courier/builtin/templates/recovery/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/recovery/valid/email.body.gotmpl new file mode 100644 index 0000000..c4ac7a1 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Recover access to your account by clicking the following link: + +{{ .RecoveryURL }} + +If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/recovery/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/recovery/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..7bea438 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/valid/email.body.plaintext.gotmpl @@ -0,0 +1,6 @@ +Recover access to your account by clicking the following link: + +{{ .RecoveryURL }} + +If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes. + diff --git a/courier/template/courier/builtin/templates/recovery/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/recovery/valid/email.subject.gotmpl new file mode 100644 index 0000000..9a47d5f --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Recover access to your account diff --git a/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.gotmpl b/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.gotmpl new file mode 100644 index 0000000..b8d9188 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.gotmpl @@ -0,0 +1,9 @@ +Hi, + +you (or someone else) entered this email address when trying to recover access to an account. + +However, this email address is not on our database of registered users and therefore the attempt has failed. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..b8d9188 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/invalid/email.body.plaintext.gotmpl @@ -0,0 +1,9 @@ +Hi, + +you (or someone else) entered this email address when trying to recover access to an account. + +However, this email address is not on our database of registered users and therefore the attempt has failed. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/recovery_code/invalid/email.subject.gotmpl b/courier/template/courier/builtin/templates/recovery_code/invalid/email.subject.gotmpl new file mode 100644 index 0000000..403d0dd --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/invalid/email.subject.gotmpl @@ -0,0 +1 @@ +Account access attempted diff --git a/courier/template/courier/builtin/templates/recovery_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/recovery_code/valid/email.body.gotmpl new file mode 100644 index 0000000..8427467 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Recover access to your account by entering the following code: + +{{ .RecoveryCode }} + +If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/recovery_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/recovery_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..8427467 --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Recover access to your account by entering the following code: + +{{ .RecoveryCode }} + +If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/recovery_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/recovery_code/valid/email.subject.gotmpl new file mode 100644 index 0000000..0890daa --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Use code {{ .RecoveryCode }} to recover access to your account diff --git a/courier/template/courier/builtin/templates/recovery_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/recovery_code/valid/sms.body.gotmpl new file mode 100644 index 0000000..7ea61cd --- /dev/null +++ b/courier/template/courier/builtin/templates/recovery_code/valid/sms.body.gotmpl @@ -0,0 +1,3 @@ +Your recovery code is: {{ .RecoveryCode }} + +@{{ .RequestURLDomain }} #{{ .RecoveryCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl new file mode 100644 index 0000000..b839053 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Complete your account registration with the following code: + +{{ .RegistrationCode }} + +This code expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..b839053 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Complete your account registration with the following code: + +{{ .RegistrationCode }} + +This code expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl new file mode 100644 index 0000000..fd48d99 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Use code {{ .RegistrationCode }} to complete your account registration diff --git a/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl new file mode 100644 index 0000000..e296bdf --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl @@ -0,0 +1,6 @@ +Your registration code is: {{ .RegistrationCode }} + +It expires in {{ .ExpiresInMinutes }} minutes. +{{ if .RequestURLDomain }} +@{{ .RequestURLDomain }} #{{ .RegistrationCode }} +{{ end -}} diff --git a/courier/template/courier/builtin/templates/test_stub/email.body.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.body.gotmpl new file mode 100644 index 0000000..db93af3 --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.body.gotmpl @@ -0,0 +1 @@ +stub email body {{ if .HTMLBody }}{{ .HTMLBody }}{{ else }}{{ .Body }}{{ end }} \ No newline at end of file diff --git a/courier/template/courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl new file mode 100644 index 0000000..07c6ef8 --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl @@ -0,0 +1,2 @@ +{{ $l := cat "lang=" .lang }} +{{ nospace $l }} diff --git a/courier/template/courier/builtin/templates/test_stub/email.body.html.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.body.html.gotmpl new file mode 100644 index 0000000..d39da46 --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.body.html.gotmpl @@ -0,0 +1,3 @@ +{{- if eq .lang "en_US" -}} +{{ template "email.body.html.en_US.gotmpl" . }} +{{- end -}} diff --git a/courier/template/courier/builtin/templates/test_stub/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.body.plaintext.gotmpl new file mode 100644 index 0000000..f22928f --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.body.plaintext.gotmpl @@ -0,0 +1 @@ +stub email body {{ .Body }} \ No newline at end of file diff --git a/courier/template/courier/builtin/templates/test_stub/email.body.sprig.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.body.sprig.gotmpl new file mode 100644 index 0000000..700a1f9 --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.body.sprig.gotmpl @@ -0,0 +1,5 @@ +{{ $t1 := title .input }} +{{ $t1 := nospace $t1 }} +{{ $t2 := upper $t1 }} +{{ $t3 := cat $t1 "," $t2 }} +{{ nospace $t3 }} diff --git a/courier/template/courier/builtin/templates/test_stub/email.subject.gotmpl b/courier/template/courier/builtin/templates/test_stub/email.subject.gotmpl new file mode 100644 index 0000000..e6562ff --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/email.subject.gotmpl @@ -0,0 +1 @@ +stub email subject {{ .Subject }} \ No newline at end of file diff --git a/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.gotmpl b/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.gotmpl new file mode 100644 index 0000000..8a54bb6 --- /dev/null +++ b/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.gotmpl @@ -0,0 +1,5 @@ +

Hello,

+

A verifiable address (email or phone) on your account was recently changed. If you made this change, no further action is needed.

+

If you did not make this change, please contact support immediately.

+

Account ID: {{ index .Identity "ID" }}
+Changed at: {{ .ChangedAt }}

diff --git a/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.plaintext.gotmpl new file mode 100644 index 0000000..d381bad --- /dev/null +++ b/courier/template/courier/builtin/templates/verifiable_address_changed/email.body.plaintext.gotmpl @@ -0,0 +1,9 @@ +Hello, + +A verifiable address (email or phone) on your account was recently +changed. If you made this change, no further action is needed. + +If you did not make this change, please contact support immediately. + +Account ID: {{ index .Identity "ID" }} +Changed at: {{ .ChangedAt }} diff --git a/courier/template/courier/builtin/templates/verifiable_address_changed/email.subject.gotmpl b/courier/template/courier/builtin/templates/verifiable_address_changed/email.subject.gotmpl new file mode 100644 index 0000000..e2077e6 --- /dev/null +++ b/courier/template/courier/builtin/templates/verifiable_address_changed/email.subject.gotmpl @@ -0,0 +1 @@ +A verifiable address on your account was changed diff --git a/courier/template/courier/builtin/templates/verifiable_address_changed/sms.body.gotmpl b/courier/template/courier/builtin/templates/verifiable_address_changed/sms.body.gotmpl new file mode 100644 index 0000000..f9ed81d --- /dev/null +++ b/courier/template/courier/builtin/templates/verifiable_address_changed/sms.body.gotmpl @@ -0,0 +1 @@ +An address on your account was changed. If this wasn't you, contact support. diff --git a/courier/template/courier/builtin/templates/verification/invalid/email.body.gotmpl b/courier/template/courier/builtin/templates/verification/invalid/email.body.gotmpl new file mode 100644 index 0000000..18565a8 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/invalid/email.body.gotmpl @@ -0,0 +1,5 @@ +Someone asked to verify this email address, but we were unable to find an account for this address. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/verification/invalid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/verification/invalid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..18565a8 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/invalid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Someone asked to verify this email address, but we were unable to find an account for this address. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/verification/invalid/email.subject.gotmpl b/courier/template/courier/builtin/templates/verification/invalid/email.subject.gotmpl new file mode 100644 index 0000000..e24f2c1 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/invalid/email.subject.gotmpl @@ -0,0 +1 @@ +Someone tried to verify this email address diff --git a/courier/template/courier/builtin/templates/verification/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/verification/valid/email.body.gotmpl new file mode 100644 index 0000000..d111cc4 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Verify your account by opening the following link: + +{{ .VerificationURL }} + +If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/verification/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/verification/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..ac28e51 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Verify your account by opening the following link: + +{{ .VerificationURL }} + +If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/verification/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/verification/valid/email.subject.gotmpl new file mode 100644 index 0000000..3f0acee --- /dev/null +++ b/courier/template/courier/builtin/templates/verification/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Please verify your email address diff --git a/courier/template/courier/builtin/templates/verification_code/invalid/email.body.gotmpl b/courier/template/courier/builtin/templates/verification_code/invalid/email.body.gotmpl new file mode 100644 index 0000000..bb3d3c1 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/invalid/email.body.gotmpl @@ -0,0 +1,7 @@ +Hi, + +someone asked to verify this email address, but we were unable to find an account for this address. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/verification_code/invalid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/verification_code/invalid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..bb3d3c1 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/invalid/email.body.plaintext.gotmpl @@ -0,0 +1,7 @@ +Hi, + +someone asked to verify this email address, but we were unable to find an account for this address. + +If this was you, check if you signed up using a different address. + +If this was not you, please ignore this email. diff --git a/courier/template/courier/builtin/templates/verification_code/invalid/email.subject.gotmpl b/courier/template/courier/builtin/templates/verification_code/invalid/email.subject.gotmpl new file mode 100644 index 0000000..e24f2c1 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/invalid/email.subject.gotmpl @@ -0,0 +1 @@ +Someone tried to verify this email address diff --git a/courier/template/courier/builtin/templates/verification_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/verification_code/valid/email.body.gotmpl new file mode 100644 index 0000000..fc8d55a --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/valid/email.body.gotmpl @@ -0,0 +1,9 @@ +Verify your account with the following code: + +{{ .VerificationCode }} + +or clicking the following link: + +{{ .VerificationURL }} + +If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/verification_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/verification_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 0000000..b6dde06 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,9 @@ +Verify your account with the following code: + +{{ .VerificationCode }} + +or clicking the following link: + +{{ .VerificationURL }} + +If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes. diff --git a/courier/template/courier/builtin/templates/verification_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/verification_code/valid/email.subject.gotmpl new file mode 100644 index 0000000..b71c784 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Use code {{ .VerificationCode }} to verify your account diff --git a/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl new file mode 100644 index 0000000..d8d3247 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl @@ -0,0 +1,6 @@ +Your verification code is: {{ .VerificationCode }} + +If this was not you, do nothing. It expires in {{ .ExpiresInMinutes }} minutes. +{{ if .RequestURLDomain }} +@{{ .RequestURLDomain }} #{{ .VerificationCode }} +{{ end -}} diff --git a/courier/template/email/authenticator_key_added.go b/courier/template/email/authenticator_key_added.go new file mode 100644 index 0000000..ca97236 --- /dev/null +++ b/courier/template/email/authenticator_key_added.go @@ -0,0 +1,55 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + AuthenticatorKeyAdded struct { + d template.Dependencies + m *AuthenticatorKeyAddedModel + } + AuthenticatorKeyAddedModel struct { + To string `json:"to"` + Identity map[string]any `json:"identity"` + AddedAt string `json:"added_at"` + TransientPayload map[string]any `json:"transient_payload"` + } +) + +func NewAuthenticatorKeyAdded(d template.Dependencies, m *AuthenticatorKeyAddedModel) *AuthenticatorKeyAdded { + return &AuthenticatorKeyAdded{d: d, m: m} +} + +func (t *AuthenticatorKeyAdded) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *AuthenticatorKeyAdded) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.subject.gotmpl", "authenticator_key_added/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Subject) + return strings.TrimSpace(subject), err +} + +func (t *AuthenticatorKeyAdded) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.body.gotmpl", "authenticator_key_added/email.body*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Body.HTML) +} + +func (t *AuthenticatorKeyAdded) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.body.plaintext.gotmpl", "authenticator_key_added/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Body.PlainText) +} + +func (t *AuthenticatorKeyAdded) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *AuthenticatorKeyAdded) TemplateType() template.TemplateType { + return template.TypeAuthenticatorKeyAdded +} diff --git a/courier/template/email/authenticator_key_added_test.go b/courier/template/email/authenticator_key_added_test.go new file mode 100644 index 0000000..34d8243 --- /dev/null +++ b/courier/template/email/authenticator_key_added_test.go @@ -0,0 +1,55 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/x" +) + +func TestAuthenticatorKeyAdded(t *testing.T) { + ctx := t.Context() + _, reg := pkg.NewFastRegistryWithMocks(t) + + // Build the Identity map exactly as the courier does in production + // (identity.Manager.sendIdentityNotifications uses x.StructToMap), so the + // template's key lookup is tested against the real key casing instead of a + // hand-crafted map. StructToMap marshals via JSON tags, so the ID lands under + // the lowercase "id" key. + id := &identity.Identity{ID: uuid.Must(uuid.FromString("00000000-0000-0000-0000-000000000001"))} + idMap, err := x.StructToMap(id) + require.NoError(t, err) + + tpl := email.NewAuthenticatorKeyAdded(reg, &email.AuthenticatorKeyAddedModel{ + To: "owner@example.com", + AddedAt: "2026-04-21T12:00:00Z", + Identity: idMap, + }) + + recipient, err := tpl.EmailRecipient() + require.NoError(t, err) + assert.Equal(t, "owner@example.com", recipient) + + subject, err := tpl.EmailSubject(ctx) + require.NoError(t, err) + assert.Contains(t, strings.ToLower(subject), "sign-in method") + + body, err := tpl.EmailBody(ctx) + require.NoError(t, err) + assert.Contains(t, body, "00000000-0000-0000-0000-000000000001", "the account ID must render in the email body") + + plain, err := tpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + assert.Contains(t, plain, "00000000-0000-0000-0000-000000000001", "the account ID must render in the plaintext body") + assert.Contains(t, plain, "2026-04-21T12:00:00Z") +} diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go new file mode 100644 index 0000000..c815055 --- /dev/null +++ b/courier/template/email/login_code_valid.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + LoginCodeValid struct { + deps template.Dependencies + model *LoginCodeValidModel + } + LoginCodeValidModel struct { + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid { + return &LoginCodeValid{deps: d, model: m} +} + +func (t *LoginCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *LoginCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.subject.gotmpl", "login_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *LoginCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.gotmpl", "login_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.HTML) +} + +func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.plaintext.gotmpl", "login_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.PlainText) +} + +func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *LoginCodeValid) TemplateType() template.TemplateType { + return template.TypeLoginCodeValid +} + +func (t *LoginCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/email/login_code_valid_test.go b/courier/template/email/login_code_valid_test.go new file mode 100644 index 0000000..ad5abe5 --- /dev/null +++ b/courier/template/email/login_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestLoginCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", template.TypeLoginCodeValid) + }) +} diff --git a/courier/template/email/recovery_code_invalid.go b/courier/template/email/recovery_code_invalid.go new file mode 100644 index 0000000..c2852e0 --- /dev/null +++ b/courier/template/email/recovery_code_invalid.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RecoveryCodeInvalid struct { + deps template.Dependencies + model *RecoveryCodeInvalidModel + } + RecoveryCodeInvalidModel struct { + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` + } +) + +func NewRecoveryCodeInvalid(d template.Dependencies, m *RecoveryCodeInvalidModel) *RecoveryCodeInvalid { + return &RecoveryCodeInvalid{deps: d, model: m} +} + +func (t *RecoveryCodeInvalid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RecoveryCodeInvalid) EmailSubject(ctx context.Context) (string, error) { + filesystem := os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)) + remoteURL := t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Subject + + subject, err := template.LoadText(ctx, t.deps, filesystem, "recovery_code/invalid/email.subject.gotmpl", "recovery_code/invalid/email.subject*", t.model, remoteURL) + + return strings.TrimSpace(subject), err +} + +func (t *RecoveryCodeInvalid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/invalid/email.body.gotmpl", "recovery_code/invalid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Body.HTML) +} + +func (t *RecoveryCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/invalid/email.body.plaintext.gotmpl", "recovery_code/invalid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Body.PlainText) +} + +func (t *RecoveryCodeInvalid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *RecoveryCodeInvalid) TemplateType() template.TemplateType { + return template.TypeRecoveryCodeInvalid +} diff --git a/courier/template/email/recovery_code_invalid_test.go b/courier/template/email/recovery_code_invalid_test.go new file mode 100644 index 0000000..5a968f9 --- /dev/null +++ b/courier/template/email/recovery_code_invalid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestRecoveryCodeInvalid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("case=test remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/invalid", template.TypeRecoveryCodeInvalid) + }) +} diff --git a/courier/template/email/recovery_code_valid.go b/courier/template/email/recovery_code_valid.go new file mode 100644 index 0000000..f222983 --- /dev/null +++ b/courier/template/email/recovery_code_valid.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RecoveryCodeValid struct { + deps template.Dependencies + model *RecoveryCodeValidModel + } + RecoveryCodeValidModel struct { + 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"` + ExpiresInMinutes int `json:"expires_in_minutes"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewRecoveryCodeValid(d template.Dependencies, m *RecoveryCodeValidModel) *RecoveryCodeValid { + return &RecoveryCodeValid{deps: d, model: m} +} + +func (t *RecoveryCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RecoveryCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.subject.gotmpl", "recovery_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RecoveryCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.body.gotmpl", "recovery_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Body.HTML) +} + +func (t *RecoveryCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.body.plaintext.gotmpl", "recovery_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Body.PlainText) +} + +func (t *RecoveryCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *RecoveryCodeValid) TemplateType() template.TemplateType { + return template.TypeRecoveryCodeValid +} +func (t *RecoveryCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/email/recovery_code_valid_test.go b/courier/template/email/recovery_code_valid_test.go new file mode 100644 index 0000000..4cf85ae --- /dev/null +++ b/courier/template/email/recovery_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestRecoveryCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/valid", template.TypeRecoveryCodeValid) + }) +} diff --git a/courier/template/email/recovery_invalid.go b/courier/template/email/recovery_invalid.go new file mode 100644 index 0000000..81bae89 --- /dev/null +++ b/courier/template/email/recovery_invalid.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RecoveryInvalid struct { + d template.Dependencies + m *RecoveryInvalidModel + } + RecoveryInvalidModel struct { + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` + } +) + +func NewRecoveryInvalid(d template.Dependencies, m *RecoveryInvalidModel) *RecoveryInvalid { + return &RecoveryInvalid{d: d, m: m} +} + +func (t *RecoveryInvalid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *RecoveryInvalid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.subject.gotmpl", "recovery/invalid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RecoveryInvalid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.body.gotmpl", "recovery/invalid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Body.HTML) +} + +func (t *RecoveryInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.body.plaintext.gotmpl", "recovery/invalid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Body.PlainText) +} + +func (t *RecoveryInvalid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *RecoveryInvalid) TemplateType() template.TemplateType { + return template.TypeRecoveryInvalid +} diff --git a/courier/template/email/recovery_invalid_test.go b/courier/template/email/recovery_invalid_test.go new file mode 100644 index 0000000..1164b2d --- /dev/null +++ b/courier/template/email/recovery_invalid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestRecoverInvalid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("case=test remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/invalid", template.TypeRecoveryInvalid) + }) +} diff --git a/courier/template/email/recovery_valid.go b/courier/template/email/recovery_valid.go new file mode 100644 index 0000000..ae681b7 --- /dev/null +++ b/courier/template/email/recovery_valid.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RecoveryValid struct { + d template.Dependencies + m *RecoveryValidModel + } + RecoveryValidModel struct { + 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"` + ExpiresInMinutes int `json:"expires_in_minutes"` + } +) + +func NewRecoveryValid(d template.Dependencies, m *RecoveryValidModel) *RecoveryValid { + return &RecoveryValid{d: d, m: m} +} + +func (t *RecoveryValid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *RecoveryValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.subject.gotmpl", "recovery/valid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RecoveryValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.body.gotmpl", "recovery/valid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.HTML) +} + +func (t *RecoveryValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.body.plaintext.gotmpl", "recovery/valid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.PlainText) +} + +func (t *RecoveryValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *RecoveryValid) TemplateType() template.TemplateType { + return template.TypeRecoveryValid +} diff --git a/courier/template/email/recovery_valid_test.go b/courier/template/email/recovery_valid_test.go new file mode 100644 index 0000000..c7513ae --- /dev/null +++ b/courier/template/email/recovery_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestRecoverValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewRecoveryValid(reg, &email.RecoveryValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/valid", template.TypeRecoveryValid) + }) +} diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go new file mode 100644 index 0000000..d589ccd --- /dev/null +++ b/courier/template/email/registration_code_valid.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string `json:"to"` + Traits map[string]any `json:"traits"` + RegistrationCode string `json:"registration_code"` + RequestURL string `json:"request_url"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.subject.gotmpl", "registration_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RegistrationCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.gotmpl", "registration_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.HTML) +} + +func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.plaintext.gotmpl", "registration_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.PlainText) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *RegistrationCodeValid) TemplateType() template.TemplateType { + return template.TypeRegistrationCodeValid +} + +func (t *RegistrationCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/email/registration_code_valid_test.go b/courier/template/email/registration_code_valid_test.go new file mode 100644 index 0000000..ebca9fe --- /dev/null +++ b/courier/template/email/registration_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestRegistrationCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", template.TypeRegistrationCodeValid) + }) +} diff --git a/courier/template/email/stub.go b/courier/template/email/stub.go new file mode 100644 index 0000000..1e3bf5a --- /dev/null +++ b/courier/template/email/stub.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + + "github.com/ory/kratos/courier/template" +) + +type ( + TestStub struct { + m *TestStubModel + } + TestStubModel struct { + To string `json:"to"` + Subject string `json:"subject"` + Body string `json:"body"` + HTMLBody string `json:"html_body,omitempty"` + } +) + +func NewTestStub(m *TestStubModel) *TestStub { + return &TestStub{m: m} +} + +func (t *TestStub) EmailRecipient() (string, error) { return t.m.To, nil } +func (t *TestStub) EmailSubject(context.Context) (string, error) { return t.m.Subject, nil } + +func (t *TestStub) EmailBody(ctx context.Context) (string, error) { + if t.m.HTMLBody != "" { + return t.m.HTMLBody, nil + } + return t.EmailBodyPlaintext(ctx) +} + +func (t *TestStub) EmailBodyPlaintext(context.Context) (string, error) { return t.m.Body, nil } +func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } +func (t *TestStub) TemplateType() template.TemplateType { return template.TypeTestStub } diff --git a/courier/template/email/verifiable_address_changed.go b/courier/template/email/verifiable_address_changed.go new file mode 100644 index 0000000..4aa8db5 --- /dev/null +++ b/courier/template/email/verifiable_address_changed.go @@ -0,0 +1,55 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerifiableAddressChanged struct { + d template.Dependencies + m *VerifiableAddressChangedModel + } + VerifiableAddressChangedModel struct { + To string `json:"to"` + Identity map[string]any `json:"identity"` + ChangedAt string `json:"changed_at"` + TransientPayload map[string]any `json:"transient_payload"` + } +) + +func NewVerifiableAddressChanged(d template.Dependencies, m *VerifiableAddressChangedModel) *VerifiableAddressChanged { + return &VerifiableAddressChanged{d: d, m: m} +} + +func (t *VerifiableAddressChanged) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *VerifiableAddressChanged) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.subject.gotmpl", "verifiable_address_changed/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Subject) + return strings.TrimSpace(subject), err +} + +func (t *VerifiableAddressChanged) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.body.gotmpl", "verifiable_address_changed/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Body.HTML) +} + +func (t *VerifiableAddressChanged) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.body.plaintext.gotmpl", "verifiable_address_changed/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Body.PlainText) +} + +func (t *VerifiableAddressChanged) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *VerifiableAddressChanged) TemplateType() template.TemplateType { + return template.TypeVerifiableAddressChanged +} diff --git a/courier/template/email/verifiable_address_changed_test.go b/courier/template/email/verifiable_address_changed_test.go new file mode 100644 index 0000000..88df516 --- /dev/null +++ b/courier/template/email/verifiable_address_changed_test.go @@ -0,0 +1,44 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/pkg" +) + +func TestVerifiableAddressChanged(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + tpl := email.NewVerifiableAddressChanged(reg, &email.VerifiableAddressChangedModel{ + To: "old@example.com", + ChangedAt: "2026-04-21T12:00:00Z", + Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001", "Traits": map[string]any{"email": "new@example.com"}}, + }) + + recipient, err := tpl.EmailRecipient() + require.NoError(t, err) + assert.Equal(t, "old@example.com", recipient) + + subject, err := tpl.EmailSubject(ctx) + require.NoError(t, err) + assert.Contains(t, strings.ToLower(subject), "address") + + body, err := tpl.EmailBody(ctx) + require.NoError(t, err) + assert.Contains(t, body, "00000000-0000-0000-0000-000000000001") + + plain, err := tpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + assert.Contains(t, plain, "00000000-0000-0000-0000-000000000001") + assert.Contains(t, plain, "2026-04-21T12:00:00Z") +} diff --git a/courier/template/email/verification_code_invalid.go b/courier/template/email/verification_code_invalid.go new file mode 100644 index 0000000..a7ebad3 --- /dev/null +++ b/courier/template/email/verification_code_invalid.go @@ -0,0 +1,79 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationCodeInvalid struct { + d template.Dependencies + m *VerificationCodeInvalidModel + } + VerificationCodeInvalidModel struct { + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` + } +) + +func NewVerificationCodeInvalid(d template.Dependencies, m *VerificationCodeInvalidModel) *VerificationCodeInvalid { + return &VerificationCodeInvalid{d: d, m: m} +} + +func (t *VerificationCodeInvalid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *VerificationCodeInvalid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText( + ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/invalid/email.subject.gotmpl", + "verification_code/invalid/email.subject*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Subject, + ) + + return strings.TrimSpace(subject), err +} + +func (t *VerificationCodeInvalid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML( + ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/invalid/email.body.gotmpl", + "verification_code/invalid/email.body*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.HTML, + ) +} + +func (t *VerificationCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/invalid/email.body.plaintext.gotmpl", + "verification_code/invalid/email.body.plaintext*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.PlainText, + ) +} + +func (t *VerificationCodeInvalid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *VerificationCodeInvalid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeInvalid +} diff --git a/courier/template/email/verification_code_invalid_test.go b/courier/template/email/verification_code_invalid_test.go new file mode 100644 index 0000000..fb4c73e --- /dev/null +++ b/courier/template/email/verification_code_invalid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestVerifyCodeInvalid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/invalid", template.TypeVerificationCodeInvalid) + }) +} diff --git a/courier/template/email/verification_code_valid.go b/courier/template/email/verification_code_valid.go new file mode 100644 index 0000000..d5df217 --- /dev/null +++ b/courier/template/email/verification_code_valid.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationCodeValid struct { + d template.Dependencies + m *VerificationCodeValidModel + } + VerificationCodeValidModel struct { + To string `json:"to"` + VerificationURL string `json:"verification_url"` + VerificationCode string `json:"verification_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + } +) + +func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid { + return &VerificationCodeValid{d: d, m: m} +} + +func (t *VerificationCodeValid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *VerificationCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText( + ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/valid/email.subject.gotmpl", + "verification_code/valid/email.subject*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Subject, + ) + + return strings.TrimSpace(subject), err +} + +func (t *VerificationCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/valid/email.body.gotmpl", + "verification_code/valid/email.body*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.HTML, + ) +} + +func (t *VerificationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, + t.d, + os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/valid/email.body.plaintext.gotmpl", + "verification_code/valid/email.body.plaintext*", + t.m, + t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.PlainText, + ) +} + +func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *VerificationCodeValid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeValid +} diff --git a/courier/template/email/verification_code_valid_test.go b/courier/template/email/verification_code_valid_test.go new file mode 100644 index 0000000..f1054db --- /dev/null +++ b/courier/template/email/verification_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestVerifyCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/valid", template.TypeVerificationCodeValid) + }) +} diff --git a/courier/template/email/verification_invalid.go b/courier/template/email/verification_invalid.go new file mode 100644 index 0000000..08485c9 --- /dev/null +++ b/courier/template/email/verification_invalid.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationInvalid struct { + d template.Dependencies + m *VerificationInvalidModel + } + VerificationInvalidModel struct { + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` + } +) + +func NewVerificationInvalid(d template.Dependencies, m *VerificationInvalidModel) *VerificationInvalid { + return &VerificationInvalid{d: d, m: m} +} + +func (t *VerificationInvalid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *VerificationInvalid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.subject.gotmpl", "verification/invalid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *VerificationInvalid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.body.gotmpl", "verification/invalid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Body.HTML) +} + +func (t *VerificationInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.body.plaintext.gotmpl", "verification/invalid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Body.PlainText) +} + +func (t *VerificationInvalid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *VerificationInvalid) TemplateType() template.TemplateType { + return template.TypeVerificationInvalid +} diff --git a/courier/template/email/verification_invalid_test.go b/courier/template/email/verification_invalid_test.go new file mode 100644 index 0000000..4caf41c --- /dev/null +++ b/courier/template/email/verification_invalid_test.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestVerifyInvalid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/invalid", template.TypeVerificationInvalid) + }) + }) +} diff --git a/courier/template/email/verification_valid.go b/courier/template/email/verification_valid.go new file mode 100644 index 0000000..11955ed --- /dev/null +++ b/courier/template/email/verification_valid.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationValid struct { + d template.Dependencies + m *VerificationValidModel + } + VerificationValidModel struct { + To string `json:"to"` + VerificationURL string `json:"verification_url"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + } +) + +func NewVerificationValid(d template.Dependencies, m *VerificationValidModel) *VerificationValid { + return &VerificationValid{d: d, m: m} +} + +func (t *VerificationValid) EmailRecipient() (string, error) { + return t.m.To, nil +} + +func (t *VerificationValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.subject.gotmpl", "verification/valid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *VerificationValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.body.gotmpl", "verification/valid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.HTML) +} + +func (t *VerificationValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.body.plaintext.gotmpl", "verification/valid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.PlainText) +} + +func (t *VerificationValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.m) +} + +func (t *VerificationValid) TemplateType() template.TemplateType { + return template.TypeVerificationValid +} diff --git a/courier/template/email/verification_valid_test.go b/courier/template/email/verification_valid_test.go new file mode 100644 index 0000000..1d67005 --- /dev/null +++ b/courier/template/email/verification_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/pkg" +) + +func TestVerifyValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + tpl := email.NewVerificationValid(reg, &email.VerificationValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/valid", template.TypeVerificationValid) + }) +} diff --git a/courier/template/load_template.go b/courier/template/load_template.go new file mode 100644 index 0000000..9716394 --- /dev/null +++ b/courier/template/load_template.go @@ -0,0 +1,190 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template + +import ( + "bytes" + "context" + "embed" + htemplate "html/template" + "io" + "io/fs" + "path/filepath" + "text/template" + + "github.com/ory/x/fetcher" + "github.com/ory/x/httpx" + + "github.com/Masterminds/sprig/v3" + lru "github.com/hashicorp/golang-lru/v2" + "github.com/pkg/errors" +) + +//go:embed courier/builtin/templates/* +var templates embed.FS + +var Cache, _ = lru.New[string, Template](16) + +type Template interface { + Execute(wr io.Writer, data interface{}) error +} + +type templateDependencies interface { + httpx.ClientProvider +} + +func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, error) { + if t, found := Cache.Get(name); found { + return t, nil + } + + file, err := filesystem.Open(name) + if err != nil { + // try to fallback to bundled templates + var fallbackErr error + file, fallbackErr = templates.Open(filepath.Join("courier/builtin/templates", name)) + if fallbackErr != nil { + // return original error from os.DirFS + return nil, errors.WithStack(err) + } + } + + defer func() { _ = file.Close() }() + + var b bytes.Buffer + if _, err := io.Copy(&b, file); err != nil { + return nil, errors.WithStack(err) + } + + var tpl Template + if html { + t, err := htemplate.New(name).Funcs(sprig.HtmlFuncMap()).Parse(b.String()) + if err != nil { + return nil, errors.WithStack(err) + } + tpl = t + } else { + t, err := template.New(name).Funcs(sprig.TxtFuncMap()).Parse(b.String()) + if err != nil { + return nil, errors.WithStack(err) + } + tpl = t + } + + _ = Cache.Add(name, tpl) + return tpl, nil +} + +func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, html bool) (t Template, err error) { + if t, found := Cache.Get(url); found { + 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)) + if err != nil { + return nil, errors.WithStack(err) + } + } else { + t, err = template.New(url).Funcs(sprig.HermeticTxtFuncMap()).Parse(string(b)) + if err != nil { + 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, nil + } + + matches, _ := fs.Glob(filesystem, name) + // make sure the file exists in the fs, otherwise fallback to built in templates + if matches == nil { + return loadBuiltInTemplate(filesystem, name, html) + } + + glob := name + if pattern != "" { + // pattern matching is used when we have more than one gotmpl for different use cases, such as i18n support + // e.g. some_template/template_name* will match some_template/template_name.body.en_US.gotmpl + matches, _ = fs.Glob(filesystem, pattern) + // set the glob string to match patterns + if matches != nil { + glob = pattern + } + } + + var tpl Template + if html { + t, err := htemplate.New(filepath.Base(name)).Funcs(sprig.HermeticHtmlFuncMap()).ParseFS(filesystem, glob) + if err != nil { + return nil, errors.WithStack(err) + } + tpl = t + } else { + t, err := template.New(filepath.Base(name)).Funcs(sprig.HermeticTxtFuncMap()).ParseFS(filesystem, glob) + if err != nil { + return nil, errors.WithStack(err) + } + tpl = t + } + + _ = Cache.Add(name, tpl) + return tpl, nil +} + +func LoadText(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) { + var t Template + var err error + if remoteURL != "" { + t, err = loadRemoteTemplate(ctx, d, remoteURL, false) + if err != nil { + return "", err + } + } else { + t, err = loadTemplate(filesystem, name, pattern, false) + if err != nil { + return "", err + } + } + + var b bytes.Buffer + if err := t.Execute(&b, model); err != nil { + return "", err + } + return b.String(), nil +} + +func LoadHTML(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) { + var t Template + var err error + if remoteURL != "" { + t, err = loadRemoteTemplate(ctx, d, remoteURL, true) + if err != nil { + return "", err + } + } else { + t, err = loadTemplate(filesystem, name, pattern, true) + if err != nil { + return "", err + } + } + + var b bytes.Buffer + if err := t.Execute(&b, model); err != nil { + return "", err + } + return b.String(), nil +} diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go new file mode 100644 index 0000000..0f8cafb --- /dev/null +++ b/courier/template/load_template_test.go @@ -0,0 +1,199 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template_test + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/fetcher" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestLoadTextTemplate(t *testing.T) { + executeTextTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { + _, reg := pkg.NewFastRegistryWithMocks(t) + tp, err := template.LoadText(t.Context(), reg, os.DirFS(dir), name, pattern, model, "") + require.NoError(t, err) + return tp + } + + executeHTMLTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { + _, reg := pkg.NewFastRegistryWithMocks(t) + tp, err := template.LoadHTML(t.Context(), reg, os.DirFS(dir), name, pattern, model, "") + require.NoError(t, err) + return tp + } + + t.Run("method=from bundled", func(t *testing.T) { + actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.gotmpl", "", nil) + assert.Contains(t, actual, "stub email") + }) + + t.Run("method=fallback to bundled", func(t *testing.T) { + 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[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[string, template.Template](16) + _, reg := pkg.NewFastRegistryWithMocks(t) + + nonhermetic := []string{"date", "date_in_zone", "date_modify", "now", "htmlDate", "htmlDateInZone", "dateInZone", "dateModify", "env", "expandenv", "getHostByName", "uuidv4", "randNumeric", "randAscii", "randAlpha", "randAlphaNum"} + + for _, tc := range nonhermetic { + t.Run("case=should not support function: "+tc, func(t *testing.T) { + _, err := template.LoadText(t.Context(), reg, x.NewStubFS(tc, []byte(fmt.Sprintf("{{ %s }}", tc))), tc, "", map[string]interface{}{}, "") + assert.ErrorContains(t, err, fmt.Sprintf("function %q not defined", tc)) + }) + } + }) + + t.Run("method=html with nested templates", func(t *testing.T) { + 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") + }) + + t.Run("method=Cache works", func(t *testing.T) { + dir := os.TempDir() + name := x.NewUUID().String() + ".body.gotmpl" + fp := filepath.Join(dir, name) + + require.NoError(t, os.WriteFile(fp, []byte("cached stub body"), 0o600)) + assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body") + + require.NoError(t, os.RemoveAll(fp)) + assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body") + }) + + t.Run("method=remote resource", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("case=base64 encoded data", func(t *testing.T) { + t.Run("html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + require.NoError(t, err) + b64 := base64.StdEncoding.EncodeToString(f) + tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "base64://"+b64) + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + require.NoError(t, err) + + b64 := base64.StdEncoding.EncodeToString(f) + + tp, err := template.LoadText(ctx, reg, nil, "", "", m, "base64://"+b64) + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + }) + + t.Run("case=file resource", func(t *testing.T) { + t.Run("case=html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + tp, err := template.LoadText(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + }) + + t.Run("case=http resource", func(t *testing.T) { + router := http.NewServeMux() + router.HandleFunc("GET /html", func(writer http.ResponseWriter, request *http.Request) { + http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + }) + router.HandleFunc("GET /plaintext", func(writer http.ResponseWriter, request *http.Request) { + http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + t.Run("case=html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, ts.URL+"/html") + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + tp, err := template.LoadText(ctx, reg, nil, "", "", m, ts.URL+"/plaintext") + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + }) + + t.Run("case=unsupported resource", func(t *testing.T) { + tp, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") + require.ErrorIs(t, err, fetcher.ErrUnknownScheme) + require.Empty(t, tp) + + tp, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") + require.ErrorIs(t, err, fetcher.ErrUnknownScheme) + require.Empty(t, tp) + }) + + t.Run("case=disallowed resources", func(t *testing.T) { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + reg.HTTPClient(ctx).RetryMax = 1 + reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond + + _, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") + assert.ErrorContains(t, err, "no such host") + + _, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") + assert.ErrorContains(t, err, "no such host") + }) + + t.Run("method=cache works", func(t *testing.T) { + tp1, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://e3sgJGwgOj0gY2F0ICJsYW5nPSIgLmxhbmcgfX0Ke3sgbm9zcGFjZSAkbCB9fQ==") + require.NoError(t, err) + + tp2, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://c3R1YiBlbWFpbCBib2R5IHt7IC5Cb2R5IH19") + require.NoError(t, err) + + assert.NotEqualf(t, tp1, tp2, "Expected remote template 1 and remote template 2 to not be equal") + }) + }) +} diff --git a/courier/template/oauth2.go b/courier/template/oauth2.go new file mode 100644 index 0000000..4da8c17 --- /dev/null +++ b/courier/template/oauth2.go @@ -0,0 +1,48 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template + +import ( + hydraclientgo "github.com/ory/hydra-client-go/v2" +) + +type ( + // OAuth2LoginRequest is a scoped view of the OAuth2 login request that + // initiated a self-service flow. It is embedded into courier template data + // so that messages can be branded per OAuth2 client without exposing the + // full flow object. + OAuth2LoginRequest struct { + Challenge string `json:"challenge"` + Client OAuth2Client `json:"client"` + } + + // OAuth2Client contains the branding-relevant, admin-controlled fields of + // the OAuth2 client that initiated a self-service flow. + OAuth2Client struct { + ClientID string `json:"client_id"` + ClientName string `json:"client_name,omitempty"` + ClientURI string `json:"client_uri,omitempty"` + LogoURI string `json:"logo_uri,omitempty"` + Metadata any `json:"metadata,omitempty"` + } +) + +// NewOAuth2LoginRequest maps the Hydra login request to the scoped view embedded +// into courier template data. Only branding-relevant, admin-controlled client +// fields are exposed to messaging channels. +func NewOAuth2LoginRequest(hlr *hydraclientgo.OAuth2LoginRequest) *OAuth2LoginRequest { + if hlr == nil { + return nil + } + return &OAuth2LoginRequest{ + Challenge: hlr.Challenge, + Client: OAuth2Client{ + ClientID: hlr.Client.GetClientId(), + ClientName: hlr.Client.GetClientName(), + ClientURI: hlr.Client.GetClientUri(), + LogoURI: hlr.Client.GetLogoUri(), + Metadata: hlr.Client.GetMetadata(), + }, + } +} diff --git a/courier/template/sms/authenticator_key_added.go b/courier/template/sms/authenticator_key_added.go new file mode 100644 index 0000000..2a5b396 --- /dev/null +++ b/courier/template/sms/authenticator_key_added.go @@ -0,0 +1,59 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + AuthenticatorKeyAdded struct { + deps template.Dependencies + model *AuthenticatorKeyAddedModel + } + AuthenticatorKeyAddedModel struct { + To string `json:"to"` + Identity map[string]any `json:"identity"` + AddedAt string `json:"added_at"` + TransientPayload map[string]any `json:"transient_payload"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewAuthenticatorKeyAdded(d template.Dependencies, m *AuthenticatorKeyAddedModel) *AuthenticatorKeyAdded { + return &AuthenticatorKeyAdded{deps: d, model: m} +} + +func (t *AuthenticatorKeyAdded) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *AuthenticatorKeyAdded) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "authenticator_key_added/sms.body.gotmpl", + "authenticator_key_added/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesAuthenticatorKeyAdded(ctx).Body.PlainText, + ) +} + +func (t *AuthenticatorKeyAdded) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *AuthenticatorKeyAdded) TemplateType() template.TemplateType { + return template.TypeAuthenticatorKeyAdded +} + +func (t *AuthenticatorKeyAdded) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/sms/authenticator_key_added_test.go b/courier/template/sms/authenticator_key_added_test.go new file mode 100644 index 0000000..3ce9caf --- /dev/null +++ b/courier/template/sms/authenticator_key_added_test.go @@ -0,0 +1,36 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestAuthenticatorKeyAddedSMS(t *testing.T) { + ctx := t.Context() + _, reg := pkg.NewFastRegistryWithMocks(t) + + tpl := sms.NewAuthenticatorKeyAdded(reg, &sms.AuthenticatorKeyAddedModel{ + To: "+15551234567", + AddedAt: "2026-04-21T12:00:00Z", + Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}, + }) + + phone, err := tpl.PhoneNumber() + require.NoError(t, err) + assert.Equal(t, "+15551234567", phone) + + body, err := tpl.SMSBody(ctx) + require.NoError(t, err) + + // Must stay under 160 chars so we do not fragment SMS billing. + assert.LessOrEqual(t, len(body), 160, "SMS body too long: %q", body) + assert.Contains(t, body, "sign-in method") +} diff --git a/courier/template/sms/login_code_valid.go b/courier/template/sms/login_code_valid.go new file mode 100644 index 0000000..3e577fa --- /dev/null +++ b/courier/template/sms/login_code_valid.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + LoginCodeValid struct { + deps template.Dependencies + model *LoginCodeValidModel + } + LoginCodeValidModel struct { + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + RequestURLDomain string `json:"request_url_domain"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid { + return &LoginCodeValid{deps: d, model: m} +} + +func (t *LoginCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *LoginCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "login_code/valid/sms.body.gotmpl", + "login_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesLoginCodeValid(ctx).Body.PlainText, + ) +} + +func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *LoginCodeValid) TemplateType() template.TemplateType { + return template.TypeLoginCodeValid +} + +func (t *LoginCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/sms/login_code_valid_test.go b/courier/template/sms/login_code_valid_test.go new file mode 100644 index 0000000..b546824 --- /dev/null +++ b/courier/template/sms/login_code_valid_test.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestNewLoginCodeValid(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + const ( + expectedPhone = "+12345678901" + otp = "012345" + ) + + tpl := sms.NewLoginCodeValid(reg, &sms.LoginCodeValidModel{To: expectedPhone, LoginCode: otp}) + + expectedBody := "Your login code is: 012345\n\nIt expires in 0 minutes.\n" + + actualBody, err := tpl.SMSBody(context.Background()) + require.NoError(t, err) + assert.Equal(t, expectedBody, actualBody) + + actualPhone, err := tpl.PhoneNumber() + require.NoError(t, err) + assert.Equal(t, expectedPhone, actualPhone) +} diff --git a/courier/template/sms/origin_bound_test.go b/courier/template/sms/origin_bound_test.go new file mode 100644 index 0000000..646e851 --- /dev/null +++ b/courier/template/sms/origin_bound_test.go @@ -0,0 +1,43 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +// TestOriginBoundSMSBodies asserts that the login, registration, and +// verification code SMS bodies include the Web OTP origin-bound last line +// `@ #`, mirroring the recovery code template. +func TestOriginBoundSMSBodies(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + t.Run("template=login_code", func(t *testing.T) { + body, err := sms.NewLoginCodeValid(reg, &sms.LoginCodeValidModel{LoginCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context()) + require.NoError(t, err) + assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"), + "body must end with the origin-bound line, got: %q", body) + }) + + t.Run("template=registration_code", func(t *testing.T) { + body, err := sms.NewRegistrationCodeValid(reg, &sms.RegistrationCodeValidModel{RegistrationCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context()) + require.NoError(t, err) + assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"), + "body must end with the origin-bound line, got: %q", body) + }) + + t.Run("template=verification_code", func(t *testing.T) { + body, err := sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{VerificationCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context()) + require.NoError(t, err) + assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"), + "body must end with the origin-bound line, got: %q", body) + }) +} diff --git a/courier/template/sms/recovery_code.go b/courier/template/sms/recovery_code.go new file mode 100644 index 0000000..ef4f3a4 --- /dev/null +++ b/courier/template/sms/recovery_code.go @@ -0,0 +1,62 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + RecoveryCodeValid struct { + deps template.Dependencies + model *RecoveryCodeValidModel + } + + RecoveryCodeValidModel struct { + To string `json:"to"` + RecoveryCode string `json:"verification_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + RequestURLDomain string `json:"request_url_domain"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewRecoveryCodeValid(d template.Dependencies, m *RecoveryCodeValidModel) *RecoveryCodeValid { + return &RecoveryCodeValid{deps: d, model: m} +} + +func (t *RecoveryCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *RecoveryCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "recovery_code/valid/sms.body.gotmpl", + "recovery_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesRecoveryCodeValid(ctx).Body.PlainText, + ) +} + +func (t *RecoveryCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} +func (t *RecoveryCodeValid) TemplateType() template.TemplateType { + return template.TypeRecoveryCodeValid +} +func (t *RecoveryCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders + +} diff --git a/courier/template/sms/registration_code_valid.go b/courier/template/sms/registration_code_valid.go new file mode 100644 index 0000000..93bd9ec --- /dev/null +++ b/courier/template/sms/registration_code_valid.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string `json:"to"` + RegistrationCode string `json:"registration_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + RequestURLDomain string `json:"request_url_domain"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "registration_code/valid/sms.body.gotmpl", + "registration_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesRegistrationCodeValid(ctx).Body.PlainText, + ) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *RegistrationCodeValid) TemplateType() template.TemplateType { + return template.TypeRegistrationCodeValid +} + +func (t *RegistrationCodeValid) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/sms/registration_code_valid_test.go b/courier/template/sms/registration_code_valid_test.go new file mode 100644 index 0000000..34536a6 --- /dev/null +++ b/courier/template/sms/registration_code_valid_test.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestNewRegistrationCodeValid(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + const ( + expectedPhone = "+12345678901" + otp = "012345" + ) + + tpl := sms.NewRegistrationCodeValid(reg, &sms.RegistrationCodeValidModel{To: expectedPhone, RegistrationCode: otp}) + + expectedBody := "Your registration code is: 012345\n\nIt expires in 0 minutes.\n" + + actualBody, err := tpl.SMSBody(context.Background()) + require.NoError(t, err) + assert.Equal(t, expectedBody, actualBody) + + actualPhone, err := tpl.PhoneNumber() + require.NoError(t, err) + assert.Equal(t, expectedPhone, actualPhone) +} diff --git a/courier/template/sms/stub.go b/courier/template/sms/stub.go new file mode 100644 index 0000000..b0b9c54 --- /dev/null +++ b/courier/template/sms/stub.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + + "github.com/ory/kratos/courier/template" +) + +type ( + TestStub struct{ m *TestStubModel } + + TestStubModel struct { + To string `json:"to"` + Body string `json:"body"` + Identity map[string]interface{} `json:"identity"` + } +) + +func NewTestStub(m *TestStubModel) *TestStub { return &TestStub{m: m} } +func (t *TestStub) PhoneNumber() (string, error) { return t.m.To, nil } +func (t *TestStub) SMSBody(context.Context) (string, error) { return t.m.Body, nil } +func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } +func (t *TestStub) TemplateType() template.TemplateType { return template.TypeTestStub } diff --git a/courier/template/sms/verifiable_address_changed.go b/courier/template/sms/verifiable_address_changed.go new file mode 100644 index 0000000..ff4d787 --- /dev/null +++ b/courier/template/sms/verifiable_address_changed.go @@ -0,0 +1,59 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerifiableAddressChanged struct { + deps template.Dependencies + model *VerifiableAddressChangedModel + } + VerifiableAddressChangedModel struct { + To string `json:"to"` + Identity map[string]any `json:"identity"` + ChangedAt string `json:"changed_at"` + TransientPayload map[string]any `json:"transient_payload"` + UserRequestHeaders http.Header `json:"-"` + } +) + +func NewVerifiableAddressChanged(d template.Dependencies, m *VerifiableAddressChangedModel) *VerifiableAddressChanged { + return &VerifiableAddressChanged{deps: d, model: m} +} + +func (t *VerifiableAddressChanged) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *VerifiableAddressChanged) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "verifiable_address_changed/sms.body.gotmpl", + "verifiable_address_changed/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesVerifiableAddressChanged(ctx).Body.PlainText, + ) +} + +func (t *VerifiableAddressChanged) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *VerifiableAddressChanged) TemplateType() template.TemplateType { + return template.TypeVerifiableAddressChanged +} + +func (t *VerifiableAddressChanged) RequestHeaders() http.Header { + return t.model.UserRequestHeaders +} diff --git a/courier/template/sms/verifiable_address_changed_test.go b/courier/template/sms/verifiable_address_changed_test.go new file mode 100644 index 0000000..6dfb93b --- /dev/null +++ b/courier/template/sms/verifiable_address_changed_test.go @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestVerifiableAddressChangedSMS(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + tpl := sms.NewVerifiableAddressChanged(reg, &sms.VerifiableAddressChangedModel{ + To: "+15551234567", + ChangedAt: "2026-04-21T12:00:00Z", + Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"}, + }) + + phone, err := tpl.PhoneNumber() + require.NoError(t, err) + assert.Equal(t, "+15551234567", phone) + + body, err := tpl.SMSBody(ctx) + require.NoError(t, err) + + // Must stay under 160 chars so we do not fragment SMS billing. + assert.LessOrEqual(t, len(body), 160, "SMS body too long: %q", body) + assert.Contains(t, body, "changed") +} diff --git a/courier/template/sms/verification_code.go b/courier/template/sms/verification_code.go new file mode 100644 index 0000000..601469f --- /dev/null +++ b/courier/template/sms/verification_code.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationCodeValid struct { + deps template.Dependencies + model *VerificationCodeValidModel + } + + VerificationCodeValidModel struct { + To string `json:"to"` + VerificationCode string `json:"verification_code"` + Identity map[string]any `json:"identity"` + RequestURL string `json:"request_url"` + RequestURLDomain string `json:"request_url_domain"` + TransientPayload map[string]any `json:"transient_payload"` + ExpiresInMinutes int `json:"expires_in_minutes"` + OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + } +) + +func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid { + return &VerificationCodeValid{deps: d, model: m} +} + +func (t *VerificationCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *VerificationCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/valid/sms.body.gotmpl", + "verification_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesVerificationCodeValid(ctx).Body.PlainText, + ) +} + +func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *VerificationCodeValid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeValid +} diff --git a/courier/template/sms/verification_code_test.go b/courier/template/sms/verification_code_test.go new file mode 100644 index 0000000..25a21a2 --- /dev/null +++ b/courier/template/sms/verification_code_test.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/pkg" +) + +func TestNewOTPMessage(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + const ( + expectedPhone = "+12345678901" + otp = "012345" + ) + + tpl := sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{To: expectedPhone, VerificationCode: otp}) + + expectedBody := "Your verification code is: 012345\n\nIf this was not you, do nothing. It expires in 0 minutes.\n" + + actualBody, err := tpl.SMSBody(context.Background()) + require.NoError(t, err) + assert.Equal(t, expectedBody, actualBody) + + actualPhone, err := tpl.PhoneNumber() + require.NoError(t, err) + assert.Equal(t, expectedPhone, actualPhone) +} diff --git a/courier/template/template.go b/courier/template/template.go new file mode 100644 index 0000000..28c1786 --- /dev/null +++ b/courier/template/template.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template + +import ( + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httpx" +) + +type Dependencies interface { + CourierConfig() config.CourierConfigs + httpx.ClientProvider +} diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go new file mode 100644 index 0000000..ba51a45 --- /dev/null +++ b/courier/template/testhelpers/testhelpers.go @@ -0,0 +1,157 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "encoding/base64" + "net/http" + "net/http/httptest" + "os" + "path" + "testing" + + "github.com/ory/kratos/courier/template/email" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" +) + +func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html string, subject string) *driver.RegistryDefault { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail, &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: plaintext, + HTML: html, + }, + Subject: subject, + })) + return reg +} + +func TestRendered(t *testing.T, ctx context.Context, tpl interface { + EmailBody(context.Context) (string, error) + EmailSubject(context.Context) (string, error) +}, +) { + rendered, err := tpl.EmailBody(ctx) + require.NoError(t, err) + assert.NotEmpty(t, rendered) + + rendered, err = tpl.EmailSubject(ctx) + require.NoError(t, err) + assert.NotEmpty(t, rendered) +} + +func TestRemoteTemplates(t *testing.T, basePath string, tmplType template.TemplateType) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + toBase64 := func(filePath string) string { + f, err := os.ReadFile(filePath) // #nosec G304 -- test code + require.NoError(t, err) + return base64.StdEncoding.EncodeToString(f) + } + + getTemplate := func(tmpl template.TemplateType, d template.Dependencies) interface { + EmailBody(context.Context) (string, error) + EmailSubject(context.Context) (string, error) + } { + switch tmpl { + case template.TypeRecoveryInvalid: + return email.NewRecoveryInvalid(d, &email.RecoveryInvalidModel{}) + case template.TypeRecoveryValid: + return email.NewRecoveryValid(d, &email.RecoveryValidModel{}) + case template.TypeRecoveryCodeValid: + return email.NewRecoveryCodeValid(d, &email.RecoveryCodeValidModel{}) + case template.TypeRecoveryCodeInvalid: + return email.NewRecoveryCodeInvalid(d, &email.RecoveryCodeInvalidModel{}) + case template.TypeTestStub: + return email.NewTestStub(&email.TestStubModel{}) + case template.TypeVerificationInvalid: + return email.NewVerificationInvalid(d, &email.VerificationInvalidModel{}) + case template.TypeVerificationValid: + return email.NewVerificationValid(d, &email.VerificationValidModel{}) + case template.TypeVerificationCodeInvalid: + return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{}) + case template.TypeVerificationCodeValid: + return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{}) + case template.TypeLoginCodeValid: + return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{}) + case template.TypeRegistrationCodeValid: + return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{}) + default: + return nil + } + } + + t.Run("case=http resource", func(t *testing.T) { + t.Parallel() + router := http.NewServeMux() + router.HandleFunc("GET /{filename}", func(writer http.ResponseWriter, request *http.Request) { + http.ServeFile(writer, request, path.Join(basePath, request.PathValue("filename"))) + }) + ts := httptest.NewServer(router) + defer ts.Close() + + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + ts.URL+"/email.body.plaintext.gotmpl", + ts.URL+"/email.body.gotmpl", + ts.URL+"/email.subject.gotmpl")) + + require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath) + + TestRendered(t, ctx, tpl) + }) + + t.Run("case=base64 resource", func(t *testing.T) { + t.Parallel() + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl")))) + + require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath) + + TestRendered(t, ctx, tpl) + }) + + t.Run("case=file resource", func(t *testing.T) { + t.Parallel() + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "file://"+path.Join(basePath, "email.body.plaintext.gotmpl"), + "file://"+path.Join(basePath, "email.body.gotmpl"), + "file://"+path.Join(basePath, "email.subject.gotmpl"))) + + require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath) + TestRendered(t, ctx, tpl) + }) + + t.Run("case=partial subject override", func(t *testing.T) { + t.Parallel() + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "", + "", + "base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl")))) + + require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath) + TestRendered(t, ctx, tpl) + }) + + t.Run("case=partial body override", func(t *testing.T) { + t.Parallel() + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")), + "")) + + require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath) + TestRendered(t, ctx, tpl) + }) +} diff --git a/courier/template/type.go b/courier/template/type.go new file mode 100644 index 0000000..43c7b36 --- /dev/null +++ b/courier/template/type.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template + +// A Template's type +// +// swagger:enum TemplateType +type TemplateType string + +const ( + TypeRecoveryInvalid TemplateType = "recovery_invalid" + TypeRecoveryValid TemplateType = "recovery_valid" + TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid" + TypeRecoveryCodeValid TemplateType = "recovery_code_valid" + TypeVerificationInvalid TemplateType = "verification_invalid" + TypeVerificationValid TemplateType = "verification_valid" + TypeVerificationCodeInvalid TemplateType = "verification_code_invalid" + TypeVerificationCodeValid TemplateType = "verification_code_valid" + TypeTestStub TemplateType = "stub" + TypeLoginCodeValid TemplateType = "login_code_valid" + TypeRegistrationCodeValid TemplateType = "registration_code_valid" + TypeVerifiableAddressChanged TemplateType = "verifiable_address_changed" + TypeAuthenticatorKeyAdded TemplateType = "authenticator_key_added" +) diff --git a/courier/test/persistence.go b/courier/test/persistence.go new file mode 100644 index 0000000..a0dd717 --- /dev/null +++ b/courier/test/persistence.go @@ -0,0 +1,373 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "errors" + "fmt" + "slices" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/sqlcon" +) + +type PersisterWrapper interface { + GetConnection(ctx context.Context) *pop.Connection + NetworkID(ctx context.Context) uuid.UUID + courier.Persister +} + +type NetworkWrapper func(t *testing.T, ctx context.Context) (uuid.UUID, PersisterWrapper) + +func TestPersister(ctx context.Context, newNetworkUnlessExisting NetworkWrapper, newNetwork NetworkWrapper) func(t *testing.T) { + return func(t *testing.T) { + nid, p := newNetworkUnlessExisting(t, ctx) + + // backdateMessage moves a message's creation time into the past so that + // it clears the CockroachDB anti-contention grace period in + // NextMessages, which defers messages enqueued within the last second + // to a later poll. + backdateMessage := func(t *testing.T, id uuid.UUID) { + createdAt := time.Now().UTC().Add(-2 * time.Second) + require.NoError(t, p.GetConnection(ctx).RawQuery( + "UPDATE courier_messages SET created_at = ?, updated_at = ? WHERE id = ? AND nid = ?", + createdAt, createdAt, id, nid).Exec()) + } + + t.Run("case=no messages in queue", func(t *testing.T) { + m, err := p.NextMessages(ctx, 10) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + assert.Len(t, m, 0) + + _, err = p.LatestQueuedMessage(ctx) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) + + messages := make([]courier.Message, 5) + t.Run("case=add messages to the queue", func(t *testing.T) { + now := time.Now().UTC() + for k := range messages { + require.NoError(t, faker.FakeData(&messages[k])) + require.NoError(t, p.AddMessage(ctx, &messages[k])) + // The timestamps are in the past (preserving the insertion + // order) so that the messages clear the CockroachDB + // anti-contention grace period in NextMessages. + createdAt := now.Add(time.Duration(k)*time.Hour - 25*time.Hour).Round(time.Second) + require.NoError(t, p.GetConnection(ctx). + RawQuery( + "UPDATE courier_messages SET created_at = ?, updated_at = ? WHERE id = ? AND nid = ?", + createdAt, + createdAt, + messages[k].ID, nid). + Exec()) + } + }) + + t.Run("case=latest message in queue", func(t *testing.T) { + expected, err := p.LatestQueuedMessage(ctx) + require.NoError(t, err) + + actual := messages[len(messages)-1] + assert.Equal(t, expected.ID, actual.ID) + assert.Equal(t, expected.Subject, actual.Subject) + }) + + t.Run("case=pull messages from the queue", func(t *testing.T) { + for k, expected := range messages { + expected.Status = courier.MessageStatusProcessing + t.Run(fmt.Sprintf("message=%d", k), func(t *testing.T) { + messages, err := p.NextMessages(ctx, 1) + require.NoError(t, err) + require.Len(t, messages, 1) + + actual := messages[0] + assert.Equal(t, expected.ID, actual.ID) + assert.Equal(t, expected.Subject, actual.Subject) + assert.Equal(t, expected.Body, actual.Body) + assert.Equal(t, expected.Status, actual.Status) + assert.Equal(t, expected.Type, actual.Type) + assert.Equal(t, expected.Recipient, actual.Recipient) + }) + } + + _, err := p.NextMessages(ctx, 10) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) + + t.Run("case=setting message status", func(t *testing.T) { + require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusQueued)) + ms, err := p.NextMessages(ctx, 1) + require.NoError(t, err) + require.Len(t, ms, 1) + assert.Equal(t, messages[0].ID, ms[0].ID) + + require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusSent)) + _, err = p.NextMessages(ctx, 1) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + + require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusAbandoned)) + _, err = p.NextMessages(ctx, 1) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) + + t.Run("case=incrementing send count", func(t *testing.T) { + originalSendCount := messages[0].SendCount + require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusQueued)) + + require.NoError(t, p.IncrementMessageSendCount(ctx, messages[0].ID)) + ms, err := p.NextMessages(ctx, 1) + require.NoError(t, err) + require.Len(t, ms, 1) + assert.Equal(t, messages[0].ID, ms[0].ID) + assert.Equal(t, originalSendCount+1, ms[0].SendCount) + }) + + t.Run("case=list messages", func(t *testing.T) { + // List by status. + { + status := courier.MessageStatusProcessing + filter := courier.ListCourierMessagesParameters{ + Status: &status, + } + ms, _, err := p.ListMessages(ctx, filter, []keysetpagination.Option{}) + + require.NoError(t, err) + require.Len(t, ms, len(messages), messages) + // Check that the 'filter by status' works. + for _, m := range ms { + require.Equal(t, status, m.Status) + } + + // Check that the 'order by created_at desc' works. + require.True(t, slices.IsSortedFunc(ms, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + } + // Query fewer items than the total, multiple times. + { + filter := courier.ListCourierMessagesParameters{} + maxSize := 2 + ms1, pagination, err := p.ListMessages(ctx, filter, []keysetpagination.Option{ + keysetpagination.WithSize(2), + }) + require.NoError(t, err) + require.NotNil(t, pagination) + require.False(t, pagination.IsLast()) + require.Len(t, ms1, maxSize) + + // Check that the 'order by created_at desc' works. + require.True(t, slices.IsSortedFunc(ms1, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + + // Second call. + // Marshal -> unmarshal the pagination token to be more realistic. + encrypted := pagination.PageToken().Encrypt(nil) + unmarshalled, err := keysetpagination.ParsePageToken(nil, encrypted) + require.NoError(t, err) + + ms2, pagination, err := p.ListMessages(ctx, filter, + []keysetpagination.Option{ + keysetpagination.WithSize(2), + keysetpagination.WithToken(unmarshalled), + }) + + require.NoError(t, err) + require.NotNil(t, pagination) + require.False(t, pagination.IsLast()) + require.Len(t, ms2, maxSize) + // Check that the 'order by created_at desc' works. + require.True(t, slices.IsSortedFunc(ms2, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + + // Check that the second call returned different elements. + require.NotEqual(t, ms1[0].ID, ms2[0].ID) + require.NotEqual(t, ms1[1].ID, ms2[1].ID) + allElements := append(ms1, ms2...) + require.True(t, slices.IsSortedFunc(allElements, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + + // Last call + ms3, pagination, err := p.ListMessages(ctx, filter, pagination.ToOptions()) + require.NoError(t, err) + require.NotNil(t, pagination) + require.True(t, pagination.IsLast()) + require.Len(t, ms3, 1) + // Check that the 'order by created_at desc' works. + require.True(t, slices.IsSortedFunc(ms3, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + + // Check that the third call returned different elements. + require.NotEqual(t, ms2[0].ID, ms3[0].ID) + allElements = append(ms1, ms2...) + allElements = append(allElements, ms3...) + require.True(t, slices.IsSortedFunc(allElements, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) })) + } + + t.Run("on another network", func(t *testing.T) { + nid1, p1 := newNetwork(t, ctx) + status := courier.MessageStatusProcessing + filter := courier.ListCourierMessagesParameters{ + Status: &status, + } + ms, _, err := p1.ListMessages(ctx, filter, []keysetpagination.Option{}) + + require.NoError(t, err) + require.Len(t, ms, 0) + + // Due to a bug in the pagination query definition, it was possible to retrieve messages from another `network` + // using the pagination query. That required that 2 message's `created_at` timestamps were equal, to trigger + // the `OR` clause of the paginated query. + // This part of the tests "simulates" this behavior, by forcing the same timestamps on multiple messages across + // different networks. + nid2, p2 := newNetwork(t, ctx) + msg1 := courier.Message{ + ID: uuid.FromStringOrNil("10000000-0000-0000-0000-000000000000"), + NID: nid1, + Status: courier.MessageStatusProcessing, + } + err = p1.GetConnection(ctx).Create(&msg1) + require.NoError(t, err) + + msg2 := courier.Message{ + ID: uuid.FromStringOrNil("20000000-0000-0000-0000-000000000000"), + NID: nid1, + Status: courier.MessageStatusProcessing, + } + err = p1.GetConnection(ctx).Create(&msg2) + require.NoError(t, err) + msg3 := courier.Message{ + ID: uuid.FromStringOrNil("30000000-0000-0000-0000-000000000000"), + NID: nid2, + Status: courier.MessageStatusProcessing, + } + err = p2.GetConnection(ctx).Create(&msg3) + require.NoError(t, err) + now := time.Now().UTC().Truncate(time.Second) + + // Set all `created_at` timestamps to the same value to force the `OR` clause of the paginated query. + // `created_at` is set by "pop" and does not allow a manual override, apart from using `pop.SetNowFunc`, but that also influences the other tests in this + // suite, as it just overrides a global function. + require.NoError(t, p1.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg1.ID, nid1).Exec()) + // get the "updated" message from the + require.NoError(t, p1.GetConnection(ctx).Where("id = ? AND nid = ?", msg1.ID, msg1.NID).First(&msg1)) + require.NoError(t, p1.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg2.ID, nid1).Exec()) + require.NoError(t, p2.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg3.ID, nid2).Exec()) + + // Use the updated first message's PageToken as the basis for the paginated request. + ms, _, err = p1.ListMessages(ctx, filter, []keysetpagination.Option{keysetpagination.WithToken(msg1.PageToken())}) + require.NoError(t, err) + + // The response should just contain messages from network1, and not from network2. + require.Len(t, ms, 1) + require.Equal(t, ms[0].NID, nid1) + require.Equal(t, ms[0].ID, msg2.ID) + }) + }) + + t.Run("case=network", func(t *testing.T) { + t.Run("generates id on creation", func(t *testing.T) { + expected := courier.Message{ID: uuid.Nil} + require.NoError(t, p.AddMessage(ctx, &expected)) + backdateMessage(t, expected.ID) + + assert.NotEqual(t, uuid.Nil, expected.ID) + assert.EqualValues(t, nid, expected.NID) + assert.EqualValues(t, nid, p.NetworkID(ctx)) + + actual, err := p.LatestQueuedMessage(ctx) + require.NoError(t, err) + assert.EqualValues(t, expected.ID, actual.ID) + assert.EqualValues(t, nid, actual.NID) + + actuals, err := p.NextMessages(ctx, 255) + require.NoError(t, err) + + actual = &actuals[0] + assert.EqualValues(t, expected.ID, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + id := x.NewUUID() + + t.Run("persists id on creation", func(t *testing.T) { + expected := courier.Message{ID: id} + require.NoError(t, p.AddMessage(ctx, &expected)) + backdateMessage(t, expected.ID) + + assert.EqualValues(t, id, expected.ID) + assert.EqualValues(t, nid, expected.NID) + assert.EqualValues(t, nid, p.NetworkID(ctx)) + + actual, err := p.LatestQueuedMessage(ctx) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + + actuals, err := p.NextMessages(ctx, 255) + require.NoError(t, err) + + actual = &actuals[0] + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := newNetwork(t, ctx) + + _, err := p.LatestQueuedMessage(ctx) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + + _, err = p.NextMessages(ctx, 255) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) + + t.Run("can not update on another network", func(t *testing.T) { + _, p := newNetwork(t, ctx) + err := p.SetMessageStatus(ctx, id, courier.MessageStatusProcessing) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=FetchMessage", func(t *testing.T) { + msgID := messages[0].ID + + message, err := p.FetchMessage(ctx, msgID) + require.NoError(t, err) + require.Equal(t, msgID, message.ID) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := newNetwork(t, ctx) + + _, err := p.FetchMessage(ctx, msgID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=RecordDispatch", func(t *testing.T) { + msgID := messages[0].ID + + err := p.RecordDispatch(ctx, msgID, courier.CourierMessageDispatchStatusFailed, errors.New("testerror")) + require.NoError(t, err) + + message, err := p.FetchMessage(ctx, msgID) + require.NoError(t, err) + + require.Len(t, message.Dispatches, 1) + assert.Equal(t, "testerror", gjson.GetBytes(message.Dispatches[0].Error, "message").String()) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := newNetwork(t, ctx) + + _, err := p.FetchMessage(ctx, msgID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4bb7caa --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Documentation + +Please find the documentation at +[www.ory.com/docs/kratos](https://www.ory.com/docs/kratos). + +To contribute to the documentation, please head over to: +[github.com/ory/docs/tree/master/docs/kratos](https://github.com/ory/docs/tree/master/docs/kratos) diff --git a/docs/sidebar.json b/docs/sidebar.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/docs/sidebar.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/driver/config/.snapshots/TestCourierEmailHTTP-case=configs_set.json b/driver/config/.snapshots/TestCourierEmailHTTP-case=configs_set.json new file mode 100644 index 0000000..2011a5b --- /dev/null +++ b/driver/config/.snapshots/TestCourierEmailHTTP-case=configs_set.json @@ -0,0 +1,15 @@ +{ + "auth": { + "config": { + "password": "YourPass", + "user": "YourUsername" + }, + "type": "basic_auth" + }, + "body": "file://some.jsonnet", + "header": { + "Content-Type": "application/json" + }, + "method": "POST", + "url": "https://example.com/email" +} diff --git a/driver/config/.snapshots/TestCourierSMS-case=configs_set.json b/driver/config/.snapshots/TestCourierSMS-case=configs_set.json new file mode 100644 index 0000000..471e001 --- /dev/null +++ b/driver/config/.snapshots/TestCourierSMS-case=configs_set.json @@ -0,0 +1,15 @@ +{ + "auth": { + "config": { + "password": "YourPass", + "user": "YourUsername" + }, + "type": "basic_auth" + }, + "body": "base64://e30=", + "header": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "method": "POST", + "url": "https://api.twilio.com/2010-04-01/Accounts/YourAccountID/Messages.json" +} diff --git a/driver/config/.snapshots/TestCourierSMS-case=defaults.json b/driver/config/.snapshots/TestCourierSMS-case=defaults.json new file mode 100644 index 0000000..19765bd --- /dev/null +++ b/driver/config/.snapshots/TestCourierSMS-case=defaults.json @@ -0,0 +1 @@ +null diff --git a/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json new file mode 100644 index 0000000..6ec4b1e --- /dev/null +++ b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json @@ -0,0 +1,19 @@ +[ + "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" +] diff --git a/driver/config/buildinfo.go b/driver/config/buildinfo.go new file mode 100644 index 0000000..184d963 --- /dev/null +++ b/driver/config/buildinfo.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +var ( + Version = "master" + Date = "undefined" + Commit = "undefined" +) diff --git a/driver/config/config.go b/driver/config/config.go new file mode 100644 index 0000000..23c06d7 --- /dev/null +++ b/driver/config/config.go @@ -0,0 +1,1732 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "bytes" + "cmp" + "context" + "crypto/fips140" + "crypto/sha512" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/inhies/go-bytesize" + "github.com/pkg/errors" + "github.com/rs/cors" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/jsonschema/v3/httploader" + "github.com/ory/kratos/embedx" + "github.com/ory/kratos/request" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/corsx" + "github.com/ory/x/crdbx" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonschemax" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/region" + "github.com/ory/x/watcherx" +) + +const ( + DefaultIdentityTraitsSchemaID = "default" + DefaultBrowserReturnURL = "default_browser_return_url" + DefaultSQLiteMemoryDSN = "sqlite://file::memory:?_fk=true" + DefaultPasswordHashingAlgorithm = "argon2" + DefaultCipherAlgorithm = "noop" + UnknownVersion = "unknown version" + ViperKeyDSN = "dsn" + ViperKeyCourierSMTPURL = "courier.smtp.connection_uri" + ViperKeyCourierSMTPClientCertPath = "courier.smtp.client_cert_path" + ViperKeyCourierSMTPClientKeyPath = "courier.smtp.client_key_path" + ViperKeyCourierTemplatesPath = "courier.template_override_path" + ViperKeyCourierTemplatesRecoveryInvalidEmail = "courier.templates.recovery.invalid.email" + ViperKeyCourierTemplatesRecoveryValidEmail = "courier.templates.recovery.valid.email" + ViperKeyCourierTemplatesRecoveryCodeInvalidEmail = "courier.templates.recovery_code.invalid.email" + ViperKeyCourierTemplatesRecoveryCodeValidEmail = "courier.templates.recovery_code.valid.email" + ViperKeyCourierTemplatesVerificationInvalidEmail = "courier.templates.verification.invalid.email" + ViperKeyCourierTemplatesVerificationValidEmail = "courier.templates.verification.valid.email" + ViperKeyCourierTemplatesVerificationCodeInvalidEmail = "courier.templates.verification_code.invalid.email" + ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email" + ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms" + ViperKeyCourierTemplatesRecoveryCodeValidSMS = "courier.templates.recovery_code.valid.sms" + ViperKeyCourierTemplatesLoginCodeValidSMS = "courier.templates.login_code.valid.sms" + ViperKeyCourierTemplatesRegistrationCodeValidSMS = "courier.templates.registration_code.valid.sms" + ViperKeyCourierTemplatesVerifiableAddressChangedEmail = "courier.templates.verifiable_address_changed.email" + ViperKeyCourierTemplatesVerifiableAddressChangedSMS = "courier.templates.verifiable_address_changed.sms" + ViperKeyCourierTemplatesAuthenticatorKeyAddedEmail = "courier.templates.authenticator_key_added.email" + ViperKeyCourierTemplatesAuthenticatorKeyAddedSMS = "courier.templates.authenticator_key_added.sms" + ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy" + ViperKeyCourierHTTPRequestConfig = "courier.http.request_config" + ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email" + ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email" + ViperKeyCourierSMTP = "courier.smtp" + ViperKeyCourierSMTPFrom = "courier.smtp.from_address" + ViperKeyCourierSMTPFromName = "courier.smtp.from_name" + ViperKeyCourierSMTPHeaders = "courier.smtp.headers" + ViperKeyCourierSMTPLocalName = "courier.smtp.local_name" + ViperKeyCourierMessageRetries = "courier.message_retries" + ViperKeyCourierWorkerPullCount = "courier.worker.pull_count" + ViperKeyCourierWorkerPullWait = "courier.worker.pull_wait" + ViperKeyCourierChannels = "courier.channels" + ViperKeySecretsDefault = "secrets.default" + ViperKeySecretsCookie = "secrets.cookie" + ViperKeySecretsCipher = "secrets.cipher" + ViperKeySecretsPagination = "secrets.pagination" + ViperKeyPublicBaseURL = "serve.public.base_url" + ViperKeyAdminBaseURL = "serve.admin.base_url" + ViperKeySessionLifespan = "session.lifespan" + ViperKeySessionSameSite = "session.cookie.same_site" + ViperKeySessionSecure = "session.cookie.secure" + ViperKeySessionDomain = "session.cookie.domain" + ViperKeySessionName = "session.cookie.name" + ViperKeySessionPath = "session.cookie.path" + ViperKeySessionPersistentCookie = "session.cookie.persistent" + 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" + ViperKeyChooseRecoveryAddress = "feature_flags.choose_recovery_address" + ViperKeyUseLegacyShowVerificationUI = "feature_flags.legacy_continue_with_verification_ui" + ViperKeyLegacyOIDCRegistrationGroup = "feature_flags.legacy_oidc_registration_node_group" + ViperKeyUseLegacyRequireVerifiedLoginError = "feature_flags.legacy_require_verified_login_error" + ViperKeyLegacyAllowInsecureOrigins = "feature_flags.legacy_allow_insecure_origins" + ViperKeyRefreshLoginChooseAddress = "feature_flags.refresh_login_choose_address" + ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" + ViperKeyCookieSameSite = "cookies.same_site" + ViperKeyCookieDomain = "cookies.domain" + ViperKeyCookiePath = "cookies.path" + ViperKeyCookieSecure = "cookies.secure" + ViperKeySelfServiceStrategyConfig = "selfservice.methods" + ViperKeySelfServiceBrowserDefaultReturnTo = "selfservice." + DefaultBrowserReturnURL + ViperKeyURLsAllowedReturnToDomains = "selfservice.allowed_return_urls" + ViperKeySelfServiceRegistrationEnabled = "selfservice.flows.registration.enabled" + ViperKeySelfServiceRegistrationLoginHints = "selfservice.flows.registration.login_hints" + ViperKeySelfServiceRegistrationEnableLegacyOneStep = "selfservice.flows.registration.enable_legacy_one_step" + ViperKeySelfServiceRegistrationFlowStyle = "selfservice.flows.registration.style" + 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" + ViperKeySecurityDisallowRefInIdentitySchemas = "security.disallow_ref_in_identity_schemas" + ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan" + ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after" + ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks" + ViperKeySelfServiceErrorUI = "selfservice.flows.error.ui_url" + ViperKeySelfServiceLogoutBrowserDefaultReturnTo = "selfservice.flows.logout.after." + DefaultBrowserReturnURL + ViperKeySelfServiceLogoutClearBrowserData = "selfservice.flows.logout.clear_browser_data" + ViperKeySelfServiceSettingsURL = "selfservice.flows.settings.ui_url" + ViperKeySelfServiceSettingsAfter = "selfservice.flows.settings.after" + ViperKeySelfServiceSettingsBeforeHooks = "selfservice.flows.settings.before.hooks" + ViperKeySelfServiceSettingsRequestLifespan = "selfservice.flows.settings.lifespan" + ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter = "selfservice.flows.settings.privileged_session_max_age" + ViperKeySelfServiceSettingsRequiredAAL = "selfservice.flows.settings.required_aal" + ViperKeySelfServiceRecoveryAfter = "selfservice.flows.recovery.after" + ViperKeySelfServiceRecoveryBeforeHooks = "selfservice.flows.recovery.before.hooks" + ViperKeySelfServiceRecoveryEnabled = "selfservice.flows.recovery.enabled" + ViperKeySelfServiceRecoveryUse = "selfservice.flows.recovery.use" + ViperKeySelfServiceRecoveryUI = "selfservice.flows.recovery.ui_url" + ViperKeySelfServiceRecoveryRequestLifespan = "selfservice.flows.recovery.lifespan" + ViperKeySelfServiceRecoveryBrowserDefaultReturnTo = "selfservice.flows.recovery.after." + DefaultBrowserReturnURL + ViperKeySelfServiceRecoveryNotifyUnknownRecipients = "selfservice.flows.recovery.notify_unknown_recipients" + ViperKeySelfServiceVerificationEnabled = "selfservice.flows.verification.enabled" + ViperKeySelfServiceVerificationUI = "selfservice.flows.verification.ui_url" + ViperKeySelfServiceVerificationRequestLifespan = "selfservice.flows.verification.lifespan" + ViperKeySelfServiceVerificationBrowserDefaultReturnTo = "selfservice.flows.verification.after." + DefaultBrowserReturnURL + ViperKeySelfServiceVerificationAfter = "selfservice.flows.verification.after" + ViperKeySelfServiceVerificationBeforeHooks = "selfservice.flows.verification.before.hooks" + ViperKeySelfServiceVerificationUse = "selfservice.flows.verification.use" + ViperKeySelfServiceVerificationNotifyUnknownRecipients = "selfservice.flows.verification.notify_unknown_recipients" + ViperKeyDefaultIdentitySchemaID = "identity.default_schema_id" + ViperKeyIdentitySchemas = "identity.schemas" + ViperKeyHasherAlgorithm = "hashers.algorithm" + ViperKeyHasherArgon2ConfigMemory = "hashers.argon2.memory" + ViperKeyHasherArgon2ConfigIterations = "hashers.argon2.iterations" + ViperKeyHasherArgon2ConfigParallelism = "hashers.argon2.parallelism" + ViperKeyHasherArgon2ConfigSaltLength = "hashers.argon2.salt_length" + ViperKeyHasherArgon2ConfigKeyLength = "hashers.argon2.key_length" + ViperKeyHasherArgon2ConfigExpectedDuration = "hashers.argon2.expected_duration" + ViperKeyHasherArgon2ConfigExpectedDeviation = "hashers.argon2.expected_deviation" + ViperKeyHasherArgon2ConfigDedicatedMemory = "hashers.argon2.dedicated_memory" + ViperKeyHasherBcryptCost = "hashers.bcrypt.cost" + ViperKeyCipherAlgorithm = "ciphers.algorithm" + ViperKeyDatabaseCleanupSleepTables = "database.cleanup.sleep.tables" + ViperKeyDatabaseCleanupBatchSize = "database.cleanup.batch_size" + ViperKeyLinkLifespan = "selfservice.methods.link.config.lifespan" + ViperKeyCodeLifespan = "selfservice.methods.code.config.lifespan" + ViperKeyCodeMaxSubmissions = "selfservice.methods.code.config.max_submissions" + 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" + ViperKeyPasswordMinLength = "selfservice.methods.password.config.min_password_length" + ViperKeyPasswordIdentifierSimilarityCheckEnabled = "selfservice.methods.password.config.identifier_similarity_check_enabled" + ViperKeyIgnoreNetworkErrors = "selfservice.methods.password.config.ignore_network_errors" + ViperKeyPasswordRegistrationProfileGroup = "selfservice.methods.password.config.password_profile_registration_node_group" + ViperKeyTOTPIssuer = "selfservice.methods.totp.config.issuer" + ViperKeyOIDCBaseRedirectURL = "selfservice.methods.oidc.config.base_redirect_uri" + ViperKeySAMLBaseRedirectURL = "selfservice.methods.saml.config.base_redirect_uri" + ViperKeyWebAuthnRPDisplayName = "selfservice.methods.webauthn.config.rp.display_name" + ViperKeyWebAuthnRPID = "selfservice.methods.webauthn.config.rp.id" + 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" + ViperKeyPasskeyAuthenticatorAttachment = "selfservice.methods.passkey.config.authenticator_selection.attachment" + ViperKeyPasskeyResidentKey = "selfservice.methods.passkey.config.authenticator_selection.resident_key" + ViperKeyPasskeyUserVerification = "selfservice.methods.passkey.config.authenticator_selection.user_verification" + ViperKeyPasskeyAttestationPreference = "selfservice.methods.passkey.config.attestation.preference" + ViperKeyPasskeyRegistrationTimeout = "selfservice.methods.passkey.config.timeouts.registration" + ViperKeyPasskeyLoginTimeout = "selfservice.methods.passkey.config.timeouts.login" + ViperKeyOrganizations = "selfservice.methods.b2b.config.organizations" + ViperKeyOAuth2ProviderURL = "oauth2_provider.url" + ViperKeyOAuth2ProviderHeader = "oauth2_provider.headers" + ViperKeyOAuth2ProviderOverrideReturnTo = "oauth2_provider.override_return_to" + ViperKeyClientHTTPNoPrivateIPRanges = "clients.http.disallow_private_ip_ranges" + ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" + ViperKeyClientSMTPNoPrivateIPRanges = "clients.smtp.disallow_private_ip_ranges" + ViperKeyWebhookHeaderAllowlist = "clients.web_hook.header_allowlist" + ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" + ViperKeyVersion = "version" + ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook" +) + +const ( + HighestAvailableAAL = "highest_available" + Argon2DefaultMemory = 128 * bytesize.MB + Argon2DefaultIterations uint32 = 1 + Argon2DefaultSaltLength uint32 = 16 + Argon2DefaultKeyLength uint32 = 32 + Argon2DefaultDuration = 500 * time.Millisecond + Argon2DefaultDeviation = 500 * time.Millisecond + Argon2DefaultDedicatedMemory = 1 * bytesize.GB + BcryptDefaultCost uint32 = 12 +) + +// DefaultSessionCookieName returns the default cookie name for the kratos session. +const DefaultSessionCookieName = "ory_kratos_session" + +type ( + Argon2 struct { + Memory bytesize.ByteSize `json:"memory"` + Iterations uint32 `json:"iterations"` + Parallelism uint8 `json:"parallelism"` + SaltLength uint32 `json:"salt_length"` + KeyLength uint32 `json:"key_length"` + ExpectedDuration time.Duration `json:"expected_duration"` + ExpectedDeviation time.Duration `json:"expected_deviation"` + DedicatedMemory bytesize.ByteSize `json:"dedicated_memory"` + } + Bcrypt struct { + Cost uint32 `json:"cost"` + } + SelfServiceHook struct { + Name string `json:"hook"` + Config json.RawMessage `json:"config"` + } + SelfServiceStrategy struct { + Enabled bool `json:"enabled"` + Config json.RawMessage `json:"config"` + } + SelfServiceStrategyCode struct { + *SelfServiceStrategy + PasswordlessEnabled bool `json:"passwordless_enabled"` + MFAEnabled bool `json:"mfa_enabled"` + } + Schema struct { + ID string `json:"id" koanf:"id"` + URL string `json:"url" koanf:"url"` + SelfserviceSelectable bool `json:"selfservice_selectable" koanf:"selfservice_selectable"` + } + PasswordPolicy struct { + HaveIBeenPwnedHost string `json:"haveibeenpwned_host"` + HaveIBeenPwnedEnabled bool `json:"haveibeenpwned_enabled"` + MaxBreaches uint `json:"max_breaches"` + IgnoreNetworkErrors bool `json:"ignore_network_errors"` + MinPasswordLength uint `json:"min_password_length"` + IdentifierSimilarityCheckEnabled bool `json:"identifier_similarity_check_enabled"` + } + Schemas []Schema + CourierEmailBodyTemplate struct { + PlainText string `json:"plaintext"` + HTML string `json:"html"` + } + CourierEmailTemplate struct { + Body *CourierEmailBodyTemplate `json:"body"` + Subject string `json:"subject"` + } + CourierSMSTemplate struct { + Body *CourierSMSTemplateBody `json:"body"` + } + CourierSMSTemplateBody struct { + PlainText string `json:"plaintext"` + } + CourierChannel struct { + ID string `json:"id" koanf:"id"` + Type string `json:"type" koanf:"type"` + SMTPConfig *SMTPConfig `json:"smtp_config" koanf:"smtp_config"` + RequestConfig request.Config `json:"request_config" koanf:"request_config"` + } + SMTPConfig struct { + ConnectionURI string `json:"connection_uri" koanf:"connection_uri"` + ClientCertPath string `json:"client_cert_path" koanf:"client_cert_path"` + ClientKeyPath string `json:"client_key_path" koanf:"client_key_path"` + FromAddress string `json:"from_address" koanf:"from_address"` + FromName string `json:"from_name" koanf:"from_name"` + 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 request.Config `json:"config" koanf:"config"` + } + Config struct { + l *logrusx.Logger + p *configx.Provider + c contextx.Contextualizer + identityMetaSchema *jsonschema.Schema + stdOutOrErr io.Writer + } + Provider interface { + Config() *Config + } + CourierConfigs interface { + CourierTemplatesRoot(ctx context.Context) string + CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRecoveryInvalid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRecoveryValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRecoveryCodeInvalid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate + CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate + CourierSMSTemplatesRecoveryCodeValid(ctx context.Context) *CourierSMSTemplate + CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate + CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate + CourierTemplatesVerifiableAddressChanged(ctx context.Context) *CourierEmailTemplate + CourierSMSTemplatesVerifiableAddressChanged(ctx context.Context) *CourierSMSTemplate + CourierTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierEmailTemplate + CourierSMSTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierSMSTemplate + CourierMessageRetries(ctx context.Context) int + CourierWorkerPullCount(ctx context.Context) int + CourierWorkerPullWait(ctx context.Context) time.Duration + CourierChannels(context.Context) ([]*CourierChannel, error) + ClientSMTPNoPrivateIPRanges(ctx context.Context) bool + } +) + +func (c *Argon2) MarshalJSON() ([]byte, error) { + type encoded struct { + Memory string `json:"memory"` + Iterations uint32 `json:"iterations"` + Parallelism uint8 `json:"parallelism"` + SaltLength uint32 `json:"salt_length"` + KeyLength uint32 `json:"key_length"` + ExpectedDuration string `json:"minimal_duration"` + ExpectedDeviation string `json:"expected_deviation"` + DedicatedMemory string `json:"dedicated_memory"` + } + + return json.Marshal(&encoded{ + Memory: c.Memory.String(), + Iterations: c.Iterations, + Parallelism: c.Parallelism, + SaltLength: c.SaltLength, + KeyLength: c.KeyLength, + ExpectedDuration: c.ExpectedDuration.String(), + ExpectedDeviation: c.ExpectedDeviation.String(), + DedicatedMemory: c.DedicatedMemory.String(), + }) +} + +var Argon2DefaultParallelism = uint8(runtime.NumCPU() * 2) + +const HookGlobal = "global" + +func HookStrategyKey(key, strategy string) string { + if strategy == HookGlobal { + return fmt.Sprintf("%s.hooks", key) + } else { + return fmt.Sprintf("%s.%s.hooks", key, strategy) + } +} + +func (s Schemas) FindSchemaByID(id string) (*Schema, error) { + for _, sc := range s { + if sc.ID == id { + return &sc, nil + } + } + + return nil, errors.Errorf("unable to find identity schema with id: %s", id) +} + +func MustNew(t testing.TB, l *logrusx.Logger, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config { + p, err := New(t.Context(), l, os.Stderr, ctxer, opts...) + require.NoError(t, err) + return p +} + +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{ + configx.WithStderrValidationReporter(), + configx.OmitKeysFromTracing("dsn", "courier.smtp.connection_uri", "secrets.default", "secrets.cookie", "secrets.cipher", "client_secret"), + configx.WithImmutables("serve", "profiling", "log"), + configx.WithExceptImmutables("serve.public.cors.allowed_origins"), + configx.WithLogrusWatcher(l), + configx.WithLogger(l), + configx.WithContext(ctx), + configx.AttachWatcher(func(event watcherx.Event, err error) { + if c == nil { + panic(errors.New("the config provider did not initialise correctly in time")) + } + if err := c.validateIdentitySchemas(ctx); err != nil { + l.WithError(err). + Errorf("The changed identity schema configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.") + } + }), + }, opts...) + + p, err := configx.New(ctx, embedx.ConfigSchema, opts...) + if err != nil { + return nil, err + } + + l.UseConfig(p) + + c = NewCustom(l, p, stdOutOrErr, ctxer) + + if !p.SkipValidation() { + if err := c.validateIdentitySchemas(ctx); err != nil { + return nil, err + } + if fips140.Enabled() { + if len(p.Strings(ViperKeySecretsPagination)) == 0 { + return nil, errors.New("you must provide `secrets.pagination` for FIPS compliance") + } + if len(p.Strings(ViperKeySecretsCipher)) == 0 { + return nil, errors.New("you must provide `secrets.cipher` for FIPS compliance") + } + if len(p.Strings(ViperKeySecretsCookie)) == 0 { + return nil, errors.New("you must provide `secrets.cookie` for FIPS compliance") + } + if len(p.Strings(ViperKeySecretsDefault)) == 0 { + return nil, errors.New("you must provide `secrets.default` for FIPS compliance") + } + } + } + + return c, nil +} + +func NewCustom(l *logrusx.Logger, p *configx.Provider, stdOutOrErr io.Writer, ctxt contextx.Contextualizer) *Config { + l.UseConfig(p) + return &Config{l: l, p: p, c: ctxt, stdOutOrErr: stdOutOrErr} +} + +func (p *Config) getIdentitySchemaValidator(ctx context.Context) (*jsonschema.Schema, error) { + if p.identityMetaSchema == nil { + c := jsonschema.NewCompiler() + err := embedx.AddSchemaResources(c, embedx.IdentityMeta) + if err != nil { + return nil, err + } + p.identityMetaSchema, err = c.Compile(ctx, embedx.IdentityMeta.GetSchemaID()) + if err != nil { + return nil, errors.WithStack(err) + } + } + + return p.identityMetaSchema, nil +} + +type validateIdentitySchemasContextKey int + +const validateIdentitySchemasClientKey validateIdentitySchemasContextKey = 1 + +func SetValidateIdentitySchemaResilientClientOptions(ctx context.Context, options []httpx.ResilientOptions) context.Context { + return context.WithValue(ctx, validateIdentitySchemasClientKey, options) +} + +func (p *Config) validateIdentitySchemas(ctx context.Context) error { + opts := []httpx.ResilientOptions{ + httpx.ResilientClientWithLogger(p.l), + httpx.ResilientClientWithMaxRetry(2), + httpx.ResilientClientWithConnectionTimeout(30 * time.Second), + } + + if o, ok := ctx.Value(validateIdentitySchemasClientKey).([]httpx.ResilientOptions); ok { + opts = o + } + + if p.ClientHTTPNoPrivateIPRanges(ctx) { + opts = append(opts, httpx.ResilientClientDisallowInternalIPs()) + } + + ctx = context.WithValue(ctx, httploader.ContextKey, httpx.NewResilientClient(opts...)) + + j, err := p.getIdentitySchemaValidator(ctx) + if err != nil { + return err + } + + ss, err := p.IdentityTraitsSchemas(ctx) + if err != nil { + return err + } + + for _, s := range ss { + resource, err := jsonschema.LoadURL(ctx, s.URL) + if err != nil { + return errors.WithStack(err) + } + defer func() { _ = resource.Close() }() + + schema, err := io.ReadAll(io.LimitReader(resource, 1024*1024)) + if err != nil { + return errors.WithStack(err) + } + + if err = j.Validate(bytes.NewBuffer(schema)); err != nil { + p.formatJsonErrors(schema, err) + return errors.WithStack(err) + } + } + return nil +} + +func (p *Config) formatJsonErrors(schema []byte, err error) { + _, _ = fmt.Fprintln(p.stdOutOrErr, "") + jsonschemax.FormatValidationErrorForCLI(p.stdOutOrErr, schema, err) +} + +func (p *Config) ServePublic(ctx context.Context) *configx.Serve { + return p.GetProvider(ctx).Serve("serve.public", p.IsInsecureDevMode(ctx), configx.Serve{ + Port: 4433, + }) +} + +func (p *Config) ServeAdmin(ctx context.Context) *configx.Serve { + return p.GetProvider(ctx).Serve("serve.admin", p.IsInsecureDevMode(ctx), configx.Serve{ + Port: 4434, + }) +} + +func (p *Config) CORSPublic(ctx context.Context) (cors.Options, bool) { + return p.GetProvider(ctx).CORS("serve.public", cors.Options{ + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Authorization", "Content-Type", "Cookie"}, + ExposedHeaders: []string{"Content-Type", "Set-Cookie"}, + AllowCredentials: true, + }) +} + +// Deprecated: use context-based [contextx.WithConfigValue] instead. +func (p *Config) Set(_ context.Context, key string, value interface{}) error { + return p.p.Set(key, value) +} + +// Deprecated: use context-based [contextx.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 %q to %q.", key, value) + } +} + +func (p *Config) SessionName(ctx context.Context) string { + return cmp.Or(p.GetProvider(ctx).String(ViperKeySessionName), DefaultSessionCookieName) +} + +func (p *Config) HasherArgon2(ctx context.Context) *Argon2 { + // warn about usage of default values and point to the docs + // warning will require https://github.com/ory/viper/issues/19 + return &Argon2{ + Memory: p.GetProvider(ctx).ByteSizeF(ViperKeyHasherArgon2ConfigMemory, Argon2DefaultMemory), + //nolint:gosec // disable G115 + Iterations: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigIterations, int(Argon2DefaultIterations))), + //nolint:gosec // disable G115 + Parallelism: uint8(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigParallelism, int(Argon2DefaultParallelism))), + //nolint:gosec // disable G115 + SaltLength: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigSaltLength, int(Argon2DefaultSaltLength))), + //nolint:gosec // disable G115 + KeyLength: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigKeyLength, int(Argon2DefaultKeyLength))), + ExpectedDuration: p.GetProvider(ctx).DurationF(ViperKeyHasherArgon2ConfigExpectedDuration, Argon2DefaultDuration), + ExpectedDeviation: p.GetProvider(ctx).DurationF(ViperKeyHasherArgon2ConfigExpectedDeviation, Argon2DefaultDeviation), + DedicatedMemory: p.GetProvider(ctx).ByteSizeF(ViperKeyHasherArgon2ConfigDedicatedMemory, Argon2DefaultDedicatedMemory), + } +} + +func (p *Config) HasherBcrypt(ctx context.Context) *Bcrypt { + cost := uint32(p.GetProvider(ctx).IntF(ViperKeyHasherBcryptCost, int(BcryptDefaultCost))) // #nosec G115 -- if the user configures a cost > MaxUint32, go falls back to MaxUint32 + if !p.IsInsecureDevMode(ctx) && cost < BcryptDefaultCost { + cost = BcryptDefaultCost + } + + return &Bcrypt{Cost: cost} +} + +func (p *Config) DefaultIdentityTraitsSchemaURL(ctx context.Context) (*url.URL, error) { + ss, err := p.IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + + search := p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID) + found, err := ss.FindSchemaByID(search) + if err != nil { + return nil, err + } + + return p.ParseURI(found.URL) +} + +func (p *Config) DefaultIdentityTraitsSchemaID(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID) +} + +func (p *Config) IdentityTraitsSchemaURL(ctx context.Context, schemaID string) (*url.URL, error) { + ss, err := p.IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + + found, err := ss.FindSchemaByID(schemaID) + if err != nil { + // default to default schema + search := p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID) + found, err = ss.FindSchemaByID(search) + if err != nil { + return nil, err + } + } + + return p.ParseURI(found.URL) +} + +func (p *Config) TOTPIssuer(ctx context.Context) string { + return p.GetProvider(ctx).StringF(ViperKeyTOTPIssuer, p.SelfPublicURL(ctx).Hostname()) +} + +func (p *Config) OIDCRedirectURIBase(ctx context.Context) *url.URL { + return p.GetProvider(ctx).URIF(ViperKeyOIDCBaseRedirectURL, p.SelfPublicURL(ctx)) +} + +func (p *Config) SAMLRedirectURIBase(ctx context.Context) *url.URL { + return p.GetProvider(ctx).URIF(ViperKeySAMLBaseRedirectURL, p.SelfPublicURL(ctx)) +} + +func (p *Config) IdentityTraitsSchemas(ctx context.Context) (ss Schemas, err error) { + if err = p.GetProvider(ctx).Unmarshal(ViperKeyIdentitySchemas, &ss); err != nil { + return ss, nil + } + + return ss, nil +} + +func (p *Config) DSN(ctx context.Context) string { + pp := p.GetProvider(ctx) + dsn := pp.String(ViperKeyDSN) + + if dsn == "memory" { + return DefaultSQLiteMemoryDSN + } + + if len(dsn) > 0 { + return dsn + } + + // Print a stack trace to aid debugging. + p.l.Fatalf("%+v", errors.Errorf("dsn must be set")) + return "" +} + +func (p *Config) DisableAPIFlowEnforcement(ctx context.Context) bool { + if p.IsInsecureDevMode(ctx) && os.Getenv("DEV_DISABLE_API_FLOW_ENFORCEMENT") == "true" { + p.l.Warn("Because \"DEV_DISABLE_API_FLOW_ENFORCEMENT=true\" and the \"--dev\" flag are set, self-service API flows will no longer check if the interaction is actually a browser flow. This is very dangerous as it allows bypassing of anti-CSRF measures, leaving the deployment highly vulnerable. This option should only be used for automated testing and never come close to real user data anywhere.") + return true + } + return false +} + +func (p *Config) ClientHTTPNoPrivateIPRanges(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeyClientHTTPNoPrivateIPRanges, false) +} + +func (p *Config) ClientHTTPPrivateIPExceptionURLs(ctx context.Context) []string { + return p.GetProvider(ctx).Strings(ViperKeyClientHTTPPrivateIPExceptionURLs) +} + +func (p *Config) ClientSMTPNoPrivateIPRanges(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeyClientSMTPNoPrivateIPRanges, false) +} + +func (p *Config) SelfServiceFlowRegistrationEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationEnabled) +} + +func (p *Config) SelfServiceFlowRegistrationLoginHints(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationLoginHints) +} + +func (p *Config) SelfServiceFlowRegistrationPasswordMethodProfileGroup(ctx context.Context) string { + switch g := p.GetProvider(ctx).String(ViperKeyPasswordRegistrationProfileGroup); g { + case "password": + return "password" + default: + return "default" + } +} + +func (p *Config) SelfServiceLegacyOIDCRegistrationGroup(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyLegacyOIDCRegistrationGroup) +} + +func (p *Config) SelfServiceFlowRegistrationTwoSteps(ctx context.Context) bool { + // The default in previous versions that legacy one-step would be disabled. If legacy is enabled, it means the + // user has explicitly set the key to true, in which case we respect it. + if useOneStep := p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationEnableLegacyOneStep); useOneStep { + p.l.Warnf("Found use of deprecated configuration key %q. Please use key %q instead and delete key %[1]q. Will use value from %[1]q to configure registration style.", ViperKeySelfServiceRegistrationEnableLegacyOneStep, ViperKeySelfServiceRegistrationFlowStyle) + return false + } + + // In all other cases, we use the new key which (like the old key) defaults to `profile_first` / two-step registration. + switch style := p.GetProvider(ctx).String(ViperKeySelfServiceRegistrationFlowStyle); style { + case "profile_first": + return true + default: + return false + } +} + +func (p *Config) SelfServiceFlowIdentitySchema(ctx context.Context, requestedSchema string) (string, error) { + if requestedSchema == p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID) { + return requestedSchema, nil + } + schemas, err := p.IdentityTraitsSchemas(ctx) + if err != nil { + return "", errors.WithStack(err) + } + for _, schema := range schemas { + if schema.ID == requestedSchema { + if !schema.SelfserviceSelectable { + return "", errors.WithStack(herodot.ErrBadRequest().WithReasonf("Requested identity schema %q is not enabled for self-service flows.", requestedSchema)) + } + return requestedSchema, nil + } + } + return "", errors.WithStack(herodot.ErrBadRequest().WithReasonf("Requested identity schema %q does not exist.", requestedSchema)) +} + +func (p *Config) SelfServiceFlowVerificationEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceVerificationEnabled) +} + +func (p *Config) UseLegacyShowVerificationUI(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyUseLegacyShowVerificationUI) +} + +func (p *Config) UseLegacyRequireVerifiedLoginError(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyUseLegacyRequireVerifiedLoginError) +} + +func (p *Config) SelfServiceFlowRecoveryEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceRecoveryEnabled) +} + +func (p *Config) SelfServiceFlowRecoveryUse(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeySelfServiceRecoveryUse) +} + +func (p *Config) SelfServiceFlowLoginBeforeHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginBeforeHooks) +} + +func (p *Config) SelfServiceFlowRecoveryBeforeHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceRecoveryBeforeHooks) +} + +func (p *Config) SelfServiceFlowVerificationBeforeHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceVerificationBeforeHooks) +} + +func (p *Config) SelfServiceFlowVerificationUse(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeySelfServiceVerificationUse) +} + +func (p *Config) SelfServiceFlowVerificationNotifyUnknownRecipients(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) +} + +func (p *Config) SelfServiceFlowSettingsBeforeHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceSettingsBeforeHooks) +} + +func (p *Config) SelfServiceFlowRegistrationBeforeHooks(ctx context.Context) []SelfServiceHook { + 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 { + pp := p.GetProvider(ctx) + val := pp.Get(key) + if val == nil { + return []SelfServiceHook{} + } + + config, err := json.Marshal(val) + if err != nil { + p.l.WithError(err).Fatalf("Unable to decode values from configuration key: %s", key) + } + + var hooks []SelfServiceHook + if err := json.Unmarshal(config, &hooks); err != nil { + p.l.WithError(err).Fatalf("Unable to encode value \"%s\" from configuration key: %s", config, key) + } + + for k := range hooks { + if len(hooks[k].Config) == 0 { + hooks[k].Config = json.RawMessage("{}") + } + } + + return hooks +} + +func (p *Config) SelfServiceFlowLoginAfterHooks(ctx context.Context, strategy string) []SelfServiceHook { + return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceLoginAfter, strategy)) +} + +func (p *Config) SelfServiceFlowSettingsAfterHooks(ctx context.Context, strategy string) []SelfServiceHook { + return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceSettingsAfter, strategy)) +} + +func (p *Config) SelfServiceFlowRegistrationAfterHooks(ctx context.Context, strategy string) []SelfServiceHook { + return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceRegistrationAfter, strategy)) +} + +func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *SelfServiceStrategy { + pp := p.GetProvider(ctx) + config := json.RawMessage("{}") + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, strategy) + + 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.") + config = json.RawMessage("{}") + } + + // The default value can easily be overwritten by setting e.g. `{"selfservice": "null"}` which means that + // 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 + } + + // Backwards compatibility for the old "passwordless_enabled" key + // This force-enables the code strategy, if passwordless is enabled, because in earlier versions it was possible to + // disable the code strategy, but enable passwordless + enabled := pp.BoolF(basePath+".enabled", defaultEnabled) + if strategy == "code" { + enabled = enabled || pp.Bool(basePath+".passwordless_enabled") + } + return &SelfServiceStrategy{ + Enabled: enabled, + Config: config, + } +} + +func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrategyCode { + pp := p.GetProvider(ctx) + config := json.RawMessage("{}") + basePath := ViperKeySelfServiceStrategyConfig + ".code" + + 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.") + config = json.RawMessage("{}") + } + + return &SelfServiceStrategyCode{ + SelfServiceStrategy: &SelfServiceStrategy{ + Enabled: pp.BoolF(basePath+".enabled", true), + Config: config, + }, + PasswordlessEnabled: pp.BoolF(basePath+".passwordless_enabled", false), + MFAEnabled: pp.BoolF(basePath+".mfa_enabled", false), + } +} + +func (p *Config) SecretsDefault(ctx context.Context) [][]byte { + pp := p.GetProvider(ctx) + secrets := pp.Strings(ViperKeySecretsDefault) + + if len(secrets) == 0 { + secrets = []string{uuid.Must(uuid.NewV4()).String()} + p.MustSet(ctx, ViperKeySecretsDefault, secrets) + } + + result := make([][]byte, len(secrets)) + for k, v := range secrets { + result[k] = []byte(v) + } + + return result +} + +func (p *Config) SecretsSession(ctx context.Context) [][]byte { + secrets := p.GetProvider(ctx).Strings(ViperKeySecretsCookie) + if len(secrets) == 0 { + return p.SecretsDefault(ctx) + } + + result := make([][]byte, len(secrets)) + for k, v := range secrets { + result[k] = []byte(v) + } + + return result +} + +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 { + cleanSecrets = append(cleanSecrets, secrets[k]) + } + } + if len(cleanSecrets) == 0 { + return [][32]byte{} + } + result := make([][32]byte, len(cleanSecrets)) + for n, s := range secrets { + for k, v := range []byte(s) { + result[n][k] = v + } + } + return result +} + +func (p *Config) SecretsPagination(ctx context.Context) [][32]byte { + secrets := p.GetProvider(ctx).Strings(ViperKeySecretsPagination) + + encryptionKeys := make([][32]byte, len(secrets)) + for i, key := range secrets { + encryptionKeys[i] = sha512.Sum512_256([]byte(key)) + } + + return encryptionKeys +} + +func (p *Config) SelfServiceBrowserDefaultReturnTo(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceBrowserDefaultReturnTo) +} + +func (p *Config) SelfPublicURL(ctx context.Context) *url.URL { + serve := p.ServePublic(ctx) + return serve.BaseURL +} + +func (p *Config) SelfAdminURL(ctx context.Context) *url.URL { + serve := p.ServeAdmin(ctx) + return serve.BaseURL +} + +func (p *Config) WebhookHeaderAllowlist(ctx context.Context) []string { + return p.GetProvider(ctx).Strings(ViperKeyWebhookHeaderAllowlist) +} + +func (p *Config) OAuth2ProviderHeader(ctx context.Context) http.Header { + hh := map[string]string{} + if err := p.GetProvider(ctx).Unmarshal(ViperKeyOAuth2ProviderHeader, &hh); err != nil { + p.l.WithError(errors.WithStack(err)). + Errorf("Configuration value from key %s could not be decoded.", ViperKeyOAuth2ProviderHeader) + return nil + } + + h := make(http.Header) + for k, v := range hh { + h.Set(k, v) + } + + return h +} + +func (p *Config) OAuth2ProviderOverrideReturnTo(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyOAuth2ProviderOverrideReturnTo) +} + +func (p *Config) OAuth2ProviderURL(ctx context.Context) *url.URL { + k := ViperKeyOAuth2ProviderURL + v := p.GetProvider(ctx).String(k) + if v == "" { + return nil + } + parsed, err := p.ParseAbsoluteOrRelativeURI(v) + if err != nil { + p.l.WithError(errors.WithStack(err)). + Errorf("Configuration value from key %s is not a valid URL: %s", k, v) + return nil + } + return parsed +} + +func (p *Config) SelfServiceFlowLoginUI(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceLoginUI) +} + +func (p *Config) SelfServiceFlowSettingsUI(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceSettingsURL) +} + +func (p *Config) SelfServiceFlowErrorURL(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceErrorUI) +} + +func (p *Config) SelfServiceFlowRegistrationUI(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceRegistrationUI) +} + +func (p *Config) SelfServiceFlowRecoveryUI(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceRecoveryUI) +} + +// SessionLifespan returns time.Hour*24 when the value is not set. +func (p *Config) SessionLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySessionLifespan, time.Hour*24) +} + +// OrganizationSessionLifespan returns the effective session lifespan for a +// session issued to the given organization. If orgID is uuid.Nil, or no +// matching organization is configured, or the organization has no +// session_lifespan override, the project-level session.lifespan is returned. +func (p *Config) OrganizationSessionLifespan(ctx context.Context, orgID uuid.UUID) time.Duration { + if orgID != uuid.Nil { + for _, org := range p.Organizations(ctx) { + if org.ID == orgID && org.SessionLifespan > 0 { + return org.SessionLifespan + } + } + } + return p.SessionLifespan(ctx) +} + +func (p *Config) SessionPersistentCookie(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySessionPersistentCookie) +} + +func (p *Config) SelfServiceBrowserAllowedReturnToDomains(ctx context.Context) (us []url.URL) { + src := p.GetProvider(ctx).Strings(ViperKeyURLsAllowedReturnToDomains) + for k, u := range src { + if len(u) == 0 { + continue + } + + parsed, err := url.ParseRequestURI(u) + if err != nil { + p.l.WithError(err).Warnf("Ignoring URL \"%s\" from configuration key \"%s.%d\".", u, ViperKeyURLsAllowedReturnToDomains, k) + continue + } + // A wildcard host must use the labelled form "*.example.com" bounded at a + // registrable domain. Reject bare "*", dot-less "*foo.com", public-suffix + // "*.com", and mid-label "foo.*.com" wildcards — each would match an + // attacker-registrable host. See corsx.ClassifyOrigin. + if corsx.ClassifyOrigin(parsed.Host).IsUnsafeWildcard() && !p.LegacyAllowInsecureOrigins(ctx) { + p.l.Warnf("Ignoring wildcard \"%s\" from configuration key \"%s.%d\".", u, ViperKeyURLsAllowedReturnToDomains, k) + continue + } + + us = append(us, *parsed) + } + + return us +} + +func (p *Config) SelfServiceFlowLoginRequestLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceLoginRequestLifespan, time.Hour) +} + +func (p *Config) SelfServiceFlowSettingsFlowLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceSettingsRequestLifespan, time.Hour) +} + +func (p *Config) SelfServiceFlowRegistrationRequestLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceRegistrationRequestLifespan, time.Hour) +} + +func (p *Config) SelfServiceFlowLogoutRedirectURL(ctx context.Context) *url.URL { + return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceLogoutBrowserDefaultReturnTo, p.SelfServiceBrowserDefaultReturnTo(ctx)) +} + +func (p *Config) CourierEmailStrategy(ctx context.Context) string { + return p.GetProvider(ctx).StringF(ViperKeyCourierDeliveryStrategy, "smtp") +} + +func (p *Config) CourierEmailRequestConfig(ctx context.Context) json.RawMessage { + if p.CourierEmailStrategy(ctx) != "http" { + return nil + } + + config, err := json.Marshal(p.GetProvider(ctx).Get(ViperKeyCourierHTTPRequestConfig)) + if err != nil { + p.l.WithError(err).Warn("Unable to marshal mailer request configuration.") + return nil + } + + return config +} + +func (p *Config) CourierTemplatesRoot(ctx context.Context) string { + return p.GetProvider(ctx).StringF(ViperKeyCourierTemplatesPath, "courier/builtin/templates") +} + +func (p *Config) CourierEmailTemplatesHelper(ctx context.Context, key string) *CourierEmailTemplate { + courierTemplate := &CourierEmailTemplate{ + Body: &CourierEmailBodyTemplate{ + PlainText: "", + HTML: "", + }, + Subject: "", + } + + if !p.GetProvider(ctx).Exists(key) { + return courierTemplate + } + + config, err := json.Marshal(p.GetProvider(ctx).Get(key)) + if err != nil { + p.l.WithError(err).Fatalf("Unable to decode values from %s.", key) + return courierTemplate + } + + if err := json.Unmarshal(config, courierTemplate); err != nil { + p.l.WithError(err).Fatalf("Unable to encode values from %s.", key) + return courierTemplate + } + return courierTemplate +} + +func (p *Config) CourierSMSTemplatesHelper(ctx context.Context, key string) *CourierSMSTemplate { + courierTemplate := &CourierSMSTemplate{ + Body: &CourierSMSTemplateBody{ + PlainText: "", + }, + } + + if !p.GetProvider(ctx).Exists(key) { + return courierTemplate + } + + config, err := json.Marshal(p.GetProvider(ctx).Get(key)) + if err != nil { + p.l.WithError(err).Fatalf("Unable to decode values from %s.", key) + return courierTemplate + } + + if err := json.Unmarshal(config, courierTemplate); err != nil { + p.l.WithError(err).Fatalf("Unable to encode values from %s.", key) + return courierTemplate + } + return courierTemplate +} + +func (p *Config) CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationInvalidEmail) +} + +func (p *Config) CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationValidEmail) +} + +func (p *Config) CourierTemplatesRecoveryInvalid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryInvalidEmail) +} + +func (p *Config) CourierTemplatesRecoveryValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryValidEmail) +} + +func (p *Config) CourierTemplatesRecoveryCodeInvalid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeInvalidEmail) +} + +func (p *Config) CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidEmail) +} + +func (p *Config) CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeInvalidEmail) +} + +func (p *Config) CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail) +} + +func (p *Config) CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidSMS) +} + +func (p *Config) CourierSMSTemplatesRecoveryCodeValid(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidSMS) +} + +func (p *Config) CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidSMS) +} + +func (p *Config) CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidSMS) +} + +func (p *Config) CourierTemplatesVerifiableAddressChanged(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerifiableAddressChangedEmail) +} + +func (p *Config) CourierSMSTemplatesVerifiableAddressChanged(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerifiableAddressChangedSMS) +} + +func (p *Config) CourierTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesAuthenticatorKeyAddedEmail) +} + +func (p *Config) CourierSMSTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesAuthenticatorKeyAddedSMS) +} + +func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail) +} + +func (p *Config) CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail) +} + +func (p *Config) CourierMessageRetries(ctx context.Context) int { + return p.GetProvider(ctx).IntF(ViperKeyCourierMessageRetries, 5) +} + +func (p *Config) CourierWorkerPullCount(ctx context.Context) int { + return p.GetProvider(ctx).Int(ViperKeyCourierWorkerPullCount) +} + +func (p *Config) CourierWorkerPullWait(ctx context.Context) time.Duration { + return p.GetProvider(ctx).Duration(ViperKeyCourierWorkerPullWait) +} + +func (p *Config) CourierSMTPHeaders(ctx context.Context) map[string]string { + return p.GetProvider(ctx).StringMap(ViperKeyCourierSMTPHeaders) +} + +func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ error) { + if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierChannels, &ccs); err != nil { + return nil, errors.WithStack(err) + } + + // load legacy configs + channel := CourierChannel{ + ID: "email", + Type: p.CourierEmailStrategy(ctx), + } + if channel.Type == "smtp" { + if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierSMTP, &channel.SMTPConfig); err != nil { + return nil, errors.WithStack(err) + } + } else { + if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierHTTPRequestConfig, &channel.RequestConfig); err != nil { + return nil, errors.WithStack(err) + } + } + ccs = append(ccs, &channel) + return ccs, nil +} + +func splitUrlAndFragment(s string) (string, string) { + i := strings.IndexByte(s, '#') + if i < 0 { + return s, "" + } + return s[:i], s[i+1:] +} + +func (p *Config) ParseAbsoluteOrRelativeURIOrFail(ctx context.Context, key string) *url.URL { + parsed, err := p.ParseAbsoluteOrRelativeURI(p.GetProvider(ctx).String(key)) + if err != nil { + p.l.WithError(errors.WithStack(err)). + Fatalf("Configuration value from key %s is not a valid URL: %s", key, p.GetProvider(ctx).String(key)) + } + return parsed +} + +func (p *Config) ParseURIOrFail(ctx context.Context, key string) *url.URL { + parsed, err := p.ParseURI(p.GetProvider(ctx).String(key)) + if err != nil { + p.l.WithField("reason", "expected scheme to be set"). + Fatalf("Configuration value from key %s is not a valid URL: %s", key, p.GetProvider(ctx).String(key)) + } + return parsed +} + +func (p *Config) ParseAbsoluteOrRelativeURI(rawUrl string) (*url.URL, error) { + u, frag := splitUrlAndFragment(rawUrl) + parsed, err := url.ParseRequestURI(u) + if err != nil { + return nil, errors.Wrapf(err, "configuration value not a valid URL: %s", rawUrl) + } + + if frag != "" { + parsed.Fragment = frag + } + + return parsed, nil +} + +func (p *Config) ParseURI(rawUrl string) (*url.URL, error) { + parsed, err := p.ParseAbsoluteOrRelativeURI(rawUrl) + if err != nil { + return nil, err + } + if parsed.Scheme == "" { + return nil, errors.Errorf("configuration value is not a valid URL: %s", rawUrl) + } + return parsed, nil +} + +func (p *Config) Tracing(ctx context.Context) *otelx.Config { + return p.GetProvider(ctx).TracingConfig("Ory Kratos") +} + +func (p *Config) IsInsecureDevMode(ctx context.Context) bool { + return p.GetProvider(ctx).Bool("dev") +} + +func (p *Config) IsBackgroundCourierEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool("watch-courier") +} + +func (p *Config) CourierExposeMetricsPort(ctx context.Context) int { + return p.GetProvider(ctx).Int("expose-metrics-port") +} + +func (p *Config) SelfServiceFlowVerificationUI(ctx context.Context) *url.URL { + return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceVerificationUI) +} + +func (p *Config) SelfServiceFlowVerificationRequestLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceVerificationRequestLifespan, time.Hour) +} + +func (p *Config) SelfServiceFlowVerificationReturnTo(ctx context.Context, defaultReturnTo *url.URL) *url.URL { + return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceVerificationBrowserDefaultReturnTo, defaultReturnTo) +} + +func (p *Config) SelfServiceFlowVerificationAfterHooks(ctx context.Context, strategy string) []SelfServiceHook { + return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceVerificationAfter, strategy)) +} + +func (p *Config) SelfServiceFlowRecoveryReturnTo(ctx context.Context, defaultReturnTo *url.URL) *url.URL { + return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, defaultReturnTo) +} + +func (p *Config) SelfServiceFlowRecoveryRequestLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceRecoveryRequestLifespan, time.Hour) +} + +func (p *Config) SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeySelfServiceRecoveryNotifyUnknownRecipients, false) +} + +func (p *Config) SelfServiceFlowLogoutClearBrowserData(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeySelfServiceLogoutClearBrowserData, false) +} + +func (p *Config) SelfServiceLinkMethodLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeyLinkLifespan, time.Hour) +} + +func (p *Config) SelfServiceCodeMethodLifespan(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeyCodeLifespan, time.Hour) +} + +func (p *Config) SelfServiceCodeMethodMaxSubmissions(ctx context.Context) int { + return p.GetProvider(ctx).IntF(ViperKeyCodeMaxSubmissions, 5) +} + +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) +} + +func (p *Config) DatabaseCleanupBatchSize(ctx context.Context) int { + return p.GetProvider(ctx).Int(ViperKeyDatabaseCleanupBatchSize) +} + +func (p *Config) SelfServiceFlowRecoveryAfterHooks(ctx context.Context, strategy string) []SelfServiceHook { + return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceRecoveryAfter, strategy)) +} + +func (p *Config) SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Hour) +} + +func (p *Config) SessionSameSiteMode(ctx context.Context) http.SameSite { + if !p.GetProvider(ctx).Exists(ViperKeySessionSameSite) { + return p.CookieSameSiteMode(ctx) + } + + switch p.GetProvider(ctx).StringF(ViperKeySessionSameSite, "Lax") { + case "Lax": + return http.SameSiteLaxMode + case "Strict": + return http.SameSiteStrictMode + case "None": + return http.SameSiteNoneMode + } + return http.SameSiteDefaultMode +} + +func (p *Config) SessionDomain(ctx context.Context) string { + if !p.GetProvider(ctx).Exists(ViperKeySessionDomain) { + return p.CookieDomain(ctx) + } + return p.GetProvider(ctx).String(ViperKeySessionDomain) +} + +func (p *Config) SessionCookieSecure(ctx context.Context) bool { + if !p.GetProvider(ctx).Exists(ViperKeySessionSecure) { + return !p.IsInsecureDevMode(ctx) + } + return p.GetProvider(ctx).Bool(ViperKeySessionSecure) +} + +func (p *Config) CookieDomain(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyCookieDomain) +} + +func (p *Config) SessionWhoAmIAAL(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeySessionWhoAmIAAL) +} + +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) +} + +// LegacyAllowInsecureOrigins restores legacy behavior where wildcard return URLs +// and CORS origins need not be bounded at a registrable domain. Insecure; only +// set by the Ory Network for grandfathered projects. Default false. +func (p *Config) LegacyAllowInsecureOrigins(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyLegacyAllowInsecureOrigins) +} + +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) +} + +func (p *Config) ChooseRecoveryAddress(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyChooseRecoveryAddress) +} + +func (p *Config) RefreshLoginChooseAddress(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyRefreshLoginChooseAddress) +} + +func (p *Config) SessionRefreshMinTimeLeft(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySessionRefreshMinTimeLeft, p.SessionLifespan(ctx)) +} + +func (p *Config) SelfServiceSettingsRequiredAAL(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeySelfServiceSettingsRequiredAAL) +} + +func (p *Config) CookieSameSiteMode(ctx context.Context) http.SameSite { + switch p.GetProvider(ctx).StringF(ViperKeyCookieSameSite, "Lax") { + case "Lax": + return http.SameSiteLaxMode + case "Strict": + return http.SameSiteStrictMode + case "None": + return http.SameSiteNoneMode + } + return http.SameSiteDefaultMode +} + +func (p *Config) SessionPath(ctx context.Context) string { + if !p.GetProvider(ctx).Exists(ViperKeySessionPath) { + return p.CookiePath(ctx) + } + return p.GetProvider(ctx).String(ViperKeySessionPath) +} + +func (p *Config) CookiePath(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyCookiePath) +} + +func (p *Config) CookieSecure(ctx context.Context) bool { + if !p.GetProvider(ctx).Exists(ViperKeyCookieSecure) { + return !p.IsInsecureDevMode(ctx) + } + return p.GetProvider(ctx).Bool(ViperKeyCookieSecure) +} + +func (p *Config) SelfServiceFlowLoginReturnTo(ctx context.Context, strategy string) *url.URL { + return p.selfServiceReturnTo(ctx, ViperKeySelfServiceLoginAfter, strategy) +} + +func (p *Config) SelfServiceFlowRegistrationReturnTo(ctx context.Context, strategy string) *url.URL { + return p.selfServiceReturnTo(ctx, ViperKeySelfServiceRegistrationAfter, strategy) +} + +func (p *Config) SelfServiceFlowSettingsReturnTo(ctx context.Context, strategy string, defaultReturnTo *url.URL) *url.URL { + return p.GetProvider(ctx).RequestURIF( + ViperKeySelfServiceSettingsAfter+"."+strategy+"."+DefaultBrowserReturnURL, + p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceSettingsAfter+"."+DefaultBrowserReturnURL, + defaultReturnTo, + ), + ) +} + +func (p *Config) selfServiceReturnTo(ctx context.Context, key string, strategy string) *url.URL { + return p.GetProvider(ctx).RequestURIF( + key+"."+strategy+"."+DefaultBrowserReturnURL, + p.GetProvider(ctx).RequestURIF(key+"."+DefaultBrowserReturnURL, + p.SelfServiceBrowserDefaultReturnTo(ctx), + ), + ) +} + +func (p *Config) ConfigVersion(ctx context.Context) string { + return p.GetProvider(ctx).StringF(ViperKeyVersion, UnknownVersion) +} + +func (p *Config) PasswordPolicyConfig(ctx context.Context) *PasswordPolicy { + return &PasswordPolicy{ + HaveIBeenPwnedHost: p.GetProvider(ctx).StringF(ViperKeyPasswordHaveIBeenPwnedHost, "api.pwnedpasswords.com"), + HaveIBeenPwnedEnabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordHaveIBeenPwnedEnabled, true), + MaxBreaches: uint(p.GetProvider(ctx).Int(ViperKeyPasswordMaxBreaches)), // #nosec G115 -- negative values are prevented by the schema validation + IgnoreNetworkErrors: p.GetProvider(ctx).BoolF(ViperKeyIgnoreNetworkErrors, true), + MinPasswordLength: uint(p.GetProvider(ctx).IntF(ViperKeyPasswordMinLength, 8)), // #nosec G115 -- negative values are prevented by the schema validation + IdentifierSimilarityCheckEnabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordIdentifierSimilarityCheckEnabled, true), + } +} + +func (p *Config) WebAuthnForPasswordless(ctx context.Context) bool { + return p.GetProvider(ctx).BoolF(ViperKeyWebAuthnPasswordless, false) +} + +func (p *Config) WebAuthnConfig(ctx context.Context) *webauthn.Config { + scheme := p.SelfPublicURL(ctx).Scheme + id := p.GetProvider(ctx).String(ViperKeyWebAuthnRPID) + origin := p.GetProvider(ctx).String(ViperKeyWebAuthnRPOrigin) + origins := p.GetProvider(ctx).StringsF(ViperKeyWebAuthnRPOrigins, []string{cmp.Or(origin, scheme+"://"+id)}) + return &webauthn.Config{ + RPDisplayName: p.GetProvider(ctx).String(ViperKeyWebAuthnRPDisplayName), + RPID: id, + RPOrigins: origins, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + UserVerification: protocol.VerificationDiscouraged, + }, + EncodeUserIDAsString: false, + } +} + +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}) + + residentKey := protocol.ResidentKeyRequirement( + p.GetProvider(ctx).StringF(ViperKeyPasskeyResidentKey, string(protocol.ResidentKeyRequirementRequired))) + requireResidentKey := residentKey == protocol.ResidentKeyRequirementRequired + + authSel := protocol.AuthenticatorSelection{ + RequireResidentKey: &requireResidentKey, + ResidentKey: residentKey, + UserVerification: protocol.UserVerificationRequirement( + p.GetProvider(ctx).StringF(ViperKeyPasskeyUserVerification, string(protocol.VerificationPreferred))), + } + // Only constrain authenticator attachment when the operator explicitly + // configures it. Omitting the field lets users register either platform + // or cross-platform authenticators, which matches the WebAuthn spec's + // "no preference" behavior. + if attachment := p.GetProvider(ctx).String(ViperKeyPasskeyAuthenticatorAttachment); attachment != "" { + authSel.AuthenticatorAttachment = protocol.AuthenticatorAttachment(attachment) + } + + cfg := &webauthn.Config{ + RPDisplayName: p.GetProvider(ctx).String(ViperKeyPasskeyRPDisplayName), + RPID: id, + RPOrigins: origins, + AuthenticatorSelection: authSel, + AttestationPreference: protocol.ConveyancePreference( + p.GetProvider(ctx).StringF(ViperKeyPasskeyAttestationPreference, string(protocol.PreferNoAttestation))), + EncodeUserIDAsString: false, + } + + if d := p.GetProvider(ctx).Duration(ViperKeyPasskeyRegistrationTimeout); d > 0 { + cfg.Timeouts.Registration = webauthn.TimeoutConfig{ + Timeout: d, + TimeoutUVD: d, + } + } + if d := p.GetProvider(ctx).Duration(ViperKeyPasskeyLoginTimeout); d > 0 { + cfg.Timeouts.Login = webauthn.TimeoutConfig{ + Timeout: d, + TimeoutUVD: d, + } + } + + return cfg +} + +type Organization struct { + ID uuid.UUID `koanf:"id"` + Domains []string `koanf:"domains"` + DefaultRegion region.Region `koanf:"default_region"` + SessionLifespan time.Duration `koanf:"session_lifespan"` +} + +func (p *Config) Organizations(ctx context.Context) (orgs []Organization) { + if err := p.GetProvider(ctx).Unmarshal(ViperKeyOrganizations, &orgs); err != nil { + return nil + } + return orgs +} + +func (p *Config) HasherPasswordHashingAlgorithm(ctx context.Context) string { + configValue := p.GetProvider(ctx).StringF(ViperKeyHasherAlgorithm, DefaultPasswordHashingAlgorithm) + switch configValue { + case "bcrypt": + return configValue + case "argon2": + fallthrough + default: + return configValue + } +} + +func (p *Config) CipherAlgorithm(ctx context.Context) string { + configValue := p.GetProvider(ctx).StringF(ViperKeyCipherAlgorithm, DefaultCipherAlgorithm) + switch configValue { + case "noop": + return configValue + case "xchacha20-poly1305": + return configValue + case "aes": + fallthrough + default: + return configValue + } +} + +func (p *Config) GetProvider(ctx context.Context) *configx.Provider { + return p.c.Config(ctx, p.p) +} + +type SessionTokenizeFormat struct { + TTL time.Duration `koanf:"ttl" json:"ttl"` + ClaimsMapperURL string `koanf:"claims_mapper_url" json:"claims_mapper_url"` + JWKSURL string `koanf:"jwks_url" json:"jwks_url"` + SubjectSource string `koanf:"subject_source" json:"subject_source"` +} + +func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTokenizeFormat, err error) { + var result SessionTokenizeFormat + path := ViperKeySessionTokenizerTemplates + "." + key + if !p.GetProvider(ctx).Exists(path) { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to find tokenizer template \"%s\".", key)) + } + + if err := p.GetProvider(ctx).Unmarshal(path, &result); err != nil { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to decode tokenizer template \"%s\": %s", key, err)) + } + + return &result, nil +} + +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 + } + + _ = p.GetProvider(ctx).Unmarshal(ViperKeyPasswordMigrationHook+".config", &hook.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) +} + +func (p *Config) SecurityDisallowRefInIdentitySchemas(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySecurityDisallowRefInIdentitySchemas) +} diff --git a/driver/config/config_test.go b/driver/config/config_test.go new file mode 100644 index 0000000..3be5850 --- /dev/null +++ b/driver/config/config_test.go @@ -0,0 +1,1596 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config_test + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ory/x/contextx" + + "github.com/ory/x/httpx" + "github.com/ory/x/randx" + + "github.com/ory/x/snapshotx" + + "github.com/ghodss/yaml" + + "github.com/go-webauthn/webauthn/protocol" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/x/configx" + + "github.com/sirupsen/logrus/hooks/test" + + "github.com/ory/x/logrusx" + "github.com/ory/x/urlx" + + _ "github.com/ory/jsonschema/v3/fileloader" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/driver/config" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestViperProvider(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("suite=loaders", func(t *testing.T) { + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithContext(ctx)) + + t.Run("group=client config", func(t *testing.T) { + assert.False(t, p.ClientHTTPNoPrivateIPRanges(ctx), "Should not have private IP ranges disabled per default") + assert.Equal(t, []string{}, p.ClientHTTPPrivateIPExceptionURLs(ctx), "Should return the correct exceptions") + + p.MustSet(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true) + assert.True(t, p.ClientHTTPNoPrivateIPRanges(ctx), "Should disallow private IP ranges if set") + + p.MustSet(ctx, config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{"https://foobar.com/baz"}) + assert.Equal(t, []string{"https://foobar.com/baz"}, p.ClientHTTPPrivateIPExceptionURLs(ctx), "Should return the correct exceptions") + }) + + t.Run("group=urls", func(t *testing.T) { + assert.Equal(t, "http://test.kratos.ory.sh/login", p.SelfServiceFlowLoginUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/settings", p.SelfServiceFlowSettingsUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/register", p.SelfServiceFlowRegistrationUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/error", p.SelfServiceFlowErrorURL(ctx).String()) + + assert.Equal(t, "http://admin.kratos.ory.sh", p.SelfAdminURL(ctx).String()) + assert.Equal(t, "http://public.kratos.ory.sh", p.SelfPublicURL(ctx).String()) + + var ds []string + for _, v := range p.SelfServiceBrowserAllowedReturnToDomains(ctx) { + ds = append(ds, v.String()) + } + + assert.Equal(t, []string{ + "http://return-to-1-test.ory.sh/", + "http://return-to-2-test.ory.sh/", + "http://*.wildcards.ory.sh", + "/return-to-relative-test/", + }, ds) + + pWithFragments := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{ + config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login", + config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings", + config.ViperKeySelfServiceRegistrationUI: "http://test.kratos.ory.sh/#/register", + config.ViperKeySelfServiceErrorUI: "http://test.kratos.ory.sh/#/error", + }), configx.SkipValidation()) + + assert.Equal(t, "http://test.kratos.ory.sh/#/login", pWithFragments.SelfServiceFlowLoginUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/#/settings", pWithFragments.SelfServiceFlowSettingsUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/#/register", pWithFragments.SelfServiceFlowRegistrationUI(ctx).String()) + assert.Equal(t, "http://test.kratos.ory.sh/#/error", pWithFragments.SelfServiceFlowErrorURL(ctx).String()) + + pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{ + config.ViperKeySelfServiceLoginUI: "/login", + config.ViperKeySelfServiceSettingsURL: "/settings", + config.ViperKeySelfServiceRegistrationUI: "/register", + config.ViperKeySelfServiceErrorUI: "/error", + }), configx.SkipValidation()) + + assert.Equal(t, "/login", pWithRelativeFragments.SelfServiceFlowLoginUI(ctx).String()) + assert.Equal(t, "/settings", pWithRelativeFragments.SelfServiceFlowSettingsUI(ctx).String()) + assert.Equal(t, "/register", pWithRelativeFragments.SelfServiceFlowRegistrationUI(ctx).String()) + assert.Equal(t, "/error", pWithRelativeFragments.SelfServiceFlowErrorURL(ctx).String()) + + for _, v := range []string{ + "#/login", + "test.kratos.ory.sh/login", + } { + logger := logrusx.New("", "") + logger.Logger.ExitFunc = func(code int) { panic("") } + hook := new(test.Hook) + logger.Logger.Hooks.Add(hook) + + pWithIncorrectUrls := config.MustNew(t, logger, &contextx.Default{}, configx.WithValues(map[string]interface{}{ + config.ViperKeySelfServiceLoginUI: v, + }), configx.SkipValidation()) + + assert.Panics(t, func() { pWithIncorrectUrls.SelfServiceFlowLoginUI(ctx) }) + + assert.Equal(t, logrus.FatalLevel, hook.LastEntry().Level) + assert.Equal(t, "Configuration value from key selfservice.flows.login.ui_url is not a valid URL: "+v, hook.LastEntry().Message) + assert.Equal(t, 1, len(hook.Entries)) + } + }) + + t.Run("group=default_return_to", func(t *testing.T) { + assert.Equal(t, "https://self-service/login/password/return_to", p.SelfServiceFlowLoginReturnTo(ctx, "password").String()) + assert.Equal(t, "https://self-service/login/return_to", p.SelfServiceFlowLoginReturnTo(ctx, "oidc").String()) + + assert.Equal(t, "https://self-service/registration/return_to", p.SelfServiceFlowRegistrationReturnTo(ctx, "password").String()) + assert.Equal(t, "https://self-service/registration/oidc/return_to", p.SelfServiceFlowRegistrationReturnTo(ctx, "oidc").String()) + + assert.Equal(t, "https://self-service/settings/password/return_to", p.SelfServiceFlowSettingsReturnTo(ctx, "password", p.SelfServiceBrowserDefaultReturnTo(ctx)).String()) + assert.Equal(t, "https://self-service/settings/return_to", p.SelfServiceFlowSettingsReturnTo(ctx, "profile", p.SelfServiceBrowserDefaultReturnTo(ctx)).String()) + + assert.Equal(t, "http://test.kratos.ory.sh:4000/", p.SelfServiceFlowLogoutRedirectURL(ctx).String()) + p.MustSet(ctx, config.ViperKeySelfServiceLogoutBrowserDefaultReturnTo, "") + assert.Equal(t, "http://return-to-3-test.ory.sh/", p.SelfServiceFlowLogoutRedirectURL(ctx).String()) + }) + + t.Run("group=identity", func(t *testing.T) { + c := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"), configx.SkipValidation()) + + ds, err := c.DefaultIdentityTraitsSchemaURL(ctx) + require.NoError(t, err) + assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String()) + + ss, err := c.IdentityTraitsSchemas(ctx) + require.NoError(t, err) + assert.Equal(t, 2, len(ss)) + assert.Contains(t, ss, config.Schema{ + ID: "default", + URL: "http://test.kratos.ory.sh/default-identity.schema.json", + }) + assert.Contains(t, ss, config.Schema{ + ID: "other", + URL: "http://test.kratos.ory.sh/other-identity.schema.json", + }) + + ds, err = c.IdentityTraitsSchemaURL(ctx, "other") + require.NoError(t, err) + assert.Equal(t, "http://test.kratos.ory.sh/other-identity.schema.json", ds.String()) + + ds, err = c.IdentityTraitsSchemaURL(ctx, "default") + require.NoError(t, err) + assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String()) + + ds, err = c.IdentityTraitsSchemaURL(ctx, "") + require.NoError(t, err) + assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String()) + + ds, err = c.IdentityTraitsSchemaURL(ctx, "does-not-exist") + require.NoError(t, err) + assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String()) + }) + + t.Run("group=serve", func(t *testing.T) { + admin := p.ServeAdmin(ctx) + assert.Equal(t, "admin.kratos.ory.sh", admin.Host) + assert.Equal(t, 1234, admin.Port) + + public := p.ServePublic(ctx) + assert.Equal(t, "public.kratos.ory.sh", public.Host) + assert.Equal(t, 1235, public.Port) + }) + + t.Run("group=dsn", func(t *testing.T) { + assert.Equal(t, "sqlite://foo.db?mode=memory&_fk=true", p.DSN(ctx)) + }) + + t.Run("group=secrets", func(t *testing.T) { + assert.Equal(t, [][]byte{ + []byte("session-key-7f8a9b77-1"), + []byte("session-key-7f8a9b77-2"), + }, p.SecretsSession(ctx)) + var cipherExpected [32]byte + for k, v := range []byte("secret-thirty-two-character-long") { + cipherExpected[k] = v + } + assert.Equal(t, [][32]byte{ + cipherExpected, + }, p.SecretsCipher(ctx)) + }) + + t.Run("group=methods", func(t *testing.T) { + for _, tc := range []struct { + id string + config string + enabled bool + }{ + {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"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"}`}, + } { + strategy := p.SelfServiceStrategy(ctx, tc.id) + assert.Equal(t, tc.enabled, strategy.Enabled) + assert.JSONEq(t, tc.config, string(strategy.Config)) + } + }) + + t.Run("method=registration", func(t *testing.T) { + assert.Equal(t, true, p.SelfServiceFlowRegistrationEnabled(ctx)) + assert.Equal(t, time.Minute*98, p.SelfServiceFlowRegistrationRequestLifespan(ctx)) + + t.Run("hook=before", func(t *testing.T) { + expHooks := []config.SelfServiceHook{ + {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) + + 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)) + }) + + for _, tc := range []struct { + strategy string + hooks []config.SelfServiceHook + }{ + { + strategy: "password", + hooks: []config.SelfServiceHook{ + {Name: "session", Config: json.RawMessage(`{}`)}, + {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/"}`)}, + }, + }, + { + strategy: "oidc", + hooks: []config.SelfServiceHook{ + // {Name: "verify", Config: json.RawMessage(`{}`)}, + {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/"}`)}, + }, + }, + { + 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","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)}, + }, + }, + } { + t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { + hooks := p.SelfServiceFlowRegistrationAfterHooks(ctx, tc.strategy) + assert.Equal(t, tc.hooks, hooks) + }) + } + }) + + t.Run("method=totp", func(t *testing.T) { + assert.Equal(t, "issuer.ory.sh", p.TOTPIssuer(ctx)) + }) + + t.Run("method=login", func(t *testing.T) { + assert.Equal(t, time.Minute*99, p.SelfServiceFlowLoginRequestLifespan(ctx)) + + t.Run("hook=before", func(t *testing.T) { + expHooks := []config.SelfServiceHook{ + {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) + + 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)) + }) + + for _, tc := range []struct { + strategy string + hooks []config.SelfServiceHook + }{ + { + strategy: "password", + 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","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","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","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)}, + }, + }, + } { + t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { + hooks := p.SelfServiceFlowLoginAfterHooks(ctx, tc.strategy) + assert.Equal(t, tc.hooks, hooks) + }) + } + }) + + t.Run("method=settings", func(t *testing.T) { + assert.Equal(t, time.Minute*99, p.SelfServiceFlowSettingsFlowLifespan(ctx)) + assert.Equal(t, time.Minute*5, p.SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)) + + for _, tc := range []struct { + strategy string + hooks []config.SelfServiceHook + }{ + { + strategy: "password", + hooks: []config.SelfServiceHook{ + {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","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","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)}, + }, + }, + } { + t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { + hooks := p.SelfServiceFlowSettingsAfterHooks(ctx, tc.strategy) + assert.Equal(t, tc.hooks, hooks) + }) + } + }) + + t.Run("method=recovery", func(t *testing.T) { + assert.Equal(t, true, p.SelfServiceFlowRecoveryEnabled(ctx)) + assert.Equal(t, time.Minute*98, p.SelfServiceFlowRecoveryRequestLifespan(ctx)) + 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","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) { + assert.Equal(t, time.Minute*97, p.SelfServiceFlowVerificationRequestLifespan(ctx)) + 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","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) { + c := p.HasherArgon2(ctx) + assert.Equal(t, &config.Argon2{ + Memory: 1048576, Iterations: 2, Parallelism: 4, + SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration, + }, c) + }) + + t.Run("group=set_provider_by_json", func(t *testing.T) { + providerConfigJSON := `{"providers": [{"id":"github-test","provider":"github","client_id":"set_json_test","client_secret":"secret","mapper_url":"http://mapper-url","scope":["user:email"]}]}` + strategyConfigJSON := fmt.Sprintf(`{"enabled":true, "config": %s}`, providerConfigJSON) + + p.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc", strategyConfigJSON) + strategy := p.SelfServiceStrategy(ctx, "oidc") + assert.JSONEq(t, providerConfigJSON, string(strategy.Config)) + }) + }) +} + +func TestBcrypt(t *testing.T) { + t.Parallel() + ctx := context.Background() + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation()) + + require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4)) + require.NoError(t, p.Set(ctx, "dev", false)) + assert.EqualValues(t, uint32(12), p.HasherBcrypt(ctx).Cost) + + require.NoError(t, p.Set(ctx, "dev", true)) + assert.EqualValues(t, uint32(4), p.HasherBcrypt(ctx).Cost) + + require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, math.MaxInt64)) // too high + assert.EqualValues(t, math.MaxUint32, p.HasherBcrypt(ctx).Cost) +} + +func TestProviderBaseURLs(t *testing.T) { + t.Parallel() + ctx := context.Background() + machineHostname, err := os.Hostname() + if err != nil { + machineHostname = "127.0.0.1" + } + + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation()) + assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String()) + assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String()) + + p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ + "serve.public.port": 4444, + "serve.admin.port": 4445, + })) + assert.Equal(t, "https://"+machineHostname+":4444/", p.SelfPublicURL(ctx).String()) + assert.Equal(t, "https://"+machineHostname+":4445/", p.SelfAdminURL(ctx).String()) + + p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ + "serve.public.host": "public.ory.sh", + "serve.admin.host": "admin.ory.sh", + "serve.public.port": 4444, + "serve.admin.port": 4445, + })) + assert.Equal(t, "https://public.ory.sh:4444/", p.SelfPublicURL(ctx).String()) + assert.Equal(t, "https://admin.ory.sh:4445/", p.SelfAdminURL(ctx).String()) + + // Set to dev mode + p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ + "serve.public.host": "public.ory.sh", + "serve.admin.host": "admin.ory.sh", + "serve.public.port": 4444, + "serve.admin.port": 4445, + "dev": true, + })) + assert.Equal(t, "http://public.ory.sh:4444/", p.SelfPublicURL(ctx).String()) + assert.Equal(t, "http://admin.ory.sh:4445/", p.SelfAdminURL(ctx).String()) +} + +func TestDefaultWebhookHeaderAllowlist(t *testing.T) { + t.Parallel() + ctx := context.Background() + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation()) + snapshotx.SnapshotT(t, p.WebhookHeaderAllowlist(ctx)) +} + +func TestViperProvider_Secrets(t *testing.T) { + t.Parallel() + ctx := context.Background() + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation()) + + def := p.SecretsDefault(ctx) + assert.NotEmpty(t, def) + assert.Equal(t, def, p.SecretsSession(ctx)) + assert.Equal(t, def, p.SecretsDefault(ctx)) + assert.Empty(t, p.SecretsCipher(ctx)) + err := p.Set(ctx, config.ViperKeySecretsCipher, []string{"short-secret-key"}) + require.NoError(t, err) + assert.Equal(t, [][32]byte{}, p.SecretsCipher(ctx)) +} + +func TestViperProvider_Defaults(t *testing.T) { + t.Parallel() + ctx := context.Background() + l := logrusx.New("", "") + + for k, tc := range []struct { + init func() *config.Config + expect func(t *testing.T, p *config.Config) + }{ + { + init: func() *config.Config { + return config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation()) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &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)) + assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx)) + assert.True(t, p.SelfServiceFlowRegistrationEnabled(ctx)) + assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) + assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &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)) + assert.True(t, p.SelfServiceFlowVerificationEnabled(ctx)) + assert.True(t, p.SelfServiceFlowRegistrationEnabled(ctx)) + assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) + assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &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)) + assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx)) + assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) + assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) + }, + }, + { + init: func() *config.Config { + return config.MustNew(t, l, &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)) + assert.True(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)) + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + p := tc.init() + + if tc.expect != nil { + tc.expect(t, p) + return + } + assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) + assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx)) + assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) + assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) + assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) + assert.False(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) + assert.False(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)) + }) + } + + t.Run("suite=ui_url", func(t *testing.T) { + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String()) + assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String()) + assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String()) + assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/recovery", p.SelfServiceFlowRecoveryUI(ctx).String()) + assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/verification", p.SelfServiceFlowVerificationUI(ctx).String()) + }) +} + +func TestViperProvider_ReturnTo(t *testing.T) { + t.Parallel() + ctx := context.Background() + l := logrusx.New("", "") + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + + p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/") + assert.Equal(t, "https://www.ory.com/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String()) + assert.Equal(t, "https://www.ory.com/", p.SelfServiceFlowRecoveryReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String()) + + p.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "https://www.ory.com/recovery") + assert.Equal(t, "https://www.ory.com/recovery", p.SelfServiceFlowRecoveryReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String()) + + p.MustSet(ctx, config.ViperKeySelfServiceVerificationBrowserDefaultReturnTo, "https://www.ory.com/verification") + assert.Equal(t, "https://www.ory.com/verification", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String()) +} + +func TestSession(t *testing.T) { + t.Parallel() + ctx := context.Background() + l := logrusx.New("", "") + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + + assert.Equal(t, "ory_kratos_session", p.SessionName(ctx)) + p.MustSet(ctx, config.ViperKeySessionName, "ory_session") + assert.Equal(t, "ory_session", p.SessionName(ctx)) + + assert.Equal(t, time.Hour*24, p.SessionRefreshMinTimeLeft(ctx)) + p.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, "1m") + assert.Equal(t, time.Minute, p.SessionRefreshMinTimeLeft(ctx)) + + assert.Equal(t, time.Hour*24, p.SessionLifespan(ctx)) + p.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + assert.Equal(t, time.Minute, p.SessionLifespan(ctx)) + + assert.Equal(t, true, p.SessionPersistentCookie(ctx)) + p.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + assert.Equal(t, false, p.SessionPersistentCookie(ctx)) + + assert.Equal(t, false, p.SessionWhoAmICaching(ctx)) + p.MustSet(ctx, config.ViperKeySessionWhoAmICaching, true) + assert.Equal(t, true, p.SessionWhoAmICaching(ctx)) +} + +func TestCookies(t *testing.T) { + t.Parallel() + ctx := context.Background() + l := logrusx.New("", "") + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + + t.Run("path", func(t *testing.T) { + assert.Equal(t, "/", p.CookiePath(ctx)) + assert.Equal(t, "/", p.SessionPath(ctx)) + + p.MustSet(ctx, config.ViperKeyCookiePath, "/cookie") + assert.Equal(t, "/cookie", p.CookiePath(ctx)) + assert.Equal(t, "/cookie", p.SessionPath(ctx)) + + p.MustSet(ctx, config.ViperKeySessionPath, "/session") + assert.Equal(t, "/cookie", p.CookiePath(ctx)) + assert.Equal(t, "/session", p.SessionPath(ctx)) + }) + + t.Run("SameSite", func(t *testing.T) { + assert.Equal(t, http.SameSiteLaxMode, p.CookieSameSiteMode(ctx)) + assert.Equal(t, http.SameSiteLaxMode, p.SessionSameSiteMode(ctx)) + + p.MustSet(ctx, config.ViperKeyCookieSameSite, "Strict") + assert.Equal(t, http.SameSiteStrictMode, p.CookieSameSiteMode(ctx)) + assert.Equal(t, http.SameSiteStrictMode, p.SessionSameSiteMode(ctx)) + + p.MustSet(ctx, config.ViperKeySessionSameSite, "None") + assert.Equal(t, http.SameSiteStrictMode, p.CookieSameSiteMode(ctx)) + assert.Equal(t, http.SameSiteNoneMode, p.SessionSameSiteMode(ctx)) + }) + + t.Run("domain", func(t *testing.T) { + assert.Equal(t, "", p.CookieDomain(ctx)) + assert.Equal(t, "", p.SessionDomain(ctx)) + + p.MustSet(ctx, config.ViperKeyCookieDomain, "www.cookie.com") + assert.Equal(t, "www.cookie.com", p.CookieDomain(ctx)) + assert.Equal(t, "www.cookie.com", p.SessionDomain(ctx)) + + p.MustSet(ctx, config.ViperKeySessionDomain, "www.session.com") + assert.Equal(t, "www.cookie.com", p.CookieDomain(ctx)) + assert.Equal(t, "www.session.com", p.SessionDomain(ctx)) + }) +} + +func TestViperProvider_DSN(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=dsn: memory", func(t *testing.T) { + p := config.MustNew(t, logrusx.New("", ""), &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("", ""), &contextx.Default{}, configx.SkipValidation()) + + dsn := "sqlite://foo.db?_fk=true" + p.MustSet(ctx, config.ViperKeyDSN, dsn) + + assert.Equal(t, dsn, p.DSN(ctx)) + }) + + t.Run("case=dsn: not set", func(t *testing.T) { + dsn := "" + + var exitCode int + l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { + exitCode = i + })) + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + + assert.Equal(t, dsn, p.DSN(ctx)) + assert.NotEqual(t, 0, exitCode) + }) +} + +func TestViperProvider_ParseURIOrFail(t *testing.T) { + t.Parallel() + + ctx := context.Background() + var exitCode int + + l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { + exitCode = i + })) + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + require.Zero(t, exitCode) + + const testKey = "testKeyNotUsedInTheRealSchema" + + for _, tc := range []struct { + u string + expected url.URL + }{ + { + u: "file:///etc/config/kratos/identity.schema.json", + expected: url.URL{ + Scheme: "file", + Path: "/etc/config/kratos/identity.schema.json", + }, + }, + { + u: "file://./identity.schema.json", + expected: url.URL{ + Scheme: "file", + Host: ".", + Path: "/identity.schema.json", + }, + }, + { + u: "base64://bG9jYWwgc3ViamVjdCA9I", + expected: url.URL{ + Scheme: "base64", + Host: "bG9jYWwgc3ViamVjdCA9I", + }, + }, + { + u: "https://foo.bar/schema.json", + expected: url.URL{ + Scheme: "https", + Host: "foo.bar", + Path: "/schema.json", + }, + }, + } { + t.Run("case=parse "+tc.u, func(t *testing.T) { + require.NoError(t, p.Set(ctx, testKey, tc.u)) + + u := p.ParseURIOrFail(ctx, testKey) + require.Zero(t, exitCode) + assert.Equal(t, tc.expected, *u) + }) + } +} + +func TestViperProvider_HaveIBeenPwned(t *testing.T) { + t.Parallel() + + ctx := context.Background() + p := config.MustNew(t, logrusx.New("", ""), &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) + }) + + t.Run("case=hibp: enabled", func(t *testing.T) { + p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedEnabled, true) + assert.Equal(t, true, p.PasswordPolicyConfig(ctx).HaveIBeenPwnedEnabled) + }) + + t.Run("case=hibp: enabled", func(t *testing.T) { + p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedEnabled, false) + assert.Equal(t, false, p.PasswordPolicyConfig(ctx).HaveIBeenPwnedEnabled) + }) + + t.Run("case=hibp: max_breaches", func(t *testing.T) { + p.MustSet(ctx, config.ViperKeyPasswordMaxBreaches, 10) + assert.Equal(t, uint(10), p.PasswordPolicyConfig(ctx).MaxBreaches) + }) + + t.Run("case=hibp: ignore_network_errors", func(t *testing.T) { + p.MustSet(ctx, config.ViperKeyIgnoreNetworkErrors, true) + assert.Equal(t, true, p.PasswordPolicyConfig(ctx).IgnoreNetworkErrors) + }) + + t.Run("case=hibp: ignore_network_errors", func(t *testing.T) { + p.MustSet(ctx, config.ViperKeyIgnoreNetworkErrors, false) + assert.Equal(t, false, p.PasswordPolicyConfig(ctx).IgnoreNetworkErrors) + }) +} + +func newTestConfig(t *testing.T, opts ...configx.OptionModifier) (c *config.Config, l *logrusx.Logger, h *test.Hook, exited *bool) { + l = logrusx.New("", "") + h = new(test.Hook) + exited = new(bool) + l.Logger.Hooks.Add(h) + l.Logger.ExitFunc = func(code int) { *exited = true } + c = config.MustNew(t, l, &contextx.Default{}, append([]configx.OptionModifier{configx.SkipValidation()}, opts...)...) + return +} + +func TestLoadingTLSConfig(t *testing.T) { + t.Parallel() + + certPath, keyPath, certBase64, keyBase64 := testhelpers.GenerateTLSCertificateFilesForTests(t) + + t.Run("case=public: no TLS config", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t) + certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public") + require.NoError(t, err) + assert.Nil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "TLS has not been configured for public, skipping", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=admin: no TLS config", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t) + certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin") + require.NoError(t, err) + assert.Nil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "TLS has not been configured for admin, skipping", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=public: loading inline base64 certificate", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyPublicTLSKeyBase64: keyBase64, + keyPublicTLSCertBase64: certBase64, + })) + certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public") + require.NoError(t, err) + assert.NotNil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "Setting up HTTPS for public", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=public: loading certificate from a file", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyPublicTLSKeyPath: keyPath, + keyPublicTLSCertPath: certPath, + })) + certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public") + require.NoError(t, err) + assert.NotNil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "Setting up HTTPS for public (automatic certificate reloading active)", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=public: failing to load inline base64 certificate", func(t *testing.T) { + p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyPublicTLSKeyBase64: "invalid", + keyPublicTLSCertBase64: certBase64, + })) + certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public") + require.ErrorContains(t, err, "unable to load TLS certificate for interface public") + assert.Nil(t, certFunc) + }) + + t.Run("case=public: failing to load certificate from a file", func(t *testing.T) { + p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyPublicTLSKeyPath: "/dev/null", + keyPublicTLSCertPath: "/dev/null", + })) + certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public") + require.ErrorContains(t, err, "unable to load TLS certificate for interface public") + assert.Nil(t, certFunc) + }) + + t.Run("case=admin: loading inline base64 certificate", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyAdminTLSKeyBase64: keyBase64, + keyAdminTLSCertBase64: certBase64, + })) + certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin") + require.NoError(t, err) + assert.NotNil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "Setting up HTTPS for admin", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=admin: loading certificate from a file", func(t *testing.T) { + p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyAdminTLSKeyPath: keyPath, + keyAdminTLSCertPath: certPath, + })) + certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin") + require.NoError(t, err) + assert.NotNil(t, certFunc) + le := hook.LastEntry() + require.NotNil(t, le) + assert.Equal(t, "Setting up HTTPS for admin (automatic certificate reloading active)", le.Message) + assert.False(t, *exited) + }) + + t.Run("case=admin: failing to load inline base64 certificate", func(t *testing.T) { + p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyAdminTLSKeyBase64: "invalid", + keyAdminTLSCertBase64: certBase64, + })) + certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin") + assert.Nil(t, certFunc) + require.ErrorContains(t, err, "unable to load TLS certificate for interface admin") + }) + + t.Run("case=admin: failing to load certificate from a file", func(t *testing.T) { + p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{ + keyAdminTLSKeyPath: "/dev/null", + keyAdminTLSCertPath: certPath, + })) + certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin") + require.ErrorContains(t, err, "unable to load TLS certificate for interface admin") + assert.Nil(t, certFunc) + }) +} + +func TestIdentitySchemaValidation(t *testing.T) { + t.Parallel() + files := []string{"stub/.identity.test.json", "stub/.identity.other.json"} + + ctx := context.Background() + ctx = config.SetValidateIdentitySchemaResilientClientOptions(ctx, []httpx.ResilientOptions{ + httpx.ResilientClientWithMaxRetry(0), + httpx.ResilientClientWithConnectionTimeout(time.Millisecond * 100), + }) + + type identity struct { + Schemas []map[string]string `json:"schemas"` + } + + type configFile struct { + identityFileName string + SelfService map[string]string `json:"selfservice"` + Courier map[string]map[string]string `json:"courier"` + DSN string `json:"dsn"` + Identity *identity `json:"identity"` + } + + setup := func(t *testing.T, file string) *configFile { + identityTest, err := os.ReadFile(file) // #nosec G304 -- test code + assert.NoError(t, err) + return &configFile{ + identityFileName: file, + SelfService: map[string]string{ + "default_browser_return_url": "https://some-return-url", + }, + Courier: map[string]map[string]string{ + "smtp": { + "connection_uri": "smtp://foo@bar", + }, + }, + DSN: "memory", + Identity: &identity{ + Schemas: []map[string]string{{"id": "default", "url": "base64://" + base64.StdEncoding.EncodeToString(identityTest)}}, + }, + } + } + + marshalAndWrite := func(t *testing.T, tmpFile *os.File, identity *configFile) { + j, err := yaml.Marshal(identity) + assert.NoError(t, err) + + _, err = tmpFile.Seek(0, 0) + require.NoError(t, err) + require.NoError(t, tmpFile.Truncate(0)) + _, err = io.Writer.Write(tmpFile, j) + assert.NoError(t, err) + assert.NoError(t, tmpFile.Sync()) + } + + testWatch := func(t *testing.T, ctx context.Context, identity *configFile) (*config.Config, *test.Hook, func([]map[string]string)) { + tdir := t.TempDir() + assert.NoError(t, os.MkdirAll(tdir, 0o750)) + configFileName := randx.MustString(8, randx.Alpha) + tmpConfig, err := os.Create(filepath.Join(tdir, configFileName+".config.yaml")) // #nosec G304 -- test code + assert.NoError(t, err) + t.Cleanup(func() { _ = tmpConfig.Close() }) + + marshalAndWrite(t, tmpConfig, identity) + + l := logrusx.New("kratos-"+tmpConfig.Name(), "test") + hook := test.NewLocal(l.Logger) + + 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 + hook.Reset() + + return conf, hook, func(schemas []map[string]string) { + identity.Identity.Schemas = schemas + marshalAndWrite(t, tmpConfig, identity) + } + } + + t.Run("case=skip invalid schema validation", func(t *testing.T) { + _, 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) { + var stdErr bytes.Buffer + _, 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") + assert.Contains(t, stdErr.String(), "minimum 1 properties allowed, but found 0") + }) + + t.Run("case=must fail on loading unreachable schemas", func(t *testing.T) { + // 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(5 * time.Second), + }) + + err := make(chan error) + go func(err chan error) { + _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.mock.identities.yaml")) + err <- e + }(err) + + select { + 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.ErrorContains(t, e, "no such host") + } + }) + + t.Run("case=validate schema is validated on file change", func(t *testing.T) { + var identities []*configFile + + for _, f := range files { + identities = append(identities, setup(t, f)) + } + + invalidIdentity := setup(t, "stub/.identity.invalid.json") + + for _, identity := range identities { + t.Run("test=identity file "+identity.identityFileName, func(t *testing.T) { + ctx, cancel := context.WithTimeout(ctx, time.Second*30) + t.Cleanup(cancel) + + _, hook, writeSchema := testWatch(t, ctx, identity) + writeSchema(invalidIdentity.Identity.Schemas) + + // There are a bunch of log messages beeing logged. We are looking for a specific one. + for { + for _, v := range hook.AllEntries() { + s, err := v.String() + require.NoError(t, err) + 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") + default: // nothing + } + } + }) + } + }) +} + +func TestPasswordless(t *testing.T) { + t.Parallel() + ctx := context.Background() + + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.SkipValidation(), + configx.WithValue(config.ViperKeyWebAuthnPasswordless, true)) + require.NoError(t, err) + + assert.True(t, conf.WebAuthnForPasswordless(ctx)) + conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, false) + assert.False(t, conf.WebAuthnForPasswordless(ctx)) +} + +func TestPasswordlessCode(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.SkipValidation(), + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{ + "passwordless_enabled": true, + "passwordless_login_fallback_enabled": true, + "config": map[string]interface{}{}, + })) + require.NoError(t, err) + + assert.True(t, conf.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) +} + +func TestChangeMinPasswordLength(t *testing.T) { + t.Parallel() + 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, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.yaml"), + configx.WithValue(config.ViperKeyPasswordMinLength, 5)) + + assert.Error(t, err) + }) + + 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, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.yaml"), + configx.WithValue(config.ViperKeyPasswordMinLength, 9)) + + assert.NoError(t, err) + }) +} + +func TestCourierEmailHTTP(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, &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, &contextx.Default{}, configx.SkipValidation()) + + assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx)) + }) +} + +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, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) + + channelConfig, err := conf.CourierChannels(ctx) + require.NoError(t, err) + 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, &contextx.Default{}, configx.SkipValidation()) + + channelConfig, err := conf.CourierChannels(ctx) + require.NoError(t, err) + assert.Len(t, channelConfig, 1) + assert.Equal(t, channelConfig[0].ID, "email") + assert.Equal(t, channelConfig[0].Type, "smtp") + }) + + t.Run("smtp urls", func(t *testing.T) { + for _, tc := range []string{ + "smtp://a:basdasdasda%2Fc@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://a:b$c@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://a/a:bc@email-smtp.eu-west-3.amazonaws.com:587", + "smtp://aa:b+c@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://user?name:password@email-smtp.eu-west-3.amazonaws.com:587/", + "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, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) + require.NoError(t, err) + cs, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, cs, 1) + assert.Equal(t, tc, cs[0].SMTPConfig.ConnectionURI) + }) + } + }) +} + +func TestCourierMessageTTL(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, &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, &contextx.Default{}, configx.SkipValidation()) + assert.Equal(t, conf.CourierMessageRetries(ctx), 5) + }) +} + +func TestTwoStep(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=nothing is set", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) + + assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) + + t.Run("case=legacy config explicit off", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, false), + configx.SkipValidation(), + ) + + assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) + + t.Run("case=legacy config explicit on", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), + configx.SkipValidation(), + ) + + assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) + + t.Run("case=new config explicit on", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first"), + configx.SkipValidation(), + ) + + assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) + + t.Run("case=new config explicit off", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "unified"), + configx.SkipValidation(), + ) + + assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) + + t.Run("case=new config explicit on but legacy off", func(t *testing.T) { + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first"), + configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), + configx.SkipValidation(), + ) + + assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx)) + }) +} + +func TestOAuth2Provider(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, &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)) + assert.True(t, conf.OAuth2ProviderOverrideReturnTo(ctx)) + }) + + t.Run("case=defaults", func(t *testing.T) { + 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)) + }) +} + +func TestWebauthn(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=multiple origins", func(t *testing.T) { + 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) + assert.Equal(t, "https://example.com/webauthn", webAuthnConfig.RPID) + assert.EqualValues(t, []string{ + "https://origin-a.example.com", + "https://origin-b.example.com", + "https://origin-c.example.com", + }, webAuthnConfig.RPOrigins) + }) + + t.Run("case=one origin", func(t *testing.T) { + 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) + assert.Equal(t, "https://example.com/webauthn", webAuthnConfig.RPID) + assert.EqualValues(t, []string{ + "https://origin-a.example.com", + }, webAuthnConfig.RPOrigins) + }) + + t.Run("case=id as origin", func(t *testing.T) { + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.yaml")) + require.NoError(t, err) + webAuthnConfig := conf.WebAuthnConfig(ctx) + assert.Equal(t, "example.com", webAuthnConfig.RPID) + assert.EqualValues(t, []string{ + "http://example.com", + }, webAuthnConfig.RPOrigins) + }) + + t.Run("case=invalid", func(t *testing.T) { + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml")) + assert.Error(t, err) + }) +} + +func TestPasskeyConfig(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=defaults", func(t *testing.T) { + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.passkey.yaml")) + require.NoError(t, err) + + c := conf.PasskeyConfig(ctx) + // Attachment is unset by default so users can register either + // platform or cross-platform authenticators. + assert.Empty(t, c.AuthenticatorSelection.AuthenticatorAttachment) + require.NotNil(t, c.AuthenticatorSelection.RequireResidentKey) + assert.True(t, *c.AuthenticatorSelection.RequireResidentKey) + assert.Equal(t, protocol.ResidentKeyRequirementRequired, c.AuthenticatorSelection.ResidentKey) + assert.Equal(t, protocol.VerificationPreferred, c.AuthenticatorSelection.UserVerification) + assert.Equal(t, protocol.PreferNoAttestation, c.AttestationPreference) + // Timeouts should be zero (use library defaults) when not configured + assert.Equal(t, time.Duration(0), c.Timeouts.Registration.Timeout) + assert.Equal(t, time.Duration(0), c.Timeouts.Login.Timeout) + }) + + t.Run("case=reads overrides from config", func(t *testing.T) { + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.passkey.options.yaml")) + require.NoError(t, err) + + c := conf.PasskeyConfig(ctx) + assert.Equal(t, protocol.CrossPlatform, c.AuthenticatorSelection.AuthenticatorAttachment) + require.NotNil(t, c.AuthenticatorSelection.RequireResidentKey) + assert.False(t, *c.AuthenticatorSelection.RequireResidentKey) + assert.Equal(t, protocol.ResidentKeyRequirementDiscouraged, c.AuthenticatorSelection.ResidentKey) + assert.Equal(t, protocol.VerificationRequired, c.AuthenticatorSelection.UserVerification) + assert.Equal(t, protocol.PreferDirectAttestation, c.AttestationPreference) + assert.Equal(t, 30*time.Second, c.Timeouts.Registration.Timeout) + assert.Equal(t, 30*time.Second, c.Timeouts.Registration.TimeoutUVD) + assert.Equal(t, 45*time.Second, c.Timeouts.Login.Timeout) + assert.Equal(t, 45*time.Second, c.Timeouts.Login.TimeoutUVD) + }) +} + +func TestCourierTemplatesConfig(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=partial template update allowed", func(t *testing.T) { + _, 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, &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, &contextx.Default{}, + configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) + + assert.NoError(t, err) + + courierTemplateConfig := &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "", + HTML: "", + }, + Subject: "", + } + + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationInvalidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationValidEmail)) + // this should return an empty courierEmailTemplate as the key does not exist + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, "a_random_key")) + + courierTemplateConfig = &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu", + HTML: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu", + }, + Subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk", + } + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail)) + + courierTemplateConfig = &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K", + HTML: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=", + }, + Subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50", + } + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryValidEmail)) + }) +} + +func TestCleanup(t *testing.T) { + t.Parallel() + ctx := context.Background() + + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) + + t.Run("group=cleanup config", func(t *testing.T) { + assert.Equal(t, p.DatabaseCleanupSleepTables(ctx), 1*time.Minute) + p.MustSet(ctx, config.ViperKeyDatabaseCleanupSleepTables, "1s") + assert.Equal(t, p.DatabaseCleanupSleepTables(ctx), time.Second) + assert.Equal(t, p.DatabaseCleanupBatchSize(ctx), 100) + p.MustSet(ctx, config.ViperKeyDatabaseCleanupBatchSize, "1") + assert.Equal(t, p.DatabaseCleanupBatchSize(ctx), 1) + }) +} + +func TestOrganizationConfigSessionLifespan(t *testing.T) { + t.Parallel() + ctx := t.Context() + orgID := uuid.Must(uuid.NewV4()) + conf := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, + configx.WithValues(map[string]interface{}{ + "selfservice.methods.b2b.config.organizations": []map[string]interface{}{ + { + "id": orgID.String(), + "domains": []string{"example.com"}, + "session_lifespan": "2h", + }, + }, + }), + configx.SkipValidation(), + ) + + orgs := conf.Organizations(ctx) + require.Len(t, orgs, 1) + assert.Equal(t, orgID, orgs[0].ID) + assert.Equal(t, 2*time.Hour, orgs[0].SessionLifespan) +} + +func TestConfigOrganizationSessionLifespan(t *testing.T) { + t.Parallel() + ctx := t.Context() + orgWithOverride := uuid.Must(uuid.NewV4()) + orgWithoutOverride := uuid.Must(uuid.NewV4()) + + conf := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, + configx.WithValues(map[string]interface{}{ + "session.lifespan": "24h", + "selfservice.methods.b2b.config.organizations": []map[string]interface{}{ + {"id": orgWithOverride.String(), "domains": []string{"a.com"}, "session_lifespan": "1h"}, + {"id": orgWithoutOverride.String(), "domains": []string{"b.com"}}, + }, + }), + configx.SkipValidation(), + ) + + t.Run("case=nil orgID returns project default", func(t *testing.T) { + t.Parallel() + assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, uuid.Nil)) + }) + + t.Run("case=org with override returns override", func(t *testing.T) { + t.Parallel() + assert.Equal(t, 1*time.Hour, conf.OrganizationSessionLifespan(ctx, orgWithOverride)) + }) + + t.Run("case=org without override returns project default", func(t *testing.T) { + t.Parallel() + assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, orgWithoutOverride)) + }) + + t.Run("case=unknown orgID returns project default", func(t *testing.T) { + t.Parallel() + assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, uuid.Must(uuid.NewV4()))) + }) +} + +func TestSelfServiceBrowserAllowedReturnToDomains(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + entry string + allowed bool + }{ + {name: "case=exact domain is kept", entry: "https://foo.com", allowed: true}, + {name: "case=labelled wildcard is kept", entry: "https://*.foo.com", allowed: true}, + {name: "case=path wildcard is kept", entry: "https://app.foo.com/*", allowed: true}, + {name: "case=bare wildcard is rejected", entry: "https://*", allowed: false}, + {name: "case=eTLD wildcard is rejected", entry: "https://*.com", allowed: false}, + {name: "case=multi-label eTLD is rejected", entry: "https://*.co.uk", allowed: false}, + {name: "case=dot-less wildcard is rejected", entry: "https://*foo.com", allowed: false}, + {name: "case=mid-label wildcard is rejected", entry: "https://foo.*.com", allowed: false}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := t.Context() + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{ + config.ViperKeyURLsAllowedReturnToDomains: []string{tc.entry}, + }), configx.SkipValidation()) + + domains := p.SelfServiceBrowserAllowedReturnToDomains(ctx) + if tc.allowed { + require.Len(t, domains, 1) + assert.Equal(t, tc.entry, domains[0].String()) + } else { + assert.Empty(t, domains) + } + }) + } +} + +func TestSelfServiceBrowserAllowedReturnToDomainsLegacyInsecure(t *testing.T) { + t.Parallel() + ctx := t.Context() + + // With legacy_allow_insecure_origins=true, unbounded wildcards that would + // otherwise be rejected must be kept. + p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{ + config.ViperKeyURLsAllowedReturnToDomains: []string{"https://*.vercel.app"}, + config.ViperKeyLegacyAllowInsecureOrigins: true, + }), configx.SkipValidation()) + + domains := p.SelfServiceBrowserAllowedReturnToDomains(ctx) + require.Len(t, domains, 1, "unbounded wildcard must be kept when legacy_allow_insecure_origins is true") + assert.Equal(t, "https://*.vercel.app", domains[0].String()) +} + +const ( + keyPublicTLSCertBase64 = "serve.public.tls.cert.base64" + keyPublicTLSKeyBase64 = "serve.public.tls.key.base64" + keyPublicTLSCertPath = "serve.public.tls.cert.path" + keyPublicTLSKeyPath = "serve.public.tls.key.path" + keyAdminTLSCertBase64 = "serve.admin.tls.cert.base64" + keyAdminTLSKeyBase64 = "serve.admin.tls.key.base64" + keyAdminTLSCertPath = "serve.admin.tls.cert.path" + keyAdminTLSKeyPath = "serve.admin.tls.key.path" +) diff --git a/driver/config/handler.go b/driver/config/handler.go new file mode 100644 index 0000000..882e30a --- /dev/null +++ b/driver/config/handler.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "crypto/sha256" + "fmt" + "net/http" + + "github.com/knadh/koanf/parsers/json" + + "github.com/ory/x/httprouterx" +) + +func RegisterConfigHashRoute(c Provider, router *httprouterx.RouterAdmin) { + router.GET("/health/config", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + if revision := c.Config().GetProvider(r.Context()).String("revision"); len(revision) > 0 { + _, _ = fmt.Fprintf(w, "%s", revision) + } else { + bytes, _ := c.Config().GetProvider(r.Context()).Marshal(json.Parser()) + _, _ = fmt.Fprintf(w, "%x", sha256.Sum256(bytes)) + } + }) +} diff --git a/driver/config/handler_test.go b/driver/config/handler_test.go new file mode 100644 index 0000000..994cdbd --- /dev/null +++ b/driver/config/handler_test.go @@ -0,0 +1,70 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config_test + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/x/contextx" + "github.com/ory/x/httprouterx" +) + +type configProvider struct { + cfg *config.Config +} + +func (c *configProvider) Config() *config.Config { + return c.cfg +} + +func TestNewConfigHashHandler(t *testing.T) { + ctx := context.Background() + cfg := pkg.NewConfigurationWithDefaults(t) + router := httprouterx.NewRouterAdmin() + config.RegisterConfigHashRoute(&configProvider{cfg: cfg}, router) + ts := contextx.NewConfigurableTestServer(router) + t.Cleanup(ts.Close) + + // first request, get baseline hash + res, err := ts.Client(ctx).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, 200, res.StatusCode) + first, err := io.ReadAll(res.Body) + require.NoError(t, err) + + // second request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, 200, res.StatusCode) + second, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, first, second) + + // third request, with config change + res, err = ts.Client(contextx.WithConfigValue(ctx, config.ViperKeySessionDomain, "foobar")).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, 200, res.StatusCode) + third, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotEqual(t, first, third) + + // fourth request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, 200, res.StatusCode) + fourth, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, first, fourth) +} diff --git a/driver/config/stub/.defaults-password.yml b/driver/config/stub/.defaults-password.yml new file mode 100644 index 0000000..b8bb832 --- /dev/null +++ b/driver/config/stub/.defaults-password.yml @@ -0,0 +1,6 @@ +selfservice: + methods: + password: + enabled: true + flows: + recovery: false diff --git a/driver/config/stub/.defaults-verification.yml b/driver/config/stub/.defaults-verification.yml new file mode 100644 index 0000000..eb63cf3 --- /dev/null +++ b/driver/config/stub/.defaults-verification.yml @@ -0,0 +1,28 @@ +selfservice: + methods: + password: + enabled: true + + flows: + settings: + privileged_session_max_age: 1m + after: + hooks: + profile: + - hook: show_verification_ui + + verification: + enabled: true + lifespan: 5s + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + +identity: + default_schema_id: default + schemas: + - id: default + url: file://test/e2e/profiles/verification/identity.traits.schema.json diff --git a/driver/config/stub/.defaults.yml b/driver/config/stub/.defaults.yml new file mode 100644 index 0000000..b9e8772 --- /dev/null +++ b/driver/config/stub/.defaults.yml @@ -0,0 +1,2 @@ +selfservice: + diff --git a/driver/config/stub/.identity.invalid.json b/driver/config/stub/.identity.invalid.json new file mode 100644 index 0000000..e8d17fc --- /dev/null +++ b/driver/config/stub/.identity.invalid.json @@ -0,0 +1,17 @@ +{ + "$id": "ory://identity-invalid-schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IdentityInvalidSchema", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": {}, + "required": [ + "other", + "email" + ], + "additionalProperties": true + } + } +} diff --git a/driver/config/stub/.identity.other.json b/driver/config/stub/.identity.other.json new file mode 100644 index 0000000..54c6fda --- /dev/null +++ b/driver/config/stub/.identity.other.json @@ -0,0 +1,32 @@ +{ + "$id": "ory://identity-other-schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IdentityOtherSchema", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "other": { + "type": "string" + }, + "email": { + "type": "string", + "title": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "required": [ + "other", + "email" + ], + "additionalProperties": true + } + } +} diff --git a/driver/config/stub/.identity.test.json b/driver/config/stub/.identity.test.json new file mode 100644 index 0000000..5608fab --- /dev/null +++ b/driver/config/stub/.identity.test.json @@ -0,0 +1,28 @@ +{ + "$id": "ory://identity-test-schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IdentitySchema", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + } + } + }, + "required": [ + "name" + ], + "additionalProperties": true + } + } +} diff --git a/driver/config/stub/.kratos.courier.channels.yaml b/driver/config/stub/.kratos.courier.channels.yaml new file mode 100644 index 0000000..16b116b --- /dev/null +++ b/driver/config/stub/.kratos.courier.channels.yaml @@ -0,0 +1,16 @@ +courier: + smtp: + connection_uri: smtp://username:password@smtp.example.com:587 + channels: + - id: phone + request_config: + url: https://ory.sh + method: GET + body: base64://ZnVuY3Rpb24oY3R4KSB7CkJvZHk6IGN0eC5ib2R5LApUbzogY3R4LnRvLEZyb206IGN0eC5mcm9tCn0= + headers: + Content-Type: application/x-www-form-urlencoded + auth: + type: basic_auth + config: + user: ABC + password: DEF diff --git a/driver/config/stub/.kratos.courier.email.http.yaml b/driver/config/stub/.kratos.courier.email.http.yaml new file mode 100644 index 0000000..e4b9104 --- /dev/null +++ b/driver/config/stub/.kratos.courier.email.http.yaml @@ -0,0 +1,24 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: https://example.com/return_to + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + delivery_strategy: http + http: + request_config: + url: https://example.com/email + body: file://some.jsonnet + header: + 'Content-Type': 'application/json' + auth: + type: basic_auth + config: + user: YourUsername + password: YourPass diff --git a/driver/config/stub/.kratos.courier.message_retries.yaml b/driver/config/stub/.kratos.courier.message_retries.yaml new file mode 100644 index 0000000..570238d --- /dev/null +++ b/driver/config/stub/.kratos.courier.message_retries.yaml @@ -0,0 +1,2 @@ +courier: + message_retries: 10 diff --git a/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml b/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml new file mode 100644 index 0000000..4eb0a5a --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml @@ -0,0 +1,30 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + # omit subject to verify it is validated + valid: + # omit template_root as it is not required on valid + email: + body: + plaintext: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + html: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4= + subject: base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50 + # omit verification here to test templates override fallback + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.remote.partial.templates.yaml b/driver/config/stub/.kratos.courier.remote.partial.templates.yaml new file mode 100644 index 0000000..bafadb2 --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.partial.templates.yaml @@ -0,0 +1,26 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + plaintext: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + verification: + valid: + email: + subject: base64://VmVyaWZpY2F0aW9uIEVtYWls + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.remote.templates.yaml b/driver/config/stub/.kratos.courier.remote.templates.yaml new file mode 100644 index 0000000..23d35b6 --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.templates.yaml @@ -0,0 +1,30 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + plaintext: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + subject: base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk + valid: + email: + body: + plaintext: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + html: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4= + subject: base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50 + # omit verification here to test templates override fallback + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.sms.yaml b/driver/config/stub/.kratos.courier.sms.yaml new file mode 100644 index 0000000..1c2fbae --- /dev/null +++ b/driver/config/stub/.kratos.courier.sms.yaml @@ -0,0 +1,28 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + sms: + enabled: true + from: '+49123456789' + request_config: + url: https://api.twilio.com/2010-04-01/Accounts/YourAccountID/Messages.json + method: POST + body: base64://e30= + header: + 'Content-Type': 'application/x-www-form-urlencoded' + auth: + type: basic_auth + config: + user: YourUsername + password: YourPass diff --git a/driver/config/stub/.kratos.invalid.identities.yaml b/driver/config/stub/.kratos.invalid.identities.yaml new file mode 100644 index 0000000..92f2583 --- /dev/null +++ b/driver/config/stub/.kratos.invalid.identities.yaml @@ -0,0 +1,14 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: other + schemas: + - id: other + url: file://stub/.identity.invalid.json + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ diff --git a/driver/config/stub/.kratos.mock.identities.yaml b/driver/config/stub/.kratos.mock.identities.yaml new file mode 100644 index 0000000..f76f578 --- /dev/null +++ b/driver/config/stub/.kratos.mock.identities.yaml @@ -0,0 +1,16 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: http://test.kratos.ory.sh/default-identity.schema.json + - id: other + url: http://test.kratos.ory.sh/other-identity.schema.json + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ diff --git a/driver/config/stub/.kratos.notify-unknown-recipients.yml b/driver/config/stub/.kratos.notify-unknown-recipients.yml new file mode 100644 index 0000000..b0f84f4 --- /dev/null +++ b/driver/config/stub/.kratos.notify-unknown-recipients.yml @@ -0,0 +1,6 @@ +selfservice: + flows: + recovery: + notify_unknown_recipients: true + verification: + notify_unknown_recipients: true diff --git a/driver/config/stub/.kratos.oauth2_provider.yaml b/driver/config/stub/.kratos.oauth2_provider.yaml new file mode 100644 index 0000000..9c009e2 --- /dev/null +++ b/driver/config/stub/.kratos.oauth2_provider.yaml @@ -0,0 +1,5 @@ +oauth2_provider: + url: https://oauth2_provider/ + headers: + Authorization: Basic + override_return_to: true diff --git a/driver/config/stub/.kratos.passkey.options.yaml b/driver/config/stub/.kratos.passkey.options.yaml new file mode 100644 index 0000000..fe74cd5 --- /dev/null +++ b/driver/config/stub/.kratos.passkey.options.yaml @@ -0,0 +1,54 @@ +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +secrets: + cookie: + - session-key-7f8a9b77-1 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + methods: + passkey: + enabled: true + config: + rp: + id: example.com + display_name: Example + origins: + - https://example.com + authenticator_selection: + attachment: "cross-platform" + resident_key: "discouraged" + user_verification: "required" + attestation: + preference: "direct" + timeouts: + registration: "30s" + login: "45s" diff --git a/driver/config/stub/.kratos.passkey.yaml b/driver/config/stub/.kratos.passkey.yaml new file mode 100644 index 0000000..3bafc68 --- /dev/null +++ b/driver/config/stub/.kratos.passkey.yaml @@ -0,0 +1,45 @@ +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +secrets: + cookie: + - session-key-7f8a9b77-1 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + methods: + passkey: + enabled: true + config: + rp: + id: example.com + display_name: Example + origins: + - https://example.com diff --git a/driver/config/stub/.kratos.webauthn.invalid.yaml b/driver/config/stub/.kratos.webauthn.invalid.yaml new file mode 100644 index 0000000..a123058 --- /dev/null +++ b/driver/config/stub/.kratos.webauthn.invalid.yaml @@ -0,0 +1,257 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + - id: other + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktb3RoZXItc2NoZW1hIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0aXRsZSI6ICJJZGVudGl0eU90aGVyU2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm90aGVyIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgIH0sCiAgICAgICAgImVtYWlsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJlbWFpbCIsCiAgICAgICAgICAib3J5LnNoL2tyYXRvcyI6IHsKICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogewogICAgICAgICAgICAgICJwYXNzd29yZCI6IHsKICAgICAgICAgICAgICAgICJpZGVudGlmaWVyIjogdHJ1ZQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJvdGhlciIsCiAgICAgICAgImVtYWlsIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +hashers: + argon2: + memory: 1MB + iterations: 2 + parallelism: 4 + salt_length: 16 + key_length: 32 + dedicated_memory: 1GB + expected_duration: 500ms + expected_deviation: 500ms + +secrets: + cookie: + - session-key-7f8a9b77-1 + - session-key-7f8a9b77-2 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + allowed_return_urls: + - http://return-to-1-test.ory.sh/ + - http://return-to-2-test.ory.sh/ + - http://*.wildcards.ory.sh + - http://*.sh + - http://*.com + - http://*.com.pl + - http://* + - /return-to-relative-test/ + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + password: + enabled: true + oidc: + enabled: true + config: + providers: + - id: github + provider: github + client_id: a + client_secret: b + mapper_url: http://test.kratos.ory.sh/default-identity.schema.json + webauthn: + enabled: true + config: + rp: + id: https://example.com/webauthn + display_name: Webauthn + origin: https://origin-a.example.com + origins: + - https://origin-a.example.com + - https://origin-b.example.com + - https://origin-c.example.com + flows: + error: + ui_url: http://test.kratos.ory.sh/error + + logout: + after: + default_browser_return_url: http://test.kratos.ory.sh:4000/ + + recovery: + enabled: true + ui_url: http://test.kratos.ory.sh/recovery + lifespan: 98m + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_recovery_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + verification: + enabled: true + lifespan: 97m + ui_url: http://test.kratos.ory.sh/verification + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_verification_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + settings: + ui_url: http://test.kratos.ory.sh/settings + lifespan: 99m + privileged_session_max_age: 5m + after: + default_browser_return_url: https://self-service/settings/return_to + password: + default_browser_return_url: https://self-service/settings/password/return_to + hooks: + - hook: web_hook + 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: + - hook: web_hook + 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: + ui_url: http://test.kratos.ory.sh/login + lifespan: 99m + before: + hooks: + - hook: web_hook + 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: + default_browser_return_url: https://self-service/login/password/return_to + hooks: + - hook: revoke_active_sessions + - hook: require_verified_address + - hook: web_hook + 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 + config: + user: test-user + password: super-secret + oidc: + hooks: + - hook: web_hook + 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: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_login_global_hook + method: POST + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + registration: + enabled: true + ui_url: http://test.kratos.ory.sh/register + lifespan: 98m + before: + hooks: + - hook: web_hook + 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: + hooks: + - hook: session + - hook: web_hook + 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 + config: + name: My-Key + value: My-Key-Value + in: header + oidc: + default_browser_return_url: https://self-service/registration/oidc/return_to + hooks: + - hook: web_hook + 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 new file mode 100644 index 0000000..95deec0 --- /dev/null +++ b/driver/config/stub/.kratos.webauthn.origin.yaml @@ -0,0 +1,253 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + - id: other + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktb3RoZXItc2NoZW1hIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0aXRsZSI6ICJJZGVudGl0eU90aGVyU2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm90aGVyIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgIH0sCiAgICAgICAgImVtYWlsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJlbWFpbCIsCiAgICAgICAgICAib3J5LnNoL2tyYXRvcyI6IHsKICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogewogICAgICAgICAgICAgICJwYXNzd29yZCI6IHsKICAgICAgICAgICAgICAgICJpZGVudGlmaWVyIjogdHJ1ZQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJvdGhlciIsCiAgICAgICAgImVtYWlsIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +hashers: + argon2: + memory: 1MB + iterations: 2 + parallelism: 4 + salt_length: 16 + key_length: 32 + dedicated_memory: 1GB + expected_duration: 500ms + expected_deviation: 500ms + +secrets: + cookie: + - session-key-7f8a9b77-1 + - session-key-7f8a9b77-2 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + allowed_return_urls: + - http://return-to-1-test.ory.sh/ + - http://return-to-2-test.ory.sh/ + - http://*.wildcards.ory.sh + - http://*.sh + - http://*.com + - http://*.com.pl + - http://* + - /return-to-relative-test/ + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + password: + enabled: true + oidc: + enabled: true + config: + providers: + - id: github + provider: github + client_id: a + client_secret: b + mapper_url: http://test.kratos.ory.sh/default-identity.schema.json + webauthn: + enabled: true + config: + rp: + id: https://example.com/webauthn + display_name: Webauthn + origin: https://origin-a.example.com + flows: + error: + ui_url: http://test.kratos.ory.sh/error + + logout: + after: + default_browser_return_url: http://test.kratos.ory.sh:4000/ + + recovery: + enabled: true + ui_url: http://test.kratos.ory.sh/recovery + lifespan: 98m + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_recovery_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + verification: + enabled: true + lifespan: 97m + ui_url: http://test.kratos.ory.sh/verification + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_verification_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + settings: + ui_url: http://test.kratos.ory.sh/settings + lifespan: 99m + privileged_session_max_age: 5m + after: + default_browser_return_url: https://self-service/settings/return_to + password: + default_browser_return_url: https://self-service/settings/password/return_to + hooks: + - hook: web_hook + 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: + - hook: web_hook + 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: + ui_url: http://test.kratos.ory.sh/login + lifespan: 99m + before: + hooks: + - hook: web_hook + 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: + default_browser_return_url: https://self-service/login/password/return_to + hooks: + - hook: revoke_active_sessions + - hook: require_verified_address + - hook: web_hook + 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 + config: + user: test-user + password: super-secret + oidc: + hooks: + - hook: web_hook + 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: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_login_global_hook + method: POST + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + registration: + enabled: true + ui_url: http://test.kratos.ory.sh/register + lifespan: 98m + before: + hooks: + - hook: web_hook + 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: + hooks: + - hook: session + - hook: web_hook + 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 + config: + name: My-Key + value: My-Key-Value + in: header + oidc: + default_browser_return_url: https://self-service/registration/oidc/return_to + hooks: + - hook: web_hook + 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 new file mode 100644 index 0000000..9efeb34 --- /dev/null +++ b/driver/config/stub/.kratos.webauthn.origins.yaml @@ -0,0 +1,256 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + - id: other + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktb3RoZXItc2NoZW1hIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0aXRsZSI6ICJJZGVudGl0eU90aGVyU2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm90aGVyIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgIH0sCiAgICAgICAgImVtYWlsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJlbWFpbCIsCiAgICAgICAgICAib3J5LnNoL2tyYXRvcyI6IHsKICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogewogICAgICAgICAgICAgICJwYXNzd29yZCI6IHsKICAgICAgICAgICAgICAgICJpZGVudGlmaWVyIjogdHJ1ZQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJvdGhlciIsCiAgICAgICAgImVtYWlsIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +hashers: + argon2: + memory: 1MB + iterations: 2 + parallelism: 4 + salt_length: 16 + key_length: 32 + dedicated_memory: 1GB + expected_duration: 500ms + expected_deviation: 500ms + +secrets: + cookie: + - session-key-7f8a9b77-1 + - session-key-7f8a9b77-2 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + allowed_return_urls: + - http://return-to-1-test.ory.sh/ + - http://return-to-2-test.ory.sh/ + - http://*.wildcards.ory.sh + - http://*.sh + - http://*.com + - http://*.com.pl + - http://* + - /return-to-relative-test/ + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + password: + enabled: true + oidc: + enabled: true + config: + providers: + - id: github + provider: github + client_id: a + client_secret: b + mapper_url: http://test.kratos.ory.sh/default-identity.schema.json + webauthn: + enabled: true + config: + rp: + id: https://example.com/webauthn + display_name: Webauthn + origins: + - https://origin-a.example.com + - https://origin-b.example.com + - https://origin-c.example.com + flows: + error: + ui_url: http://test.kratos.ory.sh/error + + logout: + after: + default_browser_return_url: http://test.kratos.ory.sh:4000/ + + recovery: + enabled: true + ui_url: http://test.kratos.ory.sh/recovery + lifespan: 98m + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_recovery_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + verification: + enabled: true + lifespan: 97m + ui_url: http://test.kratos.ory.sh/verification + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_verification_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + settings: + ui_url: http://test.kratos.ory.sh/settings + lifespan: 99m + privileged_session_max_age: 5m + after: + default_browser_return_url: https://self-service/settings/return_to + password: + default_browser_return_url: https://self-service/settings/password/return_to + hooks: + - hook: web_hook + 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: + - hook: web_hook + 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: + ui_url: http://test.kratos.ory.sh/login + lifespan: 99m + before: + hooks: + - hook: web_hook + 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: + default_browser_return_url: https://self-service/login/password/return_to + hooks: + - hook: revoke_active_sessions + - hook: require_verified_address + - hook: web_hook + 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 + config: + user: test-user + password: super-secret + oidc: + hooks: + - hook: web_hook + 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: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_login_global_hook + method: POST + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + registration: + enabled: true + ui_url: http://test.kratos.ory.sh/register + lifespan: 98m + before: + hooks: + - hook: web_hook + 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: + hooks: + - hook: session + - hook: web_hook + 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 + config: + name: My-Key + value: My-Key-Value + in: header + oidc: + default_browser_return_url: https://self-service/registration/oidc/return_to + hooks: + - hook: web_hook + 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 new file mode 100644 index 0000000..8fb1a2b --- /dev/null +++ b/driver/config/stub/.kratos.yaml @@ -0,0 +1,254 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + base_url: http://admin.kratos.ory.sh + port: 1234 + host: admin.kratos.ory.sh + public: + base_url: http://public.kratos.ory.sh + port: 1235 + host: public.kratos.ory.sh + +dsn: sqlite://foo.db?mode=memory&_fk=true + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://foo:bar@baz/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + - id: other + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktb3RoZXItc2NoZW1hIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0aXRsZSI6ICJJZGVudGl0eU90aGVyU2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm90aGVyIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgIH0sCiAgICAgICAgImVtYWlsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJlbWFpbCIsCiAgICAgICAgICAib3J5LnNoL2tyYXRvcyI6IHsKICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogewogICAgICAgICAgICAgICJwYXNzd29yZCI6IHsKICAgICAgICAgICAgICAgICJpZGVudGlmaWVyIjogdHJ1ZQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJvdGhlciIsCiAgICAgICAgImVtYWlsIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +hashers: + argon2: + memory: 1MB + iterations: 2 + parallelism: 4 + salt_length: 16 + key_length: 32 + dedicated_memory: 1GB + expected_duration: 500ms + expected_deviation: 500ms + bcrypt: + cost: 4 + +secrets: + cookie: + - session-key-7f8a9b77-1 + - session-key-7f8a9b77-2 + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + allowed_return_urls: + - http://return-to-1-test.ory.sh/ + - http://return-to-2-test.ory.sh/ + - http://*.wildcards.ory.sh + - http://*.sh + - http://*.com + - http://*.com.pl + - http://* + - /return-to-relative-test/ + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + password: + enabled: true + oidc: + enabled: true + config: + providers: + - id: github + provider: github + client_id: a + client_secret: b + mapper_url: http://test.kratos.ory.sh/default-identity.schema.json + webauthn: + enabled: true + config: + rp: + id: example.com + display_name: Webauthn + flows: + error: + ui_url: http://test.kratos.ory.sh/error + + logout: + after: + default_browser_return_url: http://test.kratos.ory.sh:4000/ + + recovery: + enabled: true + ui_url: http://test.kratos.ory.sh/recovery + lifespan: 98m + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_recovery_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + verification: + enabled: true + lifespan: 97m + ui_url: http://test.kratos.ory.sh/verification + after: + default_browser_return_url: http://test.kratos.ory.sh/dashboard + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_verification_hook + method: GET + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + settings: + ui_url: http://test.kratos.ory.sh/settings + lifespan: 99m + privileged_session_max_age: 5m + after: + default_browser_return_url: https://self-service/settings/return_to + password: + default_browser_return_url: https://self-service/settings/password/return_to + hooks: + - hook: web_hook + 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: + - hook: web_hook + 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: + ui_url: http://test.kratos.ory.sh/login + lifespan: 99m + before: + hooks: + - hook: web_hook + 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: + default_browser_return_url: https://self-service/login/password/return_to + hooks: + - hook: revoke_active_sessions + - hook: require_verified_address + - hook: web_hook + 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 + config: + user: test-user + password: super-secret + oidc: + hooks: + - hook: web_hook + 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: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/after_login_global_hook + method: POST + headers: + X-Custom-Header: test + body: /path/to/template.jsonnet + + registration: + enabled: true + ui_url: http://test.kratos.ory.sh/register + lifespan: 98m + before: + hooks: + - hook: web_hook + 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: + hooks: + - hook: session + - hook: web_hook + 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 + config: + name: My-Key + value: My-Key-Value + in: header + oidc: + default_browser_return_url: https://self-service/registration/oidc/return_to + hooks: + - hook: web_hook + 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/factory.go b/driver/factory.go new file mode 100644 index 0000000..4ecca84 --- /dev/null +++ b/driver/factory.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "io" + + "github.com/ory/x/servicelocatorx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/logrusx" +) + +func New(ctx context.Context, stdOutOrErr io.Writer, dOpts ...RegistryOption) (*RegistryDefault, error) { + r, err := NewWithoutInit(ctx, stdOutOrErr, dOpts...) + if err != nil { + return nil, err + } + + ctxter := r.Contextualizer() + if err := r.Init(ctx, ctxter, dOpts...); err != nil { + r.Logger().WithError(err).Error("Unable to initialize service registry.") + return nil, err + } + + return r, nil +} + +func NewWithoutInit(ctx context.Context, stdOutOrErr io.Writer, dOpts ...RegistryOption) (*RegistryDefault, error) { + opts := newOptions(dOpts) + sl := servicelocatorx.NewOptions(opts.serviceLocatorOptions...) + + l := sl.Logger() + if l == nil { + l = logrusx.New("Ory Kratos", config.Version) + } + + c := opts.config + if c == nil { + var err error + c, err = config.New(ctx, l, stdOutOrErr, sl.Contextualizer(), opts.configOptions...) + if err != nil { + l.WithError(err).Error("Unable to instantiate configuration.") + return nil, err + } + } + + r, err := NewRegistryFromDSN(ctx, c, l) + if err != nil { + l.WithError(err).Error("Unable to instantiate service registry.") + return nil, err + } + r.slOptions = sl + r.SetContextualizer(sl.Contextualizer()) + + return r, nil +} diff --git a/driver/factory_test.go b/driver/factory_test.go new file mode 100644 index 0000000..5ff6d17 --- /dev/null +++ b/driver/factory_test.go @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/x/configx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" +) + +func TestDriverNew(t *testing.T) { + ctx := context.Background() + r, err := driver.New( + context.Background(), + os.Stderr, + driver.WithConfigOptions( + configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), + configx.SkipValidation(), + )) + require.NoError(t, err) + + assert.EqualValues(t, config.DefaultSQLiteMemoryDSN, r.Config().DSN(ctx)) + pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + t.Cleanup(cancel) + require.NoError(t, r.Persister().Ping(pingCtx)) + + assert.NotEqual(t, uuid.Nil.String(), r.Persister().NetworkID(context.Background()).String()) + + n, err := r.Persister().DetermineNetwork(context.Background()) + require.NoError(t, err) + assert.Equal(t, r.Persister().NetworkID(context.Background()), n.ID) +} diff --git a/driver/registry.go b/driver/registry.go new file mode 100644 index 0000000..192e2be --- /dev/null +++ b/driver/registry.go @@ -0,0 +1,313 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "io/fs" + + "github.com/gorilla/sessions" + + "github.com/ory/x/httpx" + + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/logout" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/sessiontokenexchange" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/selfservice/strategy/link" + password2 "github.com/ory/kratos/selfservice/strategy/password" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/nosurf" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/healthx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" + "github.com/ory/x/servicelocatorx" +) + +type Registry interface { + Init(ctx context.Context, ctxer contextx.Contextualizer, opts ...RegistryOption) error + + SetLogger(l *logrusx.Logger) + SetJSONNetVMProvider(jsonnetsecure.VMProvider) + + // Clock returns the time source used for time-dependent behavior such as + // flow expiry. Tests can override it via RegistryDefault.SetClock. + Clock() clock.Clock + + WithCSRFHandler(c nosurf.Handler) + WithCSRFTokenGenerator(cg nosurfx.CSRFToken) + + HealthHandler(ctx context.Context) *healthx.Handler + CookieManager(ctx context.Context) sessions.StoreExact + ContinuityCookieManager(ctx context.Context) sessions.StoreExact + + RegisterPublicRoutes(ctx context.Context, public *httprouterx.RouterPublic) + RegisterAdminRoutes(ctx context.Context, admin *httprouterx.RouterAdmin) + Tracer(context.Context) *otelx.Tracer + SetTracer(*otelx.Tracer) + + config.Provider + CourierConfig() config.CourierConfigs + SetConfig(c *config.Config) + SetContextualizer(ctxer contextx.Contextualizer) + + nosurfx.CSRFProvider + httpx.WriterProvider + logrusx.Provider + httpx.ClientProvider + jsonnetsecure.VMProvider + + continuity.ManagementProvider + continuity.PersistenceProvider + + cipher.Provider + + courier.Provider + + persistence.Provider + + errorx.ManagementProvider + errorx.HandlerProvider + errorx.PersistenceProvider + + hash.HashProvider + + identity.HandlerProvider + identity.ValidationProvider + identity.PoolProvider + identity.PrivilegedPoolProvider + identity.ManagementProvider + identity.ActiveCredentialsCounterStrategyProvider + + courier.HandlerProvider + courier.PersistenceProvider + + schema.HandlerProvider + schema.IdentitySchemaProvider + + password2.ValidationProvider + + session.HandlerProvider + session.ManagementProvider + session.PersistenceProvider + session.TokenizerProvider + + settings.HandlerProvider + settings.ErrorHandlerProvider + settings.FlowPersistenceProvider + settings.StrategyProvider + + login.FlowPersistenceProvider + login.ErrorHandlerProvider + login.HooksProvider + login.HookExecutorProvider + login.HandlerProvider + login.StrategyProvider + + logout.HandlerProvider + + registration.FlowPersistenceProvider + registration.ErrorHandlerProvider + registration.HooksProvider + registration.HookExecutorProvider + registration.HandlerProvider + registration.StrategyProvider + + verification.FlowPersistenceProvider + verification.ErrorHandlerProvider + verification.HandlerProvider + verification.StrategyProvider + + sessiontokenexchange.PersistenceProvider + + link.SenderProvider + link.VerificationTokenPersistenceProvider + link.RecoveryTokenPersistenceProvider + + code.SenderProvider + code.RecoveryCodePersistenceProvider + + recovery.FlowPersistenceProvider + recovery.ErrorHandlerProvider + recovery.HandlerProvider + recovery.StrategyProvider + + nosurfx.CSRFTokenGeneratorProvider +} + +func NewRegistryFromDSN(ctx context.Context, c *config.Config, l *logrusx.Logger) (*RegistryDefault, error) { + reg := NewRegistryDefault() + + tracer, err := otelx.New("Ory Kratos", l, c.Tracing(ctx)) + if err != nil { + l.WithError(err).Fatalf("failed to initialize tracer") + tracer = otelx.NewNoop() + } + reg.SetTracer(tracer) + reg.SetLogger(l) + reg.SetConfig(c) + + return reg, nil +} + +type options struct { + skipNetworkInit bool + config *config.Config + configOptions []configx.OptionModifier + replaceTracer func(*otelx.Tracer) *otelx.Tracer + replaceIdentitySchemaProvider func(Registry) schema.IdentitySchemaProvider + inspect func(Registry) error + extraMigrations []fs.FS + extraGoMigrations popx.Migrations + replacementStrategies []NewStrategy + extraHooks map[string]NewHookFn + extraHashers map[string]NewHasherFn + extraHandlers []NewHandler + disableMigrationLogging bool + jsonnetPool jsonnetsecure.Pool + serviceLocatorOptions []servicelocatorx.Option + dbOpts []func(details *pop.ConnectionDetails) +} + +type RegistryOption func(*options) + +// WithDBOptions adds database connection options that will be applied to the +// underlying connection. +func WithDBOptions(opts ...func(details *pop.ConnectionDetails)) RegistryOption { + return func(o *options) { + o.dbOpts = append(o.dbOpts, opts...) + } +} + +func SkipNetworkInit(o *options) { + o.skipNetworkInit = true +} + +func WithJsonnetPool(pool jsonnetsecure.Pool) RegistryOption { + return func(o *options) { + o.jsonnetPool = pool + } +} + +func WithConfig(config *config.Config) RegistryOption { + return func(o *options) { + o.config = config + } +} + +func WithConfigOptions(opts ...configx.OptionModifier) RegistryOption { + return func(o *options) { + o.configOptions = append(o.configOptions, opts...) + } +} + +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 + } +} + +type NewStrategy func(deps any) any + +// WithReplaceStrategies adds a strategy to the registry. This is useful if you want to +// add a custom strategy to the registry. Default strategies with the same +// name/ID will be overwritten. +func WithReplaceStrategies(s ...NewStrategy) RegistryOption { + return func(o *options) { + o.replacementStrategies = append(o.replacementStrategies, s...) + } +} + +type NewHookFn func(config.SelfServiceHook, Registry) any + +func WithExtraHooks(hooks map[string]NewHookFn) RegistryOption { + return func(o *options) { + o.extraHooks = hooks + } +} + +type NewHasherFn func(reg Registry) hash.Hasher + +// WithExtraHashers registers additional password hashers, keyed by the +// hashers.algorithm value that selects them. An extra hasher takes precedence +// over a built-in hasher with the same name. +func WithExtraHashers(hashers map[string]NewHasherFn) RegistryOption { + return func(o *options) { + o.extraHashers = hashers + } +} + +type NewHandler func(deps any) x.Handler + +func WithExtraHandlers(handlers ...NewHandler) RegistryOption { + return func(o *options) { + o.extraHandlers = handlers + } +} + +func Inspect(f func(reg Registry) error) RegistryOption { + return func(o *options) { + o.inspect = f + } +} + +func WithExtraMigrations(m ...fs.FS) RegistryOption { + return func(o *options) { + o.extraMigrations = append(o.extraMigrations, m...) + } +} + +func WithExtraGoMigrations(m ...popx.Migration) RegistryOption { + return func(o *options) { + o.extraGoMigrations = append(o.extraGoMigrations, m...) + } +} + +func WithDisabledMigrationLogging() RegistryOption { + return func(o *options) { + o.disableMigrationLogging = true + } +} + +func WithServiceLocatorOptions(opts ...servicelocatorx.Option) RegistryOption { + return func(o *options) { + o.serviceLocatorOptions = append(o.serviceLocatorOptions, opts...) + } +} + +func newOptions(os []RegistryOption) *options { + o := new(options) + for _, f := range os { + f(o) + } + return o +} diff --git a/driver/registry_default.go b/driver/registry_default.go new file mode 100644 index 0000000..07151b0 --- /dev/null +++ b/driver/registry_default.go @@ -0,0 +1,978 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "crypto/sha256" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/cenkalti/backoff" + "github.com/dgraph-io/ristretto/v2" + "github.com/gofrs/uuid" + "github.com/gorilla/sessions" + "github.com/hashicorp/go-retryablehttp" + "github.com/lestrrat-go/jwx/jwk" + "github.com/pkg/errors" + "github.com/urfave/negroni" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/persistence/sql" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/logout" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/hook" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "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" + "github.com/ory/kratos/selfservice/strategy/webauthn" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/transaction" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/nosurf" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/contextx" + "github.com/ory/x/dbal" + "github.com/ory/x/healthx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/jwksx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" + "github.com/ory/x/prometheusx" + "github.com/ory/x/servicelocatorx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +type RegistryDefault struct { + l *logrusx.Logger + c *config.Config + + // clock is the time source for time-dependent behavior such as flow expiry. + // It is set eagerly in initCheapMembers and replaced in tests via SetClock. + clock clock.Clock + + ctxer contextx.Contextualizer + + injectedSelfserviceHooks map[string]NewHookFn + extraHandlerFactories []NewHandler + extraHandlers []x.Handler + slOptions *servicelocatorx.Options + + nosurf nosurf.Handler + trc *otelx.Tracer + writer herodot.Writer + healthxHandler *healthx.Handler + + persister persistence.Persister + migrationStatus popx.MigrationStatuses + + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + hookAddressVerifier *hook.AddressVerifier + hookShowVerificationUI *hook.ShowVerificationUIHook + hookVerifyNewAddress *hook.VerifyNewAddress + + identityHandler *identity.Handler + identityValidator *identity.Validator + identityManager *identity.Manager + identitySchemaProvider schema.IdentitySchemaProvider + + courierHandler *courier.Handler + + continuityManager *continuity.Manager + + schemaHandler *schema.Handler + + sessionHandler *session.Handler + sessionManager session.Manager + sessionTokenizer initOnce[*session.Tokenizer] + + passwordHasher initOnce[hash.Hasher] + extraHashers map[string]NewHasherFn + passwordValidator initOnce[password.Validator] + + crypter initOnce[cipher.Cipher] + + errorHandler *errorx.Handler + errorManager *errorx.Manager + + selfserviceRegistrationExecutor *registration.HookExecutor + selfserviceRegistrationHandler *registration.Handler + seflserviceRegistrationErrorHandler *registration.ErrorHandler + selfserviceRegistrationRequestErrorHandler *registration.ErrorHandler + + selfserviceLoginExecutor *login.HookExecutor + selfserviceLoginHandler *login.Handler + selfserviceLoginRequestErrorHandler *login.ErrorHandler + + selfserviceSettingsHandler *settings.Handler + selfserviceSettingsErrorHandler *settings.ErrorHandler + selfserviceSettingsExecutor *settings.HookExecutor + + selfserviceVerifyErrorHandler *verification.ErrorHandler + selfserviceVerifyManager *identity.Manager + selfserviceVerifyHandler *verification.Handler + selfserviceVerificationExecutor *verification.HookExecutor + + selfserviceLinkSender *link.Sender + selfserviceCodeSender *code.Sender + + selfserviceRecoveryErrorHandler *recovery.ErrorHandler + selfserviceRecoveryHandler *recovery.Handler + selfserviceRecoveryExecutor *recovery.HookExecutor + + selfserviceLogoutHandler *logout.Handler + + selfserviceStrategies []any + replacementSelfserviceStrategies []NewStrategy + + hydra initOnce[hydra.Hydra] + + csrfTokenGenerator nosurfx.CSRFToken + + jsonnetVMProvider initOnce[jsonnetsecure.VMProvider] + jsonnetPool jsonnetsecure.Pool + jwkFetcher initOnce[*jwksx.FetcherNext] +} + +func (m *RegistryDefault) JsonnetVM(ctx context.Context) (jsonnetsecure.VM, error) { + return m.jsonnetVMProvider.Get(func() jsonnetsecure.VMProvider { + return &jsonnetsecure.DefaultProvider{Subcommand: "jsonnet", Pool: m.jsonnetPool} + }).JsonnetVM(ctx) +} + +func (m *RegistryDefault) Audit() *logrusx.Logger { + return m.Logger().WithField("audience", "audit") +} + +func (m *RegistryDefault) RegisterPublicRoutes(ctx context.Context, router *httprouterx.RouterPublic) { + for _, h := range m.ExtraHandlers() { + h.RegisterPublicRoutes(router) + } + m.LoginHandler().RegisterPublicRoutes(router) + m.RegistrationHandler().RegisterPublicRoutes(router) + m.LogoutHandler().RegisterPublicRoutes(router) + m.SettingsHandler().RegisterPublicRoutes(router) + m.IdentityHandler().RegisterPublicRoutes(router) + m.CourierHandler().RegisterPublicRoutes(router) + m.SessionHandler().RegisterPublicRoutes(router) + m.SelfServiceErrorHandler().RegisterPublicRoutes(router) + m.SchemaHandler().RegisterPublicRoutes(router) + + m.RecoveryHandler().RegisterPublicRoutes(router) + + m.VerificationHandler().RegisterPublicRoutes(router) + + m.HealthHandler(ctx).SetHealthRoutes(router, false) + webauthnx.RegisterWebauthnRoute(router) + + for _, s := range m.selfServiceStrategies() { + s, ok := s.(x.PublicHandler) + if ok { + s.RegisterPublicRoutes(router) + } + } +} + +func (m *RegistryDefault) RegisterAdminRoutes(ctx context.Context, router *httprouterx.RouterAdmin) { + for _, h := range m.ExtraHandlers() { + h.RegisterAdminRoutes(router) + } + m.RegistrationHandler().RegisterAdminRoutes(router) + m.LoginHandler().RegisterAdminRoutes(router) + m.LogoutHandler().RegisterAdminRoutes(router) + m.SchemaHandler().RegisterAdminRoutes(router) + m.SettingsHandler().RegisterAdminRoutes(router) + m.IdentityHandler().RegisterAdminRoutes(router) + m.CourierHandler().RegisterAdminRoutes(router) + m.SelfServiceErrorHandler().RegisterAdminRoutes(router) + + m.RecoveryHandler().RegisterAdminRoutes(router) + m.SessionHandler().RegisterAdminRoutes(router) + + m.VerificationHandler().RegisterAdminRoutes(router) + + m.HealthHandler(ctx).SetHealthRoutes(router, true) + m.HealthHandler(ctx).SetVersionRoutes(router) + router.GET(prometheusx.MetricsPrometheusPath, prometheusx.Handler().ServeHTTP) + config.RegisterConfigHashRoute(m, router) + + for _, s := range m.selfServiceStrategies() { + s, ok := s.(x.AdminHandler) + if ok { + s.RegisterAdminRoutes(router) + } + } +} + +func (m *RegistryDefault) HTTPMiddlewares() []negroni.Handler { + return m.slOptions.HTTPMiddlewares() +} + +func NewRegistryDefault() *RegistryDefault { + r := &RegistryDefault{ + trc: otelx.NewNoop(), + slOptions: &servicelocatorx.Options{}, + } + r.initCheapMembers() + + return r +} + +func (m *RegistryDefault) SetLogger(l *logrusx.Logger) { + m.l = l +} + +// Clock returns the registry's time source. It is eagerly initialized in +// initCheapMembers, so this getter never lazily initializes. +func (m *RegistryDefault) Clock() clock.Clock { + return m.clock +} + +// SetClock replaces the registry's time source. Intended for tests that need +// deterministic control over time-dependent behavior such as flow expiry. +func (m *RegistryDefault) SetClock(c clock.Clock) { + m.clock = c +} + +func (m *RegistryDefault) SetJSONNetVMProvider(p jsonnetsecure.VMProvider) { + m.jsonnetVMProvider.Set(p) +} + +func (m *RegistryDefault) LogoutHandler() *logout.Handler { + if m.selfserviceLogoutHandler == nil { + m.selfserviceLogoutHandler = logout.NewHandler(m) + } + return m.selfserviceLogoutHandler +} + +func (m *RegistryDefault) HealthHandler(_ context.Context) *healthx.Handler { + if m.healthxHandler == nil { + m.healthxHandler = healthx.NewHandler(m.Writer(), config.Version, + healthx.ReadyCheckers{ + "database": func(r *http.Request) error { + return m.PingContext(r.Context()) + }, + "migrations": func(r *http.Request) error { + if m.migrationStatus != nil && !m.migrationStatus.HasPending() { + return nil + } + + status, err := m.Persister().MigrationStatus(r.Context()) + if err != nil { + return err + } + + if status.HasPending() { + return errors.Errorf("migrations have not yet been fully applied") + } + + m.migrationStatus = status + return nil + }, + }) + } + + return m.healthxHandler +} + +func (m *RegistryDefault) WithCSRFHandler(c nosurf.Handler) { + m.nosurf = c +} + +func (m *RegistryDefault) CSRFHandler() nosurf.Handler { + if m.nosurf == nil { + panic("csrf handler is not set") + } + return m.nosurf +} + +func (m *RegistryDefault) Config() *config.Config { + if m.c == nil { + panic("configuration not set") + } + return m.c +} + +func (m *RegistryDefault) CourierConfig() config.CourierConfigs { + return m.Config() +} + +func (m *RegistryDefault) selfServiceStrategies() []any { + if len(m.selfserviceStrategies) == 0 { + if m.replacementSelfserviceStrategies != nil { + // Construct self-service strategies from the replacements + for _, newStrategy := range m.replacementSelfserviceStrategies { + m.selfserviceStrategies = append(m.selfserviceStrategies, newStrategy(m)) + } + } else { + // Construct the default list of strategies + m.selfserviceStrategies = []any{ + profile.NewStrategy(m), // <- should remain first + password.NewStrategy(m), + oidc.NewStrategy(m), + code.NewStrategy(m), + link.NewStrategy(m), + totp.NewStrategy(m), + passkey.NewStrategy(m), + webauthn.NewStrategy(m), + lookup.NewStrategy(m), + idfirst.NewStrategy(m), + } + } + } + + return m.selfserviceStrategies +} + +func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + if id == "profile" { + return true + } + return m.Config().SelfServiceStrategy(ctx, id).Enabled +} + +func (m *RegistryDefault) strategyLoginEnabled(ctx context.Context, id string) bool { + return m.Config().SelfServiceStrategy(ctx, id).Enabled +} + +func (m *RegistryDefault) RegistrationStrategies(ctx context.Context, filters ...registration.StrategyFilter) (registrationStrategies registration.Strategies) { +nextStrategy: + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(registration.Strategy); ok { + for _, filter := range filters { + if !filter(s) { + continue nextStrategy + } + } + if m.strategyRegistrationEnabled(ctx, s.ID().String()) || supportsOrganizations(strategy) { + registrationStrategies = append(registrationStrategies, s) + } + } + } + return +} + +func (m *RegistryDefault) AllRegistrationStrategies() registration.Strategies { + var registrationStrategies []registration.Strategy + + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(registration.Strategy); ok { + registrationStrategies = append(registrationStrategies, s) + } + } + return registrationStrategies +} + +func (m *RegistryDefault) LoginStrategies(ctx context.Context, filters ...login.StrategyFilter) (loginStrategies login.Strategies) { +nextStrategy: + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(login.Strategy); ok { + for _, filter := range filters { + if !filter(s) { + continue nextStrategy + } + } + + if m.strategyLoginEnabled(ctx, s.ID().String()) || supportsOrganizations(strategy) { + loginStrategies = append(loginStrategies, s) + } + } + } + return +} + +// TestStrategy returns the OIDC strategy as a login.TestStrategy for +// admin-created test login flows. Only the OIDC strategy implements the +// test-mode single-provider UI. Returns nil if the OIDC strategy is not +// registered or disabled in config; callers must handle that case as a +// misconfiguration rather than crashing the process. +// +// The strategy list may contain several strategies that implement the test +// interface: derivatives of the OIDC strategy registered under a different +// credentials type (e.g. SAML or organization sign-in) inherit it through +// embedding, but they resolve providers from their own method's config +// section. Only the strategy with the OIDC credentials type reads +// selfservice.methods.oidc.config.providers, so select by credentials type +// rather than returning the first match. +func (m *RegistryDefault) TestStrategy(ctx context.Context) login.TestStrategy { + type testStrategy interface { + login.TestStrategy + login.Strategy + } + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(testStrategy); ok { + if s.ID() == identity.CredentialsTypeOIDC && m.strategyLoginEnabled(ctx, s.ID().String()) { + return s + } + } + } + + return nil +} + +// supportsOrganizations checks if a strategy implements organization-based authentication. +// Organization strategies manage their own enablement via provider configuration, +// not via the strategy-enabled config flag, so they bypass the strategyLoginEnabled / +// strategyRegistrationEnabled check. +func supportsOrganizations(s any) bool { + o, ok := s.(flow.OrganizationImplementor) + return ok && o.SupportsOrganizations() +} + +func (m *RegistryDefault) AllLoginStrategies() login.Strategies { + var loginStrategies []login.Strategy + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(login.Strategy); ok { + loginStrategies = append(loginStrategies, s) + } + } + return loginStrategies +} + +func (m *RegistryDefault) ActiveCredentialsCounterStrategies(_ context.Context) (activeCredentialsCounterStrategies []identity.ActiveCredentialsCounter) { + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(identity.ActiveCredentialsCounter); ok { + activeCredentialsCounterStrategies = append(activeCredentialsCounterStrategies, s) + } + } + return +} + +func (m *RegistryDefault) IdentityValidator() *identity.Validator { + return m.identityValidator +} + +func (m *RegistryDefault) SetConfig(c *config.Config) { + m.c = c +} + +// WithSelfserviceStrategies is only available in testing and overrides the +// selfservice strategies with the given ones. +func (m *RegistryDefault) WithSelfserviceStrategies(t testing.TB, strategies []any) Registry { + if t == nil { + panic("Passing selfservice strategies is only supported in testing") + } + m.selfserviceStrategies = strategies + return m +} + +func (m *RegistryDefault) Writer() herodot.Writer { + if m.writer == nil { + h := herodot.NewJSONWriter(m.Logger()) + m.writer = h + } + return m.writer +} + +func (m *RegistryDefault) Logger() *logrusx.Logger { + if m.l == nil { + m.l = logrusx.New("Ory Kratos", config.Version) + } + return m.l +} + +func (m *RegistryDefault) IdentityHandler() *identity.Handler { + if m.identityHandler == nil { + m.identityHandler = identity.NewHandler(m) + } + return m.identityHandler +} + +func (m *RegistryDefault) CourierHandler() *courier.Handler { + if m.courierHandler == nil { + m.courierHandler = courier.NewHandler(m) + } + return m.courierHandler +} + +func (m *RegistryDefault) SchemaHandler() *schema.Handler { + if m.schemaHandler == nil { + m.schemaHandler = schema.NewHandler(m) + } + return m.schemaHandler +} + +func (m *RegistryDefault) SessionHandler() *session.Handler { + if m.sessionHandler == nil { + m.sessionHandler = session.NewHandler(m) + } + return m.sessionHandler +} + +func (m *RegistryDefault) Cipher(ctx context.Context) cipher.Cipher { + return m.crypter.Get(func() cipher.Cipher { + switch m.c.CipherAlgorithm(ctx) { + case "xchacha20-poly1305": + return cipher.NewCryptChaCha20(m.Config()) + case "aes": + return cipher.NewCryptAES(m.Config()) + default: + m.l.Logger.Warning("No encryption configuration found. The default algorithm (noop) will be used, resulting in sensitive data being stored in plaintext") + return cipher.NewNoop() + } + }) +} + +// WithHashers registers additional password hashers, keyed by the +// hashers.algorithm value that selects them. See WithExtraHashers. +func (m *RegistryDefault) WithHashers(hashers map[string]NewHasherFn) { + m.extraHashers = hashers +} + +func (m *RegistryDefault) Hasher(ctx context.Context) hash.Hasher { + return m.passwordHasher.Get(func() hash.Hasher { + alg := m.c.HasherPasswordHashingAlgorithm(ctx) + if newHasher, ok := m.extraHashers[alg]; ok { + return newHasher(m) + } + if alg == "bcrypt" { + return hash.NewHasherBcrypt(m) + } + return hash.NewHasherArgon2(m) + }) +} + +func (m *RegistryDefault) PasswordValidator() password.Validator { + m.passwordValidator.Get(func() password.Validator { + val, err := password.NewDefaultPasswordValidatorStrategy(m) + if err != nil { + m.Logger().WithError(err).Fatal("could not initialize DefaultPasswordValidator") + } + return val + }) + return m.passwordValidator.value +} + +func (m *RegistryDefault) SelfServiceErrorHandler() *errorx.Handler { + if m.errorHandler == nil { + m.errorHandler = errorx.NewHandler(m) + } + return m.errorHandler +} + +// cookieStoreKeyPairs derives the (hashKey, blockKey) pairs securecookie +// expects: each secret signs, and its SHA-256 digest encrypts. This way a +// single configured secret yields both signing and encryption, and rotation +// rotates both. +func cookieStoreKeyPairs(secrets [][]byte) [][]byte { + keys := make([][]byte, 0, 2*len(secrets)) + for _, k := range secrets { + encrypt := sha256.Sum256(k) + keys = append(keys, k, encrypt[:]) + } + return keys +} + +func (m *RegistryDefault) CookieManager(ctx context.Context) sessions.StoreExact { + cs := sessions.NewCookieStore(cookieStoreKeyPairs(m.Config().SecretsSession(ctx))...) + cs.Options.Secure = m.Config().SessionCookieSecure(ctx) + cs.Options.HttpOnly = true + + if domain := m.Config().SessionDomain(ctx); domain != "" { + cs.Options.Domain = domain + } + + if path := m.Config().SessionPath(ctx); path != "" { + cs.Options.Path = path + } + + if sameSite := m.Config().SessionSameSiteMode(ctx); sameSite != 0 { + cs.Options.SameSite = sameSite + } + + cs.Options.MaxAge = 0 + if m.Config().SessionPersistentCookie(ctx) { + cs.Options.MaxAge = int(m.Config().SessionLifespan(ctx).Seconds()) + cs.MaxAge(cs.Options.MaxAge) + } + return cs +} + +func (m *RegistryDefault) ContinuityCookieManager(ctx context.Context) sessions.StoreExact { + // To support hot reloading, this can not be instantiated only once. + secrets := m.Config().SecretsSession(ctx) + // Cookies issued before the derived-block-key fix used the raw secrets as + // (hashKey, blockKey) pairs. Appending the raw secrets after the derived + // pairs keeps those cookies readable as a decode fallback (encoding always + // uses the first pair). Remove once cookies issued by pre-fix versions + // have expired. + cs := sessions.NewCookieStore(append(cookieStoreKeyPairs(secrets), secrets...)...) + cs.Options.Secure = m.Config().CookieSecure(ctx) + cs.Options.HttpOnly = true + cs.Options.SameSite = http.SameSiteLaxMode + return cs +} + +func (m *RegistryDefault) Tracer(context.Context) *otelx.Tracer { + if m.trc == nil { + return otelx.NewNoop() + } + return m.trc +} + +func (m *RegistryDefault) SetTracer(t *otelx.Tracer) { + m.trc = t +} + +func (m *RegistryDefault) SessionManager() session.Manager { + return m.sessionManager +} + +func (m *RegistryDefault) Hydra() hydra.Hydra { + return m.hydra.Get(func() hydra.Hydra { + return hydra.NewDefaultHydra(m) + }) +} + +func (m *RegistryDefault) SetHydra(h hydra.Hydra) { + m.hydra.Set(h) +} + +func (m *RegistryDefault) SelfServiceErrorManager() *errorx.Manager { + return m.errorManager +} + +func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualizer, opts ...RegistryOption) error { + if m.persister != nil { + // The DSN connection can not be hot-reloaded! + panic("RegistryDefault.Init() must not be called more than once.") + } + + o := newOptions(opts) + + m.jsonnetPool = o.jsonnetPool + + if o.replaceTracer != nil { + m.trc = o.replaceTracer(m.trc) + } + + if o.replacementStrategies != nil { + m.replacementSelfserviceStrategies = o.replacementStrategies + } + + if o.extraHooks != nil { + m.WithHooks(o.extraHooks) + } + if o.extraHashers != nil { + m.WithHashers(o.extraHashers) + } + if o.extraHandlers != nil { + m.WithExtraHandlers(o.extraHandlers) + } + + if o.replaceIdentitySchemaProvider != nil { + m.identitySchemaProvider = o.replaceIdentitySchemaProvider(m) + } + + bc := backoff.NewExponentialBackOff() + bc.MaxElapsedTime = time.Minute * 5 + bc.Reset() + err := backoff.Retry(func() error { + m.SetContextualizer(ctxer) + + pool, idlePool, connMaxLifetime, connMaxIdleTime, cleanedDSN := sqlcon.ParseConnectionOptions(m.l, m.Config().DSN(ctx)) + dbOpts := &pop.ConnectionDetails{ + URL: sqlcon.FinalizeDSN(m.l, cleanedDSN), + IdlePool: idlePool, + ConnMaxLifetime: connMaxLifetime, + ConnMaxIdleTime: connMaxIdleTime, + Pool: pool, + TracerProvider: m.Tracer(ctx).Provider(), + } + + for _, f := range o.dbOpts { + f(dbOpts) + } + + scheme, _, _ := sqlxx.ExtractSchemeFromDSN(dbOpts.URL) + if !dbOpts.AllowMinPool && scheme == "postgres" && strings.Contains(dbOpts.URL, "pool_min_conns=") { + err := errors.Errorf("attempting to use the option 'pool_min_conns' with Postgres, but the pgxpool connection pool is disabled, this will be rejected by the database: dsn=%s dbOpts.AllowMinPool=%v", dbOpts.URL, dbOpts.AllowMinPool) + return backoff.Permanent(err) + } + if (scheme == "sqlite" || scheme == "mysql") && strings.Contains(dbOpts.URL, "pool_min_conns=") { + err := errors.Errorf("attempting to use the option 'pool_min_conns' with %s, but connection pooling is not supported for this case: dsn=%s", scheme, dbOpts.URL) + return backoff.Permanent(err) + } + + m.Logger(). + WithField("pool", pool). + WithField("idlePool", idlePool). + WithField("connMaxLifetime", connMaxLifetime). + Debug("Connecting to SQL Database") + c, err := pop.NewConnection(dbOpts) + if err != nil { + m.Logger().WithError(err).Warnf("Unable to connect to database, retrying.") + return errors.WithStack(err) + } + if err := c.Open(); err != nil { + m.Logger().WithError(err).Warnf("Unable to open database, retrying.") + return errors.WithStack(err) + } + p, err := sql.NewPersister(m, c, + sql.WithExtraMigrations(o.extraMigrations...), + sql.WithExtraGoMigrations(o.extraGoMigrations...), + sql.WithDisabledLogging(o.disableMigrationLogging)) + if err != nil { + m.Logger().WithError(err).Warnf("Unable to initialize persister, retrying.") + return err + } + + pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := c.Store.SQLDB().PingContext(pingCtx); err != nil { + m.Logger().WithError(err).Warnf("Unable to ping database, retrying.") + return err + } + + // if dsn is memory we have to run the migrations on every start + if dbal.IsMemorySQLite(m.Config().DSN(ctx)) || m.Config().DSN(ctx) == "memory" { + m.Logger().Infoln("Ory Kratos is running migrations on every startup as DSN is memory. This means your data is lost when Kratos terminates.") + if err := p.MigrateUp(ctx); err != nil { + m.Logger().WithError(err).Warnf("Unable to run migrations, retrying.") + return err + } + } + + if o.skipNetworkInit { + m.persister = p + return nil + } + + net, err := p.DetermineNetwork(ctx) + if err != nil { + m.Logger().WithError(err).Warnf("Unable to determine network, retrying.") + return err + } + + m.persister = p.WithNetworkID(net.ID) + return nil + }, bc) + if err != nil { + return err + } + + if o.inspect != nil { + if err := o.inspect(m); err != nil { + return errors.WithStack(err) + } + } + + return nil +} + +func (m *RegistryDefault) SetPersister(p persistence.Persister) { + m.persister = p +} + +func (m *RegistryDefault) Courier(ctx context.Context) (courier.Courier, error) { + return courier.NewCourier(ctx, m) +} + +func (m *RegistryDefault) ContinuityManager() *continuity.Manager { + return m.continuityManager +} + +func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } +func (m *RegistryDefault) ContinuityPersister() continuity.Persister { return m.persister } +func (m *RegistryDefault) IdentityPool() identity.Pool { return m.persister } +func (m *RegistryDefault) PrivilegedIdentityPool() identity.PrivilegedPool { return m.persister } +func (m *RegistryDefault) FlowForTokenExchange() session.FlowForTokenExchange { + return m +} + +func (m *RegistryDefault) GetFlowForTokenExchange(ctx context.Context, flowID uuid.UUID) (any, error) { + rf, err := m.RegistrationFlowPersister().GetRegistrationFlow(ctx, flowID) + if err == nil { + return rf, nil + } + if !errors.Is(err, sqlcon.ErrNoRows()) { + return nil, err + } + + lf, err := m.LoginFlowPersister().GetLoginFlow(ctx, flowID) + if err == nil { + return lf, nil + } + if !errors.Is(err, sqlcon.ErrNoRows()) { + return nil, err + } + + return nil, errors.WithStack(sqlcon.ErrNoRows()) +} +func (m *RegistryDefault) RegistrationFlowPersister() registration.FlowPersister { return m.persister } +func (m *RegistryDefault) RecoveryFlowPersister() recovery.FlowPersister { return m.persister } +func (m *RegistryDefault) LoginFlowPersister() login.FlowPersister { return m.persister } +func (m *RegistryDefault) SettingsFlowPersister() settings.FlowPersister { return m.persister } +func (m *RegistryDefault) SelfServiceErrorPersister() errorx.Persister { return m.persister } +func (m *RegistryDefault) SessionPersister() session.Persister { return m.persister } +func (m *RegistryDefault) CourierPersister() courier.Persister { return m.persister } +func (m *RegistryDefault) RecoveryTokenPersister() link.RecoveryTokenPersister { return m.persister } +func (m *RegistryDefault) RecoveryCodePersister() code.RecoveryCodePersister { return m.persister } +func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { return m.persister } +func (m *RegistryDefault) VerificationTokenPersister() link.VerificationTokenPersister { + return m.persister +} + +func (m *RegistryDefault) VerificationCodePersister() code.VerificationCodePersister { + return m.persister +} + +func (m *RegistryDefault) RegistrationCodePersister() code.RegistrationCodePersister { + return m.persister +} +func (m *RegistryDefault) PendingTraitsChangePersister() identity.PendingTraitsChangePersister { + return m.Persister() +} + +func (m *RegistryDefault) TransactionalPersisterProvider() transaction.Persister { + return m.persister +} + +func (m *RegistryDefault) PingContext(ctx context.Context) error { + return m.persister.Ping(ctx) +} + +func (m *RegistryDefault) Ping() error { + return m.persister.Ping(context.Background()) +} + +func (m *RegistryDefault) WithCSRFTokenGenerator(cg nosurfx.CSRFToken) { + m.csrfTokenGenerator = cg +} + +func (m *RegistryDefault) GenerateCSRFToken(r *http.Request) string { + if m.csrfTokenGenerator == nil { + m.csrfTokenGenerator = nosurfx.DefaultCSRFToken + } + return m.csrfTokenGenerator(r) +} + +func (m *RegistryDefault) IdentityManager() *identity.Manager { + return m.identityManager +} + +func (m *RegistryDefault) HTTPClient(_ context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client { + opts = append([]httpx.ResilientOptions{ + httpx.ResilientClientWithLogger(m.Logger()), + httpx.ResilientClientWithMaxRetry(2), + httpx.ResilientClientWithConnectionTimeout(30 * time.Second), + }, opts...) + + // One of the few exceptions, this usually should not be hot reloaded. + if m.Config().ClientHTTPNoPrivateIPRanges(contextx.RootContext) { + opts = append( + opts, + httpx.ResilientClientDisallowInternalIPs(), + // One of the few exceptions, this usually should not be hot reloaded. + httpx.ResilientClientAllowInternalIPRequestsTo(m.Config().ClientHTTPPrivateIPExceptionURLs(contextx.RootContext)...), + ) + } + return httpx.NewResilientClient(opts...) +} + +func (m *RegistryDefault) SetContextualizer(ctxer contextx.Contextualizer) { + m.ctxer = ctxer +} + +func (m *RegistryDefault) Contextualizer() contextx.Contextualizer { + if m.ctxer == nil { + panic("registry Contextualizer not set") + } + return m.ctxer +} + +func (m *RegistryDefault) JWKSFetcher() *jwksx.FetcherNext { + return m.jwkFetcher.Get(func() *jwksx.FetcherNext { + maxItems := int64(10_000_000) + cache, _ := ristretto.NewCache(&ristretto.Config[[]byte, jwk.Set]{ + NumCounters: maxItems * 10, + MaxCost: maxItems, + BufferItems: 64, + Metrics: true, + IgnoreInternalCost: true, + Cost: func(value jwk.Set) int64 { + return 1 + }, + }) + return jwksx.NewFetcherNext(cache) + }) +} + +func (m *RegistryDefault) SessionTokenizer() *session.Tokenizer { + return m.sessionTokenizer.Get(func() *session.Tokenizer { return session.NewTokenizer(m) }) +} + +func (m *RegistryDefault) ExtraHandlers() []x.Handler { + if m.extraHandlers == nil { + for _, newHandler := range m.extraHandlerFactories { + m.extraHandlers = append(m.extraHandlers, newHandler(m)) + } + } + return m.extraHandlers +} + +// initCheapMembers initializes members that are cheap to initialize. +func (m *RegistryDefault) initCheapMembers() { + m.clock = clock.New() + m.identityValidator = identity.NewValidator(m) + m.identitySchemaProvider = schema.NewDefaultIdentityTraitsProvider(m) + m.identityManager = identity.NewManager(m) + m.sessionManager = session.NewManagerHTTP(m) + m.errorManager = errorx.NewManager(m) + m.continuityManager = continuity.NewManager(m) +} + +type initOnce[T any] struct { + value T + init sync.Once +} + +// Set sets the value of the initOnce, marking it as initialized. +func (o *initOnce[T]) Set(val T) { + // We need to both set the value and run the init function to mark the value as + // initialized. + o.init.Do(func() { o.value = val }) + o.value = val +} + +// Get returns the value of the initOnce, initializing it with newT if it has not +// been initialized yet. `newT` is only called once, even if Get is called +// concurrently from multiple goroutines or with different `newT` functions. +func (o *initOnce[T]) Get(newT func() T) T { + o.init.Do(func() { o.value = newT() }) + return o.value +} diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go new file mode 100644 index 0000000..a179248 --- /dev/null +++ b/driver/registry_default_hooks.go @@ -0,0 +1,142 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/request" + "github.com/ory/kratos/selfservice/hook" +) + +func (m *RegistryDefault) HookVerifier() *hook.Verifier { + if m.hookVerifier == nil { + m.hookVerifier = hook.NewVerifier(m) + } + return m.hookVerifier +} + +func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { + if m.hookSessionIssuer == nil { + m.hookSessionIssuer = hook.NewSessionIssuer(m) + } + return m.hookSessionIssuer +} + +func (m *RegistryDefault) HookSessionDestroyer() *hook.SessionDestroyer { + if m.hookSessionDestroyer == nil { + m.hookSessionDestroyer = hook.NewSessionDestroyer(m) + } + return m.hookSessionDestroyer +} + +func (m *RegistryDefault) HookAddressVerifier() *hook.AddressVerifier { + if m.hookAddressVerifier == nil { + m.hookAddressVerifier = hook.NewAddressVerifier(m) + } + return m.hookAddressVerifier +} + +func (m *RegistryDefault) HookShowVerificationUI() *hook.ShowVerificationUIHook { + if m.hookShowVerificationUI == nil { + m.hookShowVerificationUI = hook.NewShowVerificationUIHook(m) + } + return m.hookShowVerificationUI +} + +func (m *RegistryDefault) HookVerifyNewAddress() *hook.VerifyNewAddress { + if m.hookVerifyNewAddress == nil { + m.hookVerifyNewAddress = hook.NewVerifyNewAddress(m) + } + return m.hookVerifyNewAddress +} + +func (m *RegistryDefault) HookNotifyPreviousAddresses(c *hook.NotifyPreviousAddressesConfig) *hook.NotifyPreviousAddresses { + return hook.NewNotifyPreviousAddresses(m, c) +} + +func (m *RegistryDefault) WithHooks(hooks map[string]NewHookFn) { + m.injectedSelfserviceHooks = hooks +} +func (m *RegistryDefault) WithExtraHandlers(handlers []NewHandler) { + m.extraHandlerFactories = handlers +} + +func getHooks[T any](m *RegistryDefault, credentialsType string, configs []config.SelfServiceHook) ([]T, error) { + hooks := make([]T, 0, len(configs)) + + var addSessionIssuer bool +allHooksLoop: + for _, hookConfig := range configs { + switch hookConfig.Name { + case hook.KeySessionIssuer: + // The session issuer hook always needs to come last. + addSessionIssuer = true + case hook.KeySessionDestroyer: + if h, ok := any(m.HookSessionDestroyer()).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyWebHook: + cfg := request.Config{} + if err := json.Unmarshal(hookConfig.Config, &cfg); err != nil { + m.l.WithError(err).WithField("raw_config", string(hookConfig.Config)).Error("failed to unmarshal hook configuration, ignoring hook") + return nil, errors.WithStack(fmt.Errorf("failed to unmarshal webhook configuration for %s: %w", credentialsType, err)) + } + if h, ok := any(hook.NewWebHook(m, &cfg)).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyRequireVerifiedAddress: + if h, ok := any(m.HookAddressVerifier()).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyVerificationUI: + if h, ok := any(m.HookShowVerificationUI()).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyVerifier: + if h, ok := any(m.HookVerifier()).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyVerifyNewAddress: + if h, ok := any(m.HookVerifyNewAddress()).(T); ok { + hooks = append(hooks, h) + } + case hook.KeyNotifyPreviousAddresses: + cfg := &hook.NotifyPreviousAddressesConfig{} + if len(hookConfig.Config) > 0 { + if err := json.Unmarshal(hookConfig.Config, cfg); err != nil { + m.l.WithError(err).WithField("raw_config", string(hookConfig.Config)).Error("failed to unmarshal hook configuration, ignoring hook") + return nil, errors.WithStack(fmt.Errorf("failed to unmarshal notify_previous_addresses configuration for %s: %w", credentialsType, err)) + } + } + if h, ok := any(m.HookNotifyPreviousAddresses(cfg)).(T); ok { + hooks = append(hooks, h) + } + default: + for name, newHook := range m.injectedSelfserviceHooks { + if name == hookConfig.Name { + if h, ok := newHook(hookConfig, m).(T); ok { + hooks = append(hooks, h) + } + continue allHooksLoop + } + } + m.l. + WithField("for", credentialsType). + WithField("hook", hookConfig.Name). + Warn("A configuration for a non-existing hook was found and will be ignored.") + } + } + if addSessionIssuer { + if h, ok := any(m.HookSessionIssuer()).(T); ok { + hooks = append(hooks, h) + } + } + + return hooks, nil +} diff --git a/driver/registry_default_login.go b/driver/registry_default_login.go new file mode 100644 index 0000000..3149d64 --- /dev/null +++ b/driver/registry_default_login.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" +) + +func (m *RegistryDefault) LoginHookExecutor() *login.HookExecutor { + if m.selfserviceLoginExecutor == nil { + m.selfserviceLoginExecutor = login.NewHookExecutor(m) + } + return m.selfserviceLoginExecutor +} + +func (m *RegistryDefault) PreLoginHooks(ctx context.Context) ([]login.PreHookExecutor, error) { + return getHooks[login.PreHookExecutor](m, "", m.Config().SelfServiceFlowLoginBeforeHooks(ctx)) +} + +func (m *RegistryDefault) PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]login.PostHookExecutor, error) { + hooks, err := getHooks[login.PostHookExecutor](m, string(credentialsType), m.Config().SelfServiceFlowLoginAfterHooks(ctx, string(credentialsType))) + if err != nil { + return nil, err + } + if len(hooks) > 0 { + return hooks, nil + } + + // since we don't want merging hooks defined in a specific strategy and global hooks + // global hooks are added only if no strategy specific hooks are defined + return getHooks[login.PostHookExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowLoginAfterHooks(ctx, config.HookGlobal)) +} + +func (m *RegistryDefault) LoginHandler() *login.Handler { + if m.selfserviceLoginHandler == nil { + m.selfserviceLoginHandler = login.NewHandler(m) + } + + return m.selfserviceLoginHandler +} + +func (m *RegistryDefault) LoginFlowErrorHandler() *login.ErrorHandler { + if m.selfserviceLoginRequestErrorHandler == nil { + m.selfserviceLoginRequestErrorHandler = login.NewFlowErrorHandler(m) + } + + return m.selfserviceLoginRequestErrorHandler +} diff --git a/driver/registry_default_recovery.go b/driver/registry_default_recovery.go new file mode 100644 index 0000000..2a2a7fc --- /dev/null +++ b/driver/registry_default_recovery.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/code" +) + +func (m *RegistryDefault) RecoveryFlowErrorHandler() *recovery.ErrorHandler { + if m.selfserviceRecoveryErrorHandler == nil { + m.selfserviceRecoveryErrorHandler = recovery.NewErrorHandler(m) + } + + return m.selfserviceRecoveryErrorHandler +} + +func (m *RegistryDefault) RecoveryHandler() *recovery.Handler { + if m.selfserviceRecoveryHandler == nil { + m.selfserviceRecoveryHandler = recovery.NewHandler(m) + } + + return m.selfserviceRecoveryHandler +} + +func (m *RegistryDefault) RecoveryStrategies(ctx context.Context) (recoveryStrategies recovery.Strategies) { + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(recovery.Strategy); ok { + if m.Config().SelfServiceStrategy(ctx, s.RecoveryStrategyID()).Enabled { + recoveryStrategies = append(recoveryStrategies, s) + } + } + } + return +} + +// GetActiveRecoveryStrategies returns the currently active recovery strategies. +// It returns a list of all strategies and the specific primary strategy. +// If no primary recovery strategy has been set, an error is returned. +func (m *RegistryDefault) GetActiveRecoveryStrategies(ctx context.Context) (active recovery.Strategies, primary recovery.Strategy, err error) { + as := m.Config().SelfServiceFlowRecoveryUse(ctx) + s, ps, err := m.RecoveryStrategies(ctx).ActiveStrategies(as) + if err != nil { + return nil, ps, errors.WithStack(herodot.ErrBadRequest(). + WithReasonf("You attempted recovery using %s, which is not enabled or does not exist. An administrator needs to enable this recovery method.", as)) + } + return s, ps, nil +} + +func (m *RegistryDefault) AllRecoveryStrategies() (recoveryStrategies recovery.Strategies) { + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(recovery.Strategy); ok { + recoveryStrategies = append(recoveryStrategies, s) + } + } + return +} + +func (m *RegistryDefault) RecoveryExecutor() *recovery.HookExecutor { + if m.selfserviceRecoveryExecutor == nil { + m.selfserviceRecoveryExecutor = recovery.NewHookExecutor(m) + } + return m.selfserviceRecoveryExecutor +} + +func (m *RegistryDefault) PreRecoveryHooks(ctx context.Context) ([]recovery.PreHookExecutor, error) { + return getHooks[recovery.PreHookExecutor](m, "", m.Config().SelfServiceFlowRecoveryBeforeHooks(ctx)) +} + +func (m *RegistryDefault) PostRecoveryHooks(ctx context.Context) ([]recovery.PostHookExecutor, error) { + return getHooks[recovery.PostHookExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal)) +} + +func (m *RegistryDefault) CodeSender() *code.Sender { + if m.selfserviceCodeSender == nil { + m.selfserviceCodeSender = code.NewSender(m) + } + + return m.selfserviceCodeSender +} diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go new file mode 100644 index 0000000..eccfa7c --- /dev/null +++ b/driver/registry_default_registration.go @@ -0,0 +1,85 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "slices" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/registration" +) + +func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]registration.PostHookPrePersistExecutor, error) { + hooks, err := getHooks[registration.PostHookPrePersistExecutor](m, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) + if err != nil { + return nil, err + } + + return hooks, nil +} + +func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]registration.PostHookPostPersistExecutor, error) { + hooks, err := getHooks[registration.PostHookPostPersistExecutor](m, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) + if err != nil { + return nil, err + } + if len(hooks) == 0 { + // since we don't want merging hooks defined in a specific strategy and + // global hooks are added only if no strategy specific hooks are defined + hooks, err = getHooks[registration.PostHookPostPersistExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, config.HookGlobal)) + if err != nil { + return nil, err + } + } + + // WARNING - If you remove this, no verification emails / sms will be sent post-registration. + if m.Config().SelfServiceFlowVerificationEnabled(ctx) { + hooks = slices.Insert(hooks, 0, registration.PostHookPostPersistExecutor(m.HookVerifier())) + } + + return hooks, nil +} + +func (m *RegistryDefault) PreRegistrationHooks(ctx context.Context) ([]registration.PreHookExecutor, error) { + return getHooks[registration.PreHookExecutor](m, "", m.Config().SelfServiceFlowRegistrationBeforeHooks(ctx)) +} + +func (m *RegistryDefault) RegistrationExecutor() *registration.HookExecutor { + if m.selfserviceRegistrationExecutor == nil { + m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m) + } + return m.selfserviceRegistrationExecutor +} + +func (m *RegistryDefault) RegistrationHookExecutor() *registration.HookExecutor { + if m.selfserviceRegistrationExecutor == nil { + m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m) + } + return m.selfserviceRegistrationExecutor +} + +func (m *RegistryDefault) RegistrationErrorHandler() *registration.ErrorHandler { + if m.seflserviceRegistrationErrorHandler == nil { + m.seflserviceRegistrationErrorHandler = registration.NewErrorHandler(m) + } + return m.seflserviceRegistrationErrorHandler +} + +func (m *RegistryDefault) RegistrationHandler() *registration.Handler { + if m.selfserviceRegistrationHandler == nil { + m.selfserviceRegistrationHandler = registration.NewHandler(m) + } + + return m.selfserviceRegistrationHandler +} + +func (m *RegistryDefault) RegistrationFlowErrorHandler() *registration.ErrorHandler { + if m.selfserviceRegistrationRequestErrorHandler == nil { + m.selfserviceRegistrationRequestErrorHandler = registration.NewErrorHandler(m) + } + + return m.selfserviceRegistrationRequestErrorHandler +} diff --git a/driver/registry_default_schemas.go b/driver/registry_default_schemas.go new file mode 100644 index 0000000..63ff0c6 --- /dev/null +++ b/driver/registry_default_schemas.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + + "github.com/ory/kratos/schema" +) + +// IdentityTraitsSchemas returns the identity traits schemas. The provider is +// eagerly initialized in initCheapMembers (or replaced in Init), so this getter +// only reads the field and never lazily initializes it. +func (m *RegistryDefault) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { + return m.identitySchemaProvider.IdentityTraitsSchemas(ctx) +} diff --git a/driver/registry_default_schemas_test.go b/driver/registry_default_schemas_test.go new file mode 100644 index 0000000..6e6efc1 --- /dev/null +++ b/driver/registry_default_schemas_test.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/schema" + "github.com/ory/x/urlx" +) + +func TestRegistryDefault_IdentityTraitsSchemas(t *testing.T) { + ctx := context.Background() + + conf, reg := pkg.NewFastRegistryWithMocks(t) + defaultSchema := schema.Schema{ + ID: "default", + URL: urlx.ParseOrPanic("file://default.schema.json"), + RawURL: "file://default.schema.json", + } + altSchema := schema.Schema{ + ID: "alt", + URL: urlx.ParseOrPanic("file://other.schema.json"), + RawURL: "file://other.schema.json", + } + + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, []config.Schema{ + {ID: altSchema.ID, URL: altSchema.RawURL}, + {ID: defaultSchema.ID, URL: defaultSchema.RawURL}, + }) + + ss, err := reg.IdentityTraitsSchemas(context.Background()) + require.NoError(t, err) + assert.Equal(t, 2, ss.Total()) + assert.Contains(t, ss, defaultSchema) + assert.Contains(t, ss, altSchema) +} diff --git a/driver/registry_default_sessiontokenexchange.go b/driver/registry_default_sessiontokenexchange.go new file mode 100644 index 0000000..8b12ef6 --- /dev/null +++ b/driver/registry_default_sessiontokenexchange.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import "github.com/ory/kratos/selfservice/sessiontokenexchange" + +func (m *RegistryDefault) SessionTokenExchangePersister() sessiontokenexchange.Persister { + return m.Persister() +} diff --git a/driver/registry_default_settings.go b/driver/registry_default_settings.go new file mode 100644 index 0000000..57d3d49 --- /dev/null +++ b/driver/registry_default_settings.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "slices" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow/settings" +) + +func (m *RegistryDefault) PostSettingsPrePersistHooks(ctx context.Context, settingsType string) ([]settings.PostHookPrePersistExecutor, error) { + return getHooks[settings.PostHookPrePersistExecutor](m, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType)) +} + +func (m *RegistryDefault) PreSettingsHooks(ctx context.Context) ([]settings.PreHookExecutor, error) { + return getHooks[settings.PreHookExecutor](m, "", m.Config().SelfServiceFlowSettingsBeforeHooks(ctx)) +} + +func (m *RegistryDefault) PostSettingsPostPersistHooks(ctx context.Context, settingsType string) ([]settings.PostHookPostPersistExecutor, error) { + hooks, err := getHooks[settings.PostHookPostPersistExecutor](m, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType)) + if err != nil { + return nil, err + } + if len(hooks) == 0 { + // since we don't want merging hooks defined in a specific strategy and + // global hooks are added only if no strategy specific hooks are defined + hooks, err = getHooks[settings.PostHookPostPersistExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, config.HookGlobal)) + if err != nil { + return nil, err + } + } + + if m.Config().SelfServiceFlowVerificationEnabled(ctx) { + hooks = slices.Insert(hooks, 0, settings.PostHookPostPersistExecutor(m.HookVerifier())) + } + + return hooks, nil +} + +func (m *RegistryDefault) SettingsHookExecutor() *settings.HookExecutor { + if m.selfserviceSettingsExecutor == nil { + m.selfserviceSettingsExecutor = settings.NewHookExecutor(m) + } + return m.selfserviceSettingsExecutor +} + +func (m *RegistryDefault) SettingsHandler() *settings.Handler { + if m.selfserviceSettingsHandler == nil { + m.selfserviceSettingsHandler = settings.NewHandler(m) + } + return m.selfserviceSettingsHandler +} + +func (m *RegistryDefault) SettingsFlowErrorHandler() *settings.ErrorHandler { + if m.selfserviceSettingsErrorHandler == nil { + m.selfserviceSettingsErrorHandler = settings.NewErrorHandler(m) + } + return m.selfserviceSettingsErrorHandler +} + +func (m *RegistryDefault) SettingsStrategies(ctx context.Context, filters ...settings.StrategyFilter) (profileStrategies settings.Strategies) { +nextStrategy: + for _, strategy := range m.selfServiceStrategies() { + s, ok := strategy.(settings.Strategy) + if !ok { + continue + } + for _, filter := range filters { + if !filter(s) { + continue nextStrategy + } + } + if m.Config().SelfServiceStrategy(ctx, s.SettingsStrategyID()).Enabled || supportsOrganizations(strategy) { + profileStrategies = append(profileStrategies, s) + } + } + return +} + +func (m *RegistryDefault) AllSettingsStrategies() settings.Strategies { + var profileStrategies []settings.Strategy + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(settings.Strategy); ok { + profileStrategies = append(profileStrategies, s) + } + } + return profileStrategies +} diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go new file mode 100644 index 0000000..21bd955 --- /dev/null +++ b/driver/registry_default_test.go @@ -0,0 +1,1101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver_test + +import ( + "context" + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/logrusx" + "github.com/ory/x/randx" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/request" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/hook" +) + +func TestDriverDefault_Hooks(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("type=verification", func(t *testing.T) { + t.Parallel() + // BEFORE hooks + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { + return []verification.PreHookExecutor{} + }, + }, + { + uc: "Two web_hooks are configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + h, err := reg.PreVerificationHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + + // AFTER hooks + for _, tc := range []struct { + uc string + prep func(conf *config.Config) + config map[string]any + expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor + }{ + { + uc: "No hooks configured", + prep: func(conf *config.Config) {}, + expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { + return []verification.PostHookExecutor{} + }, + }, + { + uc: "Multiple web_hooks configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + h, err := reg.PostVerificationHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + }) + + t.Run("type=recovery", func(t *testing.T) { + t.Parallel() + // BEFORE hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{} }, + }, + { + uc: "Two web_hooks are configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PreRecoveryHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + + // AFTER hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{} }, + }, + { + uc: "Multiple web_hooks configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PostRecoveryHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + }) + + t.Run("type=registration", func(t *testing.T) { + t.Parallel() + // BEFORE hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { + return []registration.PreHookExecutor{} + }, + }, + { + uc: "Two web_hooks are configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PreRegistrationHooks(ctx) + require.NoError(t, err) + + assert.EqualValues(t, tc.expect(reg), h) + }) + } + + // AFTER hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { + return []registration.PostHookPostPersistExecutor{} + }, + }, + { + uc: "Only session hook configured for password strategy", + 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{ + hook.NewVerifier(reg), + hook.NewSessionIssuer(reg), + } + }, + }, + { + uc: "A session hook and a web_hook are configured for password strategy", + 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, &request.Config{URL: "foo", Method: "POST", TemplateURI: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewSessionIssuer(reg), + } + }, + }, + { + uc: "Two web_hooks are configured on a global level", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + { + uc: "Hooks are configured on a global level, as well as on a strategy level", + 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"}, + }, + 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, &request.Config{Method: "GET", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewSessionIssuer(reg), + } + }, + }, + { + uc: "show_verification_ui is configured", + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "show_verification_ui"}, + }, + }, + expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { + return []registration.PostHookPostPersistExecutor{ + hook.NewShowVerificationUIHook(reg), + } + }, + }, + } { + t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + }) + + t.Run("type=login", func(t *testing.T) { + t.Parallel() + // BEFORE hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []login.PreHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{} }, + }, + { + uc: "Two web_hooks are configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PreLoginHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + + // AFTER hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []login.PostHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{} }, + }, + { + uc: "Only revoke_active_sessions hook configured for password strategy", + 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{ + hook.NewSessionDestroyer(reg), + } + }, + }, + { + uc: "Only require_verified_address hook configured for password strategy", + 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{ + hook.NewAddressVerifier(reg), + } + }, + }, + { + uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", + 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, &request.Config{TemplateURI: "bar", Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewAddressVerifier(reg), + hook.NewSessionDestroyer(reg), + } + }, + }, + { + uc: "Two web_hooks are configured on a global level", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + { + uc: "Hooks are configured on a global level, as well as on a strategy level", + 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"}, + }, + 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, &request.Config{Method: "GET", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewSessionDestroyer(reg), + hook.NewAddressVerifier(reg), + } + }, + }, + } { + t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + }) + + t.Run("type=settings", func(t *testing.T) { + t.Parallel() + // BEFORE hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{} }, + }, + { + uc: "Two web_hooks are configured", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PreSettingsHooks(ctx) + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + + // AFTER hooks + for _, tc := range []struct { + uc string + config map[string]any + expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor + }{ + { + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { + return []settings.PostHookPostPersistExecutor{} + }, + }, + { + uc: "Only verify hook configured for the strategy", + 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). + }, + expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { + return []settings.PostHookPostPersistExecutor{ + hook.NewVerifier(reg), + } + }, + }, + { + uc: "A verify hook and a web_hook are configured for profile strategy", + 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, &request.Config{TemplateURI: "bar", Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + { + uc: "Two web_hooks are configured on a global level", + 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, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + { + uc: "Hooks are configured on a global level, as well as on a strategy level", + 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, &request.Config{Method: "GET", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + } { + t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h, err := reg.PostSettingsPostPersistHooks(ctx, "profile") + require.NoError(t, err) + + assert.Equal(t, tc.expect(reg), h) + }) + } + }) +} + +func TestDriverDefault_Strategies(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("case=registration", func(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + config map[string]any + expect []string + }{ + { + name: "no strategies", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + }, + expect: []string{"profile"}, + }, + { + name: "only password", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + }, + expect: []string{"profile", "password"}, + }, + { + name: "oidc and password", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + }, + expect: []string{"profile", "password", "oidc"}, + }, + { + name: "oidc, password and totp", + 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{"profile", "password", "oidc"}, + }, + { + name: "password and code", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, + }, + expect: []string{"profile", "password", "code"}, + }, + } { + t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + 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()) + } + }) + } + }) + + t.Run("case=login", func(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + config map[string]any + expect []string + }{ + { + name: "no strategies", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + }, + }, + { + name: "only password", + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + }, + expect: []string{"password"}, + }, + { + name: "oidc and password", + 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", + 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", + 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", + 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) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + 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()) + } + }) + } + }) + + t.Run("case=recovery", func(t *testing.T) { + t.Parallel() + for k, tc := range []struct { + config map[string]any + expect []string + }{ + { + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false, + }, + }, + { + 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) { + t.Parallel() + + ctx := contextx.WithConfigValues(ctx, tc.config) + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + s := reg.RecoveryStrategies(ctx) + require.Len(t, s, len(tc.expect)) + for k, e := range tc.expect { + assert.Equal(t, e, s[k].RecoveryStrategyID()) + } + }) + } + }) + + t.Run("case=settings", func(t *testing.T) { + t.Parallel() + l := logrusx.New("", "") + + for k, tc := range []struct { + configOptions []configx.OptionModifier + expect []string + }{ + { + 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, + })}, + }, + { + 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"}, + }, + { + 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"}, + }, + { + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + })}, + expect: []string{"profile", "password"}, + }, + { + configOptions: []configx.OptionModifier{ + configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), + configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), + }, + expect: []string{"profile", "password"}, + }, + } { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + conf := config.MustNew(t, l, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...) + + reg, err := driver.NewRegistryFromDSN(ctx, conf, l) + require.NoError(t, err) + + s := reg.SettingsStrategies(ctx) + require.Len(t, s, len(tc.expect)) + + for k, e := range tc.expect { + assert.Equal(t, e, s[k].SettingsStrategyID()) + } + }) + } + }) +} + +func TestDefaultRegistry_AllStrategies(t *testing.T) { + t.Parallel() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("case=all login strategies", func(t *testing.T) { + 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 { + assert.Equal(t, e, s[k].ID().String()) + } + }) + + t.Run("case=all registration strategies", func(t *testing.T) { + expects := []string{"profile", "password", "oidc", "code", "passkey", "webauthn"} + s := reg.AllRegistrationStrategies() + require.Len(t, s, len(expects)) + for k, e := range expects { + assert.Equal(t, e, s[k].ID().String()) + } + }) + + t.Run("case=all settings strategies", func(t *testing.T) { + expects := []string{"profile", "password", "oidc", "totp", "passkey", "webauthn", "lookup_secret"} + s := reg.AllSettingsStrategies() + require.Len(t, s, len(expects)) + for k, e := range expects { + assert.Equal(t, e, s[k].SettingsStrategyID()) + } + }) + + t.Run("case=all recovery strategies", func(t *testing.T) { + expects := []string{"code", "link"} + s := reg.AllRecoveryStrategies() + require.Len(t, s, len(expects)) + for k, e := range expects { + assert.Equal(t, e, s[k].RecoveryStrategyID()) + } + }) +} + +func TestGetActiveRecoveryStrategy(t *testing.T) { + t.Parallel() + ctx := context.Background() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("returns error if active strategy is disabled", func(t *testing.T) { + ctx := contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceRecoveryUse: "code", + }) + + _, _, err := reg.GetActiveRecoveryStrategies(ctx) + require.Error(t, err) + }) + + t.Run("returns active strategies", func(t *testing.T) { + for _, sID := range []string{ + "code", "link", + } { + t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { + ctx := contextx.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceRecoveryUse: sID, + }) + + s, ps, err := reg.GetActiveRecoveryStrategies(ctx) + require.NoError(t, err) + require.Len(t, s, 1) + require.Equal(t, sID, ps.RecoveryStrategyID()) + }) + } + }) +} + +func TestGetActiveVerificationStrategy(t *testing.T) { + t.Parallel() + ctx := context.Background() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + t.Run("returns error if active strategy is disabled", func(t *testing.T) { + ctx := contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceVerificationUse: "code", + }) + _, _, err := reg.GetActiveVerificationStrategies(ctx) + require.Error(t, err) + }) + + t.Run("returns active strategy", func(t *testing.T) { + for _, sID := range []string{ + "code", "link", + } { + t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { + ctx := contextx.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceVerificationUse: sID, + }) + + _, s, err := reg.GetActiveVerificationStrategies(ctx) + require.NoError(t, err) + require.Equal(t, sID, s.VerificationStrategyID()) + }) + } + }) +} + +func TestDriverDefault_ExtraHashers(t *testing.T) { + t.Parallel() + // The OSS config schema only allows argon2 and bcrypt. The context + // override simulates a downstream distribution whose patched schema + // permits the extra algorithm. + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyHasherAlgorithm, "pbkdf2") + _, reg := pkg.NewFastRegistryWithMocks(t) + reg.WithHashers(map[string]driver.NewHasherFn{ + "pbkdf2": func(_ driver.Registry) hash.Hasher { + return &hash.Pbkdf2{Algorithm: "sha256", Iterations: 100_000, SaltLength: 16, KeyLength: 32} + }, + }) + + h := reg.Hasher(ctx) + require.IsType(t, &hash.Pbkdf2{}, h) + + generated, err := h.Generate(ctx, []byte("password")) + require.NoError(t, err) + assert.True(t, hash.IsPbkdf2Sha256Hash(generated), "%s", generated) + require.NoError(t, hash.Compare(ctx, []byte("password"), generated)) +} + +func TestContinuityCookieManager(t *testing.T) { + t.Parallel() + + const sensitiveValue = "continuity-container-reference" + + writeCookie := func(t *testing.T, store sessions.Store) *http.Cookie { + t.Helper() + r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) + w := httptest.NewRecorder() + sess, err := store.New(r, continuity.CookieName) + require.NoError(t, err) + sess.Values["value"] = sensitiveValue + require.NoError(t, store.Save(r, w, sess)) + cookies := w.Result().Cookies() + require.Len(t, cookies, 1) + return cookies[0] + } + + t.Run("case=encrypts the continuity cookie", func(t *testing.T) { + t.Parallel() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + secret := randx.MustString(32, randx.AlphaNum) + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{secret}) + + cookie := writeCookie(t, reg.ContinuityCookieManager(ctx)) + + // A codec that only knows the signing key must not be able to decode the + // cookie. If it can, the cookie is signed but not encrypted. + values := map[any]any{} + signatureOnly := securecookie.New([]byte(secret), nil) + require.Errorf(t, signatureOnly.Decode(continuity.CookieName, cookie.Value, &values), + "the continuity cookie must be encrypted, but decoding it without the encryption key succeeded: %+v", values) + + // The cookie must decode with the same key pairing CookieManager uses: + // hash key = secret, block key = SHA-256(secret). + blockKey := sha256.Sum256([]byte(secret)) + withEncryption := securecookie.New([]byte(secret), blockKey[:]) + require.NoError(t, withEncryption.Decode(continuity.CookieName, cookie.Value, &values)) + assert.Equal(t, sensitiveValue, values["value"]) + }) + + t.Run("case=decodes cookies issued before secret rotation", func(t *testing.T) { + t.Parallel() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + oldSecret := randx.MustString(32, randx.AlphaNum) + newSecret := randx.MustString(32, randx.AlphaNum) + + oldCtx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{oldSecret}) + cookie := writeCookie(t, reg.ContinuityCookieManager(oldCtx)) + + rotatedCtx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{newSecret, oldSecret}) + r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) + r.AddCookie(cookie) + sess, err := reg.ContinuityCookieManager(rotatedCtx).New(r, continuity.CookieName) + require.NoError(t, err) + assert.Equal(t, sensitiveValue, sess.Values["value"]) + }) + + // Cookies issued before the key pairing fix used the raw secrets as + // (hashKey, blockKey) pairs. They must remain readable during rollout. + t.Run("case=decodes legacy cookie signed with a single secret", func(t *testing.T) { + t.Parallel() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + secret := randx.MustString(32, randx.AlphaNum) + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{secret}) + + // The legacy store had a single signed-but-unencrypted codec. + legacy := securecookie.New([]byte(secret), nil) + encoded, err := legacy.Encode(continuity.CookieName, map[any]any{"value": sensitiveValue}) + require.NoError(t, err) + + r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) + r.AddCookie(&http.Cookie{Name: continuity.CookieName, Value: encoded}) + sess, err := reg.ContinuityCookieManager(ctx).New(r, continuity.CookieName) + require.NoError(t, err) + assert.Equal(t, sensitiveValue, sess.Values["value"]) + }) + + t.Run("case=decodes legacy cookie encrypted with the second secret", func(t *testing.T) { + t.Parallel() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + firstSecret := randx.MustString(32, randx.AlphaNum) + secondSecret := randx.MustString(32, randx.AlphaNum) + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{firstSecret, secondSecret}) + + // The legacy store treated the second secret as the AES block key of + // the first codec. + legacy := securecookie.New([]byte(firstSecret), []byte(secondSecret)) + encoded, err := legacy.Encode(continuity.CookieName, map[any]any{"value": sensitiveValue}) + require.NoError(t, err) + + r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) + r.AddCookie(&http.Cookie{Name: continuity.CookieName, Value: encoded}) + sess, err := reg.ContinuityCookieManager(ctx).New(r, continuity.CookieName) + require.NoError(t, err) + assert.Equal(t, sensitiveValue, sess.Values["value"]) + }) +} diff --git a/driver/registry_default_verification.go b/driver/registry_default_verification.go new file mode 100644 index 0000000..d4e01a0 --- /dev/null +++ b/driver/registry_default_verification.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/link" +) + +func (m *RegistryDefault) VerificationFlowPersister() verification.FlowPersister { + return m.persister +} + +func (m *RegistryDefault) VerificationFlowErrorHandler() *verification.ErrorHandler { + if m.selfserviceVerifyErrorHandler == nil { + m.selfserviceVerifyErrorHandler = verification.NewErrorHandler(m) + } + + return m.selfserviceVerifyErrorHandler +} + +func (m *RegistryDefault) VerificationManager() *identity.Manager { + if m.selfserviceVerifyManager == nil { + m.selfserviceVerifyManager = identity.NewManager(m) + } + + return m.selfserviceVerifyManager +} + +func (m *RegistryDefault) VerificationHandler() *verification.Handler { + if m.selfserviceVerifyHandler == nil { + m.selfserviceVerifyHandler = verification.NewHandler(m) + } + + return m.selfserviceVerifyHandler +} + +func (m *RegistryDefault) LinkSender() *link.Sender { + if m.selfserviceLinkSender == nil { + m.selfserviceLinkSender = link.NewSender(m) + } + + return m.selfserviceLinkSender +} + +// GetActiveVerificationStrategies returns the currently active verification strategies. +// It returns a list of all strategies and the specific primary strategy. +// If no primary verification strategy has been set, an error is returned. +func (m *RegistryDefault) GetActiveVerificationStrategies(ctx context.Context) (active verification.Strategies, primary verification.PrimaryStrategy, err error) { + as := m.Config().SelfServiceFlowVerificationUse(ctx) + s, ps, err := m.VerificationStrategies(ctx).ActiveStrategies(as) + if err != nil { + return nil, ps, errors.WithStack(herodot.ErrBadRequest(). + WithReasonf("The active verification strategy %s is not enabled. Please enable it in the configuration.", as)) + } + return s, ps, nil +} + +func (m *RegistryDefault) VerificationStrategies(ctx context.Context) (verificationStrategies verification.Strategies) { + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(verification.Strategy); ok { + if m.Config().SelfServiceStrategy(ctx, s.VerificationStrategyID()).Enabled { + verificationStrategies = append(verificationStrategies, s) + } + } + } + return +} + +func (m *RegistryDefault) AllVerificationStrategies() (verificationStrategies verification.Strategies) { + for _, strategy := range m.selfServiceStrategies() { + if s, ok := strategy.(verification.Strategy); ok { + verificationStrategies = append(verificationStrategies, s) + } + } + + return +} + +func (m *RegistryDefault) VerificationExecutor() *verification.HookExecutor { + if m.selfserviceVerificationExecutor == nil { + m.selfserviceVerificationExecutor = verification.NewHookExecutor(m) + } + return m.selfserviceVerificationExecutor +} + +func (m *RegistryDefault) PreVerificationHooks(ctx context.Context) ([]verification.PreHookExecutor, error) { + return getHooks[verification.PreHookExecutor](m, "", m.Config().SelfServiceFlowVerificationBeforeHooks(ctx)) +} + +func (m *RegistryDefault) PostVerificationHooks(ctx context.Context) ([]verification.PostHookExecutor, error) { + return getHooks[verification.PostHookExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowVerificationAfterHooks(ctx, config.HookGlobal)) +} diff --git a/embedx/embedx.go b/embedx/embedx.go new file mode 100644 index 0000000..148d693 --- /dev/null +++ b/embedx/embedx.go @@ -0,0 +1,116 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package embedx + +import ( + "bytes" + _ "embed" + "io" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + "github.com/ory/x/otelx" +) + +//go:embed config.schema.json +var ConfigSchema []byte + +//go:embed identity_meta.schema.json +var IdentityMetaSchema []byte + +//go:embed identity_extension.schema.json +var IdentityExtensionSchema []byte + +type SchemaType int + +const ( + Config SchemaType = iota + 1 + IdentityMeta + IdentityExtension +) + +type Schema struct { + id string + data []byte + dependencies []*Schema +} + +var ( + identityExt = &Schema{ + id: gjson.GetBytes(IdentityExtensionSchema, "$id").Str, + data: IdentityExtensionSchema, + dependencies: nil, + } + + schemas = map[SchemaType]*Schema{ + Config: { + id: gjson.GetBytes(ConfigSchema, "$id").Str, + data: ConfigSchema, + dependencies: nil, + }, + IdentityMeta: { + id: gjson.GetBytes(IdentityMetaSchema, "$id").Str, + data: IdentityMetaSchema, + dependencies: []*Schema{ + identityExt, + }, + }, + IdentityExtension: identityExt, + } +) + +func getSchema(schema SchemaType) (*Schema, error) { + if val, ok := schemas[schema]; ok { + return val, nil + } + return nil, errors.Errorf("the specified schema type (%d) is not supported", int(schema)) +} + +func (s SchemaType) GetSchemaID() string { + return schemas[s].id +} + +// AddSchemaResources adds the specified schemas including their dependencies to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddSchemaResources(c interface { + AddResource(url string, r io.Reader) error +}, opts ...SchemaType) error { + var sc []*Schema + + for _, o := range opts { + s, err := getSchema(o) + if err != nil { + return err + } + sc = append(sc, s) + } + + if err := addSchemaResources(c, sc); err != nil { + return err + } + + if err := otelx.AddConfigSchema(c); err != nil { + return err + } + + return configx.AddSchemaResources(c) +} + +func addSchemaResources(c interface { + AddResource(url string, r io.Reader) error +}, schemas []*Schema) error { + for _, s := range schemas { + if err := c.AddResource(s.id, bytes.NewReader(s.data)); err != nil { + return errors.WithStack(err) + } + if s.dependencies != nil { + if err := addSchemaResources(c, s.dependencies); err != nil { + return err + } + } + } + return nil +} diff --git a/embedx/embedx_test.go b/embedx/embedx_test.go new file mode 100644 index 0000000..c06410f --- /dev/null +++ b/embedx/embedx_test.go @@ -0,0 +1,106 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package embedx + +import ( + "context" + "embed" + "io/fs" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/assert" + + "github.com/ory/jsonschema/v3" +) + +func TestAddSchemaResources(t *testing.T) { + for _, tc := range []struct { + description string + dependencies []SchemaType + extraDependencies []SchemaType + mustFail bool + failMessage string + }{ + { + description: "config schema", + dependencies: []SchemaType{Config}, + extraDependencies: nil, + }, + { + description: "identity schema with dependencies", + dependencies: []SchemaType{IdentityMeta}, + extraDependencies: []SchemaType{IdentityExtension}, + }, + { + description: "multiple schemas", + dependencies: []SchemaType{Config, IdentityMeta, IdentityExtension}, + extraDependencies: nil, + }, + { + description: "verify dependencies are also loaded", + dependencies: []SchemaType{IdentityMeta}, + extraDependencies: []SchemaType{IdentityExtension}, + }, + { + description: "must fail on unsupported schema types", + dependencies: []SchemaType{4, 10}, + mustFail: true, + failMessage: "the specified schema type (4) is not supported", + }, + } { + t.Run("case="+tc.description, func(t *testing.T) { + c := jsonschema.NewCompiler() + err := AddSchemaResources(c, tc.dependencies...) + + if tc.mustFail { + assert.Errorf(t, err, "an error must be thrown on `%s`", tc.description) + assert.EqualError(t, err, tc.failMessage) + } else { + assert.NoError(t, err) + } + + if !tc.mustFail { + for _, s := range append(tc.dependencies, tc.extraDependencies...) { + _, err := c.Compile(context.Background(), s.GetSchemaID()) + assert.NoError(t, err) + } + } + }) + } +} + +//go:embed testdata/identity_meta.* +var identityMetaTestCases embed.FS + +func TestIdentityMetaSchema(t *testing.T) { + c := jsonschema.NewCompiler() + err := AddSchemaResources(c, IdentityMeta, IdentityExtension) + require.NoError(t, err) + + schema, err := c.Compile(context.Background(), IdentityMeta.GetSchemaID()) + require.NoError(t, err) + + require.NoError(t, fs.WalkDir(identityMetaTestCases, ".", func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + + t.Run("case="+path, func(t *testing.T) { + f, err := identityMetaTestCases.Open(path) + require.NoError(t, err) + + err = schema.Validate(f) + if strings.HasSuffix(path, "invalid.schema.json") { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + + return nil + })) +} diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json new file mode 100644 index 0000000..cd68dcc --- /dev/null +++ b/embedx/identity_extension.schema.json @@ -0,0 +1,108 @@ +{ + "$id": "ory://identity-extension", + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "properties": { + "ory.sh/kratos": { + "type": "object", + "additionalProperties": false, + "properties": { + "credentials": { + "type": "object", + "additionalProperties": false, + "properties": { + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "boolean" + } + } + }, + "webauthn": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "boolean" + } + } + }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "boolean" + } + } + }, + "totp": { + "type": "object", + "additionalProperties": false, + "properties": { + "account_name": { + "type": "boolean" + } + } + }, + "code": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "boolean" + }, + "via": { + "type": "string", + "enum": ["email", "sms"] + } + } + } + } + }, + "verification": { + "type": "object", + "additionalProperties": false, + "properties": { + "via": { + "type": "string", + "enum": ["email", "sms"] + } + } + }, + "recovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "via": { + "type": "string", + "enum": ["email", "sms"] + } + } + }, + "organizations": { + "type": "object", + "additionalProperties": false, + "properties": { + "matcher": { + "type": "string", + "enum": ["email_domain"] + } + } + } + } + } + } + }, + { + "patternProperties": { + ".*": { + "$ref": "#" + } + } + } + ] +} diff --git a/embedx/identity_meta.schema.json b/embedx/identity_meta.schema.json new file mode 100644 index 0000000..27c2c8e --- /dev/null +++ b/embedx/identity_meta.schema.json @@ -0,0 +1,70 @@ +{ + "$id": "ory://identity-meta", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "allOf": [ + { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + { + "properties": { + "properties": { + "type": "object", + "required": ["traits"], + "properties": { + "traits": { + "type": "object", + "required": ["properties"], + "properties": { + "type": { + "const": "object" + }, + "properties": { + "type": "object", + "minProperties": 1, + "patternProperties": { + ".*": { + "type": "object", + "if": { + "properties": { + "ory.sh/kratos": { + "type": "object", + "properties": { + "verification": {} + }, + "required": ["verification"] + } + }, + "required": ["ory.sh/kratos"] + }, + "then": { + "properties": { + "format": { + "enum": [ + "email", + "tel", + "date", + "time", + "date-time", + "no-validate" + ] + } + } + }, + "allOf": [ + { + "$ref": "ory://identity-extension" + } + ] + } + } + } + } + } + } + } + }, + "required": ["properties"] + } + ] +} diff --git a/embedx/testdata/identity_meta.no_traits.invalid.schema.json b/embedx/testdata/identity_meta.no_traits.invalid.schema.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/embedx/testdata/identity_meta.no_traits.invalid.schema.json @@ -0,0 +1 @@ +{} diff --git a/embedx/testdata/identity_meta.simple.valid.schema.json b/embedx/testdata/identity_meta.simple.valid.schema.json new file mode 100644 index 0000000..6f090b3 --- /dev/null +++ b/embedx/testdata/identity_meta.simple.valid.schema.json @@ -0,0 +1,23 @@ +{ + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/embedx/testdata/identity_meta.verification_format.invalid.schema.json b/embedx/testdata/identity_meta.verification_format.invalid.schema.json new file mode 100644 index 0000000..1dc46bd --- /dev/null +++ b/embedx/testdata/identity_meta.verification_format.invalid.schema.json @@ -0,0 +1,23 @@ +{ + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "unknown", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/examples/go/identity/create/main.go b/examples/go/identity/create/main.go new file mode 100644 index 0000000..be515e9 --- /dev/null +++ b/examples/go/identity/create/main.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + ory "github.com/ory/client-go" + "github.com/ory/kratos/examples/go/pkg" + "github.com/ory/kratos/x" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func createIdentity() *ory.Identity { + ctx := context.Background() + + identity, res, err := client.IdentityAPI.CreateIdentity(ctx). + CreateIdentityBody(ory.CreateIdentityBody{ + SchemaId: "default", + Traits: map[string]interface{}{ + "email": "dev+" + x.NewUUID().String() + "@ory.sh", + }, + }).Execute() + pkg.SDKExitOnError(err, res) + + return identity +} + +func main() { + pkg.PrintJSONPretty(createIdentity()) +} diff --git a/examples/go/identity/create/main_test.go b/examples/go/identity/create/main_test.go new file mode 100644 index 0000000..850595e --- /dev/null +++ b/examples/go/identity/create/main_test.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/examples/go/pkg" +) + +func TestFunc(t *testing.T) { + client = pkg.TestClient(t) + require.NotEmpty(t, createIdentity().Id) +} diff --git a/examples/go/identity/delete/main.go b/examples/go/identity/delete/main.go new file mode 100644 index 0000000..6857c2a --- /dev/null +++ b/examples/go/identity/delete/main.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "github.com/ory/kratos/examples/go/pkg" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func deleteIdentity() { + ctx := context.Background() + + identity := pkg.CreateIdentity(client) + + res, err := client.IdentityAPI.DeleteIdentity(ctx, identity.Id).Execute() + pkg.SDKExitOnError(err, res) +} + +func main() { + deleteIdentity() + fmt.Println("Success") +} diff --git a/examples/go/identity/delete/main_test.go b/examples/go/identity/delete/main_test.go new file mode 100644 index 0000000..3e7bd1d --- /dev/null +++ b/examples/go/identity/delete/main_test.go @@ -0,0 +1,18 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + deleteIdentity() +} diff --git a/examples/go/identity/get/main.go b/examples/go/identity/get/main.go new file mode 100644 index 0000000..dcd5cef --- /dev/null +++ b/examples/go/identity/get/main.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + ory "github.com/ory/client-go" + "github.com/ory/kratos/examples/go/pkg" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func getIdentity() *ory.Identity { + ctx := context.Background() + created := pkg.CreateIdentity(client) + + identity, res, err := client.IdentityAPI.GetIdentity(ctx, created.Id).IncludeCredential([]string{"password"}).Execute() + pkg.SDKExitOnError(err, res) + + return identity +} + +func main() { + pkg.PrintJSONPretty(getIdentity()) +} diff --git a/examples/go/identity/get/main_test.go b/examples/go/identity/get/main_test.go new file mode 100644 index 0000000..d5185d5 --- /dev/null +++ b/examples/go/identity/get/main_test.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestFunc(t *testing.T) { + client = pkg.TestClient(t) + require.NotEmpty(t, getIdentity().Id) +} diff --git a/examples/go/identity/update/main.go b/examples/go/identity/update/main.go new file mode 100644 index 0000000..46b868f --- /dev/null +++ b/examples/go/identity/update/main.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + ory "github.com/ory/client-go" + "github.com/ory/kratos/examples/go/pkg" + "github.com/ory/kratos/x" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func updateIdentity() *ory.Identity { + ctx := context.Background() + toUpdate := pkg.CreateIdentity(client) + + identity, res, err := client.IdentityAPI.UpdateIdentity(ctx, toUpdate.Id).UpdateIdentityBody(ory.UpdateIdentityBody{ + Traits: map[string]interface{}{ + "email": "dev+not-" + x.NewUUID().String() + "@ory.sh", + }, + }).Execute() + pkg.SDKExitOnError(err, res) + + return identity +} + +func main() { + pkg.PrintJSONPretty(updateIdentity()) +} diff --git a/examples/go/identity/update/main_test.go b/examples/go/identity/update/main_test.go new file mode 100644 index 0000000..28cd8d7 --- /dev/null +++ b/examples/go/identity/update/main_test.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + identity := updateIdentity() + require.NotEmpty(t, identity.Id) + assert.Contains(t, identity.Traits.(map[string]interface{})["email"].(string), "dev+not") +} diff --git a/examples/go/pkg/common.go b/examples/go/pkg/common.go new file mode 100644 index 0000000..f145eb1 --- /dev/null +++ b/examples/go/pkg/common.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pkg + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/cookiejar" + "os" + "testing" + + ory "github.com/ory/client-go" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" +) + +func PrintJSONPretty(v interface{}) { + out, _ := json.MarshalIndent(v, "", " ") + fmt.Println(string(out)) +} + +func TestClient(t *testing.T) *ory.APIClient { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + return NewSDKForSelfHosted(publicURL) +} + +func NewSDK(project string) *ory.APIClient { + return NewSDKForSelfHosted(fmt.Sprintf("https://%s.projects.oryapis.com/api/kratos/public", project)) +} + +func NewSDKForSelfHosted(endpoint string) *ory.APIClient { + conf := ory.NewConfiguration() + conf.Servers = ory.ServerConfigurations{{URL: endpoint}} + cj, _ := cookiejar.New(nil) + conf.HTTPClient = &http.Client{Jar: cj} + return ory.NewAPIClient(conf) +} + +func ExitOnError(err error) { + if err == nil { + return + } + out, _ := json.MarshalIndent(err, "", " ") + fmt.Printf("%s\n\nAn error occurred: %+v\n", out, err) + os.Exit(1) +} + +func SDKExitOnError(err error, res *http.Response) { + if err == nil { + return + } + var body []byte + if res != nil { + body, _ = json.MarshalIndent(json.RawMessage(x.MustReadAll(res.Body)), "", " ") + } + out, _ := json.MarshalIndent(err, "", " ") + fmt.Printf("%s\n\nAn error occurred: %+v\nbody: %s\n", out, err, body) + os.Exit(1) +} diff --git a/examples/go/pkg/resources.go b/examples/go/pkg/resources.go new file mode 100644 index 0000000..a6cd432 --- /dev/null +++ b/examples/go/pkg/resources.go @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pkg + +import ( + "context" + "log" + "strings" + + "github.com/gofrs/uuid" + + ory "github.com/ory/client-go" +) + +func RandomCredentials() (email, password string) { + email = "dev+" + uuid.Must(uuid.NewV4()).String() + "@ory.sh" + password = strings.ReplaceAll(uuid.Must(uuid.NewV4()).String(), "-", "") + return +} + +// CreateIdentityWithSession creates an identity and an Ory Session Token for it. +func CreateIdentityWithSession(c *ory.APIClient, email, password string) (*ory.Session, string) { + ctx := context.Background() + + if email == "" { + email, _ = RandomCredentials() + } + + if password == "" { + _, password = RandomCredentials() + } + + // Initialize a registration flow + flow, _, err := c.FrontendAPI.CreateNativeRegistrationFlow(ctx).Execute() + ExitOnError(err) + + // Submit the registration flow + result, res, err := c.FrontendAPI.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( + ory.UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(&ory.UpdateRegistrationFlowWithPasswordMethod{ + Method: "password", + Password: password, + Traits: map[string]interface{}{"email": email}, + }), + ).Execute() + SDKExitOnError(err, res) + + if result.Session == nil { + log.Fatalf("The server is expected to create sessions for new registrations.") + } + + return result.Session, *result.SessionToken +} + +func CreateIdentity(c *ory.APIClient) *ory.Identity { + ctx := context.Background() + + email, _ := RandomCredentials() + identity, _, err := c.IdentityAPI.CreateIdentity(ctx).CreateIdentityBody(ory.CreateIdentityBody{ + SchemaId: "default", + Traits: map[string]interface{}{ + "email": email, + }}).Execute() + ExitOnError(err) + return identity +} diff --git a/examples/go/pkg/stub/identity.schema.json b/examples/go/pkg/stub/identity.schema.json new file mode 100644 index 0000000..e49651b --- /dev/null +++ b/examples/go/pkg/stub/identity.schema.json @@ -0,0 +1,36 @@ +{ + "$id": "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 + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/examples/go/pkg/stub/kratos.yaml b/examples/go/pkg/stub/kratos.yaml new file mode 100644 index 0000000..da76a28 --- /dev/null +++ b/examples/go/pkg/stub/kratos.yaml @@ -0,0 +1,87 @@ +version: v0.6.3-alpha.1 + +dsn: memory + +serve: + public: + cors: + enabled: true + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + allowed_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + link: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + use: link + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + use: link + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - hook: session + +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: + argon2: + parallelism: 1 + memory: 128MB + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_id: default + schemas: + - id: default + url: file://../../pkg/stub/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/examples/go/selfservice/error/main.go b/examples/go/selfservice/error/main.go new file mode 100644 index 0000000..ca9eda5 --- /dev/null +++ b/examples/go/selfservice/error/main.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func getError() *ory.FlowError { + e, res, err := client.FrontendAPI.GetFlowError(context.Background()).Id("stub:500").Execute() + pkg.SDKExitOnError(err, res) + return e +} + +func main() { + pkg.PrintJSONPretty( + getError(), + ) +} diff --git a/examples/go/selfservice/error/main_test.go b/examples/go/selfservice/error/main_test.go new file mode 100644 index 0000000..a5bd6e2 --- /dev/null +++ b/examples/go/selfservice/error/main_test.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestError(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + e := getError() + require.NotEmpty(t, e.Id) +} diff --git a/examples/go/selfservice/login/main.go b/examples/go/selfservice/login/main.go new file mode 100644 index 0000000..c94f67e --- /dev/null +++ b/examples/go/selfservice/login/main.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func performLogin() *ory.SuccessfulNativeLogin { + ctx := context.Background() + + // Create a temporary user + email, password := pkg.RandomCredentials() + _, _ = pkg.CreateIdentityWithSession(client, email, password) + + // Initialize the flow + flow, res, err := client.FrontendAPI.CreateNativeLoginFlow(ctx).Execute() + pkg.SDKExitOnError(err, res) + + // If you want, print the flow here: + // + // pkg.PrintJSONPretty(flow) + + // Submit the form + result, res, err := client.FrontendAPI.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody( + ory.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(&ory.UpdateLoginFlowWithPasswordMethod{ + Method: "password", + Password: password, + PasswordIdentifier: &email, + }), + ).Execute() + pkg.SDKExitOnError(err, res) + + return result +} + +func main() { + pkg.PrintJSONPretty( + performLogin(), + ) +} diff --git a/examples/go/selfservice/login/main_test.go b/examples/go/selfservice/login/main_test.go new file mode 100644 index 0000000..cf02e8c --- /dev/null +++ b/examples/go/selfservice/login/main_test.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + result := performLogin() + require.NotEmpty(t, result.Session) +} diff --git a/examples/go/selfservice/logout/main.go b/examples/go/selfservice/logout/main.go new file mode 100644 index 0000000..9d2bd02 --- /dev/null +++ b/examples/go/selfservice/logout/main.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func performLogout() { + // Create a temporary user + email, password := pkg.RandomCredentials() + _, sessionToken := pkg.CreateIdentityWithSession(client, email, password) + + // Log out using session token + res, err := client.FrontendAPI.PerformNativeLogout(context.Background()). + PerformNativeLogoutBody(ory.PerformNativeLogoutBody{SessionToken: sessionToken}).Execute() + pkg.SDKExitOnError(err, res) +} + +func main() { + performLogout() + fmt.Println("Logout successful!") +} diff --git a/examples/go/selfservice/logout/main_test.go b/examples/go/selfservice/logout/main_test.go new file mode 100644 index 0000000..90ea4ab --- /dev/null +++ b/examples/go/selfservice/logout/main_test.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + performLogout() +} diff --git a/examples/go/selfservice/recovery/main.go b/examples/go/selfservice/recovery/main.go new file mode 100644 index 0000000..790d5dc --- /dev/null +++ b/examples/go/selfservice/recovery/main.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func performRecovery(email string) *ory.RecoveryFlow { + ctx := context.Background() + + // Initialize the flow + flow, res, err := client.FrontendAPI.CreateNativeRecoveryFlow(ctx).Execute() + pkg.SDKExitOnError(err, res) + + // If you want, print the flow here: + // + // pkg.PrintJSONPretty(flow) + + // Submit the form + afterSubmit, res, err := client.FrontendAPI.UpdateRecoveryFlow(ctx).Flow(flow.Id). + UpdateRecoveryFlowBody(ory.UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(&ory.UpdateRecoveryFlowWithLinkMethod{ + Email: email, + Method: "link", + })).Execute() + pkg.SDKExitOnError(err, res) + + return afterSubmit +} + +func main() { + pkg.PrintJSONPretty( + performRecovery("someone@foobar.com"), + ) +} diff --git a/examples/go/selfservice/recovery/main_test.go b/examples/go/selfservice/recovery/main_test.go new file mode 100644 index 0000000..1a63254 --- /dev/null +++ b/examples/go/selfservice/recovery/main_test.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/examples/go/pkg" + ory "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + flow := performRecovery("dev+" + uuid.Must(uuid.NewV4()).String() + "@ory.sh") + require.NotEmpty(t, flow.Id) + assert.EqualValues(t, ory.RECOVERYFLOWSTATE_SENT_EMAIL, flow.State) +} diff --git a/examples/go/selfservice/registration/main.go b/examples/go/selfservice/registration/main.go new file mode 100644 index 0000000..a51e0e2 --- /dev/null +++ b/examples/go/selfservice/registration/main.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func initRegistration() *ory.SuccessfulNativeRegistration { + ctx := context.Background() + + // Initialize the flow + flow, res, err := client.FrontendAPI.CreateNativeRegistrationFlow(ctx).Execute() + pkg.SDKExitOnError(err, res) + + // If you want, print the flow here: + // + // pkg.PrintJSONPretty(flow) + + email, password := pkg.RandomCredentials() + + // Submit the registration flow + result, res, err := client.FrontendAPI.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( + ory.UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(&ory.UpdateRegistrationFlowWithPasswordMethod{ + Method: "password", + Password: password, + Traits: map[string]interface{}{"email": email}, + }), + ).Execute() + pkg.SDKExitOnError(err, res) + return result +} + +func main() { + pkg.PrintJSONPretty( + initRegistration(), + ) +} diff --git a/examples/go/selfservice/registration/main_test.go b/examples/go/selfservice/registration/main_test.go new file mode 100644 index 0000000..03ad7c1 --- /dev/null +++ b/examples/go/selfservice/registration/main_test.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + result := initRegistration() + require.NotEmpty(t, result.Identity.Id) +} diff --git a/examples/go/selfservice/settings/main.go b/examples/go/selfservice/settings/main.go new file mode 100644 index 0000000..725b3d8 --- /dev/null +++ b/examples/go/selfservice/settings/main.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +var ctx = context.Background() + +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() + pkg.SDKExitOnError(err, res) + + // If you want, print the flow here: + // + // pkg.PrintJSONPretty(flow) + + return sessionToken, flow +} + +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( + ory.UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(&ory.UpdateSettingsFlowWithPasswordMethod{ + Method: "password", + Password: "not-" + password, + }), + ).Execute() + pkg.SDKExitOnError(err, res) + + return result +} + +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( + ory.UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody(&ory.UpdateSettingsFlowWithProfileMethod{ + Method: "profile", + Traits: map[string]interface{}{ + "email": "not-" + email, + }, + }), + ).Execute() + pkg.SDKExitOnError(err, res) + + return result +} + +func main() { + pkg.PrintJSONPretty( + changePassword(pkg.RandomCredentials()), + ) + pkg.PrintJSONPretty( + changeTraits(pkg.RandomCredentials()), + ) +} diff --git a/examples/go/selfservice/settings/main_test.go b/examples/go/selfservice/settings/main_test.go new file mode 100644 index 0000000..ec16c06 --- /dev/null +++ b/examples/go/selfservice/settings/main_test.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + ory "github.com/ory/kratos/pkg/httpclient" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestSettings(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + email, password := pkg.RandomCredentials() + result := changePassword(email, password) + require.NotEmpty(t, result.Id) + assert.EqualValues(t, ory.SETTINGSFLOWSTATE_SUCCESS, result.State) + + email, password = pkg.RandomCredentials() + result = changeTraits(email, password) + require.NotEmpty(t, result.Id) + assert.EqualValues(t, ory.SETTINGSFLOWSTATE_SUCCESS, result.State) + assert.Equal(t, "not-"+email, result.Identity.Traits.(map[string]interface{})["email"].(string)) +} diff --git a/examples/go/selfservice/verification/main.go b/examples/go/selfservice/verification/main.go new file mode 100644 index 0000000..86feb5b --- /dev/null +++ b/examples/go/selfservice/verification/main.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func performVerification(email string) *ory.VerificationFlow { + ctx := context.Background() + + // Initialize the flow + flow, res, err := client.FrontendAPI.CreateNativeVerificationFlow(ctx).Execute() + pkg.SDKExitOnError(err, res) + + // If you want, print the flow here: + // + // pkg.PrintJSONPretty(flow) + + // Submit the form + afterSubmit, res, err := client.FrontendAPI.UpdateVerificationFlow(ctx).Flow(flow.Id). + UpdateVerificationFlowBody(ory.UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(&ory.UpdateVerificationFlowWithLinkMethod{ + Email: email, + Method: "link", + })).Execute() + pkg.SDKExitOnError(err, res) + + return afterSubmit +} + +func main() { + pkg.PrintJSONPretty( + performVerification("someone@foobar.com"), + ) +} diff --git a/examples/go/selfservice/verification/main_test.go b/examples/go/selfservice/verification/main_test.go new file mode 100644 index 0000000..baf7319 --- /dev/null +++ b/examples/go/selfservice/verification/main_test.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/examples/go/pkg" + ory "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + flow := performVerification("dev+" + uuid.Must(uuid.NewV4()).String() + "@ory.sh") + require.NotEmpty(t, flow.Id) + assert.EqualValues(t, ory.VERIFICATIONFLOWSTATE_SENT_EMAIL, flow.State) +} diff --git a/examples/go/session/tosession/main.go b/examples/go/session/tosession/main.go new file mode 100644 index 0000000..34ef34a --- /dev/null +++ b/examples/go/session/tosession/main.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "github.com/ory/kratos/examples/go/pkg" + + ory "github.com/ory/client-go" +) + +// If you use Open Source this would be: +// +// var client = pkg.NewSDKForSelfHosted("http://127.0.0.1:4433") +var client = pkg.NewSDK("playground") + +func toSession() *ory.Session { + // Create a temporary user + email, password := pkg.RandomCredentials() + _, sessionToken := pkg.CreateIdentityWithSession(client, email, password) + + session, res, err := client.FrontendAPI.ToSessionExecute(ory.FrontendAPIToSessionRequest{}. + XSessionToken(sessionToken)) + pkg.SDKExitOnError(err, res) + return session +} + +func main() { + session := toSession() + pkg.PrintJSONPretty(session) +} diff --git a/examples/go/session/tosession/main_test.go b/examples/go/session/tosession/main_test.go new file mode 100644 index 0000000..c066cdf --- /dev/null +++ b/examples/go/session/tosession/main_test.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/examples/go/pkg" + + "github.com/stretchr/testify/require" +) + +func TestFunc(t *testing.T) { + publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml", nil) + client = pkg.NewSDKForSelfHosted(publicURL) + + session := toSession() + require.NotEmpty(t, session.Id) + assert.Contains(t, session.Identity.Traits.(map[string]interface{})["email"].(string), "dev+") +} diff --git a/gen/oidc/v1/state.pb.go b/gen/oidc/v1/state.pb.go new file mode 100644 index 0000000..a205bd7 --- /dev/null +++ b/gen/oidc/v1/state.pb.go @@ -0,0 +1,236 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: oidc/v1/state.proto + +package oidcv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + 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 FlowKind int32 + +const ( + FlowKind_FLOW_KIND_UNSPECIFIED FlowKind = 0 + FlowKind_FLOW_KIND_LOGIN FlowKind = 1 + FlowKind_FLOW_KIND_REGISTRATION FlowKind = 2 + FlowKind_FLOW_KIND_SETTINGS FlowKind = 3 +) + +// Enum value maps for FlowKind. +var ( + FlowKind_name = map[int32]string{ + 0: "FLOW_KIND_UNSPECIFIED", + 1: "FLOW_KIND_LOGIN", + 2: "FLOW_KIND_REGISTRATION", + 3: "FLOW_KIND_SETTINGS", + } + FlowKind_value = map[string]int32{ + "FLOW_KIND_UNSPECIFIED": 0, + "FLOW_KIND_LOGIN": 1, + "FLOW_KIND_REGISTRATION": 2, + "FLOW_KIND_SETTINGS": 3, + } +) + +func (x FlowKind) Enum() *FlowKind { + p := new(FlowKind) + *p = x + return p +} + +func (x FlowKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FlowKind) Descriptor() protoreflect.EnumDescriptor { + return file_oidc_v1_state_proto_enumTypes[0].Descriptor() +} + +func (FlowKind) Type() protoreflect.EnumType { + return &file_oidc_v1_state_proto_enumTypes[0] +} + +func (x FlowKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FlowKind.Descriptor instead. +func (FlowKind) EnumDescriptor() ([]byte, []int) { + return file_oidc_v1_state_proto_rawDescGZIP(), []int{0} +} + +type State struct { + state protoimpl.MessageState `protogen:"open.v1"` + 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"` + FlowKind FlowKind `protobuf:"varint,5,opt,name=flow_kind,json=flowKind,proto3,enum=oidc.v1.FlowKind" json:"flow_kind,omitempty"` + // request_base_url is the scheme://host[:port] the browser used at flow + // init. It is captured so the OIDC/SAML callback can be redirected back + // onto the developer proxy / custom domain the user started on. Empty + // when there is nothing to rewrite. + RequestBaseUrl string `protobuf:"bytes,6,opt,name=request_base_url,json=requestBaseUrl,proto3" json:"request_base_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *State) Reset() { + *x = State{} + 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 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 "" +} + +func (x *State) GetFlowKind() FlowKind { + if x != nil { + return x.FlowKind + } + return FlowKind_FLOW_KIND_UNSPECIFIED +} + +func (x *State) GetRequestBaseUrl() string { + if x != nil { + return x.RequestBaseUrl + } + return "" +} + +var File_oidc_v1_state_proto protoreflect.FileDescriptor + +const file_oidc_v1_state_proto_rawDesc = "" + + "\n" + + "\x13oidc/v1/state.proto\x12\aoidc.v1\"\x8c\x02\n" + + "\x05State\x12\x17\n" + + "\aflow_id\x18\x01 \x01(\fR\x06flowId\x12J\n" + + "\"session_token_exchange_code_sha512\x18\x02 \x01(\fR\x1esessionTokenExchangeCodeSha512\x12\x1f\n" + + "\vprovider_id\x18\x03 \x01(\tR\n" + + "providerId\x12#\n" + + "\rpkce_verifier\x18\x04 \x01(\tR\fpkceVerifier\x12.\n" + + "\tflow_kind\x18\x05 \x01(\x0e2\x11.oidc.v1.FlowKindR\bflowKind\x12(\n" + + "\x10request_base_url\x18\x06 \x01(\tR\x0erequestBaseUrl*n\n" + + "\bFlowKind\x12\x19\n" + + "\x15FLOW_KIND_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fFLOW_KIND_LOGIN\x10\x01\x12\x1a\n" + + "\x16FLOW_KIND_REGISTRATION\x10\x02\x12\x16\n" + + "\x12FLOW_KIND_SETTINGS\x10\x03B|\n" + + "\vcom.oidc.v1B\n" + + "StateProtoP\x01Z$github.com/ory/kratos/oidc/v1;oidcv1\xa2\x02\x03OXX\xaa\x02\aOidc.V1\xca\x02\aOidc\\V1\xe2\x02\x13Oidc\\V1\\GPBMetadata\xea\x02\bOidc::V1b\x06proto3" + +var ( + file_oidc_v1_state_proto_rawDescOnce sync.Once + file_oidc_v1_state_proto_rawDescData []byte +) + +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(unsafe.Slice(unsafe.StringData(file_oidc_v1_state_proto_rawDesc), len(file_oidc_v1_state_proto_rawDesc))) + }) + return file_oidc_v1_state_proto_rawDescData +} + +var file_oidc_v1_state_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_oidc_v1_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_oidc_v1_state_proto_goTypes = []any{ + (FlowKind)(0), // 0: oidc.v1.FlowKind + (*State)(nil), // 1: oidc.v1.State +} +var file_oidc_v1_state_proto_depIdxs = []int32{ + 0, // 0: oidc.v1.State.flow_kind:type_name -> oidc.v1.FlowKind + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] 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 + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_oidc_v1_state_proto_rawDesc), len(file_oidc_v1_state_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_oidc_v1_state_proto_goTypes, + DependencyIndexes: file_oidc_v1_state_proto_depIdxs, + EnumInfos: file_oidc_v1_state_proto_enumTypes, + MessageInfos: file_oidc_v1_state_proto_msgTypes, + }.Build() + File_oidc_v1_state_proto = out.File + file_oidc_v1_state_proto_goTypes = nil + file_oidc_v1_state_proto_depIdxs = nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eab5680 --- /dev/null +++ b/go.mod @@ -0,0 +1,332 @@ +module github.com/ory/kratos + +go 1.26 + +replace ( + github.com/coreos/go-oidc/v3 => github.com/ory/go-oidc/v3 v3.0.0-20250124100243-69986dfaf891 + + 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 + + github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 + + // 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. + github.com/ory/client-go => ./pkg/client-go + + github.com/ory/x => ./oryx +) + +require ( + dario.cat/mergo v1.0.2 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 + github.com/bradleyjkemp/cupaloy/v2 v2.8.0 + github.com/bwmarrin/discordgo v0.28.1 + github.com/coreos/go-oidc/v3 v3.11.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/dghubble/oauth1 v0.7.3 + github.com/dgraph-io/ristretto/v2 v2.4.0 + github.com/fatih/color v1.19.0 + github.com/ghodss/yaml v1.0.0 + 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-webauthn/webauthn v0.11.2 + github.com/gobuffalo/httptest v1.5.2 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 + github.com/golang/mock v1.6.0 + github.com/google/go-jsonnet v0.21.0 + github.com/gorilla/sessions v1.3.0 + github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf + github.com/jarcoal/httpmock v1.3.1 + github.com/jmoiron/sqlx v1.4.0 + github.com/knadh/koanf/parsers/json v0.1.0 + github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 // indirect + github.com/lestrrat-go/jwx/v2 v2.1.1 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/montanaflynn/stats v0.7.1 + github.com/ory/analytics-go/v5 v5.0.1 + github.com/ory/client-go v0.0.0-00010101000000-000000000000 + github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 + github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 + github.com/ory/hydra-client-go/v2 v2.2.1 + github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e + github.com/ory/mail/v3 v3.0.1-0.20260413103059-df54acc74133 + github.com/ory/nosurf v1.2.7 + github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 + github.com/peterhellberg/link v1.2.0 + 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.2.0 + github.com/rs/cors v1.11.1 + github.com/samber/lo v1.46.0 + github.com/sirupsen/logrus v1.9.3 + github.com/slack-go/slack v0.23.1 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.19.0 + github.com/tidwall/sjson v1.2.5 + github.com/urfave/negroni v1.0.0 + github.com/wI2L/jsondiff v0.6.0 + github.com/zmb3/spotify/v2 v2.4.2 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + golang.org/x/crypto v0.53.0 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.56.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + golang.org/x/text v0.39.0 + google.golang.org/grpc v1.82.1 +) + +require ( + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/cockroachdb/cockroach-go/v2 v2.4.3 + github.com/google/go-github/v89 v89.0.0 + github.com/gorilla/pat v1.0.2 + github.com/ian-kent/go-log v0.0.0-20160113211217-5731446c36ab + github.com/mailhog/MailHog-Server v1.0.1 + github.com/mailhog/data v1.0.1 + github.com/moby/moby/api v1.54.2 + github.com/ory/dockertest/v4 v4.0.0-beta.4 + github.com/ory/x v0.0.0-00010101000000-000000000000 +) + +require ( + connectrpc.com/connect v1.20.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/XSAM/otelsql v0.41.0 // indirect + github.com/a8m/envsubst v1.4.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cortesi/modd v0.8.1 // indirect + github.com/cortesi/moddwatch v0.1.0 // indirect + github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/elliotchance/orderedmap v1.7.1 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/inflect 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/validate v0.24.0 // indirect + github.com/go-swagger/go-swagger v0.31.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobuffalo/plush/v5 v5.0.7 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/ian-kent/envconf v0.0.0-20141026121121-c19809918c02 // 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/jackc/pgx/v5 v5.10.0 // indirect + github.com/jaegertracing/jaeger-idl v0.9.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/landlock-lsm/go-landlock v0.8.1 // indirect + github.com/mailhog/MailHog v1.0.1 // indirect + github.com/mailhog/MailHog-UI v1.0.1 // indirect + github.com/mailhog/http v1.0.1 // indirect + github.com/mailhog/mhsendmail v0.2.0 // indirect + github.com/mailhog/smtp v1.0.1 // indirect + github.com/mailhog/storage v1.0.1 // indirect + github.com/mikefarah/yq/v4 v4.45.1 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/ogier/pflag v0.0.1 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rjeczalik/notify v0.9.3 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/smartystreets/goconvey v1.8.1 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.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/tinylib/msgp v1.2.5 // indirect + github.com/toqueteos/webbrowser v1.2.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/tools v0.47.0 // indirect + gopkg.in/alecthomas/kingpin.v2 v2.2.6 // 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 + kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect + modernc.org/libc v1.72.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.52.0 // indirect + mvdan.cc/sh/v3 v3.6.0 // indirect +) + +require ( + code.dny.dev/ssrf v0.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/avast/retry-go/v4 v4.6.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/boombuler/barcode v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-crypt/x v0.2.18 // indirect + github.com/go-jose/go-jose/v3 v3.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/jsonpointer v0.21.2 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-webauthn/x v0.1.14 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/fizz v1.14.4 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.10 // indirect + github.com/gobuffalo/nulls v0.4.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.22 // 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.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/go-tpm v0.9.1 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/securecookie v1.1.2 + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // 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.2 // indirect + github.com/knadh/koanf/parsers/toml v0.1.0 // indirect + github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect + github.com/knadh/koanf/providers/posflag v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.2.2 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // 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.31 + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.12.0 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/nyaruka/phonenumbers v1.7.4 + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.68.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // 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.1.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/cast v1.9.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.44.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect; / indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.44.0 // indirect; / indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sys v0.46.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/protobuf v1.36.11 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +tool ( + github.com/cortesi/modd/cmd/modd + github.com/go-swagger/go-swagger/cmd/swagger + github.com/mailhog/MailHog + github.com/mikefarah/yq/v4 + golang.org/x/tools/cmd/goimports +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3b3ff75 --- /dev/null +++ b/go.sum @@ -0,0 +1,1200 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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.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= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +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/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= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +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= +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= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +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/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8= +github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/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/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +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-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +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/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/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +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/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= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +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.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/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +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/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= +github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +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/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +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/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elliotchance/orderedmap v1.7.1 h1:8SR2DB391dw0HVI9572ElrY+KU0Q89OCXYwWZx7aAZc= +github.com/elliotchance/orderedmap v1.7.1/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +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/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +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/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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +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.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.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +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.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +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/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/helpers v0.6.10 h1:puKDCOrJ0EIq5ScnTRgKyvEZ05xQa+gwRGCpgoh6Ek8= +github.com/gobuffalo/helpers v0.6.10/go.mod h1:r52L6VSnByLJFOmURp1irvzgSakk7RodChi1YbGwk8I= +github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= +github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= +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/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.22 h1:bPQr5PsiTg54UGMsfvnIAvFmUfxzD/ri+wbpu7PlmTM= +github.com/gobuffalo/plush/v4 v4.1.22/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/plush/v5 v5.0.7 h1:nI8sIt5tZAN2tCZHeaXkH7HAvxvvk3sJHG2TtrKeSHM= +github.com/gobuffalo/plush/v5 v5.0.7/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU= +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/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.2.0+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/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +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.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +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/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= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/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.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/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= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0= +github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho= +github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= +github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.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/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= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +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-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +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/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/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +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/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/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +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 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.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +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/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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.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= +github.com/ian-kent/go-log v0.0.0-20160113211217-5731446c36ab/go.mod h1:6HitiSDIbT2r0dab4CoKoMAtR7tb0ORQ3OmjkjCZ+zk= +github.com/ian-kent/goose v0.0.0-20141221090059-c3541ea826ad h1:5UZIY1lPvsBrRQRgyt00lJ1J6HH6CwWAVQB6azyAA1c= +github.com/ian-kent/goose v0.0.0-20141221090059-c3541ea826ad/go.mod h1:VHyJj0/IJFmpYvVqWFIN2HgjCatXujj7XaLLyOMC23M= +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-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +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/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +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/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaegertracing/jaeger-idl v0.9.0 h1:dI4olA7ArW3cjXwVbic/aYKDbdlfe7V+9wPQqAdzu8Y= +github.com/jaegertracing/jaeger-idl v0.9.0/go.mod h1:W+9vbcr2cVZyS6z/cbr540EOzSkKYml3hmaWEavxkB0= +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.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +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/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/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/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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= +github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= +github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/landlock-lsm/go-landlock v0.8.1 h1:Krs1co16IzN7bQcFYIdtNF+BKwZem3geRBkVsZtlCKU= +github.com/landlock-lsm/go-landlock v0.8.1/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +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.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +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.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.31 h1:/OM9oNl/fzyldpv5HKZ9m7bTywa7COUfg8gujd9nJ54= +github.com/lestrrat-go/jwx v1.2.31/go.mod h1:eQJKoRwWcLg4PfD5CFA5gIZGxhPgoPYq9pZISdxLf0c= +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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailhog/MailHog v1.0.1 h1:NDExFIj+JGzXT3kmG31r7Okrn78Sk/5p9lP/TV8OE4E= +github.com/mailhog/MailHog v1.0.1/go.mod h1:QlN3aQB5Kx2ZoQy439EWkjWHyJrgqNDsjfknyQOBCOI= +github.com/mailhog/MailHog-Server v1.0.1 h1:mK9inUHV2p6pO55cHZTCdZ8D4aXzd+M9wvqtU0XmWcM= +github.com/mailhog/MailHog-Server v1.0.1/go.mod h1:ScCrImbapPxdrQ85qoxkMygsSiY74ohIj1knLzuN8J0= +github.com/mailhog/MailHog-UI v1.0.1 h1:B3mVLiVLd4amNVQtiklr+srI3eMKiMzYSXXqDGa7JzA= +github.com/mailhog/MailHog-UI v1.0.1/go.mod h1:zLEw2DaBMXpL6nmpdB8S5U1Y3MMSATlTcjUYSTAB7HQ= +github.com/mailhog/data v1.0.1 h1:7I+opBvVdi4EMJaihXavM98jp/ovt4o6mz47446RAW8= +github.com/mailhog/data v1.0.1/go.mod h1:tjR/iXRhbSUKHzAAMd99RygVaDB5rIDC/bmWc363MzU= +github.com/mailhog/http v1.0.1 h1:i3sxAt7/WcdRXdKJZgiDRkWIAYScnrqqHQTZSxXkM0I= +github.com/mailhog/http v1.0.1/go.mod h1:91oqUCI9ZoSDY2cTj4pWDJVBHCK1U762V2a4if4KlOw= +github.com/mailhog/mhsendmail v0.2.0 h1:C5HUC4obHfXIkttLfGBUopYbsJmh+bnExGWHBpWQ8IA= +github.com/mailhog/mhsendmail v0.2.0/go.mod h1:B0778+OoPEc5aEFqatEnSO4ZWl9FCTxvaY+c7OOQadM= +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.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +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.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mikefarah/yq/v4 v4.45.1 h1:EW+HjKEVa55pUYFJseEHEHdQ0+ulunY+q42zF3M7ZaQ= +github.com/mikefarah/yq/v4 v4.45.1/go.mod h1:djgN2vD749hpjVNGYTShr5Kmv5LYljhCG3lUTuEe3LM= +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/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.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nyaruka/phonenumbers v1.7.4 h1:6gHEciDijqfY1EHiZTQLidEB2lsvU0rFvOL08EoIrhg= +github.com/nyaruka/phonenumbers v1.7.4/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs= +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= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +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/v4 v4.0.0-beta.4 h1:QcrNrobOP+5IjSDmS4//EuBtwiFuznQhi5xTe8oFSoM= +github.com/ory/dockertest/v4 v4.0.0-beta.4/go.mod h1:p9kfE14tzK8+WU4F9YbIZlzhCzQ2pH7H1KIfBKrF3DM= +github.com/ory/go-oidc/v3 v3.0.0-20250124100243-69986dfaf891 h1:HjpfYsY85wpheyMwR9EEk3347I0QsCllRMJShods3jc= +github.com/ory/go-oidc/v3 v3.0.0-20250124100243-69986dfaf891/go.mod h1:Jxfv2TPRvdJuLfmkvokss8dkguhMmer2UvARU6SWy0Y= +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.9-0.20260330111132-da75ef0fbc22 h1:xSbAB0yH2CUPqf9tiei3EFo7xpqY5nsAGdcrwqH9o6o= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22/go.mod h1:by6EjQDF4bDC2OuWOBd9EBRrTjtl0+kxSzyvuizM0Iw= +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.9-0.20250317235931-280c5fc7bf0e h1:4tUrC7x4YWRVMFp+c64KACNSGchW1zXo4l6Pa9/1hA8= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e/go.mod h1:XWLxVK4un/iuIcrw+6lCeanbF3NZwO5k6RdLeu/loQk= +github.com/ory/mail/v3 v3.0.1-0.20260413103059-df54acc74133 h1:8VGf5rxmZDXFyWN//A42MJMRwSrm1e9J5c2JPbOd5Qw= +github.com/ory/mail/v3 v3.0.1-0.20260413103059-df54acc74133/go.mod h1:ET3OgvMlMWp9Arq4/M7ToQu+NiRgLB/or0hYjFwu6Tw= +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.4.2-0.20260714091811-7f2223c43ab6 h1:LNreY5V2pXsUgiY/rVaU5uhIsOUHzZ+PP/NUgfqubr0= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6/go.mod h1:vd8H2inBRK+ZF+r5jDdx/fYPwqWrbVoAD4t7vAfXeA4= +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/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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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-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.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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 v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rakutentech/jwk-go v1.2.0 h1:vNJwedPkRR+32V5WGNj0JP4COes93BGERvzQLBjLy4c= +github.com/rakutentech/jwk-go v1.2.0/go.mod h1:pI0bYVntqaJ27RCpaC75MTUacheW0Rk4+8XzWWe1OWM= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= +github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= +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.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +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.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80= +github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +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.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +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.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/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +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/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/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/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +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.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 h1:pV2nZ1iE87X9ym+crkD9k15zTLbN08+IC4C7sk9sQYM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1/go.mod h1:nvgrM8LaG2+5G7WxbtjEPiSkg87+d0/ltZZK70p7FVo= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +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.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0 h1:zv7PRYGLrQHkdeZj0c5SNAZOJcw55XgaTezUkNpwA+w= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0/go.mod h1:3+VZyCi6hFW+UuxFF+wSOvwsOwncfBpQfP7Qdb3JXKg= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +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= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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-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= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +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-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +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-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= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190215142949-d0b11bdaac8a/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-20190412213103-97732733099d/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-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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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.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.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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +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.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +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/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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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-20190425150028-36563e24a262/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-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-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-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-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= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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-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= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +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-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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +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/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= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +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/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= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +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/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +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.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +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.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +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-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= +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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= +modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= +modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= +modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +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= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +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= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hash/hash_comparator.go b/hash/hash_comparator.go new file mode 100644 index 0000000..a7941f8 --- /dev/null +++ b/hash/hash_comparator.go @@ -0,0 +1,814 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" //nolint:all // System compatibility for imported passwords + "crypto/pbkdf2" + "crypto/sha1" //nolint:all // System compatibility for imported passwords + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "fmt" + "hash" + "regexp" + "strings" + + "github.com/go-crypt/crypt" + "github.com/go-crypt/crypt/algorithm/md5crypt" + "github.com/go-crypt/crypt/algorithm/shacrypt" + "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" //nolint:gosec // disable G115 G501 -- compatibility for imported passwords + "golang.org/x/crypto/scrypt" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/otelx" +) + +var ErrUnknownHashAlgorithm = errors.New("unknown hash algorithm") + +func NewCryptDecoder() *crypt.Decoder { + decoder := crypt.NewDecoder() + + // The register function only returns an error if the decoder is nil or the algorithm is already registered. + // This is never the case here, if it is, we did something horribly wrong. + if err := md5crypt.RegisterDecoderCommon(decoder); err != nil { + panic(err) + } + + if err := shacrypt.RegisterDecoder(decoder); err != nil { + panic(err) + } + + return decoder +} + +var CryptDecoder = NewCryptDecoder() + +type SupportedHasher struct { + Comparator func(ctx context.Context, password, hash []byte) error + Name string + Is func(hash []byte) bool +} + +func AddSupportedHasher(s SupportedHasher) { + supportedHashers = append(supportedHashers, s) +} + +var supportedHashers = []SupportedHasher{ + { + Comparator: CompareMD5Crypt, + Name: "md5crypt", + Is: IsMD5CryptHash, + }, + { + Comparator: CompareBcrypt, + Name: "bcrypt", + Is: IsBcryptHash, + }, + { + Comparator: CompareSHA256Crypt, + Name: "sha256crypt", + Is: IsSHA256CryptHash, + }, + { + Comparator: CompareSHA512Crypt, + Name: "sha512crypt", + Is: IsSHA512CryptHash, + }, + { + Comparator: CompareArgon2id, + Name: "argon2id", + Is: IsArgon2idHash, + }, + { + Comparator: CompareArgon2i, + Name: "argon2i", + Is: IsArgon2iHash, + }, + { + Comparator: ComparePbkdf2, + Name: "pbkdf2", + Is: IsPbkdf2Hash, + }, + { + Comparator: CompareScrypt, + Name: "scrypt", + Is: IsScryptHash, + }, + { + Comparator: CompareSSHA, + Name: "ssha", + Is: IsSSHAHash, + }, + { + Comparator: CompareSHA, + Name: "sha", + Is: IsSHAHash, + }, + { + Comparator: CompareFirebaseScrypt, + Name: "firebasescrypt", + Is: IsFirebaseScryptHash, + }, + { + Comparator: CompareMD5, + Name: "md5", + Is: IsMD5Hash, + }, + { + Comparator: CompareHMAC, + Name: "hmac", + Is: IsHMACHash, + }, +} + +func Compare(ctx context.Context, password, hash []byte) (err error) { + ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Compare") + defer otelx.End(span, &err) + + for _, h := range supportedHashers { + if h.Is(hash) { + span.SetAttributes(attribute.String("hash.type", h.Name)) + return h.Comparator(ctx, password, hash) + } + } + + span.SetAttributes(attribute.String("hash.type", "unknown")) + return errors.WithStack(ErrUnknownHashAlgorithm) +} + +func CompareMD5Crypt(_ context.Context, password, hash []byte) error { + // the password has successfully been validated (has prefix `$md5-crypt`), + // the decoder expect the module crypt identifier instead (`$1`), which means we need to replace the prefix + // before decoding + hash = bytes.TrimPrefix(hash, []byte("$md5-crypt")) + hash = append([]byte("$1"), hash...) + + return compareCryptHelper(password, string(hash)) +} + +func CompareBcrypt(ctx context.Context, password, hash []byte) error { + if err := validateBcryptPasswordLength(password); err != nil { + return err + } + if err := validateBcryptHashCost(hash); err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + err := bcrypt.CompareHashAndPassword(hash, password) + if err != nil { + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return errors.WithStack(ErrMismatchedHashAndPassword) + } + return err + } + + return nil +} + +func CompareSHA256Crypt(_ context.Context, password, hash []byte) error { + hash = bytes.TrimPrefix(hash, []byte("$sha256-crypt")) + hash = append([]byte("$5"), hash...) + + return compareCryptHelper(password, string(hash)) +} + +func CompareSHA512Crypt(_ context.Context, password, hash []byte) error { + hash = bytes.TrimPrefix(hash, []byte("$sha512-crypt")) + hash = append([]byte("$6"), hash...) + + return compareCryptHelper(password, string(hash)) +} + +func CompareArgon2id(ctx context.Context, password, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + // Derive the key from the other password using the same parameters. + //nolint:gosec // disable G115 + otherHash := argon2.IDKey(password, salt, p.Iterations, uint32(p.Memory), p.Parallelism, p.KeyLength) + + return comparePasswordHashConstantTime(hash, otherHash) +} + +func CompareArgon2i(ctx context.Context, password, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + // Derive the key from the other password using the same parameters. + otherHash := argon2.Key(password, salt, p.Iterations, uint32(p.Memory), p.Parallelism, p.KeyLength) // #nosec G115 -- memory (KiB) would need to be 2^32 kibibyte to overflow uint32 + + return comparePasswordHashConstantTime(hash, otherHash) +} + +func ComparePbkdf2(ctx context.Context, password, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodePbkdf2Hash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + // Derive the key from the other password using the same parameters. + otherHash, err := pbkdf2.Key(getPseudorandomFunctionForPbkdf2(p.Algorithm), string(password), salt, int(p.Iterations), int(p.KeyLength)) + if err != nil { + return errors.WithStack(err) + } + + return comparePasswordHashConstantTime(hash, otherHash) +} + +func CompareScrypt(ctx context.Context, password, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeScryptHash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + // Derive the key from the other password using the same parameters. + otherHash, err := scrypt.Key(password, salt, int(p.Cost), int(p.Block), int(p.Parrellization), int(p.KeyLength)) + if err != nil { + return errors.WithStack(err) + } + + return comparePasswordHashConstantTime(hash, otherHash) +} + +func CompareSSHA(ctx context.Context, password, hash []byte) error { + hasher, salt, hash, err := decodeSSHAHash(string(hash)) + if err != nil { + return err + } + + raw := append(password[:], salt[:]...) + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + return CompareSHAHelper(hasher, raw, hash) +} + +func CompareSHA(ctx context.Context, password, hash []byte) error { + hasher, pf, salt, hash, err := decodeSHAHash(string(hash)) + if err != nil { + return err + } + + r := strings.NewReplacer("{SALT}", string(salt), "{PASSWORD}", string(password)) + raw := []byte(r.Replace(string(pf))) + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + return CompareSHAHelper(hasher, raw, hash) +} + +func CompareFirebaseScrypt(ctx context.Context, password, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, saltSeparator, hash, signerKey, err := decodeFirebaseScryptHash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + // Derive the key from the other password using the same parameters. + // FirebaseScript algorithm implementation from https://github.com/Aoang/firebase-scrypt + ck, err := scrypt.Key(password, append(salt, saltSeparator...), int(p.Cost), int(p.Block), int(p.Parrellization), 32) + if err != nil { + return errors.WithStack(err) + } + + var block cipher.Block + if block, err = aes.NewCipher(ck); err != nil { + return errors.WithStack(err) + } + + cipherText := make([]byte, aes.BlockSize+len(signerKey)) + stream := cipher.NewCTR(block, cipherText[:aes.BlockSize]) + stream.XORKeyStream(cipherText[aes.BlockSize:], signerKey) + otherHash := cipherText[aes.BlockSize:] + + return comparePasswordHashConstantTime(hash, otherHash) +} + +func CompareMD5(ctx context.Context, password, hash []byte) error { + // Extract the hash from the encoded password + pf, salt, hash, err := decodeMD5Hash(string(hash)) + if err != nil { + return err + } + + arg := password + if salt != nil { + r := strings.NewReplacer("{SALT}", string(salt), "{PASSWORD}", string(password)) + arg = []byte(r.Replace(string(pf))) + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + //#nosec G401 -- compatibility for imported passwords + otherHash := md5.Sum(arg) + + return comparePasswordHashConstantTime(hash, otherHash[:]) +} + +func CompareHMAC(ctx context.Context, password, hash []byte) error { + // Extract the hash from the encoded password + hasher, hash, key, err := decodeHMACHash(string(hash)) + if err != nil { + return err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return ctx.Err() + } + + mac := hmac.New(hasher, key) + _, err = mac.Write(password) + if err != nil { + return err + } + + otherHash := []byte(hex.EncodeToString(mac.Sum(nil))) + + return comparePasswordHashConstantTime(hash, otherHash) +} + +var ( + isMD5CryptHash = regexp.MustCompile(`^\$md5-crypt\$`) + isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`) + isSHA256CryptHash = regexp.MustCompile(`^\$sha256-crypt\$`) + isSHA512CryptHash = regexp.MustCompile(`^\$sha512-crypt\$`) + isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`) + isArgon2iHash = regexp.MustCompile(`^\$argon2i\$`) + isPbkdf2Hash = regexp.MustCompile(`^\$pbkdf2-sha[0-9]{1,3}\$`) + isPbkdf2Sha256Hash = regexp.MustCompile(`^\$pbkdf2-sha256\$`) + isScryptHash = regexp.MustCompile(`^\$scrypt\$`) + isSSHAHash = regexp.MustCompile(`^{SSHA(256|512)?}.*`) + isSHAHash = regexp.MustCompile(`^\$sha(1|256|512)\$`) + isFirebaseScryptHash = regexp.MustCompile(`^\$firescrypt\$`) + isMD5Hash = regexp.MustCompile(`^\$md5\$`) + isHMACHash = regexp.MustCompile(`^\$hmac-(md4|md5|sha1|sha224|sha256|sha384|sha512)\$`) +) + +func IsMD5CryptHash(hash []byte) bool { return isMD5CryptHash.Match(hash) } +func IsBcryptHash(hash []byte) bool { return isBcryptHash.Match(hash) } +func IsSHA256CryptHash(hash []byte) bool { return isSHA256CryptHash.Match(hash) } +func IsSHA512CryptHash(hash []byte) bool { return isSHA512CryptHash.Match(hash) } +func IsArgon2idHash(hash []byte) bool { return isArgon2idHash.Match(hash) } +func IsArgon2iHash(hash []byte) bool { return isArgon2iHash.Match(hash) } +func IsPbkdf2Hash(hash []byte) bool { return isPbkdf2Hash.Match(hash) } +func IsPbkdf2Sha256Hash(hash []byte) bool { return isPbkdf2Sha256Hash.Match(hash) } +func IsScryptHash(hash []byte) bool { return isScryptHash.Match(hash) } +func IsSSHAHash(hash []byte) bool { return isSSHAHash.Match(hash) } +func IsSHAHash(hash []byte) bool { return isSHAHash.Match(hash) } +func IsFirebaseScryptHash(hash []byte) bool { return isFirebaseScryptHash.Match(hash) } +func IsMD5Hash(hash []byte) bool { return isMD5Hash.Match(hash) } +func IsHMACHash(hash []byte) bool { return isHMACHash.Match(hash) } + +func IsValidHashFormat(hash []byte) bool { + for _, h := range supportedHashers { + if h.Is(hash) { + return true + } + } + + return false +} + +func decodeArgon2idHash(encodedHash string) (p *config.Argon2, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + var version int + _, err = fmt.Sscanf(parts[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + p = new(config.Argon2) + _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism) + if err != nil { + return nil, nil, nil, err + } + if err := validateArgon2Params(uint64(p.Memory), p.Iterations, p.Parallelism); err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32 + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, nil, err + } + p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32 + + return p, salt, hash, nil +} + +// pbkdf2Params carries the cost parameters decoded from a stored PBKDF2 hash. +// The configurable hasher is hash.Pbkdf2; this type only serves comparison of +// stored or imported hashes. +type pbkdf2Params struct { + Algorithm string + Iterations uint32 + SaltLength uint32 + KeyLength uint32 +} + +// decodePbkdf2Hash decodes PBKDF2 encoded password hash. +// format: $pbkdf2-$i=,l=$$ +func decodePbkdf2Hash(encodedHash string) (p *pbkdf2Params, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 5 { + return nil, nil, nil, ErrInvalidHash + } + + p = new(pbkdf2Params) + digestParts := strings.SplitN(parts[1], "-", 2) + if len(digestParts) != 2 { + return nil, nil, nil, ErrInvalidHash + } + p.Algorithm = digestParts[1] + + _, err = fmt.Sscanf(parts[2], "i=%d,l=%d", &p.Iterations, &p.KeyLength) + if err != nil { + return nil, nil, nil, err + } + if err := validatePbkdf2Params(p.Iterations); err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32 + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32 + + return p, salt, hash, nil +} + +// decodeScryptHash decodes Scrypt encoded password hash. +// format: $scrypt$ln=,r=,p=$$ +func decodeScryptHash(encodedHash string) (p *Scrypt, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 5 { + return nil, nil, nil, ErrInvalidHash + } + + p = new(Scrypt) + + _, err = fmt.Sscanf(parts[2], "ln=%d,r=%d,p=%d", &p.Cost, &p.Block, &p.Parrellization) + if err != nil { + return nil, nil, nil, err + } + if err := validateScryptParams(p.Cost, p.Block, p.Parrellization); err != nil { + return nil, nil, nil, err + } + + salt, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32 + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32 + + return p, salt, hash, nil +} + +// decodeSHAHash decodes SHA[1|256|512] encoded password hash in custom PHC format. +// format: $sha1$pf=$$ +func decodeSHAHash(encodedHash string) (hasher string, pf, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + + if len(parts) != 5 { + return "", nil, nil, nil, ErrInvalidHash + } + + hasher = parts[1] + + _, err = fmt.Sscanf(parts[2], "pf=%s", &pf) + if err != nil { + return "", nil, nil, nil, err + } + + pf, err = base64.StdEncoding.Strict().DecodeString(string(pf)) + if err != nil { + return "", nil, nil, nil, err + } + + salt, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return "", nil, nil, nil, err + } + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return "", nil, nil, nil, err + } + + return hasher, pf, salt, hash, nil +} + +// CompareSHAHelper compares the raw password with the hash using the given hasher. +func CompareSHAHelper(hasher string, raw []byte, hash []byte) error { + var sha []byte + + switch hasher { + case "sha1": + sum := sha1.Sum(raw) //#nosec G401 -- compatibility for imported passwords + sha = sum[:] + case "sha256": + sum := sha256.Sum256(raw) + sha = sum[:] + case "sha512": + sum := sha512.Sum512(raw) + sha = sum[:] + default: + return errors.WithStack(ErrMismatchedHashAndPassword) + } + + encodedHash := []byte(base64.StdEncoding.EncodeToString(hash)) + newEncodedHash := []byte(base64.StdEncoding.EncodeToString(sha)) + + return comparePasswordHashConstantTime(encodedHash, newEncodedHash) +} + +func compareCryptHelper(password []byte, hash string) error { + digest, err := CryptDecoder.Decode(hash) + if err != nil { + return err + } + + if digest.MatchBytes(password) { + return nil + } + + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +var regexSSHA = regexp.MustCompile(`\{([^}]*)\}`) + +// decodeSSHAHash decodes SSHA[1|256|512] encoded password hash in usual {SSHA...} format. +func decodeSSHAHash(encodedHash string) (hasher string, salt, hash []byte, err error) { + match := regexSSHA.FindStringSubmatch(string(encodedHash)) + + var index_of_salt_begin int + var index_of_hash_begin int + + switch match[1] { + case "SSHA": + hasher = "sha1" + index_of_hash_begin = 6 + index_of_salt_begin = 20 + + case "SSHA256": + hasher = "sha256" + index_of_hash_begin = 9 + index_of_salt_begin = 32 + + case "SSHA512": + hasher = "sha512" + index_of_hash_begin = 9 + index_of_salt_begin = 64 + + default: + return "", nil, nil, ErrInvalidHash + } + + decoded, err := base64.StdEncoding.DecodeString(string(encodedHash[index_of_hash_begin:])) + if err != nil { + return "", nil, nil, ErrInvalidHash + } + + if len(decoded) < index_of_salt_begin+1 { + return "", nil, nil, ErrInvalidHash + } + + salt = decoded[index_of_salt_begin:] + hash = decoded[:index_of_salt_begin] + + return hasher, salt, hash, nil +} + +// decodeFirebaseScryptHash decodes Firebase Scrypt encoded password hash. +// format: $firescrypt$ln=,r=,p=$$$$ +func decodeFirebaseScryptHash(encodedHash string) (p *Scrypt, salt, saltSeparator, hash, signerKey []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 7 { + return nil, nil, nil, nil, nil, ErrInvalidHash + } + + p = new(Scrypt) + + _, err = fmt.Sscanf(parts[2], "ln=%d,r=%d,p=%d", &p.Cost, &p.Block, &p.Parrellization) + if err != nil { + return nil, nil, nil, nil, nil, err + } + if err := validateFirebaseScryptParams(p.Cost, p.Block, p.Parrellization); err != nil { + return nil, nil, nil, nil, nil, err + } + // convert from firebase config "mem_cost" to + // scrypt CPU/memory cost parameter, which must be a power of two greater than 1. + p.Cost = 1 << p.Cost + + salt, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, nil, nil, err + } + p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32 + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, nil, nil, err + } + // Are all firebase script hashes of length 32? + p.KeyLength = 32 + + saltSeparator, err = base64.StdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + signerKey, err = base64.StdEncoding.Strict().DecodeString(parts[6]) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + return p, salt, saltSeparator, hash, signerKey, nil +} + +// decodeMD5Hash decodes MD5 encoded password hash. +// format without salt: $md5$ +// format with salt $md5$pf=$$ +func decodeMD5Hash(encodedHash string) (pf, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + + switch len(parts) { + case 3: + hash, err := base64.StdEncoding.Strict().DecodeString(parts[2]) + return nil, nil, hash, err + case 5: + _, err = fmt.Sscanf(parts[2], "pf=%s", &pf) + if err != nil { + return nil, nil, nil, err + } + + pf, err := base64.StdEncoding.Strict().DecodeString(string(pf)) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + + return pf, salt, hash, nil + default: + return nil, nil, nil, ErrInvalidHash + } +} + +// decodeHMACHash decodes HMAC encoded password hash. +// format : $hmac-$$ +func decodeHMACHash(encodedHash string) (hasher func() hash.Hash, hash, key []byte, err error) { + parts := strings.Split(encodedHash, "$") + + if len(parts) != 4 { + return nil, nil, nil, ErrInvalidHash + } + + hashMatch := isHMACHash.FindStringSubmatch(encodedHash) + + if len(hashMatch) != 2 { + return nil, nil, nil, errors.WithStack(ErrUnknownHashAlgorithm) + } + + switch hashMatch[1] { + case "md4": + hasher = md4.New //#nosec G401 -- compatibility for imported passwords + case "md5": + hasher = md5.New //#nosec G401 -- compatibility for imported passwords + case "sha1": + hasher = sha1.New //#nosec G401 -- compatibility for imported passwords + case "sha224": + hasher = sha256.New224 + case "sha256": + hasher = sha256.New + case "sha384": + hasher = sha512.New384 + case "sha512": + hasher = sha512.New + default: + return nil, nil, nil, errors.WithStack(ErrUnknownHashAlgorithm) + } + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[2]) + if err != nil { + return nil, nil, nil, err + } + + key, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + + return hasher, hash, key, nil +} + +func comparePasswordHashConstantTime(hash, otherHash []byte) error { + // use subtle.ConstantTimeCompare() to prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} diff --git a/hash/hash_limits.go b/hash/hash_limits.go new file mode 100644 index 0000000..ca9c8f8 --- /dev/null +++ b/hash/hash_limits.go @@ -0,0 +1,162 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" +) + +// ErrHashParametersOutOfBounds is returned when an imported or stored hash +// declares cost parameters that exceed the bounds enforced by this package. +// The bounds exist to prevent attacker-controlled hash parameters from causing +// memory exhaustion (process OOM) or unbounded CPU consumption when the +// password comparator is invoked. +var ErrHashParametersOutOfBounds = errors.New("hash parameters exceed allowed bounds") + +// Bounds for cost parameters embedded in imported password hashes. Each value +// is chosen to comfortably accommodate strong real-world configurations while +// bounding worst-case memory and CPU consumption when the comparator runs at +// login time. +const ( + // maxScryptLogN bounds the firebase-scrypt ln parameter. + // firebase-scrypt computes N = 1 << ln. Memory is 128 * N * r bytes. + // Firebase's strongest documented profile is ln=14 (~16 MiB at r=8); 17 + // leaves headroom for legacy configs while bounding allocation at + // ~128 MiB at r=8. + maxScryptLogN uint32 = 17 + + // maxScryptN bounds plain scrypt's N directly. OWASP's Password Storage + // Cheat Sheet recommends N=2^17 (131072) as the minimum for password + // hashing; no widely-deployed password-hash system documents a larger + // value. Higher N appears only in file-encryption tooling (tarsnap, age) + // where multi-second cost is acceptable. The Kratos default and RFC 7914's + // example both use N=16384 (2^14). Combined with the r=8 ceiling, this + // caps memory at 128 MiB per compare. + maxScryptN uint32 = 1 << 17 + + // maxScryptR bounds scrypt's r (block size). RFC 7914 specifies r=8 + // verbatim; all five OWASP recommended scrypt configurations use r=8. + // No documented production password hash uses r > 8. With N=2^17 this + // caps memory at 128 MiB per compare. + maxScryptR uint32 = 8 + + // maxScryptP bounds scrypt's p (parallelization). p=1 is the standard + // default; OWASP's highest documented config uses p=10. Go's scrypt + // implementation runs p iterations serially, so p multiplies CPU + // linearly without any parallelism gain. + maxScryptP uint32 = 10 + + // maxArgon2MemoryKiB bounds argon2 memory at 1 GiB. Kratos default is + // 128 MiB; OWASP recommends 64 MiB. 1 GiB matches the existing + // Argon2DefaultDedicatedMemory ceiling in driver/config. + maxArgon2MemoryKiB uint32 = 1 << 20 + + // maxArgon2Iterations bounds argon2 t. RFC 9106 recommends t∈{1,3}; + // OWASP configurations use up to t=5; libsodium SENSITIVE preset uses + // t=4. Bitwarden's user-configurable hard cap is 10, the highest + // documented production value. + maxArgon2Iterations uint32 = 10 + + // maxArgon2Parallelism bounds argon2 p. Default is 2*NumCPU; 16 covers + // up to 8-core hosts. + maxArgon2Parallelism uint8 = 16 + + // maxPbkdf2Iterations bounds PBKDF2 i. OWASP 2023 recommends 600k + // (SHA-256) / 210k (SHA-512). 10M ≈ 10× the strongest published + // recommendation and bounds CPU to a few seconds on modern hardware. + maxPbkdf2Iterations uint32 = 10_000_000 + + // maxBcryptCost bounds bcrypt cost. The format spec allows 4–31, but + // cost grows exponentially: cost 12 (Kratos, PHP, Django default) is + // ~250 ms; cost 14 (high-security guidance) is ~1 s; cost 15 (practical + // max for interactive use) is ~2 s; cost 17 is ~8 s. No mainstream + // platform defaults above cost 12. + maxBcryptCost = 15 +) + +func validateFirebaseScryptParams(logN, r, p uint32) error { + if logN > maxScryptLogN { + return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt ln=%d exceeds max %d", logN, maxScryptLogN) + } + if r == 0 || r > maxScryptR { + return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt r=%d not in [1, %d]", r, maxScryptR) + } + if p == 0 || p > maxScryptP { + return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt p=%d not in [1, %d]", p, maxScryptP) + } + return nil +} + +func validateScryptParams(n, r, p uint32) error { + if n == 0 || n > maxScryptN { + return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt N=%d not in [1, %d]", n, maxScryptN) + } + if r == 0 || r > maxScryptR { + return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt r=%d not in [1, %d]", r, maxScryptR) + } + if p == 0 || p > maxScryptP { + return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt p=%d not in [1, %d]", p, maxScryptP) + } + return nil +} + +func validateArgon2Params(memoryKiB uint64, iterations uint32, parallelism uint8) error { + if memoryKiB == 0 || memoryKiB > uint64(maxArgon2MemoryKiB) { + return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 m=%d KiB not in [1, %d]", memoryKiB, maxArgon2MemoryKiB) + } + if iterations == 0 || iterations > maxArgon2Iterations { + return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 t=%d not in [1, %d]", iterations, maxArgon2Iterations) + } + if parallelism == 0 || parallelism > maxArgon2Parallelism { + return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 p=%d not in [1, %d]", parallelism, maxArgon2Parallelism) + } + return nil +} + +func validatePbkdf2Params(iterations uint32) error { + if iterations == 0 || iterations > maxPbkdf2Iterations { + return errors.Wrapf(ErrHashParametersOutOfBounds, "pbkdf2 i=%d not in [1, %d]", iterations, maxPbkdf2Iterations) + } + return nil +} + +func validateBcryptHashCost(hashed []byte) error { + cost, err := bcrypt.Cost(hashed) + if err != nil { + return err + } + if cost > maxBcryptCost { + return errors.Wrapf(ErrHashParametersOutOfBounds, "bcrypt cost=%d exceeds max %d", cost, maxBcryptCost) + } + return nil +} + +// ValidateImportedHash performs cost-parameter bounds checking on a hash +// before it is persisted via the admin identity import API. It is the +// import-time counterpart to the bounds checks in the compare* paths and +// exists so we fail fast on malicious imports rather than persisting a hash +// that would crash the process at every login attempt. +// +// Hashers without attacker-controlled cost parameters (md5-crypt, sha-crypt, +// the static SHA/MD5/HMAC families, SSHA) pass through unchanged. +func ValidateImportedHash(hashed []byte) error { + switch { + case IsBcryptHash(hashed): + return validateBcryptHashCost(hashed) + case IsArgon2idHash(hashed), IsArgon2iHash(hashed): + _, _, _, err := decodeArgon2idHash(string(hashed)) + return err + case IsPbkdf2Hash(hashed): + _, _, _, err := decodePbkdf2Hash(string(hashed)) + return err + case IsScryptHash(hashed): + _, _, _, err := decodeScryptHash(string(hashed)) + return err + case IsFirebaseScryptHash(hashed): + _, _, _, _, _, err := decodeFirebaseScryptHash(string(hashed)) + return err + } + return nil +} diff --git a/hash/hash_limits_test.go b/hash/hash_limits_test.go new file mode 100644 index 0000000..482780a --- /dev/null +++ b/hash/hash_limits_test.go @@ -0,0 +1,150 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/hash" +) + +// TestCompareRejectsExtremeHashParameters reproduces the bug-bounty-reported +// DoS class: an attacker-controlled hash with extreme cost parameters causes +// the comparator to allocate gigabytes (or run for hours) and crash or hang +// the Kratos process at login time. Each case here would, without the bounds +// in hash_limits.go, drive the underlying primitive into pathological +// behavior. The comparator must error early instead. +func TestCompareRejectsExtremeHashParameters(t *testing.T) { + t.Parallel() + ctx := t.Context() + + cases := []struct { + name string + hash string + }{ + { + // The originally reported attack: ln=31 → N=2^31 → ~256 GiB at r=8. + name: "firescrypt ln=31 (reported)", + hash: "$firescrypt$ln=31,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5", + }, + { + name: "firescrypt ln=18", + hash: "$firescrypt$ln=18,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5", + }, + { + name: "firescrypt r=1024", + hash: "$firescrypt$ln=14,r=1024,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5", + }, + { + name: "firescrypt p=1024", + hash: "$firescrypt$ln=14,r=8,p=1024$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5", + }, + { + // Plain scrypt: ln=2097153 (one above 2^21) — far beyond cap. + name: "scrypt N too large", + hash: "$scrypt$ln=2097153,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==", + }, + { + name: "scrypt r=1024", + hash: "$scrypt$ln=16384,r=1024,p=1$c2FsdHNhbHRzYWx0$aGFzaA==", + }, + { + // Argon2: 8 GiB requested. + name: "argon2id m=8GiB", + hash: "$argon2id$v=19$m=8388608,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw", + }, + { + name: "argon2id t=1000", + hash: "$argon2id$v=19$m=65536,t=1000,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw", + }, + { + name: "argon2i m=8GiB", + hash: "$argon2i$v=19$m=8388608,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c", + }, + { + // PBKDF2: 1B iterations. + name: "pbkdf2 i=1_000_000_000", + hash: "$pbkdf2-sha256$i=1000000000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI", + }, + { + // Bcrypt cost=20 → ~64s per compare. Format-valid but resource + // exhaustion risk. + name: "bcrypt cost=20", + hash: "$2a$20$abcdefghijklmnopqrstuugn/uitWMnDoOdJaSWqW1pjEXZ8eOSCqK", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := hash.Compare(ctx, []byte("any-password"), []byte(tc.hash)) + require.Error(t, err, "expected bounds-exceeded error; without it the process would OOM or hang") + require.ErrorIs(t, err, hash.ErrHashParametersOutOfBounds) + }) + } +} + +// TestValidateImportedHashRejectsExtremeParameters covers the import-time +// counterpart: malicious hashes must be rejected at the admin import API +// boundary, before they are ever persisted, so that a single poisoned row +// cannot crash-loop the service on every login attempt. +func TestValidateImportedHashRejectsExtremeParameters(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + hash string + }{ + {name: "firescrypt ln=31", hash: "$firescrypt$ln=31,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5"}, + {name: "scrypt N too large", hash: "$scrypt$ln=2097153,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA=="}, + {name: "argon2id m=8GiB", hash: "$argon2id$v=19$m=8388608,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}, + {name: "argon2i t=1000", hash: "$argon2i$v=19$m=65536,t=1000,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"}, + {name: "pbkdf2 i=1B", hash: "$pbkdf2-sha256$i=1000000000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"}, + {name: "bcrypt cost=20", hash: "$2a$20$abcdefghijklmnopqrstuugn/uitWMnDoOdJaSWqW1pjEXZ8eOSCqK"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := hash.ValidateImportedHash([]byte(tc.hash)) + require.Error(t, err) + require.ErrorIs(t, err, hash.ErrHashParametersOutOfBounds) + }) + } +} + +// TestValidateImportedHashAcceptsRealisticHashes guards against bounds being +// set so tightly that legitimate, real-world hashes are rejected. The values +// here mirror the existing valid-hash test cases in TestCompare. +func TestValidateImportedHashAcceptsRealisticHashes(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + hash string + }{ + {name: "bcrypt cost=12", hash: "$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6"}, + {name: "bcrypt cost=15", hash: "$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO"}, + {name: "argon2id m=32 t=2 p=4", hash: "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}, + {name: "argon2i m=65536 t=3 p=4", hash: "$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"}, + {name: "pbkdf2-sha256 i=100000", hash: "$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"}, + {name: "pbkdf2-sha512 i=100000", hash: "$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc"}, + {name: "scrypt ln=16384 r=8 p=1", hash: "$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="}, + {name: "firescrypt ln=14", hash: "$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw=="}, + // Hashes without parametrized cost (no allocation/CPU vector) — must + // pass through without error. + {name: "md5", hash: "$md5$CY9rzUYh03PK3k6DJie09g=="}, + {name: "sha256-crypt", hash: "$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78"}, + {name: "ssha", hash: "{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.NoError(t, hash.ValidateImportedHash([]byte(tc.hash))) + }) + } +} diff --git a/hash/hasher.go b/hash/hasher.go new file mode 100644 index 0000000..88c52b3 --- /dev/null +++ b/hash/hasher.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "context" +) + +// Hasher provides methods for generating and comparing password hashes. +type Hasher interface { + // Generate returns a hash derived from the password or an error if the hash method failed. + Generate(ctx context.Context, password []byte) ([]byte, error) + + // Understands returns whether the given hash can be understood by this hasher. + Understands(hash []byte) bool +} + +type HashProvider interface { + Hasher(ctx context.Context) Hasher +} + +const tracingComponent = "github.com/ory/kratos/hash" diff --git a/hash/hasher_argon2.go b/hash/hasher_argon2.go new file mode 100644 index 0000000..7cef12c --- /dev/null +++ b/hash/hasher_argon2.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/inhies/go-bytesize" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/otelx" +) + +var ( + ErrInvalidHash = errors.New("the encoded hash is not in the correct format") + ErrIncompatibleVersion = errors.New("incompatible version of argon2") + ErrMismatchedHashAndPassword = errors.New("passwords do not match") +) + +type Argon2 struct { + c Argon2Configuration +} + +type Argon2Configuration interface { + config.Provider +} + +func NewHasherArgon2(c Argon2Configuration) *Argon2 { + return &Argon2{c: c} +} + +func toKB(mem bytesize.ByteSize) uint32 { + //nolint:gosec // disable G115 + return uint32(mem / bytesize.KB) +} + +func (h *Argon2) Generate(ctx context.Context, password []byte) (_ []byte, err error) { + conf := h.c.Config().HasherArgon2(ctx) + + _, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes( + attribute.String("hash.type", "argon2id"), + attribute.String("hash.config", fmt.Sprintf("%#v", conf)), + )) + defer otelx.End(span, &err) + + salt := make([]byte, conf.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return nil, ctx.Err() + } + + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash := argon2.IDKey(password, salt, conf.Iterations, toKB(conf.Memory), conf.Parallelism, conf.KeyLength) + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, toKB(conf.Memory), conf.Iterations, conf.Parallelism, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +func (h *Argon2) Understands(hash []byte) bool { + return IsArgon2idHash(hash) +} diff --git a/hash/hasher_bcrypt.go b/hash/hasher_bcrypt.go new file mode 100644 index 0000000..1de5fff --- /dev/null +++ b/hash/hasher_bcrypt.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "context" + "fmt" + + "github.com/ory/kratos/text" + "github.com/ory/x/otelx" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/schema" + + "golang.org/x/crypto/bcrypt" + + "github.com/ory/kratos/driver/config" +) + +type Bcrypt struct { + c BcryptConfiguration +} + +type BcryptConfiguration interface { + config.Provider +} + +func NewHasherBcrypt(c BcryptConfiguration) *Bcrypt { + return &Bcrypt{c: c} +} + +func (h *Bcrypt) Generate(ctx context.Context, password []byte) (_ []byte, err error) { + conf := h.c.Config().HasherBcrypt(ctx) + + _, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes( + attribute.String("hash.type", "bcrypt"), + attribute.String("hash.config", fmt.Sprintf("%#v", conf)), + )) + defer otelx.End(span, &err) + + if err := validateBcryptPasswordLength(password); err != nil { + return nil, err + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return nil, ctx.Err() + } + + hash, err := bcrypt.GenerateFromPassword(password, int(conf.Cost)) + if err != nil { + return nil, err + } + + return hash, nil +} + +func validateBcryptPasswordLength(password []byte) error { + // Bcrypt truncates the password to the first 72 bytes, following the OpenBSD implementation, + // so if password is longer than 72 bytes, function returns an error + // See https://en.wikipedia.org/wiki/Bcrypt#User_input + if len(password) > 72 { + return schema.NewPasswordPolicyViolationError( + "#/password", + text.NewErrorValidationPasswordMaxLength(72, len(password)), + ) + } + return nil +} + +func (h *Bcrypt) Understands(hash []byte) bool { + return IsBcryptHash(hash) +} diff --git a/hash/hasher_pbkdf2.go b/hash/hasher_pbkdf2.go new file mode 100644 index 0000000..b2b62bc --- /dev/null +++ b/hash/hasher_pbkdf2.go @@ -0,0 +1,95 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +import ( + "bytes" + "context" + "crypto/pbkdf2" + "crypto/rand" + "crypto/sha1" //#nosec G505 -- compatibility for imported passwords + "crypto/sha256" + "crypto/sha3" + "crypto/sha512" + "encoding/base64" + "fmt" + "hash" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" +) + +type Pbkdf2 struct { + Algorithm string + Iterations uint32 + SaltLength uint32 + KeyLength uint32 +} + +func (h *Pbkdf2) Generate(ctx context.Context, password []byte) (_ []byte, err error) { + _, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes( + attribute.String("hash.type", "pbkdf2"), + attribute.String("hash.config", fmt.Sprintf("%#v", h)), + )) + defer otelx.End(span, &err) + + salt := make([]byte, h.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, errors.WithStack(err) + } + + // ensure that the context is not canceled before doing the heavy lifting + if ctx.Err() != nil { + return nil, ctx.Err() + } + + key, err := pbkdf2.Key(getPseudorandomFunctionForPbkdf2(h.Algorithm), string(password), salt, int(h.Iterations), int(h.KeyLength)) + if err != nil { + return nil, errors.WithStack(err) + } + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$pbkdf2-%s$i=%d,l=%d$%s$%s", + h.Algorithm, + h.Iterations, + h.KeyLength, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(key), + ); err != nil { + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +func (h *Pbkdf2) Understands(hash []byte) bool { + return IsPbkdf2Hash(hash) +} + +func getPseudorandomFunctionForPbkdf2(alg string) func() hash.Hash { + switch alg { + case "sha1": + return sha1.New + case "sha224": + // Kept for backwards compatibility: this historically mapped to + // SHA3-224, not SHA2-224. Changing it would break verification of + // previously imported hashes. + return func() hash.Hash { return sha3.New224() } + case "sha256": + return sha256.New + case "sha384": + // Kept for backwards compatibility: SHA3-384, see above. + return func() hash.Hash { return sha3.New384() } + case "sha512": + return sha512.New + default: + return sha256.New + } +} diff --git a/hash/hasher_scrypt.go b/hash/hasher_scrypt.go new file mode 100644 index 0000000..17991b5 --- /dev/null +++ b/hash/hasher_scrypt.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash + +type Scrypt struct { + Cost uint32 + Block uint32 + Parrellization uint32 + SaltLength uint32 + KeyLength uint32 +} diff --git a/hash/hasher_test.go b/hash/hasher_test.go new file mode 100644 index 0000000..82eb940 --- /dev/null +++ b/hash/hasher_test.go @@ -0,0 +1,556 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hash_test + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/hash" + "github.com/ory/kratos/pkg" +) + +func mkpw(t *testing.T, length int) []byte { + pw := make([]byte, length) + _, err := rand.Read(pw) + require.NoError(t, err) + return pw +} + +func TestArgonHasher(t *testing.T) { + t.Parallel() + ctx := t.Context() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h := hash.NewHasherArgon2(reg) + for _, pwLength := range []int{ + 8, + 16, + 32, + 64, + 128, + } { + pwLength := pwLength + t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) { + t.Parallel() + pw := mkpw(t, pwLength) + hs, err := h.Generate(ctx, pw) + require.NoError(t, err) + assert.NotEqual(t, pw, hs) + + t.Logf("hash: %s", hs) + require.NoError(t, hash.CompareArgon2id(ctx, pw, hs)) + + mod := make([]byte, len(pw)) + copy(mod, pw) + mod[len(pw)-1] = ^pw[len(pw)-1] + require.Error(t, hash.CompareArgon2id(ctx, mod, hs)) + }) + } +} + +func TestBcryptHasherGeneratesErrorWhenPasswordIsLong(t *testing.T) { + t.Parallel() + ctx := t.Context() + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + hasher := hash.NewHasherBcrypt(reg) + + password := mkpw(t, 73) + res, err := hasher.Generate(ctx, password) + + assert.Error(t, err, "password is too long") + assert.Nil(t, res) +} + +func TestBcryptHasherGeneratesHash(t *testing.T) { + t.Parallel() + ctx := t.Context() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + hasher := hash.NewHasherBcrypt(reg) + for _, pwLength := range []int{ + 8, + 16, + 32, + 64, + 72, + } { + pwLength := pwLength + t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) { + t.Parallel() + pw := mkpw(t, pwLength) + hs, err := hasher.Generate(ctx, pw) + + assert.Nil(t, err) + assert.True(t, hasher.Understands(hs)) + + // Valid format: $2a$12$[22 character salt][31 character hash] + assert.Equal(t, 60, len(string(hs)), "invalid bcrypt hash length") + assert.Equal(t, "$2a$12$", string(hs)[:7], "invalid bcrypt identifier") + }) + } +} + +func TestComparatorBcryptFailsWhenPasswordIsTooLong(t *testing.T) { + t.Parallel() + ctx := t.Context() + password := mkpw(t, 73) + err := hash.CompareBcrypt(ctx, password, []byte("hash")) + + assert.Error(t, err, "password is too long") +} + +func TestComparatorBcryptSuccess(t *testing.T) { + t.Parallel() + ctx := t.Context() + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + hasher := hash.NewHasherBcrypt(reg) + for _, pwLength := range []int{ + 8, + 16, + 32, + 64, + 72, + } { + pwLength := pwLength + t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) { + t.Parallel() + pw := mkpw(t, pwLength) + + hs, err := hasher.Generate(ctx, pw) + + assert.Nil(t, err) + assert.True(t, hasher.Understands(hs)) + + err = hash.CompareBcrypt(ctx, pw, hs) + assert.Nil(t, err, "hash validation fails") + }) + } +} + +func TestComparatorBcryptFail(t *testing.T) { + t.Parallel() + ctx := t.Context() + for _, pwLength := range []int{ + 8, + 16, + 32, + 64, + 72, + } { + pwLength := pwLength + t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) { + t.Parallel() + pw := mkpw(t, pwLength) + mod := make([]byte, len(pw)) + copy(mod, pw) + mod[len(pw)-1] = ^pw[len(pw)-1] + + err := hash.CompareBcrypt(ctx, pw, mod) + assert.Error(t, err) + }) + } +} + +func TestPbkdf2Hasher(t *testing.T) { + t.Parallel() + ctx := t.Context() + for _, pwLength := range []int{ + 8, + 16, + 32, + 64, + 128, + } { + pwLength := pwLength + t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) { + t.Parallel() + for _, algorithm := range []string{ + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + } { + algorithm := algorithm + t.Run(fmt.Sprintf("algorithm=%s", algorithm), func(t *testing.T) { + t.Parallel() + hasher := &hash.Pbkdf2{ + Algorithm: algorithm, + Iterations: 100_000, + SaltLength: 32, + KeyLength: 32, + } + pw := mkpw(t, pwLength) + t.Logf("%d", pwLength) + hs, err := hasher.Generate(ctx, pw) + require.NoError(t, err) + assert.NotEqual(t, pw, hs) + + t.Logf("hash: %s", hs) + require.NoError(t, hash.ComparePbkdf2(ctx, pw, hs)) + + assert.True(t, hasher.Understands(hs)) + + mod := make([]byte, len(pw)) + copy(mod, pw) + mod[len(pw)-1] = ^pw[len(pw)-1] + require.Error(t, hash.ComparePbkdf2(ctx, mod, hs)) + }) + } + }) + } +} + +func TestCompare(t *testing.T) { + t.Parallel() + ctx := t.Context() + + t.Run("unknown", func(t *testing.T) { + t.Parallel() + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$unknown$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6"))) + }) + + t.Run("bcrypt", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6"))) + assert.Nil(t, hash.CompareBcrypt(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL7"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO"))) + assert.Nil(t, hash.CompareBcrypt(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZb1"))) + }) + + t.Run("Argon2", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"))) + assert.Nil(t, hash.CompareArgon2id(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRN2"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"))) + assert.Nil(t, hash.CompareArgon2i(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$pZ+27D6B0bCi0DwSmANF1w$4RNCUu4Uyu7eTIvzIdSuKz+I9idJlX/ykn6J10/W0EU"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4"))) + assert.Nil(t, hash.CompareArgon2id(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp5"))) + }) + + t.Run("pbkdf2", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"))) + assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpp"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc"))) + assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPww"))) + + // The sha224 and sha384 variants historically map to SHA3-224 and + // SHA3-384 (not SHA2), see getPseudorandomFunctionForPbkdf2. These + // fixtures were generated with that mapping and pin it. + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o"))) + assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o"))) + assert.Error(t, hash.Compare(ctx, []byte("wrong"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o"))) + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck"))) + assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck"))) + assert.Error(t, hash.Compare(ctx, []byte("wrong"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck"))) + + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$aaaa$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXcc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpII"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$I=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc"))) + }) + + t.Run("scrypt", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Nil(t, hash.CompareScrypt(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYF="))) + + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$(MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16385,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("tesu"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + assert.Error(t, hash.Compare(ctx, []byte("tesu"), []byte("$scrypt$ln=abc,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="))) + }) + + t.Run("firescrypt", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw=="))) + assert.Nil(t, hash.CompareFirebaseScrypt(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw=="))) + assert.Error(t, hash.Compare(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyc=="))) + }) + + t.Run("SSHA", func(t *testing.T) { + t.Parallel() + assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a"))) + assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a"))) + assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a"))) + assert.Error(t, hash.Compare(ctx, []byte(""), []byte("{SSHA}tooshort"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK"))) + assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK"))) + assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK"))) + + assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M="))) + assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M="))) + assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M="))) + assert.Error(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHAnotExistent}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M="))) + }) + + t.Run("sha1", func(t *testing.T) { + t.Parallel() + //pf: {SALT}{PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + assert.Error(t, hash.Compare(ctx, []byte("tset"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + // wrong salt + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$cDJvb3ZrZGJ6cQ==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + // salt not encoded + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$5opmkgz03r$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + assert.Nil(t, hash.Compare(ctx, []byte("BwS^514g^cv@Z"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$99h9net4BXl7qdTRaiGUobLROxM="))) + // no format string + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + // wrong number of parameters + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ="))) + // pf: ??staticPrefix??{SALT}{PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=Pz9zdGF0aWNQcmVmaXg/P3tTQUxUfXtQQVNTV09SRH0=$NW9wbWtnejAzcg==$SAAxMUn7jxckQXkBmsVF0nHwqso="))) + // pf: {PASSWORD}%%{SALT} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1BBU1NXT1JEfSUle1NBTFR9$NW9wbWtnejAzcg==$YX0AW8/MW5ojUlnzTaR43ucHCog="))) + // pf: ${PASSWORD}${SALT}$ + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=JHtQQVNTV09SRH0ke1NBTFR9JA==$NW9wbWtnejAzcg==$iE5n1yjX3oAdxRHwZ4u57I4LpQo="))) + }) + + t.Run("sha256", func(t *testing.T) { + t.Parallel() + //pf: {SALT}{PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY="))) + assert.Nil(t, hash.CompareSHA(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY="))) + assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY="))) + //pf: {SALT}$${PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9JCR7UEFTU1dPUkR9$NW9wbWtnejAzcg==$HokCOi9OtiZaZRvnkgemV3B4UUHpI7kA8zq/EZWH2NY="))) + }) + + t.Run("sha512", func(t *testing.T) { + t.Parallel() + //pf: {SALT}{PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw=="))) + assert.Nil(t, hash.CompareSHA(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw=="))) + assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw=="))) + //pf: {SALT}$${PASSWORD} + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9JCR7UEFTU1dPUkR9$NW9wbWtnejAzcg==$1F9BPW8UtdJkZ9Dhlf+D4X4dJ9xfuH8y04EfuCP2k4aGPPq/aWxU9/xe3LydHmYW1/K3zu3NFO9ETVrZettz3w=="))) + }) + + t.Run("sha unknown", func(t *testing.T) { + t.Parallel() + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$shaNotExistent$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw=="))) + }) + + t.Run("md5", func(t *testing.T) { + t.Parallel() + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5$CY9rzUYh03PK3k6DJie09g=="))) + assert.Nil(t, hash.CompareMD5(ctx, []byte("test"), []byte("$md5$CY9rzUYh03PK3k6DJie09g=="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$WhBei51A4TKXgNYuoiZdig=="))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$Dk/E5LQLsx4yt8QbUbvpdg=="))) + + assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$ptoWyof5SobW+pbZu2QXoQ=="))) + assert.Nil(t, hash.CompareMD5(ctx, []byte("ory"), []byte("$md5$ptoWyof5SobW+pbZu2QXoQ=="))) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$4skj967KRHFsnPFoL5dMMw=="))) + + assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$$")), hash.ErrInvalidHash) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$$$"))) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=$$"))) + assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$Z$")), base64.CorruptInputError(0)) + assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$Z$")), base64.CorruptInputError(0)) + assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$MTIz$Z")), base64.CorruptInputError(0)) + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9e1BBU1NXT1JEfQ==$MTIz$q+RdKCgc+ipCAcm5ChQwlQ=="))) // pf={SALT}{PASSWORD} salt=123 + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9e1BBU1NXT1JEfQ==$MTIz$hh8ZTp1hGPPZQqcr4+UXSQ=="))) + + assert.Nil(t, hash.CompareMD5(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9JCR7UEFTU1dPUkR9$MTIzNA==$ud392Z8rfZ+Ou7ZFXYLKbA=="))) // pf={SALT}$${PASSWORD} salt=1234 + assert.Error(t, hash.CompareMD5(ctx, []byte("test1"), []byte("$md5$pf=e1NBTFR9JCR7UEFTU1dPUkR9$MTIzNA==$ud392Z8rfZ+Ou7ZFXYLKbA=="))) + + assert.Nil(t, hash.CompareMD5(ctx, []byte("ory"), []byte("$md5$pf=e1BBU1NXT1JEfXtTQUxUfSQ/$MTIzNDU2Nzg5$8PhwWanVRnpJAFK4NUjR0w=="))) // pf={PASSWORD}{SALT}$? salt=123456789 + assert.Error(t, hash.CompareMD5(ctx, []byte("ory1"), []byte("$md5$pf=e1BBU1NXT1JEfXtTQUxUfSQ/$MTIzNDU2Nzg5$8PhwWanVRnpJAFK4NUjR0w=="))) + }) + + t.Run("md5-crypt", func(t *testing.T) { + t.Parallel() + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$TVEiiKNb$SN6/pUaRQS/E8Jh46As2C/"))) + assert.Nil(t, hash.CompareMD5Crypt(ctx, []byte("test"), []byte("$md5-crypt$TVEiiKNb$SN6/pUaRQS/E8Jh46As2C/"))) + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$$whuMjZj.HMFoaTaZRRtkO0"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b."))) + + assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b."))) + assert.Nil(t, hash.CompareMD5Crypt(ctx, []byte("ory"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b."))) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$E7zjruqF$RTglYR1CzBHwwiTk9nVzx1"))) + + assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$$")), hash.ErrMismatchedHashAndPassword) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$$$"))) + // per crypt(5), `md5crypt` can be run without a salt, but the salt section must still be present + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$whuMjZj.HMFoaTaZRRtkO0")), "md5crypt decode error: provided encoded hash has an invalid format") + }) + + t.Run("sha256-crypt", func(t *testing.T) { + t.Parallel() + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78"))) + assert.Nil(t, hash.CompareSHA256Crypt(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3"))) + + assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3"))) + assert.Nil(t, hash.CompareSHA256Crypt(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3"))) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$T95kH8e37IGVdxzJ$gLeaNa6qRog.bx4Bzqp63ceWItH6nSAal6c3WmT5GHB"))) + + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$$")), "shacrypt decode error: provided encoded hash has an invalid format") + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$$$"))) + }) + + t.Run("sha512-crypt", func(t *testing.T) { + t.Parallel() + + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512-crypt$rounds=656000$3LVbIAVxR//cRajw$uuNasMW.RYxlGzIRFU1Was70BPSa933AjxhZIGJdJBOlqJAHlgqa0yuiuq5JHF/ryNGryJkj87G9i3G2HPSXg1"))) + assert.Nil(t, hash.CompareSHA512Crypt(ctx, []byte("test"), []byte("$sha512-crypt$rounds=656000$3LVbIAVxR//cRajw$uuNasMW.RYxlGzIRFU1Was70BPSa933AjxhZIGJdJBOlqJAHlgqa0yuiuq5JHF/ryNGryJkj87G9i3G2HPSXg1"))) + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$5$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3"))) + + assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$0baQbxBrfpKqvizk$Q9cYk1MeNAlECPgpG3jjfNI2DumLqd0yHbxzLdxiX6nsSD5i9n0awcbiCf8R5DzpIYxeBPznPcb1wtzlgUKtH0"))) + assert.Nil(t, hash.CompareSHA512Crypt(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$0baQbxBrfpKqvizk$Q9cYk1MeNAlECPgpG3jjfNI2DumLqd0yHbxzLdxiX6nsSD5i9n0awcbiCf8R5DzpIYxeBPznPcb1wtzlgUKtH0"))) + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$hNcDLFO63bkYVDZf$Mt9dhH0xqfxWZ6Pu8zXw.Ku5f15IRTweuaDcUc.ObXWGn7B1h8YIWLmArZd8psd2mrUVswCXLAVptmISr.8iI/"))) + + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$$")), "shacrypt decode error: provided encoded hash has an invalid format") + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$$$"))) + }) + + t.Run("hmac errors", func(t *testing.T) { + t.Parallel() + + //Missing Key + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk="))) + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$")), hash.ErrMismatchedHashAndPassword) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$"))) + //Missing Password Hash + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$MTIzNDU=")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$MTIzNDU="))) + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$$MTIzNDU=")), hash.ErrMismatchedHashAndPassword) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$$MTIzNDU="))) + //Missing Password Hash and Key + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$"))) + //Missing Hash Algorithm + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + //Missing Invalid Hash Algorithm + assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm) + assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + }) + + t.Run("hmac-md4", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNA==")), hash.ErrMismatchedHashAndPassword) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + }) + + t.Run("hmac-md5", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + }) + + t.Run("hmac-sha1", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + + }) + + t.Run("hmac-sha224", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + + }) + + t.Run("hmac-sha256", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + + }) + + t.Run("hmac-sha384", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + + }) + + t.Run("hmac-sha512", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + + }) +} diff --git a/hydra/fake.go b/hydra/fake.go new file mode 100644 index 0000000..4087418 --- /dev/null +++ b/hydra/fake.go @@ -0,0 +1,70 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hydra + +import ( + "context" + "errors" + + "github.com/ory/herodot" + hydraclientgo "github.com/ory/hydra-client-go/v2" +) + +const ( + FakeInvalidLoginChallenge = "2e98454e-031b-4870-9ad6-8517df1ce604" + FakeValidLoginChallenge = "5ff59a39-ecc5-467e-bb10-26644c0700ee" + FakePostLoginURL = "https://www.example.com/fake-post-login" + FakeClientID = "fake-oauth2-client-id" + FakeClientName = "Fake OAuth2 Client" + FakeClientLogoURI = "https://www.example.com/logo.png" +) + +var ErrFakeAcceptLoginRequestFailed = errors.New("failed to accept login request") + +type FakeHydra struct { + Skip bool + RequestURL string +} + +var _ Hydra = &FakeHydra{} + +func NewFake() *FakeHydra { + return &FakeHydra{ + RequestURL: "https://www.ory.com", + } +} + +func (h *FakeHydra) AcceptLoginRequest(_ context.Context, params AcceptLoginRequestParams) (string, error) { + if params.SessionID == "" { + return "", errors.New("session id must not be empty") + } + switch params.LoginChallenge { + case FakeInvalidLoginChallenge: + return "", ErrFakeAcceptLoginRequestFailed + case FakeValidLoginChallenge: + return FakePostLoginURL, nil + default: + panic("unknown fake login_challenge " + params.LoginChallenge) + } +} + +func (h *FakeHydra) GetLoginRequest(_ context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) { + switch loginChallenge { + case FakeInvalidLoginChallenge: + return nil, herodot.ErrBadRequest().WithReasonf("Unable to get OAuth 2.0 Login Challenge.") + case FakeValidLoginChallenge: + return &hydraclientgo.OAuth2LoginRequest{ + Challenge: loginChallenge, + RequestUrl: h.RequestURL, + Skip: h.Skip, + Client: hydraclientgo.OAuth2Client{ + ClientId: hydraclientgo.PtrString(FakeClientID), + ClientName: hydraclientgo.PtrString(FakeClientName), + LogoUri: hydraclientgo.PtrString(FakeClientLogoURI), + }, + }, nil + default: + panic("unknown fake login_challenge " + loginChallenge) + } +} diff --git a/hydra/hydra.go b/hydra/hydra.go new file mode 100644 index 0000000..7576ae1 --- /dev/null +++ b/hydra/hydra.go @@ -0,0 +1,172 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hydra + +import ( + "context" + "net/http" + "time" + + "github.com/ory/x/httpx" + "github.com/ory/x/sqlxx" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/session" +) + +type ( + hydraDependencies interface { + config.Provider + httpx.ClientProvider + } + Provider interface { + Hydra() Hydra + } + AcceptLoginRequestParams struct { + LoginChallenge string + IdentityID string + SessionID string + AuthenticationMethods session.AuthenticationMethods + } + Hydra interface { + AcceptLoginRequest(ctx context.Context, params AcceptLoginRequestParams) (string, error) + GetLoginRequest(ctx context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) + } + DefaultHydra struct { + d hydraDependencies + } +) + +func NewDefaultHydra(d hydraDependencies) *DefaultHydra { + return &DefaultHydra{ + d: d, + } +} + +func GetLoginChallengeID(conf *config.Config, r *http.Request) (sqlxx.NullString, error) { + if !r.URL.Query().Has("login_challenge") { + return "", nil + } else if conf.OAuth2ProviderURL(r.Context()) == nil { + return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason("refusing to parse login_challenge query parameter because " + config.ViperKeyOAuth2ProviderURL + " is invalid or unset")) + } + + loginChallenge := r.URL.Query().Get("login_challenge") + if loginChallenge == "" { + return "", errors.WithStack(herodot.ErrBadRequest().WithReason("the login_challenge parameter is present but empty")) + } + + return sqlxx.NullString(loginChallenge), nil +} + +func (h *DefaultHydra) getAdminURL(ctx context.Context) (string, error) { + u := h.d.Config().OAuth2ProviderURL(ctx) + if u == nil { + return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason(config.ViperKeyOAuth2ProviderURL + " is not configured")) + } + return u.String(), nil +} + +func (h *DefaultHydra) getAdminAPIClient(ctx context.Context) (hydraclientgo.OAuth2API, error) { + url, err := h.getAdminURL(ctx) + if err != nil { + return nil, err + } + + configuration := hydraclientgo.NewConfiguration() + configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}} + + client := h.d.HTTPClient(ctx).StandardClient() + if header := h.d.Config().OAuth2ProviderHeader(ctx); header != nil { + client.Transport = httpx.WrapTransportWithHeader(client.Transport, header) + } + + configuration.HTTPClient = client + return hydraclientgo.NewAPIClient(configuration).OAuth2API, nil +} + +func (h *DefaultHydra) AcceptLoginRequest(ctx context.Context, params AcceptLoginRequestParams) (string, error) { + remember := h.d.Config().SessionPersistentCookie(ctx) + rememberFor := int64(h.d.Config().SessionLifespan(ctx) / time.Second) + + alr := hydraclientgo.NewAcceptOAuth2LoginRequest(params.IdentityID) + alr.IdentityProviderSessionId = ¶ms.SessionID + alr.Remember = &remember + alr.RememberFor = &rememberFor + alr.Amr = []string{} + for _, r := range params.AuthenticationMethods { + alr.Amr = append(alr.Amr, string(r.Method)) + } + + aa, err := h.getAdminAPIClient(ctx) + if err != nil { + return "", err + } + + resp, r, err := aa.AcceptOAuth2LoginRequest(ctx).LoginChallenge(params.LoginChallenge).AcceptOAuth2LoginRequest(*alr).Execute() + if err != nil { + innerErr := herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to accept OAuth 2.0 Login Challenge.") + if r != nil { + innerErr = innerErr. + WithDetail("status_code", r.StatusCode). + WithDebug(err.Error()) + } + + if openApiErr := new(hydraclientgo.GenericOpenAPIError); errors.As(err, &openApiErr) { + switch oauth2Err := openApiErr.Model().(type) { + case hydraclientgo.ErrorOAuth2: + innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint()) + case *hydraclientgo.ErrorOAuth2: + innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint()) + } + } + + return "", errors.WithStack(innerErr) + } + + return resp.RedirectTo, nil +} + +func (h *DefaultHydra) GetLoginRequest(ctx context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) { + if loginChallenge == "" { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid login_challenge")) + } + + aa, err := h.getAdminAPIClient(ctx) + if err != nil { + return nil, err + } + + hlr, r, err := aa.GetOAuth2LoginRequest(ctx).LoginChallenge(loginChallenge).Execute() + if err != nil { + var innerErr *herodot.DefaultError + if r == nil || r.StatusCode >= 500 { + innerErr = herodot.ErrInternalServerError() + } else { + innerErr = herodot.ErrBadRequest() + } + innerErr = innerErr.WithReasonf("Unable to get OAuth 2.0 Login Challenge.") + if r != nil { + innerErr = innerErr. + WithDetail("status_code", r.StatusCode). + WithDebug(err.Error()) + } + + if openApiErr := new(hydraclientgo.GenericOpenAPIError); errors.As(err, &openApiErr) { + switch oauth2Err := openApiErr.Model().(type) { + case hydraclientgo.ErrorOAuth2: + innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint()) + case *hydraclientgo.ErrorOAuth2: + innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint()) + } + } + + return nil, errors.WithStack(innerErr) + } + + return hlr, nil +} diff --git a/hydra/hydra_test.go b/hydra/hydra_test.go new file mode 100644 index 0000000..7ac2b0f --- /dev/null +++ b/hydra/hydra_test.go @@ -0,0 +1,105 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hydra_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "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" +) + +func requestFromChallenge(s string) *http.Request { + return &http.Request{URL: urlx.ParseOrPanic("https://hydra?login_challenge=" + s)} +} + +func TestGetLoginChallengeID(t *testing.T) { + uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a" + blobChallenge := "1337deadbeefcafe" + defaultConfig := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation()) + configWithHydra := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ + config.ViperKeyOAuth2ProviderURL: "https://hydra", + })) + + type args struct { + conf *config.Config + r *http.Request + } + tests := []struct { + name string + args args + want string + assertErr assert.ErrorAssertionFunc + }{ + { + name: "no login challenge; hydra is not configured", + args: args{ + conf: defaultConfig, + r: &http.Request{URL: urlx.ParseOrPanic("https://hydra")}, + }, + want: "", + assertErr: assert.NoError, + }, + { + name: "no login challenge; hydra is configured", + args: args{ + conf: configWithHydra, + r: &http.Request{URL: urlx.ParseOrPanic("https://hydra")}, + }, + want: "", + assertErr: assert.NoError, + }, + { + name: "empty login challenge; hydra is configured", + args: args{ + conf: configWithHydra, + r: requestFromChallenge(""), + }, + want: "", + assertErr: assert.Error, + }, + { + name: "login_challenge is present; Hydra is not configured", + args: args{ + conf: defaultConfig, + r: requestFromChallenge(uuidChallenge), + }, + want: "", + assertErr: assert.Error, + }, + { + name: "login_challenge is present; hydra is configured", + args: args{ + conf: configWithHydra, + r: requestFromChallenge(uuidChallenge), + }, + want: uuidChallenge, + assertErr: assert.NoError, + }, + { + name: "login_challenge is present & non-uuid; hydra is configured", + args: args{ + conf: configWithHydra, + r: requestFromChallenge(blobChallenge), + }, + want: blobChallenge, + assertErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := hydra.GetLoginChallengeID(tt.args.conf, tt.args.r) + tt.assertErr(t, err) + assert.Equal(t, sqlxx.NullString(tt.want), got) + }) + } +} diff --git a/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=admin.json b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=admin.json new file mode 100644 index 0000000..61870ef --- /dev/null +++ b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=admin.json @@ -0,0 +1,23 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + ], + "config": { + "hashed_password": "foo", + "some-random-key": " some-random-value" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=public.json b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=public.json new file mode 100644 index 0000000..61870ef --- /dev/null +++ b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password-endpoint=public.json @@ -0,0 +1,23 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + ], + "config": { + "hashed_password": "foo", + "some-random-key": " some-random-value" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password.json b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password.json new file mode 100644 index 0000000..3d14c7d --- /dev/null +++ b/identity/.snapshots/TestHandler-case=PATCH_should_allow_to_update_credential_password.json @@ -0,0 +1,22 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + ], + "config": { + "hashed_password": "secret", + "some-random-key": " some-random-value" + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "c6eaf1da-4c4e-5da0-aa91-77299464d869@ory.sh" + }, + "metadata_public": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json new file mode 100644 index 0000000..f3d035f --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json @@ -0,0 +1,69 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "config": { + "providers": [ + { + "subject": "import-2", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "import-2", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "import-2@ory.sh" + ], + "config": { + }, + "version": 1 + }, + "saml": { + "type": "saml", + "identifiers": [ + "okta:import-saml-2", + "onelogin:import-saml-2" + ], + "config": { + "providers": [ + { + "subject": "import-saml-2", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "import-saml-2", + "provider": "onelogin", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-2@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA.json new file mode 100644 index 0000000..32ff0d5 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-6@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-6@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA256.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA256.json new file mode 100644 index 0000000..6b3a9df --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA256.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-7@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-7@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA512.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA512.json new file mode 100644 index 0000000..2cad04d --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=SSHA512.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-8@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-8@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2i.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2i.json new file mode 100644 index 0000000..d095dae --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2i.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-2@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-2@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2id.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2id.json new file mode 100644 index 0000000..6651b80 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=argon2id.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-3@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-3@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=bcrypt2.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=bcrypt2.json new file mode 100644 index 0000000..44f814d --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=bcrypt2.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-1@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-1@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json new file mode 100644 index 0000000..09cefd0 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-9@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-9@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=md5.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=md5.json new file mode 100644 index 0000000..33ecd00 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=md5.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-5@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-5@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=pkbdf2.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=pkbdf2.json new file mode 100644 index 0000000..f709c97 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=pkbdf2.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-0@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-0@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=scrypt.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=scrypt.json new file mode 100644 index 0000000..d9e8179 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=scrypt.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-4@ory.sh" + ], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-4@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_not-normalized_email.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_not-normalized_email.json new file mode 100644 index 0000000..a9656fb --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_not-normalized_email.json @@ -0,0 +1,34 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "uppercased@ory.sh" + ], + "config": {}, + "version": 1 + } + }, + "schema_id": "customer", + "state": "active", + "traits": { + "email": "UpperCased@ory.sh" + }, + "verifiable_addresses": [ + { + "value": "uppercased@ory.sh", + "verified": true, + "via": "email", + "status": "completed" + } + ], + "recovery_addresses": [ + { + "value": "uppercased@ory.sh", + "via": "email" + } + ], + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json new file mode 100644 index 0000000..53bc6ac --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json @@ -0,0 +1,72 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "config": { + "providers": [ + { + "subject": "import-org-3", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" + }, + { + "subject": "import-org-3", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" + } + ] + }, + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "import-3@ory.sh" + ], + "config": {}, + "version": 1 + }, + "saml": { + "type": "saml", + "identifiers": [ + "okta:import-saml-org-3", + "onelogin:import-saml-org-3" + ], + "config": { + "providers": [ + { + "subject": "import-saml-org-3", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" + }, + { + "subject": "import-saml-org-3", + "provider": "onelogin", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" + } + ] + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-3@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} 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 0000000..00f3a7f --- /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": 1 + } + }, + "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_be_able_to_import_users-without_any_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json new file mode 100644 index 0000000..79228d8 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-1@ory.sh" + ], + "config": {}, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-1@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_traits.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_traits.json new file mode 100644 index 0000000..fabb2d6 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_traits.json @@ -0,0 +1,8 @@ +{ + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-admin.json b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-admin.json new file mode 100644 index 0000000..f998999 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-admin.json @@ -0,0 +1,44 @@ +{ + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + "credentials": [ + { + "public_key": "cFFFQ0F5WWdBU0ZZSU1KTFFoSnhRUnpobktQVGNQQ1VPRE9teFlEWW8yb2JybTliaHA1bHZTWjNJbGdnWGpoWnZKYVBVcUY5UFhxWnFUZFdZUFI3UitiMm4vV2krSXhLS1hzUzRyVT0=", + "attestation_type": "none", + "authenticator": { + "aaguid": "cmM0QUFqVzh4Z3BraXdzbDhmQlZBdz09", + "sign_count": 0, + "clone_warning": false + }, + "display_name": "test", + "added_at": "2022-12-16T14:11:55Z", + "is_passwordless": true + }, + { + "public_key": "cFFFQ0F5WWdBU0ZZSU1KTFFoSnhRUnpobktQVGNQQ1VPRE9teFlEWW8yb2JybTliaHA1bHZTWjNJbGdnWGpoWnZKYVBVcUY5UFhxWnFUZFdZUFI3UitiMm4vV2krSXhLS1hzUzRyVT0=", + "attestation_type": "none", + "authenticator": { + "aaguid": "cmM0QUFqVzh4Z3BraXdzbDhmQlZBdz09", + "sign_count": 0, + "clone_warning": false + }, + "display_name": "test", + "added_at": "2022-12-16T14:11:55Z", + "is_passwordless": true + } + ], + "user_handle": "RWY1SmlNcE1Sd3V6YXVXcy85SjBnUT09" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-public.json b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-public.json new file mode 100644 index 0000000..f998999 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_and_multiple_fido_mfa_type-public.json @@ -0,0 +1,44 @@ +{ + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + "credentials": [ + { + "public_key": "cFFFQ0F5WWdBU0ZZSU1KTFFoSnhRUnpobktQVGNQQ1VPRE9teFlEWW8yb2JybTliaHA1bHZTWjNJbGdnWGpoWnZKYVBVcUY5UFhxWnFUZFdZUFI3UitiMm4vV2krSXhLS1hzUzRyVT0=", + "attestation_type": "none", + "authenticator": { + "aaguid": "cmM0QUFqVzh4Z3BraXdzbDhmQlZBdz09", + "sign_count": 0, + "clone_warning": false + }, + "display_name": "test", + "added_at": "2022-12-16T14:11:55Z", + "is_passwordless": true + }, + { + "public_key": "cFFFQ0F5WWdBU0ZZSU1KTFFoSnhRUnpobktQVGNQQ1VPRE9teFlEWW8yb2JybTliaHA1bHZTWjNJbGdnWGpoWnZKYVBVcUY5UFhxWnFUZFdZUFI3UitiMm4vV2krSXhLS1hzUzRyVT0=", + "attestation_type": "none", + "authenticator": { + "aaguid": "cmM0QUFqVzh4Z3BraXdzbDhmQlZBdz09", + "sign_count": 0, + "clone_warning": false + }, + "display_name": "test", + "added_at": "2022-12-16T14:11:55Z", + "is_passwordless": true + } + ], + "user_handle": "RWY1SmlNcE1Sd3V6YXVXcy85SjBnUT09" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-admin.json b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-admin.json new file mode 100644 index 0000000..6f76024 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-admin.json @@ -0,0 +1,32 @@ +{ + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + "credentials": [ + { + "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==" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-public.json b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-public.json new file mode 100644 index 0000000..6f76024 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_delete_credential_of_a_specific_user_and_no_longer_be_able_to_retrieve_it-type=remove_webauthn_passwordless_type-public.json @@ -0,0 +1,32 @@ +{ + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + "credentials": [ + { + "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==" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "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 0000000..a9624ce --- /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\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"subject\":\"baz\",\"provider\":\"zab\"}]}" diff --git a/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_0.json b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_0.json new file mode 100644 index 0000000..399733b --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_0.json @@ -0,0 +1,27 @@ +{ + "credentials": { + "password": { + "type": "password", + "version": 1 + } + }, + "external_id": "external-id-Batch-Import-0", + "metadata_admin": { + "admin-0": "admin" + }, + "metadata_public": { + "public-0": "public" + }, + "organization_id": null, + "schema_id": "multiple_emails", + "state": "active", + "traits": { + "emails": [ + "Batch-Import-0-0@ory.sh", + "Batch-Import-0-1@ory.sh", + "Batch-Import-0-2@ory.sh", + "Batch-Import-0-3@ory.sh" + ], + "username": "Batch-Import-0-0@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_1.json b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_1.json new file mode 100644 index 0000000..f594fed --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_1.json @@ -0,0 +1,26 @@ +{ + "credentials": { + "password": { + "type": "password", + "version": 1 + } + }, + "metadata_admin": { + "admin-1": "admin" + }, + "metadata_public": { + "public-1": "public" + }, + "organization_id": null, + "schema_id": "multiple_emails", + "state": "active", + "traits": { + "emails": [ + "batch-import-1-0@ory.sh", + "batch-import-1-1@ory.sh", + "batch-import-1-2@ory.sh", + "batch-import-1-3@ory.sh" + ], + "username": "batch-import-1-0@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_2.json b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_2.json new file mode 100644 index 0000000..a8ca34c --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_2.json @@ -0,0 +1,27 @@ +{ + "credentials": { + "password": { + "type": "password", + "version": 1 + } + }, + "external_id": "external-id-batch-import-2", + "metadata_admin": { + "admin-2": "admin" + }, + "metadata_public": { + "public-2": "public" + }, + "organization_id": null, + "schema_id": "multiple_emails", + "state": "active", + "traits": { + "emails": [ + "batch-import-2-0@ory.sh", + "batch-import-2-1@ory.sh", + "batch-import-2-2@ory.sh", + "batch-import-2-3@ory.sh" + ], + "username": "batch-import-2-0@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_3.json b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_3.json new file mode 100644 index 0000000..921c613 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=PATCH_identities-case=success-assert=identity_3.json @@ -0,0 +1,26 @@ +{ + "credentials": { + "password": { + "type": "password", + "version": 1 + } + }, + "metadata_admin": { + "admin-3": "admin" + }, + "metadata_public": { + "public-3": "public" + }, + "organization_id": null, + "schema_id": "multiple_emails", + "state": "active", + "traits": { + "emails": [ + "batch-import-3-0@ory.sh", + "batch-import-3-1@ory.sh", + "batch-import-3-2@ory.sh", + "batch-import-3-3@ory.sh" + ], + "username": "batch-import-3-0@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_and_webauthn_credentials_included.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_and_webauthn_credentials_included.json new file mode 100644 index 0000000..13d2457 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_and_webauthn_credentials_included.json @@ -0,0 +1,41 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "bar", + "zab" + ], + "config": { + "some": "secret" + }, + "version": 1 + }, + "webauthn": { + "type": "webauthn", + "identifiers": [ + "bar", + "foo" + ], + "config": { + "some": "secret", + "user_handle": "rVIFaWRcTTuQLkXFmQWpgA==" + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_credentials_included.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_credentials_included.json new file mode 100644 index 0000000..486b3f3 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_with_password_credentials_included.json @@ -0,0 +1,37 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "bar", + "zab" + ], + "config": { + "some": "secret" + }, + "version": 1 + }, + "webauthn": { + "type": "webauthn", + "identifiers": [ + "bar", + "foo" + ], + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_without_credentials_included.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_without_credentials_included.json new file mode 100644 index 0000000..19987d0 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_get_identity_with_credentials-case=should_get_identity_without_credentials_included.json @@ -0,0 +1,34 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "bar", + "zab" + ], + "version": 1 + }, + "webauthn": { + "type": "webauthn", + "identifiers": [ + "bar", + "foo" + ], + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": {}, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_lookup_secret_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_lookup_secret_credentials.json new file mode 100644 index 0000000..f08c874 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_lookup_secret_credentials.json @@ -0,0 +1,44 @@ +{ + "credentials": { + "lookup_secret": { + "type": "lookup_secret", + "config": { + "recovery_codes": [ + { + "code": "first-code", + "used_at": null + }, + { + "code": "second-code", + "used_at": null + }, + { + "code": "third-code", + "used_at": null + }, + { + "code": "fourth-code", + "used_at": null + } + ] + }, + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "lookup-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "lookup-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_oidc_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_oidc_credentials.json new file mode 100644 index 0000000..e9f8b20 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_oidc_credentials.json @@ -0,0 +1,30 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "identifiers": [ + "github:user-subject-2", + "google:user-subject-1" + ], + "config": { + }, + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "oidc-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "oidc-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_passkey_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_passkey_credentials.json new file mode 100644 index 0000000..a2e6c5c --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_passkey_credentials.json @@ -0,0 +1,29 @@ +{ + "credentials": { + "passkey": { + "type": "passkey", + "identifiers": [ + "user-handle-1" + ], + "config": { + }, + "version": 0 + }, + "password": { + "type": "password", + "identifiers": [ + "passkey-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "passkey-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_saml_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_saml_credentials.json new file mode 100644 index 0000000..793b096 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_saml_credentials.json @@ -0,0 +1,30 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "saml-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + }, + "saml": { + "type": "saml", + "identifiers": [ + "auth0:user-subject-2", + "okta:user-subject-1" + ], + "config": { + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "saml-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_totp_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_totp_credentials.json new file mode 100644 index 0000000..8b84124 --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_totp_credentials.json @@ -0,0 +1,27 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "totp-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + }, + "totp": { + "type": "totp", + "config": { + "totp_url": "totp://example.com?secret=NBSWY3DPEHPK3PXQ\u0026issuer=ORY" + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "totp-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_webauthn_credentials.json b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_webauthn_credentials.json new file mode 100644 index 0000000..522653d --- /dev/null +++ b/identity/.snapshots/TestHandler-suite=create_and_update-case=should_update_an_identity_with_webauthn_credentials.json @@ -0,0 +1,27 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "webauthn-import-updated@ory.sh" + ], + "config": {}, + "version": 1 + }, + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + }, + "version": 1 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "webauthn-import-updated@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_with_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_with_organization.json new file mode 100644 index 0000000..f33b9ef --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_with_organization.json @@ -0,0 +1,21 @@ +{ + "type": "oidc", + "identifiers": [ + "github:12345" + ], + "config": { + "providers": [ + { + "subject": "12345", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_without_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_without_organization.json new file mode 100644 index 0000000..d22cba0 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_new_credential_without_organization.json @@ -0,0 +1,20 @@ +{ + "type": "oidc", + "identifiers": [ + "github:12345" + ], + "config": { + "providers": [ + { + "subject": "12345", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_with_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_with_organization.json new file mode 100644 index 0000000..8992fa2 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_with_organization.json @@ -0,0 +1,29 @@ +{ + "type": "oidc", + "identifiers": [ + "google:67890", + "github:12345" + ], + "config": { + "providers": [ + { + "subject": "67890", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "12345", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_without_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_without_organization.json new file mode 100644 index 0000000..c227d75 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_credential_without_organization.json @@ -0,0 +1,28 @@ +{ + "type": "oidc", + "identifiers": [ + "google:67890", + "github:12345" + ], + "config": { + "providers": [ + { + "subject": "67890", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "12345", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_with_multiple_providers.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_with_multiple_providers.json new file mode 100644 index 0000000..3d8779c --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-OIDC_update_with_multiple_providers.json @@ -0,0 +1,37 @@ +{ + "type": "oidc", + "identifiers": [ + "google:67890", + "github:12345", + "gitlab:abcdef" + ], + "config": { + "providers": [ + { + "subject": "67890", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "12345", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + }, + { + "subject": "abcdef", + "provider": "gitlab", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_with_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_with_organization.json new file mode 100644 index 0000000..2d67d78 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_with_organization.json @@ -0,0 +1,21 @@ +{ + "type": "saml", + "identifiers": [ + "okta:user123" + ], + "config": { + "providers": [ + { + "subject": "user123", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_without_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_without_organization.json new file mode 100644 index 0000000..934659c --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_new_credential_without_organization.json @@ -0,0 +1,20 @@ +{ + "type": "saml", + "identifiers": [ + "okta:user123" + ], + "config": { + "providers": [ + { + "subject": "user123", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_with_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_with_organization.json new file mode 100644 index 0000000..c79f1f7 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_with_organization.json @@ -0,0 +1,29 @@ +{ + "type": "saml", + "identifiers": [ + "onelogin:user456", + "okta:user123" + ], + "config": { + "providers": [ + { + "subject": "user456", + "provider": "onelogin", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "user123", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_without_organization.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_without_organization.json new file mode 100644 index 0000000..1e3e1a2 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_credential_without_organization.json @@ -0,0 +1,28 @@ +{ + "type": "saml", + "identifiers": [ + "onelogin:user456", + "okta:user123" + ], + "config": { + "providers": [ + { + "subject": "user456", + "provider": "onelogin", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "user123", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_with_multiple_providers.json b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_with_multiple_providers.json new file mode 100644 index 0000000..d5bdd5d --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsOIDCSAML-SAML_update_with_multiple_providers.json @@ -0,0 +1,37 @@ +{ + "type": "saml", + "identifiers": [ + "onelogin:user456", + "okta:user123", + "auth0:user789" + ], + "config": { + "providers": [ + { + "subject": "user456", + "provider": "onelogin", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "user123", + "provider": "okta", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "", + "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" + }, + { + "subject": "user789", + "provider": "auth0", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportCredentialsPassword-import_clear_text_password.json b/identity/.snapshots/TestImportCredentialsPassword-import_clear_text_password.json new file mode 100644 index 0000000..8ae3010 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsPassword-import_clear_text_password.json @@ -0,0 +1,11 @@ +{ + "password": { + "type": "password", + "identifiers": null, + "config": { + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportCredentialsPassword-import_hashed_password.json b/identity/.snapshots/TestImportCredentialsPassword-import_hashed_password.json new file mode 100644 index 0000000..8ae3010 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsPassword-import_hashed_password.json @@ -0,0 +1,11 @@ +{ + "password": { + "type": "password", + "identifiers": null, + "config": { + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportCredentialsPassword-import_with_password_migration_hook.json b/identity/.snapshots/TestImportCredentialsPassword-import_with_password_migration_hook.json new file mode 100644 index 0000000..7f40b20 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsPassword-import_with_password_migration_hook.json @@ -0,0 +1,12 @@ +{ + "password": { + "type": "password", + "identifiers": null, + "config": { + "use_password_migration_hook": true + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportCredentialsPassword-update_existing_password_credential.json b/identity/.snapshots/TestImportCredentialsPassword-update_existing_password_credential.json new file mode 100644 index 0000000..8ae3010 --- /dev/null +++ b/identity/.snapshots/TestImportCredentialsPassword-update_existing_password_credential.json @@ -0,0 +1,11 @@ +{ + "password": { + "type": "password", + "identifiers": null, + "config": { + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportLookupSecretCredentials-new_lookup_secret_credential.json b/identity/.snapshots/TestImportLookupSecretCredentials-new_lookup_secret_credential.json new file mode 100644 index 0000000..fd36152 --- /dev/null +++ b/identity/.snapshots/TestImportLookupSecretCredentials-new_lookup_secret_credential.json @@ -0,0 +1,19 @@ +{ + "type": "lookup_secret", + "identifiers": null, + "config": { + "recovery_codes": [ + { + "code": "code1", + "used_at": null + }, + { + "code": "code2", + "used_at": null + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportLookupSecretCredentials-update_existing_lookup_secret_credential.json b/identity/.snapshots/TestImportLookupSecretCredentials-update_existing_lookup_secret_credential.json new file mode 100644 index 0000000..4a3fa3d --- /dev/null +++ b/identity/.snapshots/TestImportLookupSecretCredentials-update_existing_lookup_secret_credential.json @@ -0,0 +1,23 @@ +{ + "type": "lookup_secret", + "identifiers": null, + "config": { + "recovery_codes": [ + { + "code": "existing-code", + "used_at": null + }, + { + "code": "new-code1", + "used_at": null + }, + { + "code": "new-code2", + "used_at": null + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestImportPasskeyCredentials-new_passkey_credentials.json b/identity/.snapshots/TestImportPasskeyCredentials-new_passkey_credentials.json new file mode 100644 index 0000000..50d74ed --- /dev/null +++ b/identity/.snapshots/TestImportPasskeyCredentials-new_passkey_credentials.json @@ -0,0 +1,32 @@ +{ + "passkey": { + "type": "passkey", + "identifiers": [ + "user1" + ], + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsx", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "dXNlcjE=" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportPasskeyCredentials-new_user_handle_added_to_identifiers.json b/identity/.snapshots/TestImportPasskeyCredentials-new_user_handle_added_to_identifiers.json new file mode 100644 index 0000000..354dd83 --- /dev/null +++ b/identity/.snapshots/TestImportPasskeyCredentials-new_user_handle_added_to_identifiers.json @@ -0,0 +1,33 @@ +{ + "passkey": { + "type": "passkey", + "identifiers": [ + "existingUser", + "newUser" + ], + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsx", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "bmV3VXNlcg==" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportPasskeyCredentials-update_existing_credentials_-_add_new,_update_existing.json b/identity/.snapshots/TestImportPasskeyCredentials-update_existing_credentials_-_add_new,_update_existing.json new file mode 100644 index 0000000..3c4cdce --- /dev/null +++ b/identity/.snapshots/TestImportPasskeyCredentials-update_existing_credentials_-_add_new,_update_existing.json @@ -0,0 +1,40 @@ +{ + "passkey": { + "type": "passkey", + "identifiers": [ + "existingUser" + ], + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsxLXVwZGF0ZWQ=", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDM=", + "public_key": "cGsz", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "ZXhpc3RpbmdVc2Vy" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportTOTPCredentials-new_totp_credential.json b/identity/.snapshots/TestImportTOTPCredentials-new_totp_credential.json new file mode 100644 index 0000000..e3fec37 --- /dev/null +++ b/identity/.snapshots/TestImportTOTPCredentials-new_totp_credential.json @@ -0,0 +1,12 @@ +{ + "totp": { + "type": "totp", + "identifiers": null, + "config": { + "totp_url": "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP\u0026issuer=Example" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportTOTPCredentials-update_existing_totp_credential.json b/identity/.snapshots/TestImportTOTPCredentials-update_existing_totp_credential.json new file mode 100644 index 0000000..7a8571a --- /dev/null +++ b/identity/.snapshots/TestImportTOTPCredentials-update_existing_totp_credential.json @@ -0,0 +1,12 @@ +{ + "totp": { + "type": "totp", + "identifiers": null, + "config": { + "totp_url": "otpauth://totp/Example:alice@example.com?secret=NEWSECRET\u0026issuer=Example" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportWebAuthnCredentials-new_webauthn_credentials.json b/identity/.snapshots/TestImportWebAuthnCredentials-new_webauthn_credentials.json new file mode 100644 index 0000000..0e1d489 --- /dev/null +++ b/identity/.snapshots/TestImportWebAuthnCredentials-new_webauthn_credentials.json @@ -0,0 +1,30 @@ +{ + "webauthn": { + "type": "webauthn", + "identifiers": null, + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsx", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "dXNlcjE=" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportWebAuthnCredentials-override_existing_user_handle.json b/identity/.snapshots/TestImportWebAuthnCredentials-override_existing_user_handle.json new file mode 100644 index 0000000..9c432ae --- /dev/null +++ b/identity/.snapshots/TestImportWebAuthnCredentials-override_existing_user_handle.json @@ -0,0 +1,30 @@ +{ + "webauthn": { + "type": "webauthn", + "identifiers": null, + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsx", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "bmV3VXNlcg==" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestImportWebAuthnCredentials-update_existing_credentials_-_add_new,_update_existing.json b/identity/.snapshots/TestImportWebAuthnCredentials-update_existing_credentials_-_add_new,_update_existing.json new file mode 100644 index 0000000..480906d --- /dev/null +++ b/identity/.snapshots/TestImportWebAuthnCredentials-update_existing_credentials_-_add_new,_update_existing.json @@ -0,0 +1,38 @@ +{ + "webauthn": { + "type": "webauthn", + "identifiers": null, + "config": { + "credentials": [ + { + "id": "Y3JlZDE=", + "public_key": "cGsxLXVwZGF0ZWQ=", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDI=", + "public_key": "cGsy", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + }, + { + "id": "Y3JlZDM=", + "public_key": "cGsz", + "attestation_type": "", + "display_name": "", + "added_at": "0001-01-01T00:00:00Z", + "is_passwordless": false + } + ], + "user_handle": "ZXhpc3RpbmdVc2Vy" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestMarshalIdentityWithAll.json b/identity/.snapshots/TestMarshalIdentityWithAll.json new file mode 100644 index 0000000..e659f27 --- /dev/null +++ b/identity/.snapshots/TestMarshalIdentityWithAll.json @@ -0,0 +1,12 @@ +{ + "password": { + "type": "password", + "identifiers": null, + "config": { + "some": "secret" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=0.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=0.json new file mode 100644 index 0000000..9d4512a --- /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 0000000..9d4512a --- /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 0000000..b189c30 --- /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 0000000..b189c30 --- /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 0000000..968d3fa --- /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 0000000..0a7e732 --- /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=14.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=14.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=14.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=15.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=15.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=15.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=18.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=18.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=18.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=19.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=19.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=19.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=2.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=2.json new file mode 100644 index 0000000..5a19603 --- /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=20.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=20.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=20.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=21.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=21.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=21.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=22.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=22.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=22.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=23.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=23.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=23.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=24.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=24.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=24.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=25.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=25.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=25.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=26.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=26.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=26.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=27.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=27.json new file mode 100644 index 0000000..9d4512a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=27.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=28.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=28.json new file mode 100644 index 0000000..affc5c2 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=28.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo.@docomo.ne.jp" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=29.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=29.json new file mode 100644 index 0000000..affc5c2 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=29.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo.@docomo.ne.jp" + } + ] + }, + "version": 1, + "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 0000000..9d4512a --- /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 0000000..5a19603 --- /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 0000000..5a19603 --- /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 0000000..2663d5b --- /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 0000000..6b8c682 --- /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 0000000..6b8c682 --- /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 0000000..b189c30 --- /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/TestToNode.json b/identity/.snapshots/TestToNode.json new file mode 100644 index 0000000..43295a2 --- /dev/null +++ b/identity/.snapshots/TestToNode.json @@ -0,0 +1,59 @@ +{ + "type": "text", + "group": "lookup_secret", + "attributes": { + "text": { + "id": 1050015, + "text": "used, bar, baz, used", + "type": "info", + "context": { + "secrets": [ + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:38 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:38Z", + "used_at_unix": 1629199958 + } + }, + { + "id": 1050009, + "text": "bar", + "type": "info", + "context": { + "secret": "bar" + } + }, + { + "id": 1050009, + "text": "baz", + "type": "info", + "context": { + "secret": "baz" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:48 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:48Z", + "used_at_unix": 1629199968 + } + } + ] + } + }, + "id": "lookup_secret_codes", + "node_type": "text" + }, + "messages": [], + "meta": { + "label": { + "id": 1050010, + "text": "These are your back up recovery codes. Please keep them in a safe place!", + "type": "info" + } + } +} diff --git a/identity/.snapshots/TestUpgradeCredentials-empty_credentials.json b/identity/.snapshots/TestUpgradeCredentials-empty_credentials.json new file mode 100644 index 0000000..81b0c2a --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-empty_credentials.json @@ -0,0 +1,11 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "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_correct_value.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_correct_value.json new file mode 100644 index 0000000..3673c69 --- /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 0000000..3673c69 --- /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 0000000..9163f3c --- /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 0000000..3673c69 --- /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 0000000..3673c69 --- /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 0000000..a52b01f --- /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=password-from=v0_with_already_normalized_phone.json b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_already_normalized_phone.json new file mode 100644 index 0000000..1024674 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_already_normalized_phone.json @@ -0,0 +1,23 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "+4917667111638" + ], + "config": {}, + "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=password-from=v0_with_email.json b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_email.json new file mode 100644 index 0000000..f5c8f31 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_email.json @@ -0,0 +1,23 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test@example.org" + ], + "config": {}, + "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=password-from=v0_with_mixed_identifiers.json b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_mixed_identifiers.json new file mode 100644 index 0000000..eec3a13 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_mixed_identifiers.json @@ -0,0 +1,25 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "+4917667111638", + "test@example.org", + "username123" + ], + "config": {}, + "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=password-from=v0_with_phone_number.json b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_phone_number.json new file mode 100644 index 0000000..1024674 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v0_with_phone_number.json @@ -0,0 +1,23 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "+4917667111638" + ], + "config": {}, + "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=password-from=v1.json b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v1.json new file mode 100644 index 0000000..1024674 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=password-from=v1.json @@ -0,0 +1,23 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "+4917667111638" + ], + "config": {}, + "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=v0.json b/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json new file mode 100644 index 0000000..0236be2 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json @@ -0,0 +1,53 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [ + "4d64fa08-20fc-450d-bebd-ebd7c7b6e249" + ], + "config": { + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": false + }, + { + "id": "1Q4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": false + } + ], + "user_handle": "TWT6CCD8RQ2+vevXx7biSQ==" + }, + "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 new file mode 100644 index 0000000..4931ed3 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v1.json @@ -0,0 +1,38 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [], + "config": { + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": true + } + ], + "user_handle": "2gZaSs9fTEeGmsBlC4gfgg==" + }, + "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/TestWithDeclassifiedCredentials-case=include-multi-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=oidc.json new file mode 100644 index 0000000..bb7bf6d --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=oidc.json @@ -0,0 +1,10 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=password.json new file mode 100644 index 0000000..c3af785 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=password.json @@ -0,0 +1,13 @@ +{ + "type": "password", + "identifiers": [ + "zab", + "bar" + ], + "config": { + "some": "secret" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=saml.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=saml.json new file mode 100644 index 0000000..d3e9ddb --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=saml.json @@ -0,0 +1,10 @@ +{ + "type": "saml", + "identifiers": [ + "qux", + "quz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=webauthn.json new file mode 100644 index 0000000..f3299b4 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-multi-credential=webauthn.json @@ -0,0 +1,13 @@ +{ + "type": "webauthn", + "identifiers": [ + "foo", + "bar" + ], + "config": { + "some": "secret" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=oidc.json new file mode 100644 index 0000000..bb7bf6d --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=oidc.json @@ -0,0 +1,10 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=password.json new file mode 100644 index 0000000..1939a8f --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-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=include-webauthn-credential=saml.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=saml.json new file mode 100644 index 0000000..d3e9ddb --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=saml.json @@ -0,0 +1,10 @@ +{ + "type": "saml", + "identifiers": [ + "qux", + "quz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=webauthn.json new file mode 100644 index 0000000..f3299b4 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=include-webauthn-credential=webauthn.json @@ -0,0 +1,13 @@ +{ + "type": "webauthn", + "identifiers": [ + "foo", + "bar" + ], + "config": { + "some": "secret" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=oidc.json new file mode 100644 index 0000000..bb7bf6d --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=oidc.json @@ -0,0 +1,10 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=password.json new file mode 100644 index 0000000..1939a8f --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-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=no-include-credential=saml.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=saml.json new file mode 100644 index 0000000..d3e9ddb --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=saml.json @@ -0,0 +1,10 @@ +{ + "type": "saml", + "identifiers": [ + "qux", + "quz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-credential=webauthn.json new file mode 100644 index 0000000..1b7dcd8 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=no-include-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/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json new file mode 100644 index 0000000..935daa2 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json @@ -0,0 +1,21 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "config": { + "providers": [ + { + "initial_id_token": "foo", + "initial_access_token": "", + "initial_refresh_token": "", + "subject": "bar", + "provider": "oidc1" + } + ] + }, + "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 0000000..1939a8f --- /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=saml.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=saml.json new file mode 100644 index 0000000..d3e9ddb --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=saml.json @@ -0,0 +1,10 @@ +{ + "type": "saml", + "identifiers": [ + "qux", + "quz" + ], + "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 0000000..1b7dcd8 --- /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/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=oidc.json new file mode 100644 index 0000000..bb7bf6d --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=oidc.json @@ -0,0 +1,10 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=password.json new file mode 100644 index 0000000..1939a8f --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-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=saml-credential=saml.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=saml.json new file mode 100644 index 0000000..822e9e3 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=saml.json @@ -0,0 +1,18 @@ +{ + "type": "saml", + "identifiers": [ + "qux", + "quz" + ], + "config": { + "providers": [ + { + "subject": "qux", + "provider": "saml1" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-credential=webauthn.json new file mode 100644 index 0000000..1b7dcd8 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=saml-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 new file mode 100644 index 0000000..ae7ab83 --- /dev/null +++ b/identity/address.go @@ -0,0 +1,9 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +const ( + AddressTypeEmail = "email" + AddressTypeSMS = "sms" +) diff --git a/identity/conflict_lookup_test.go b/identity/conflict_lookup_test.go new file mode 100644 index 0000000..e604c64 --- /dev/null +++ b/identity/conflict_lookup_test.go @@ -0,0 +1,126 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" +) + +func TestFindFirstConflictReturnsLowestIndexMatch(t *testing.T) { + match, err := findFirstConflict(t.Context(), []conflictLookup{ + func(ctx context.Context) (*conflictMatch, error) { + return nil, sqlcon.ErrNoRows() + }, + func(ctx context.Context) (*conflictMatch, error) { + return &conflictMatch{address: "lookup-01", addressType: AddressTypeEmail}, nil + }, + func(ctx context.Context) (*conflictMatch, error) { + return &conflictMatch{address: "lookup-02", addressType: CredentialsTypePassword.String()}, nil + }, + }) + + require.NoError(t, err) + require.NotNil(t, match) + assert.Equal(t, "lookup-01", match.address) +} + +func TestFindFirstConflictReturnsNoRowsWhenNothingMatches(t *testing.T) { + var calls atomic.Int32 + var lookups []conflictLookup + for range 20 { + lookups = append(lookups, func(ctx context.Context) (*conflictMatch, error) { + calls.Add(1) + return nil, sqlcon.ErrNoRows() + }) + } + + _, err := findFirstConflict(t.Context(), lookups) + + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + assert.EqualValues(t, 20, calls.Load()) +} + +func TestFindFirstConflictBoundsConcurrency(t *testing.T) { + const lookupCount = 20 + started := make(chan struct{}, lookupCount) + release := make(chan struct{}) + var active atomic.Int32 + + var lookups []conflictLookup + for range lookupCount { + lookups = append(lookups, func(ctx context.Context) (*conflictMatch, error) { + if n := active.Add(1); n > maxParallelConflictLookups { + t.Errorf("%d lookups ran concurrently although the limit is %d", n, maxParallelConflictLookups) + } + defer active.Add(-1) + started <- struct{}{} + select { + case <-release: + case <-ctx.Done(): + return nil, ctx.Err() + } + return nil, sqlcon.ErrNoRows() + }) + } + + done := make(chan error, 1) + go func() { + _, err := findFirstConflict(t.Context(), lookups) + done <- err + }() + + for range maxParallelConflictLookups { + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("timed out waiting for the first lookups to start") + } + } + + select { + case <-started: + t.Fatal("a lookup started although the concurrency limit was reached") + case <-time.After(20 * time.Millisecond): + } + + close(release) + require.ErrorIs(t, <-done, sqlcon.ErrNoRows()) +} + +func TestFindFirstConflictInTransactionShortCircuits(t *testing.T) { + ctx := popx.WithTransaction(t.Context(), &pop.Connection{}) + require.True(t, popx.InTransaction(ctx)) + + var calls []string + record := func(name string, match bool) conflictLookup { + return func(ctx context.Context) (*conflictMatch, error) { + calls = append(calls, name) + if match { + return &conflictMatch{address: name}, nil + } + return nil, sqlcon.ErrNoRows() + } + } + + match, err := findFirstConflict(ctx, []conflictLookup{ + record("lookup-00", false), + record("lookup-01", true), + record("lookup-02", true), + }) + + require.NoError(t, err) + require.NotNil(t, match) + assert.Equal(t, "lookup-01", match.address) + assert.Equal(t, []string{"lookup-00", "lookup-01"}, calls) +} diff --git a/identity/credentials.go b/identity/credentials.go new file mode 100644 index 0000000..6025cfb --- /dev/null +++ b/identity/credentials.go @@ -0,0 +1,356 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "reflect" + "slices" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/wI2L/jsondiff" + + "github.com/ory/herodot" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/sqlxx" +) + +// Authenticator Assurance Level (AAL) +// +// The authenticator assurance level can be one of "aal1", "aal2", or "aal3". A higher number means that it is harder +// for an attacker to compromise the account. +// +// Generally, "aal1" implies that one authentication factor was used while AAL2 implies that two factors (e.g. +// password + TOTP) have been used. +// +// To learn more about these levels please head over to: https://www.ory.com/kratos/docs/concepts/credentials +// +// swagger:model authenticatorAssuranceLevel +type AuthenticatorAssuranceLevel string + +const ( + NoAuthenticatorAssuranceLevel AuthenticatorAssuranceLevel = "aal0" + AuthenticatorAssuranceLevel1 AuthenticatorAssuranceLevel = "aal1" + AuthenticatorAssuranceLevel2 AuthenticatorAssuranceLevel = "aal2" +) + +type NullableAuthenticatorAssuranceLevel struct { + sql.NullString +} + +// NewNullableAuthenticatorAssuranceLevel returns a new NullableAuthenticatorAssuranceLevel +func NewNullableAuthenticatorAssuranceLevel(aal AuthenticatorAssuranceLevel) NullableAuthenticatorAssuranceLevel { + switch aal { + case NoAuthenticatorAssuranceLevel: + fallthrough + case AuthenticatorAssuranceLevel1: + fallthrough + case AuthenticatorAssuranceLevel2: + return NullableAuthenticatorAssuranceLevel{sql.NullString{ + String: string(aal), + Valid: true, + }} + default: + return NullableAuthenticatorAssuranceLevel{sql.NullString{}} + } +} + +// ToAAL returns the AuthenticatorAssuranceLevel value of the given NullableAuthenticatorAssuranceLevel. +func (n NullableAuthenticatorAssuranceLevel) ToAAL() (AuthenticatorAssuranceLevel, bool) { + if !n.Valid { + return "", false + } + switch n.String { + case string(NoAuthenticatorAssuranceLevel): + return NoAuthenticatorAssuranceLevel, true + case string(AuthenticatorAssuranceLevel1): + return AuthenticatorAssuranceLevel1, true + case string(AuthenticatorAssuranceLevel2): + return AuthenticatorAssuranceLevel2, true + default: + return "", false + } +} + +// CredentialsType represents several different credential types, like password credentials, passwordless credentials, +// and so on. +// +// swagger:enum CredentialsType +type CredentialsType string + +// Please make sure to add all of these values to the test that ensures they are created during migration +const ( + CredentialsTypePassword CredentialsType = "password" + CredentialsTypeOIDC CredentialsType = "oidc" + CredentialsTypeTOTP CredentialsType = "totp" + CredentialsTypeLookup CredentialsType = "lookup_secret" + CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeCodeAuth CredentialsType = "code" + CredentialsTypePasskey CredentialsType = "passkey" + CredentialsTypeProfile CredentialsType = "profile" + CredentialsTypeSAML CredentialsType = "saml" + CredentialsTypeDeviceAuthn CredentialsType = "deviceauthn" + CredentialsTypeIdentifierFirst CredentialsType = "identifier_first" // TODO(jonas): Used only for SDK compatibility. We should refactor all the places that use "CredentialType" as a method identifier (flow.Active fields, etc.) +) + +func (c CredentialsType) String() string { + return string(c) +} + +func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { + switch c { + case CredentialsTypePassword: + return node.PasswordGroup + case CredentialsTypeOIDC, CredentialsTypeSAML: + return node.OpenIDConnectGroup + case CredentialsTypeTOTP: + return node.TOTPGroup + case CredentialsTypeWebAuthn: + return node.WebAuthnGroup + case CredentialsTypeLookup: + return node.LookupGroup + case CredentialsTypeCodeAuth: + return node.CodeGroup + case CredentialsTypePasskey: + return node.PasskeyGroup + case CredentialsTypeDeviceAuthn: + return node.DeviceAuthnGroup + case CredentialsTypeIdentifierFirst: + return node.IdentifierFirstGroup + default: + return node.DefaultGroup + } +} + +var AllCredentialTypes = []CredentialsType{ + CredentialsTypePassword, + CredentialsTypeOIDC, + // CredentialsTypeSAML, placeholder for the OEL version + CredentialsTypeTOTP, + CredentialsTypeLookup, + CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, + CredentialsTypePasskey, + CredentialsTypeDeviceAuthn, +} + +const ( + // CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). + // It is not used within the credentials object itself. + CredentialsTypeRecoveryLink CredentialsType = "link_recovery" + CredentialsTypeRecoveryCode CredentialsType = "code_recovery" +) + +// ParseCredentialsType parses a string into a CredentialsType or returns false as the second argument. +func ParseCredentialsType(in string) (CredentialsType, bool) { + switch t := CredentialsType(in); t { + case CredentialsTypePassword, + CredentialsTypeOIDC, + CredentialsTypeSAML, + CredentialsTypeTOTP, + CredentialsTypeLookup, + CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, + CredentialsTypeRecoveryLink, + CredentialsTypeRecoveryCode, + CredentialsTypeDeviceAuthn, + CredentialsTypePasskey: + return t, true + } + return "", false +} + +// Credentials represents a specific credential type +// +// swagger:model identityCredentials +type Credentials struct { + ID uuid.UUID `json:"-" db:"id"` + + // Type discriminates between different types of credentials. + Type CredentialsType `json:"type" db:"-"` + IdentityCredentialTypeID uuid.UUID `json:"-" db:"identity_credential_type_id"` + + // Identifiers represent a list of unique identifiers this credential type matches. + Identifiers []string `json:"identifiers" db:"-"` + + // Config contains the concrete credential payload. This might contain the bcrypt-hashed password, the email + // for passwordless authentication or access_token and refresh tokens from OpenID Connect flows. + Config sqlxx.JSONRawMessage `json:"config,omitempty" db:"config"` + + // Version refers to the version of the credential. Useful when changing the config schema. + Version int `json:"version" db:"version"` + + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (c Credentials) TableName(context.Context) string { + return "identity_credentials" +} + +func (c Credentials) GetID() uuid.UUID { + return c.ID +} + +// Signature returns a unique string signature for the credential. +func (c Credentials) Signature() string { + sortedIdentifiers := slices.Clone(c.Identifiers) + slices.Sort(sortedIdentifiers) + identifiersStr := strings.Join(sortedIdentifiers, ",") + + // Normalize JSON config to remove whitespace and key ordering differences + var normalizedConfig any + if len(c.Config) > 0 { + if err := json.Unmarshal(c.Config, &normalizedConfig); err != nil { + // there is not much we can do when unmarshal fails except use the raw value + normalizedConfig = c.Config + } + } + return fmt.Sprintf("%v|%v|%d|%+v|%v|%v", c.Type, identifiersStr, c.Version, normalizedConfig, c.IdentityID, c.NID) +} + +type ( + // swagger:ignore + CredentialIdentifier struct { + ID uuid.UUID `db:"id"` + Identifier string `db:"identifier"` + IdentityID *uuid.UUID `json:"-" db:"identity_id"` + IdentityCredentialsID uuid.UUID `json:"-" db:"identity_credential_id"` + IdentityCredentialsTypeID uuid.UUID `json:"-" db:"identity_credential_type_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + } + + // swagger:ignore + CredentialsTypeTable struct { + ID uuid.UUID `json:"-" db:"id"` + Name CredentialsType `json:"-" db:"name"` + } + + // swagger:ignore + ActiveCredentialsCounter interface { + ID() CredentialsType + CountActiveFirstFactorCredentials(context.Context, map[CredentialsType]Credentials) (int, error) + CountActiveMultiFactorCredentials(context.Context, map[CredentialsType]Credentials) (int, error) + } + + // swagger:ignore + ActiveCredentialsCounterStrategyProvider interface { + ActiveCredentialsCounterStrategies(context.Context) []ActiveCredentialsCounter + } +) + +func (c CredentialsTypeTable) TableName(context.Context) string { + return "identity_credential_types" +} + +func (c CredentialIdentifier) TableName(context.Context) string { + return "identity_credential_identifiers" +} + +// ValidateCredentialsIntegrity returns a BadRequest error if any entry in +// the credentials map has a Type field that does not equal its map key. +// +// The internal helpers (SetCredentials, SetCredentialsWithConfig, +// UpsertCredentialsConfig) always force these two to match, so this only +// trips for callers that write to the map directly — notably the PATCH +// /admin/identities/{id} handler, which decodes the patched JSON straight +// into Identity. Caught early, this is a malformed request. Caught late, +// it has two destructive failure modes: a misleading 500 from the +// persister when the type is unknown, or a silent overwrite of the +// existing credential's config (e.g. hashed_password) when schema +// validation later repairs Type but not Config. +func ValidateCredentialsIntegrity(creds map[CredentialsType]Credentials) error { + for k, c := range creds { + if c.Type != k { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "credentials.%s.type must equal %q, got %q", k, string(k), string(c.Type), + )) + } + } + return nil +} + +func CredentialsEqual(a, b map[CredentialsType]Credentials) bool { + if len(a) != len(b) { + return false + } + + if len(a) == 0 && len(b) == 0 { + return true + } + + for k, expect := range b { + actual, found := a[k] + if !found { + return false + } + + // 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 + } + + expectIdentifiers, actualIdentifiers := make(map[string]struct{}, len(expect.Identifiers)), make(map[string]struct{}, len(actual.Identifiers)) + for _, i := range expect.Identifiers { + expectIdentifiers[i] = struct{}{} + } + for _, i := range actual.Identifiers { + actualIdentifiers[i] = struct{}{} + } + if !reflect.DeepEqual(expectIdentifiers, actualIdentifiers) { + return false + } + } + + return true +} + +// UpdateConfig adapts a typed, in-place credential-config mutation into the +// raw mutate form PrivilegedPool.UpdateCredentialsConfig takes: +// +// pool.UpdateCredentialsConfig(ctx, id, ct, identity.UpdateConfig(mutate)) +// +// T must model the complete stored config shape: unknown fields do not +// survive the roundtrip. An empty or JSON-null config is rejected before +// mutate runs. mutate must be pure (it may run more than once on database +// retries); returning an error aborts without persisting. +func UpdateConfig[T any](mutate func(*T) error) func(config []byte) ([]byte, error) { + return func(config []byte) ([]byte, error) { + if len(config) == 0 || bytes.Equal(config, []byte("null")) { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("The stored credential configuration is empty and cannot be updated.")) + } + var decoded T + if err := json.Unmarshal(config, &decoded); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("The stored credential configuration could not be decoded.").WithDebug(err.Error()).WithWrap(err)) + } + if err := mutate(&decoded); err != nil { + return nil, err + } + encoded, err := json.Marshal(&decoded) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("The updated credential configuration could not be encoded.").WithDebug(err.Error()).WithWrap(err)) + } + return encoded, nil + } +} diff --git a/identity/credentials_code.go b/identity/credentials_code.go new file mode 100644 index 0000000..8ebe493 --- /dev/null +++ b/identity/credentials_code.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "encoding/json" + "strings" + + "github.com/ory/herodot" + + "github.com/pkg/errors" + + "github.com/ory/x/stringsx" +) + +type CodeChannel string + +// Scan implements the sql.Scanner interface for CodeChannel +// to support proper scanning from database values while removing +// any trailing whitespace that might be present in +// PostgreSQL and CockroachDB CHAR fields. +func (c *CodeChannel) Scan(src interface{}) error { + if src == nil { + *c = "" + return nil + } + + switch s := src.(type) { + case string: + *c = CodeChannel(strings.TrimSpace(s)) + return nil + case []byte: + *c = CodeChannel(strings.TrimSpace(string(s))) + return nil + default: + return errors.Errorf("cannot scan %T into CodeChannel", src) + } +} + +const ( + 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 + Channel CodeChannel `json:"channel"` + + // The address for this code + Address string `json:"address"` +} + +func ErrInvalidCodeAddressType() *herodot.DefaultError { + return herodot.ErrMisconfiguration().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 + } + + *c = CredentialsCodeAddress(ac) + return nil +} diff --git a/identity/credentials_code_test.go b/identity/credentials_code_test.go new file mode 100644 index 0000000..4758850 --- /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_lookup.go b/identity/credentials_lookup.go new file mode 100644 index 0000000..832f441 --- /dev/null +++ b/identity/credentials_lookup.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "time" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + + "github.com/ory/x/sqlxx" +) + +// CredentialsConfig is the struct that is being used as part of the identity credentials. +type CredentialsLookupConfig struct { + // List of recovery codes + RecoveryCodes []RecoveryCode `json:"recovery_codes"` +} + +func (c *CredentialsLookupConfig) ToNode() *node.Node { + messages := make([]text.Message, len(c.RecoveryCodes)) + formatted := make([]string, len(c.RecoveryCodes)) + for k, code := range c.RecoveryCodes { + if time.Time(code.UsedAt).IsZero() { + messages[k] = *text.NewInfoSelfServiceSettingsLookupSecret(code.Code) + formatted[k] = code.Code + } else { + messages[k] = *text.NewInfoSelfServiceSettingsLookupSecretUsed(time.Time(code.UsedAt).UTC()) + formatted[k] = "used" + } + } + + return node.NewTextField(node.LookupCodes, text.NewInfoSelfServiceSettingsLookupSecretList(formatted, messages), node.LookupGroup). + WithMetaLabel(text.NewInfoSelfServiceSettingsLookupSecretsLabel()) +} + +// RecoveryCode represents one recovery code associated with a lookup_secret credential. +// +// Recovery codes can be used once and are invalidated after use. +// +// swagger:model identityCredentialsLookupSecretCode +type RecoveryCode struct { + // A recovery code + Code string `json:"code"` + + // UsedAt indicates whether and when a recovery code was used. + UsedAt sqlxx.NullTime `json:"used_at,omitempty"` +} diff --git a/identity/credentials_lookup_test.go b/identity/credentials_lookup_test.go new file mode 100644 index 0000000..c0f53d3 --- /dev/null +++ b/identity/credentials_lookup_test.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + _ "embed" + "testing" + "time" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/x/sqlxx" +) + +func TestToNode(t *testing.T) { + c := identity.CredentialsLookupConfig{RecoveryCodes: []identity.RecoveryCode{ + {Code: "foo", UsedAt: sqlxx.NullTime(time.Unix(1629199958, 0).UTC())}, + {Code: "bar"}, + {Code: "baz"}, + {Code: "oof", UsedAt: sqlxx.NullTime(time.Unix(1629199968, 0).UTC())}, + }} + + testhelpers.SnapshotTExcept(t, c.ToNode(), []string{}) +} diff --git a/identity/credentials_migrate.go b/identity/credentials_migrate.go new file mode 100644 index 0000000..bb49798 --- /dev/null +++ b/identity/credentials_migrate.go @@ -0,0 +1,148 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/x" + + "github.com/pkg/errors" +) + +func UpgradeWebAuthnCredentials(i *Identity, c *Credentials) (err error) { + if c.Type != CredentialsTypeWebAuthn { + return nil + } + + version := c.Version + if version == 0 { + if gjson.GetBytes(c.Config, "user_handle").String() == "" { + id, err := json.Marshal(i.ID[:]) + if err != nil { + return errors.WithStack(err) + } + + c.Config, err = sjson.SetRawBytes(c.Config, "user_handle", id) + if err != nil { + return errors.WithStack(err) + } + } + + index := -1 + var err error + gjson.GetBytes(c.Config, "credentials").ForEach(func(_, value gjson.Result) bool { + index++ + + if value.Get("is_passwordless").Exists() { + return true + } + + c.Config, err = sjson.SetBytes(c.Config, fmt.Sprintf("credentials.%d.is_passwordless", index), false) + return err == nil + }) + if err != nil { + return errors.WithStack(err) + } + + c.Version = 1 + } + return nil +} + +// UpgradeCredentials migrates a set of older WebAuthn, Code, and +// Password credentials to newer ones. +func UpgradeCredentials(i *Identity) error { + for k := range i.Credentials { + c := i.Credentials[k] + if err := UpgradeWebAuthnCredentials(i, &c); err != nil { + return errors.WithStack(err) + } + if err := UpgradeCodeCredentials(&c); err != nil { + return errors.WithStack(err) + } + if err := UpgradePasswordCredentials(&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 +} + +// UpgradePasswordCredentials will make a graceful attempt to normalize +// the format of a credential's identifier. Version is always bumped to v1. +func UpgradePasswordCredentials(c *Credentials) (err error) { + if c.Type != CredentialsTypePassword { + return nil + } + + if c.Version == 0 { + // iterate over identifier values + // and normalize format + for i := range c.Identifiers { + c.Identifiers[i] = x.GracefulNormalization(c.Identifiers[i]) + } + slices.Sort(c.Identifiers) + c.Identifiers = slices.Compact(c.Identifiers) + c.Version = 1 + } + return nil +} diff --git a/identity/credentials_migrate_test.go b/identity/credentials_migrate_test.go new file mode 100644 index 0000000..da3522c --- /dev/null +++ b/identity/credentials_migrate_test.go @@ -0,0 +1,194 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + _ "embed" + "testing" + + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/snapshotx" +) + +//go:embed stub/webauthn/v0.json +var webAuthnV0 []byte + +//go:embed stub/webauthn/v1.json +var webAuthnV1 []byte + +func TestUpgradeCredentials(t *testing.T) { + t.Run("empty credentials", func(t *testing.T) { + i := &Identity{} + + err := UpgradeCredentials(i) + require.NoError(t, err) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotTExcept(t, &wc, nil) + }) + + 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) { + run(t, []string{"4d64fa08-20fc-450d-bebd-ebd7c7b6e249"}, string(webAuthnV0), 0, CredentialsTypeWebAuthn, 1) + }) + + t.Run("from=v1", func(t *testing.T) { + run(t, []string{}, string(webAuthnV1), 1, CredentialsTypeWebAuthn, 1) + }) + }) + + t.Run("type=password", func(t *testing.T) { + t.Run("from=v0 with phone number", func(t *testing.T) { + i := &Identity{ + ID: uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249"), + Credentials: map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"+49 176 671 11 638"}, + Type: CredentialsTypePassword, + Version: 0, + Config: []byte(`{}`), + }, + }, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotT(t, &wc) + assert.Equal(t, 1, i.Credentials[CredentialsTypePassword].Version) + + // Verify only normalized identifier is present + identifiers := i.Credentials[CredentialsTypePassword].Identifiers + assert.Contains(t, identifiers, "+4917667111638", "Should contain E.164 normalized phone number") + }) + + t.Run("from=v0 with email", func(t *testing.T) { + i := &Identity{ + ID: uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249"), + Credentials: map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{" Test@Example.ORG "}, + Type: CredentialsTypePassword, + Version: 0, + Config: []byte(`{}`), + }, + }, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotT(t, &wc) + assert.Equal(t, 1, i.Credentials[CredentialsTypePassword].Version) + + // Verify only normalized identifier is present + identifiers := i.Credentials[CredentialsTypePassword].Identifiers + assert.Contains(t, identifiers, "test@example.org", "Should contain normalized email") + }) + + t.Run("from=v0 with already normalized phone", func(t *testing.T) { + i := &Identity{ + ID: uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249"), + Credentials: map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"+4917667111638"}, + Type: CredentialsTypePassword, + Version: 0, + Config: []byte(`{}`), + }, + }, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotT(t, &wc) + assert.Equal(t, 1, i.Credentials[CredentialsTypePassword].Version) + + // Verify no duplicate is created when already normalized + identifiers := i.Credentials[CredentialsTypePassword].Identifiers + assert.Len(t, identifiers, 1, "Should not create duplicate when already normalized") + assert.Contains(t, identifiers, "+4917667111638", "Should contain the normalized phone number") + }) + + t.Run("from=v0 with mixed identifiers", func(t *testing.T) { + i := &Identity{ + ID: uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249"), + Credentials: map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"+49 176 671 11 638", " Test@Example.ORG ", "username123"}, + Type: CredentialsTypePassword, + Version: 0, + Config: []byte(`{}`), + }, + }, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotT(t, &wc) + assert.Equal(t, 1, i.Credentials[CredentialsTypePassword].Version) + + // Verify only normalized identifiers are present + identifiers := i.Credentials[CredentialsTypePassword].Identifiers + assert.Contains(t, identifiers, "+4917667111638", "Should contain normalized phone") + assert.Contains(t, identifiers, "test@example.org", "Should contain normalized email") + assert.Contains(t, identifiers, "username123", "Should contain original username") + }) + + t.Run("from=v1", func(t *testing.T) { + run(t, []string{"+4917667111638"}, `{}`, 1, CredentialsTypePassword, 1) + }) + }) +} diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go new file mode 100644 index 0000000..8a8ab1c --- /dev/null +++ b/identity/credentials_oidc.go @@ -0,0 +1,123 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/kratos/x" +) + +// CredentialsOIDC is contains the configuration for credentials of the type oidc. +// +// swagger:model identityCredentialsOidc +type CredentialsOIDC struct { + Providers []CredentialsOIDCProvider `json:"providers"` +} + +// CredentialsOIDCProvider is contains a specific OpenID COnnect credential for a particular connection (e.g. Google). +// +// swagger:model identityCredentialsOidcProvider +type CredentialsOIDCProvider struct { + Subject string `json:"subject"` + Provider string `json:"provider"` + InitialIDToken string `json:"initial_id_token"` + InitialAccessToken string `json:"initial_access_token"` + InitialRefreshToken string `json:"initial_refresh_token"` + Organization string `json:"organization,omitempty"` + UseAutoLink bool `json:"use_auto_link,omitzero"` +} + +// 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(tokens *CredentialsOIDCEncryptedTokens, provider, subject, organization string) (*Credentials, error) { + return NewOIDCLikeCredentials(tokens, CredentialsTypeOIDC, provider, subject, organization) +} + +// NewOIDCLikeCredentials creates a new OIDC-like credential. +func NewOIDCLikeCredentials(tokens *CredentialsOIDCEncryptedTokens, t CredentialsType, provider, subject, organization string) (*Credentials, error) { + if provider == "" { + return nil, errors.New("received empty provider in oidc credentials") + } + + if subject == "" { + return nil, errors.New("received empty provider in oidc credentials") + } + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Subject: subject, + Provider: provider, + InitialIDToken: tokens.GetIDToken(), + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), + Organization: organization, + }, + }, + }); err != nil { + return nil, errors.WithStack(x.PseudoPanic. + WithDebugf("Unable to encode password options to JSON: %s", err)) + } + + return &Credentials{ + Type: t, + Identifiers: []string{OIDCUniqueID(provider, subject)}, + Config: b.Bytes(), + }, 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) +} + +func (c *CredentialsOIDC) Organization() string { + for _, p := range c.Providers { + if p.Organization != "" { + return p.Organization + } + } + + return "" +} diff --git a/identity/credentials_oidc_test.go b/identity/credentials_oidc_test.go new file mode 100644 index 0000000..f52a077 --- /dev/null +++ b/identity/credentials_oidc_test.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewCredentialsOIDC(t *testing.T) { + _, err := NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "", "not-empty", "") + require.Error(t, err) + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "", "") + require.Error(t, err) + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "not-empty", "") + require.NoError(t, err) +} diff --git a/identity/credentials_password.go b/identity/credentials_password.go new file mode 100644 index 0000000..4a6e7eb --- /dev/null +++ b/identity/credentials_password.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +// CredentialsPassword is contains the configuration for credentials of the type password. +// +// swagger:model identityCredentialsPassword +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 0000000..6e62720 --- /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_test.go b/identity/credentials_test.go new file mode 100644 index 0000000..c532156 --- /dev/null +++ b/identity/credentials_test.go @@ -0,0 +1,260 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/mohae/deepcopy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/sqlxx" +) + +func TestCredentialsEqual(t *testing.T) { + original := map[CredentialsType]Credentials{ + "foo": {Type: "foo", Identifiers: []string{"bar"}, Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`)}, + } + + derived := deepcopy.Copy(original).(map[CredentialsType]Credentials) + assert.EqualValues(t, original, derived) + derived["foo"].Identifiers[0] = "baz" + assert.NotEqual(t, original, derived) +} + +func TestAALOrder(t *testing.T) { + assert.True(t, NoAuthenticatorAssuranceLevel < AuthenticatorAssuranceLevel1) + assert.True(t, AuthenticatorAssuranceLevel1 < AuthenticatorAssuranceLevel2) +} + +func TestParseCredentialsType(t *testing.T) { + for _, tc := range []struct { + input string + expected CredentialsType + }{ + {"password", CredentialsTypePassword}, + {"oidc", CredentialsTypeOIDC}, + {"totp", CredentialsTypeTOTP}, + {"webauthn", CredentialsTypeWebAuthn}, + {"deviceauthn", CredentialsTypeDeviceAuthn}, + {"lookup_secret", CredentialsTypeLookup}, + {"link_recovery", CredentialsTypeRecoveryLink}, + {"code_recovery", CredentialsTypeRecoveryCode}, + } { + t.Run("case="+tc.input, func(t *testing.T) { + actual, ok := ParseCredentialsType(tc.input) + require.True(t, ok) + assert.Equal(t, tc.expected, actual) + }) + } + + t.Run("case=unknown", func(t *testing.T) { + _, ok := ParseCredentialsType("unknown") + require.False(t, ok) + }) +} + +func TestCredentials_Hash(t *testing.T) { + baseID := uuid.Must(uuid.NewV4()) + baseNID := uuid.Must(uuid.NewV4()) + + for _, tc := range []struct { + name string + cred1 Credentials + cred2 Credentials + expectEqual bool + description string + }{ + { + name: "same json with different whitespace", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar","baz":"qux"}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{ + "foo": "bar", + "baz": "qux" + }`), + Version: 1, + }, + expectEqual: true, + description: "hashes should be equal for same JSON with different whitespace", + }, + { + name: "same json with different key order", + cred1: Credentials{ + Type: CredentialsTypeCodeAuth, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"address":"test@example.com","channel":"email"}]}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeCodeAuth, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"test@example.com"}]}`), + Version: 1, + }, + expectEqual: true, + description: "hashes should be equal for same JSON with different key order", + }, + { + name: "nested json with different key order", + cred1: Credentials{ + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"credentials":[{"id":"abc","public_key":"xyz","type":"webauthn"}]}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"credentials":[{"type":"webauthn","public_key":"xyz","id":"abc"}]}`), + Version: 1, + }, + expectEqual: true, + description: "hashes should be equal for nested JSON with different key order", + }, + { + name: "same identifiers in different order", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"a@example.com", "b@example.com", "c@example.com"}, + Config: sqlxx.JSONRawMessage(`{}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"c@example.com", "a@example.com", "b@example.com"}, + Config: sqlxx.JSONRawMessage(`{}`), + Version: 1, + }, + expectEqual: true, + description: "hashes should be equal for same identifiers in different order", + }, + { + name: "different json config", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"different"}`), + Version: 1, + }, + expectEqual: false, + description: "hashes should be different for different JSON content", + }, + { + name: "different types", + cred1: Credentials{ + Type: CredentialsTypePassword, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + expectEqual: false, + description: "hashes should be different for different types", + }, + { + name: "different identifiers", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test1@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test2@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + expectEqual: false, + description: "hashes should be different for different identifiers", + }, + { + name: "different versions", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 2, + }, + expectEqual: false, + description: "hashes should be different for different versions", + }, + { + name: "different identity IDs", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + IdentityID: baseID, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + IdentityID: uuid.Must(uuid.NewV4()), + }, + expectEqual: false, + description: "hashes should be different for different identity IDs", + }, + { + name: "different NIDs", + cred1: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + NID: baseNID, + }, + cred2: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"test@example.com"}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + Version: 1, + NID: uuid.Must(uuid.NewV4()), + }, + expectEqual: false, + description: "hashes should be different for different NIDs", + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + hash1 := tc.cred1.Signature() + hash2 := tc.cred2.Signature() + + if tc.expectEqual { + assert.Equal(t, hash1, hash2, tc.description) + } else { + assert.NotEqual(t, hash1, hash2, tc.description) + } + }) + } +} diff --git a/identity/credentials_totp.go b/identity/credentials_totp.go new file mode 100644 index 0000000..bb17c70 --- /dev/null +++ b/identity/credentials_totp.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +// CredentialsConfig is the struct that is being used as part of the identity credentials. +type CredentialsTOTPConfig struct { + // TOTPURL is the TOTP URL + // + // For more details see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + TOTPURL string `json:"totp_url"` +} diff --git a/identity/credentials_webauthn.go b/identity/credentials_webauthn.go new file mode 100644 index 0000000..817fce4 --- /dev/null +++ b/identity/credentials_webauthn.go @@ -0,0 +1,251 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "time" + + "github.com/go-webauthn/webauthn/protocol" + + "github.com/go-webauthn/webauthn/webauthn" + + "github.com/ory/kratos/x/webauthnx/aaguid" +) + +// 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"` + UserHandle []byte `json:"user_handle"` +} + +// swagger:model identityCredentialsWebAuthnList +type CredentialsWebAuthn []CredentialWebAuthn + +func CredentialFromWebAuthn(credential *webauthn.Credential, isPasswordless bool) *CredentialWebAuthn { + cred := &CredentialWebAuthn{ + ID: credential.ID, + PublicKey: credential.PublicKey, + IsPasswordless: isPasswordless, + AttestationType: credential.AttestationType, + Transport: credential.Transport, + 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) { + for k := range c { + result = append(result, *c[k].ToWebAuthn()) + } + return result +} + +// PasswordlessOnly returns only passwordless credentials. +func (c CredentialsWebAuthn) PasswordlessOnly(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 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 +} + +// UpdateFromLogin persists the mutable authenticator state returned by the +// go-webauthn library after a successful login assertion onto the matching +// stored credential. It implements the relying-party side of the W3C WebAuthn +// storage contract for the fields that change across assertions +// (https://www.w3.org/TR/webauthn-2/#sctn-sign-counter): +// +// - SignCount: incremented by the library when the assertion reports a higher +// counter; left unchanged (and CloneWarning set) when it did not advance, a +// possible cloned authenticator. +// - CloneWarning: persisted so the flag survives on the credential. +// - BackupState: refreshed because it flips when the user starts or stops +// syncing the credential (for example a device-bound passkey later added to +// iCloud or Google). BackupEligible is immutable by spec and is left alone. +// +// It returns whether the stored config changed so the caller can skip a needless +// write. +// +// Synced passkeys (for example Apple iCloud or Google) always report +// SignCount = 0. For them go-webauthn neither advances the counter nor raises a +// clone warning, so this method is a no-op for the counter and never blocks the +// login. +func (c CredentialsWebAuthn) UpdateFromLogin(updated *webauthn.Credential) (changed bool) { + if updated == nil { + return false + } + + for k := range c { + if !bytes.Equal(c[k].ID, updated.ID) { + continue + } + + if c[k].Authenticator == nil { + c[k].Authenticator = &AuthenticatorWebAuthn{} + } + + // Only ever advance the counter, never lower it. go-webauthn already + // leaves SignCount unchanged when the assertion does not advance it, so + // in practice updated >= stored; the strict < guards against a regressing + // counter ever weakening clone detection by writing a lower value. + if c[k].Authenticator.SignCount < updated.Authenticator.SignCount { + c[k].Authenticator.SignCount = updated.Authenticator.SignCount + changed = true + } + if c[k].Authenticator.CloneWarning != updated.Authenticator.CloneWarning { + c[k].Authenticator.CloneWarning = updated.Authenticator.CloneWarning + changed = true + } + + // BackupState is the only credential flag that legitimately changes over + // a credential's lifetime; the others are fixed at registration. Sync it + // so the stored state stays current for later risk decisions. Legacy + // credentials with no Flags are skipped on purpose: BackupEligible is + // unknown for them, so we do not synthesize a Flags struct here; the + // login filter upgrades those separately before the assertion. + if c[k].Flags != nil && c[k].Flags.BackupState != updated.Flags.BackupState { + c[k].Flags.BackupState = updated.Flags.BackupState + changed = true + } + + return changed + } + + return false +} + +func (c *CredentialWebAuthn) ToWebAuthn() *webauthn.Credential { + wc := &webauthn.Credential{ + ID: c.ID, + PublicKey: c.PublicKey, + AttestationType: c.AttestationType, + 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 +} + +// CredentialWebAuthn contains information about a webauthn credential. +// +// swagger:model identityCredentialsWebAuthn +type CredentialWebAuthn struct { + 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"` +} + +// CredentialWebAuthnFlags contains information about the flags of a webauthn credential. +// +// swagger:model identityCredentialsWebAuthnFlags +type CredentialWebAuthnFlags struct { + UserPresent bool `json:"user_present"` + UserVerified bool `json:"user_verified"` + BackupEligible bool `json:"backup_eligible"` + BackupState bool `json:"backup_state"` +} + +// CredentialWebAuthnAttestation contains information about attestation of a webauthn credential. +// +// swagger:model identityCredentialsWebAuthnAttestation +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"` +} + +// AuthenticatorWebAuthn contains information about the authenticator of a webauthn credential. +// +// swagger:model identityCredentialsWebAuthnAuthenticator +type AuthenticatorWebAuthn struct { + AAGUID []byte `json:"aaguid"` + SignCount uint32 `json:"sign_count"` + CloneWarning bool `json:"clone_warning"` +} diff --git a/identity/credentials_webauthn_test.go b/identity/credentials_webauthn_test.go new file mode 100644 index 0000000..de02eee --- /dev/null +++ b/identity/credentials_webauthn_test.go @@ -0,0 +1,182 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestCredentialConversion(t *testing.T) { + expected := &webauthn.Credential{ + ID: []byte("abcdef"), + PublicKey: []byte("foobar"), + AttestationType: "test", + Transport: []protocol.AuthenticatorTransport{protocol.Internal, protocol.Hybrid}, + Authenticator: webauthn.Authenticator{ + AAGUID: []byte("baz"), + SignCount: 1, + CloneWarning: false, + }, + } + + actual := CredentialFromWebAuthn(expected, false).ToWebAuthn() + assert.Equal(t, expected, actual) + + actualList := CredentialsWebAuthn{*CredentialFromWebAuthn(expected, false)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2, nil) + assert.Equal(t, []webauthn.Credential{*expected}, actualList) + + actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel1, nil) + assert.Equal(t, []webauthn.Credential{*expected}, actualList) + + actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2, nil) + assert.Len(t, actualList, 0) + + 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 TestUpdateFromLogin(t *testing.T) { + t.Parallel() + + // newCreds builds a single stored credential with the given starting sign count. + newCreds := func(signCount uint32) CredentialsWebAuthn { + return CredentialsWebAuthn{{ + ID: []byte("cred-1"), + Authenticator: &AuthenticatorWebAuthn{SignCount: signCount}, + }} + } + + // validated mimics the *webauthn.Credential returned by the library: it has + // the matching ID and the post-assertion authenticator state. + validated := func(id []byte, signCount uint32, cloneWarning bool) *webauthn.Credential { + return &webauthn.Credential{ + ID: id, + Authenticator: webauthn.Authenticator{ + SignCount: signCount, + CloneWarning: cloneWarning, + }, + } + } + + t.Run("case=advances the stored sign count", func(t *testing.T) { + c := newCreds(3) + assert.True(t, c.UpdateFromLogin(validated([]byte("cred-1"), 10, false))) + assert.EqualValues(t, 10, c[0].Authenticator.SignCount) + assert.False(t, c[0].Authenticator.CloneWarning) + }) + + t.Run("case=flags a clone when the counter did not advance", func(t *testing.T) { + // go-webauthn keeps SignCount unchanged and sets CloneWarning when the + // counter regresses or stalls (on a non-zero counter). The clone warning + // is newly set, so the config must be persisted. + c := newCreds(10) + assert.True(t, c.UpdateFromLogin(validated([]byte("cred-1"), 10, true))) + assert.True(t, c[0].Authenticator.CloneWarning) + assert.EqualValues(t, 10, c[0].Authenticator.SignCount) + }) + + t.Run("case=does not lower a regressing sign count", func(t *testing.T) { + // The library never returns a lower counter (it flags a clone and leaves + // the count unchanged), but UpdateFromLogin must never decrement the + // stored counter, which would weaken clone detection. + c := newCreds(10) + assert.False(t, c.UpdateFromLogin(validated([]byte("cred-1"), 5, false))) + assert.EqualValues(t, 10, c[0].Authenticator.SignCount) + }) + + t.Run("case=synced passkey reporting a static zero counter is a no-op", func(t *testing.T) { + // Synced passkeys (Apple/Google) always report 0; go-webauthn leaves the + // counter at 0 and never raises a clone warning, so nothing changes. + c := newCreds(0) + assert.False(t, c.UpdateFromLogin(validated([]byte("cred-1"), 0, false))) + assert.EqualValues(t, 0, c[0].Authenticator.SignCount) + }) + + t.Run("case=no matching credential is a no-op", func(t *testing.T) { + c := newCreds(3) + assert.False(t, c.UpdateFromLogin(validated([]byte("other"), 99, false))) + assert.EqualValues(t, 3, c[0].Authenticator.SignCount) + }) + + t.Run("case=nil validated credential is a no-op", func(t *testing.T) { + c := newCreds(3) + assert.False(t, c.UpdateFromLogin(nil)) + assert.EqualValues(t, 3, c[0].Authenticator.SignCount) + }) + + t.Run("case=initialises a missing authenticator before updating", func(t *testing.T) { + c := CredentialsWebAuthn{{ID: []byte("cred-1")}} + assert.True(t, c.UpdateFromLogin(validated([]byte("cred-1"), 5, false))) + require.NotNil(t, c[0].Authenticator) + assert.EqualValues(t, 5, c[0].Authenticator.SignCount) + }) + + // withBackup builds a stored credential that carries backup flags. + withBackup := func(signCount uint32, backupState bool) CredentialsWebAuthn { + return CredentialsWebAuthn{{ + ID: []byte("cred-1"), + Authenticator: &AuthenticatorWebAuthn{SignCount: signCount}, + Flags: &CredentialWebAuthnFlags{BackupEligible: true, BackupState: backupState}, + }} + } + + // validatedWithBackup is like validated but also reports the post-assertion + // backup flags the library copies from the authenticator data. + validatedWithBackup := func(id []byte, signCount uint32, backupState bool) *webauthn.Credential { + v := validated(id, signCount, false) + v.Flags = webauthn.CredentialFlags{BackupEligible: true, BackupState: backupState} + return v + } + + t.Run("case=refreshes backup state when the credential gets synced", func(t *testing.T) { + // A passkey registered device-bound (BS=false) and later synced to + // iCloud/Google reports BS=true on the next login. + c := withBackup(3, false) + assert.True(t, c.UpdateFromLogin(validatedWithBackup([]byte("cred-1"), 4, true))) + assert.True(t, c[0].Flags.BackupState) + }) + + t.Run("case=a backup state change alone triggers a persist", func(t *testing.T) { + // The counter is unchanged, but the credential stopped being synced, so + // the flipped backup state must still be written back. + c := withBackup(3, true) + assert.True(t, c.UpdateFromLogin(validatedWithBackup([]byte("cred-1"), 3, false))) + assert.False(t, c[0].Flags.BackupState) + }) + + t.Run("case=an unchanged backup state is a no-op", func(t *testing.T) { + c := withBackup(3, true) + assert.False(t, c.UpdateFromLogin(validatedWithBackup([]byte("cred-1"), 3, true))) + assert.True(t, c[0].Flags.BackupState) + }) +} + +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/error_test.go b/identity/error_test.go new file mode 100644 index 0000000..a4dabd6 --- /dev/null +++ b/identity/error_test.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrDuplicateCredentials(t *testing.T) { + inner := errors.New("inner error") + err := &ErrDuplicateCredentials{inner, nil, nil, ""} + assert.ErrorIs(t, err, inner) +} diff --git a/identity/expandables.go b/identity/expandables.go new file mode 100644 index 0000000..66ba7c0 --- /dev/null +++ b/identity/expandables.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import "github.com/ory/x/sqlxx" + +type Expandable = sqlxx.Expandable +type Expandables = sqlxx.Expandables + +// Each value must be the exact name of an Identity struct field: the values are passed to pop's +// Eager/EagerPreload for reflection-based loading, and the persister runs one query per value. +// That is why the two address expandables cannot be merged into one: they name distinct fields +// backed by distinct tables. To load both, compose them in an Expandables set (see ExpandDefault). +const ( + ExpandFieldVerifiableAddresses Expandable = "VerifiableAddresses" + ExpandFieldRecoveryAddresses Expandable = "RecoveryAddresses" + ExpandFieldCredentials Expandable = "Credentials" +) + +// ExpandNothing expands nothing +var ExpandNothing = Expandables{} + +// ExpandDefault expands the default fields: +// +// - Verifiable addresses +// - Recovery addresses +var ExpandDefault = Expandables{ + ExpandFieldVerifiableAddresses, + ExpandFieldRecoveryAddresses, +} + +// ExpandCredentials expands the identity's credentials. +var ExpandCredentials = Expandables{ + ExpandFieldCredentials, +} + +// ExpandEverything expands all the fields of an identity. +var ExpandEverything = Expandables{ + ExpandFieldVerifiableAddresses, + ExpandFieldRecoveryAddresses, + ExpandFieldCredentials, +} + +// ExpandEverythingButCredentials expands all the fields of an identity except its credentials. +// Expanding credentials is by far the most expensive part of fetching an identity, so prefer +// this expansion on hot paths that never read the credentials. +var ExpandEverythingButCredentials = Expandables{ + ExpandFieldVerifiableAddresses, + ExpandFieldRecoveryAddresses, +} diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go new file mode 100644 index 0000000..4aeae9a --- /dev/null +++ b/identity/extension_credentials.go @@ -0,0 +1,153 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +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" +) + +type SchemaExtensionCredentials struct { + i *Identity + v map[CredentialsType][]string + addresses []CredentialsCodeAddress + l sync.Mutex +} + +func NewSchemaExtensionCredentials(i *Identity) *SchemaExtensionCredentials { + return &SchemaExtensionCredentials{i: i} +} + +func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}) { + cred, ok := r.i.GetCredentials(ct) + if !ok { + cred = &Credentials{ + Type: ct, + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage{}, + } + } + + if r.v == nil { + r.v = make(map[CredentialsType][]string) + } + + normalized := x.GracefulNormalization(fmt.Sprintf("%s", value)) + + r.v[ct] = stringslice.Unique(append(r.v[ct], normalized)) + cred.Identifiers = r.v[ct] + r.i.SetCredentials(ct, *cred) +} + +func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { + r.l.Lock() + defer r.l.Unlock() + + if s.Credentials.Password.Identifier { + r.setIdentifier(CredentialsTypePassword, value) + } + + if s.Credentials.WebAuthn.Identifier { + r.setIdentifier(CredentialsTypeWebAuthn, value) + } + + if s.Credentials.Code.Identifier { + 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 + + // For the email channel with a non-strict format (e.g. `no-validate`), + // the schema `pattern` alone gates the value and graceful normalization + // keeps the stored address in sync with the login-code lookup. SMS + // always needs E.164 normalization. + var formatString string + if raw, ok := s.RawSchema["format"]; ok { + str, ok := raw.(string) + if !ok { + // Defensive: the draft-07 meta-schema already rejects a + // non-string "format" at compile time. + return &jsonschema.ValidationError{Message: `the "format" field must be a string`} + } + formatString = str + } + var normalized string + if via == CodeChannelEmail && formatString != "" && formatString != "email" { + normalized = x.GracefulNormalization(fmt.Sprintf("%s", value)) + } else { + n, err := x.NormalizeIdentifier(fmt.Sprintf("%s", value), string(via)) + if err != nil { + return &jsonschema.ValidationError{Message: err.Error()} + } + normalized = n + } + + conf.Addresses = append(conf.Addresses, CredentialsCodeAddress{ + Channel: via, + Address: normalized, + }) + + 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 + }) + + if r.v == nil { + r.v = make(map[CredentialsType][]string) + } + + 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 +} + +func (r *SchemaExtensionCredentials) Finish() error { + return nil +} diff --git a/identity/extension_credentials_test.go b/identity/extension_credentials_test.go new file mode 100644 index 0000000..346dae9 --- /dev/null +++ b/identity/extension_credentials_test.go @@ -0,0 +1,336 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/jsonschema/v3" + _ "github.com/ory/jsonschema/v3/fileloader" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +func TestSchemaExtensionCredentials(t *testing.T) { + for k, tc := range []struct { + 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", + expectedIdentifiers: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypePassword, + }, + { + doc: `{"emails":["foo😀@ory.sh","foo😀@ory.sh","bar@ory.sh"], "username": "Foo🥳🖐baR"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"foo😀@ory.sh", "bar@ory.sh", "foo🥳🖐bar"}, + ct: identity.CredentialsTypePassword, + }, + { + doc: `{"emails":["föo@ory.sh","föo@ory.sh","bar@ory.sh"], "username": "foobar"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"föo@ory.sh", "bar@ory.sh"}, + ct: identity.CredentialsTypeWebAuthn, + }, + { + doc: `{"emails":["FÖÖ@ory.sh","bar@ory.sh"], "username": "foobär"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"föö@ory.sh", "bar@ory.sh", "foobär"}, + 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", + expectedIdentifiers: []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"}, + 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", + expectedIdentifiers: []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"}, + 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, + }, + + { + 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{"foo@ory.sh", "bar@ory.sh", "+4917667111638"}, + ct: identity.CredentialsTypePassword, + }, + { + doc: `{"email":"foo@ory.sh","phone":"+49 176 671 11 638"}`, + schema: "file://./stub/extension/credentials/code-phone-email.schema.json", + expectedIdentifiers: []string{"foo@ory.sh", "+4917667111638"}, + existing: &identity.Credentials{ + Identifiers: []string{"foo@ory.sh", "+49 176 671 11 638"}, + }, + ct: identity.CredentialsTypePassword, + }, + { + doc: `{"emails":["bar@o😶ry.sh"], "username": "Foo🥳🖐baR"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + ct: identity.CredentialsTypePassword, + expectErr: errors.New("I[#/emails/0] S[#/properties/emails/items/format] \"bar@o😶ry.sh\" is not valid \"email\""), + }, + { + doc: `{"email":"foo@öry.sh"}`, + schema: "file://./stub/extension/credentials/webauthn.schema.json", + expectErr: errors.New(`I[#/email] S[#/properties/email/format] "foo@öry.sh" is not valid "email"`), + ct: identity.CredentialsTypeWebAuthn, + }, + + // Invisible and bidi-control characters must be stripped from both + // emails and usernames. Each injected rune below has been used in + // real homograph / duplicate-account bypasses: the poisoned input + // must canonicalize to the same baseline an attacker is trying to + // impersonate, so the UNIQUE constraint on + // identity_credential_identifiers rejects the duplicate. + // + // The runes are built with fmt.Sprintf("%c", …) on purpose: literal + // invisible characters in test source are unreviewable, and a literal + // U+FEFF (BOM) is rejected by the Go compiler outright. + + // Zero-width space (U+200B) inside an email local part. + { + doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh"],"username":"zwsp-email"}`, 0x200B), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "zwsp-email"}, + ct: identity.CredentialsTypePassword, + }, + // Several different invisibles (ZWSP, ZWJ, soft hyphen) in one email. + { + doc: fmt.Sprintf(`{"emails":["a%cd%cmi%cn@ory.sh"],"username":"multi-email"}`, 0x200B, 0x200D, 0x00AD), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "multi-email"}, + ct: identity.CredentialsTypePassword, + }, + // BOM (U+FEFF) prefix and word joiner (U+2060) suffix in an email. + { + doc: fmt.Sprintf(`{"emails":["%cadmin%c@ory.sh"],"username":"bom-email"}`, 0xFEFF, 0x2060), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "bom-email"}, + ct: identity.CredentialsTypePassword, + }, + // Right-to-left override (U+202E, Trojan-source style) in an email. + { + doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh"],"username":"rlo-email"}`, 0x202E), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "rlo-email"}, + ct: identity.CredentialsTypePassword, + }, + // Zero-width space (U+200B) inside a username. + { + doc: fmt.Sprintf(`{"emails":["zwsp-user@ory.sh"],"username":"ad%cmin"}`, 0x200B), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"zwsp-user@ory.sh", "admin"}, + ct: identity.CredentialsTypePassword, + }, + // Default-ignorable runes outside category Cf in a username: ZWNJ, + // combining grapheme joiner (U+034F), Hangul filler (U+3164), and + // variation selector-16 (U+FE0F) all collapse to the baseline. + { + doc: fmt.Sprintf(`{"emails":["multi-user@ory.sh"],"username":"a%cd%cm%ci%cn"}`, 0x200C, 0x034F, 0x3164, 0xFE0F), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"multi-user@ory.sh", "admin"}, + ct: identity.CredentialsTypePassword, + }, + // Bidi override (U+202E) plus word joiner (U+2060) and soft hyphen + // (U+00AD) wrapping a username. + { + doc: fmt.Sprintf(`{"emails":["rlo-user@ory.sh"],"username":"%cad%cmin%c"}`, 0x202E, 0x2060, 0x00AD), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"rlo-user@ory.sh", "admin"}, + ct: identity.CredentialsTypePassword, + }, + // Two differently-poisoned variants of the same email must dedupe to + // a single identifier — this is the duplicate-account bypass itself. + { + doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh","ad%cmin@ory.sh"],"username":"dedup"}`, 0x200B, 0x00AD), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "dedup"}, + ct: identity.CredentialsTypePassword, + }, + // Boundary: NFKC does not fold script confusables. A Cyrillic "а" + // (U+0430) is a distinct letter from Latin "a", so this username + // stays distinct. Confusable-script detection is intentionally out + // of scope for identifier normalization. + { + doc: fmt.Sprintf(`{"emails":["homoglyph@ory.sh"],"username":"%cdmin"}`, 0x0430), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"homoglyph@ory.sh", fmt.Sprintf("%cdmin", 0x0430)}, + ct: identity.CredentialsTypePassword, + }, + // Two emails that differ only by a confusable character — Latin "a" + // versus Cyrillic "а" (U+0430) — are not folded by NFKC, so both are + // taken as distinct identifiers rather than collapsing into one. + { + doc: fmt.Sprintf(`{"emails":["admin@ory.sh","%cdmin@ory.sh"],"username":"confusable-emails"}`, 0x0430), + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"admin@ory.sh", "аdmin@ory.sh", "confusable-emails"}, + ct: identity.CredentialsTypePassword, + }, + { + doc: `{"email":"foo.@docomo.ne.jp"}`, + schema: "file://./stub/extension/credentials/code-no-validate.schema.json", + expectedIdentifiers: []string{"foo.@docomo.ne.jp"}, + ct: identity.CredentialsTypeCodeAuth, + }, + // Uppercase input under no-validate must normalize to match the + // login-code lookup, which compares x.GracefulNormalization(identifier). + { + doc: `{"email":"FOO.@docomo.ne.jp"}`, + schema: "file://./stub/extension/credentials/code-no-validate.schema.json", + expectedIdentifiers: []string{"foo.@docomo.ne.jp"}, + ct: identity.CredentialsTypeCodeAuth, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c := jsonschema.NewCompiler() + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + i := new(identity.Identity) + e := identity.NewSchemaExtensionCredentials(i) + if tc.existing != nil { + i.SetCredentials(tc.ct, *tc.existing) + } + + runner.AddRunner(e).Register(c) + err = c.MustCompile(t.Context(), tc.schema).Validate(bytes.NewBufferString(tc.doc)) + if tc.expectErr != nil { + require.EqualError(t, err, tc.expectErr.Error()) + return + } + require.NoError(t, err) + require.NoError(t, e.Finish()) + + credentials, ok := i.GetCredentials(tc.ct) + require.True(t, ok) + assert.ElementsMatch(t, tc.expectedIdentifiers, credentials.Identifiers) + snapshotx.SnapshotT(t, credentials, snapshotx.ExceptPaths("identifiers")) + }) + } +} + +func TestSchemaExtensionCredentialsCodeNoValidateAddress(t *testing.T) { + c := jsonschema.NewCompiler() + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + i := new(identity.Identity) + e := identity.NewSchemaExtensionCredentials(i) + runner.AddRunner(e).Register(c) + + require.NoError(t, c.MustCompile(t.Context(), "file://./stub/extension/credentials/code-no-validate.schema.json"). + Validate(bytes.NewBufferString(`{"email":"FOO.@docomo.ne.jp"}`))) + require.NoError(t, e.Finish()) + + cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + + var conf identity.CredentialsCode + require.NoError(t, json.Unmarshal(cred.Config, &conf)) + require.Len(t, conf.Addresses, 1) + // The stored address must equal what login-code normalization produces, + // or loginSendCode can never match it (strategy_login.go:521). + assert.Equal(t, x.GracefulNormalization("FOO.@docomo.ne.jp"), conf.Addresses[0].Address) + assert.Equal(t, identity.CodeChannelEmail, conf.Addresses[0].Channel) +} diff --git a/identity/extension_recovery.go b/identity/extension_recovery.go new file mode 100644 index 0000000..7766ad5 --- /dev/null +++ b/identity/extension_recovery.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "maps" + "slices" + "strings" + "sync" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" +) + +type SchemaExtensionRecovery struct { + l sync.Mutex + v []RecoveryAddress + i *Identity +} + +func NewSchemaExtensionRecovery(i *Identity) *SchemaExtensionRecovery { + return &SchemaExtensionRecovery{i: i} +} + +func (r *SchemaExtensionRecovery) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { + r.l.Lock() + defer r.l.Unlock() + + var address *RecoveryAddress + switch s.Recovery.Via { + case "email": + // An absent or `email` format keeps the strict RFC check; any other + // format (e.g. `no-validate`) lets the schema `pattern` gate the value. + var formatString string + if raw, ok := s.RawSchema["format"]; ok { + str, ok := raw.(string) + if !ok { + // Defensive: the draft-07 meta-schema already rejects a + // non-string "format" at compile time. + return ctx.Error("format", `the "format" field must be a string`) + } + formatString = str + } + if formatString == "" || formatString == "email" { + formatter, ok := jsonschema.Formats["email"] + if !ok { + supportedKeys := slices.Collect(maps.Keys(jsonschema.Formats)) + return ctx.Error("format", "format %q is not supported. Supported formats are [%s]", "email", strings.Join(supportedKeys, ", ")) + } + + if !formatter(value) { + return ctx.Error("format", "%q is not valid %q", value, "email") + } + } + + address = NewRecoveryEmailAddress(x.NormalizeEmailIdentifier(fmt.Sprintf("%s", value)), r.i.ID) + + case "sms": + normalized, err := x.NormalizeIdentifier(fmt.Sprintf("%s", value), s.Recovery.Via) + if err != nil { + return ctx.Error("format", "%q is not valid \"tel\" for %q", value, s.Recovery.Via) + } + + address = NewRecoverySMSAddress(normalized, r.i.ID) + + case "": + return nil + default: + return ctx.Error("", "recovery.via has unknown value %q", s.Recovery.Via) + } + + if has := r.has(r.i.RecoveryAddresses, address); has != nil { + if r.has(r.v, address) == nil { + r.v = append(r.v, *has) + } + return nil + } + + if has := r.has(r.v, address); has == nil { + r.v = append(r.v, *address) + } + + return nil +} + +func (r *SchemaExtensionRecovery) has(haystack []RecoveryAddress, needle *RecoveryAddress) *RecoveryAddress { + // Normalize both sides so pre-normalization persisted values still match. + normalizedNeedle := x.GracefulNormalization(needle.Value) + for _, has := range haystack { + if has.Via == needle.Via && x.GracefulNormalization(has.Value) == normalizedNeedle { + return &has + } + } + return nil +} + +func (r *SchemaExtensionRecovery) Finish() error { + r.i.RecoveryAddresses = r.v + return nil +} diff --git a/identity/extension_recovery_test.go b/identity/extension_recovery_test.go new file mode 100644 index 0000000..ef90e03 --- /dev/null +++ b/identity/extension_recovery_test.go @@ -0,0 +1,376 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/ory/jsonschema/v3" + _ "github.com/ory/jsonschema/v3/fileloader" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSchemaExtensionRecovery(t *testing.T) { + iid := x.NewUUID() + breakGlassOrgID := uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + for k, tc := range []struct { + expectErr error + schema string + doc string + expect []RecoveryAddress + existing []RecoveryAddress + description string + }{ + { + description: "valid email, no existing", + doc: `{"username":"foo@ory.sh"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "valid email, some existing, no overlap", + doc: `{"username":"foo@ory.sh"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "bar@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "valid emails, some existing, overlap", + doc: `{"emails":["baz@ory.sh","foo@ory.sh"]}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "baz@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "valid emails, no existing, overlap", + doc: `{"emails":["foo@ory.sh","foo@ory.sh","baz@ory.sh"]}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "baz@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "invalid email", + doc: `{"emails":["foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expectErr: errors.New("I[#/username] S[#/properties/username/format] \"foobar\" is not valid \"email\""), + }, + { + description: "valid emails, no existing", + doc: `{"emails":["foo@ory.sh","bar@ory.sh","bar@ory.sh"], "username": "foobar@ory.sh"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "foobar@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "valid phone number normalization", + doc: `{"telephoneNumber":"+33 6 12 34 56 78"}`, + schema: "file://./stub/extension/recovery/sms.schema.json", + expect: []RecoveryAddress{ + { + Value: "+33612345678", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + }, + { + description: "valid phone number, no existing", + doc: `{"telephoneNumber":"+68672098006"}`, + schema: "file://./stub/extension/recovery/sms.schema.json", + expect: []RecoveryAddress{ + { + Value: "+68672098006", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + }, + { + description: "valid phone number, some existing, no overlap", + doc: `{"telephoneNumber":"+68672098006"}`, + schema: "file://./stub/extension/recovery/sms.schema.json", + expect: []RecoveryAddress{ + { + Value: "+68672098006", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "+12 345 67890123", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + }, + { + description: "valid phone number, some existing, overlap", + doc: `{"telephoneNumber":"+68672098006"}`, + schema: "file://./stub/extension/recovery/sms.schema.json", + expect: []RecoveryAddress{ + { + Value: "+68672098006", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "+68672098006", + Via: AddressTypeSMS, + IdentityID: iid, + }, + { + Value: "+33 07856952", + Via: AddressTypeSMS, + IdentityID: iid, + }, + }, + }, + { + description: "invalid phone number", + doc: `{"telephoneNumber": "foobar"}`, + schema: "file://./stub/extension/recovery/sms.schema.json", + // We get 2 errors: one from the JSON schema `format` validation and one from the normalization error. + expectErr: errors.New("I[#/telephoneNumber] S[#/properties/telephoneNumber] validation failed\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\"\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\" for \"sms\""), + }, + { + description: "phone number that fails normalization", + doc: `{"telephoneNumber": "+1234"}`, // Too short to be a valid E.164 number + schema: "file://./stub/extension/recovery/sms.schema.json", + // The JSON schema format validator rejects it, and normalization also fails + expectErr: errors.New("I[#/telephoneNumber] S[#/properties/telephoneNumber] validation failed\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"+1234\" is not valid \"tel\"\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"+1234\" is not valid \"tel\" for \"sms\""), + }, + { + description: "break_glass_for_organization preserved on existing recovery address", + doc: `{"username":"foo@ory.sh"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + BreakGlassForOrganization: breakGlassOrgID, + }, + }, + existing: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + BreakGlassForOrganization: breakGlassOrgID, + }, + }, + }, + { + description: "break_glass null preserved on existing recovery address", + doc: `{"username":"foo@ory.sh"}`, + schema: "file://./stub/extension/recovery/email.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + existing: []RecoveryAddress{ + { + Value: "foo@ory.sh", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + description: "docomo email under no-validate is accepted", + doc: `{"email":"foo.@docomo.ne.jp"}`, + schema: "file://./stub/extension/recovery/no-validate.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo.@docomo.ne.jp", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + // Uppercase input on the lenient path must still lowercase so it + // matches the recovery-address lookup normalization. + description: "docomo email under no-validate is normalized", + doc: `{"email":"FOO.@docomo.ne.jp"}`, + schema: "file://./stub/extension/recovery/no-validate.schema.json", + expect: []RecoveryAddress{ + { + Value: "foo.@docomo.ne.jp", + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + } { + t.Run(fmt.Sprintf("case=%d description=%s", k, tc.description), func(t *testing.T) { + id := &Identity{ID: iid, RecoveryAddresses: tc.existing} + c := jsonschema.NewCompiler() + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + e := NewSchemaExtensionRecovery(id) + runner.AddRunner(e).Register(c) + + err = c.MustCompile(t.Context(), tc.schema).Validate(bytes.NewBufferString(tc.doc)) + if tc.expectErr != nil { + require.EqualError(t, err, tc.expectErr.Error()) + return + } + + require.NoError(t, e.Finish()) + + addresses := id.RecoveryAddresses + require.Len(t, addresses, len(tc.expect)) + + for _, actual := range addresses { + var found bool + for _, expect := range tc.expect { + if reflect.DeepEqual(actual, expect) { + found = true + break + } + } + assert.True(t, found, "%+v not in %+v", actual, tc.expect) + } + }) + } +} + +// TestSchemaExtensionRecovery_PreservesExistingRecordWhenValueIsNotNormalized +// is a regression test for the same root cause as #11283's verification bug, +// applied to recovery addresses: identities whose persisted +// RecoveryAddress.Value is in pre-normalization raw form get their existing +// record (ID, BreakGlassForOrganization, timestamps) dropped on every +// re-validation because the extension now emits the E.164-normalized form +// and has() compares values byte-wise. +func TestSchemaExtensionRecovery_PreservesExistingRecordWhenValueIsNotNormalized(t *testing.T) { + iid := x.NewUUID() + existingID := x.NewUUID() + breakGlassOrgID := uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + + rawPhone := "+49 0176 67111638" + id := &Identity{ + ID: iid, + RecoveryAddresses: []RecoveryAddress{{ + ID: existingID, + Value: rawPhone, + Via: AddressTypeSMS, + IdentityID: iid, + BreakGlassForOrganization: breakGlassOrgID, + }}, + } + + c := jsonschema.NewCompiler() + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + e := NewSchemaExtensionRecovery(id) + runner.AddRunner(e).Register(c) + + err = c.MustCompile(t.Context(), "file://./stub/extension/recovery/sms.schema.json"). + Validate(bytes.NewBufferString(fmt.Sprintf(`{"telephoneNumber":%q}`, rawPhone))) + require.NoError(t, err) + require.NoError(t, e.Finish()) + + require.Len(t, id.RecoveryAddresses, 1) + assert.Equal(t, existingID, id.RecoveryAddresses[0].ID, + "existing recovery record ID must be retained on re-validation with raw phone") + assert.Equal(t, breakGlassOrgID, id.RecoveryAddresses[0].BreakGlassForOrganization, + "break_glass_for_organization must be retained on re-validation with raw phone") +} diff --git a/identity/extension_verification.go b/identity/extension_verification.go new file mode 100644 index 0000000..2288ab3 --- /dev/null +++ b/identity/extension_verification.go @@ -0,0 +1,131 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "maps" + "slices" + "strings" + "sync" + "time" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" +) + +func init() { + jsonschema.Formats["no-validate"] = func(v interface{}) bool { + return true + } +} + +type SchemaExtensionVerification struct { + lifespan time.Duration + l sync.Mutex + v []VerifiableAddress + i *Identity +} + +func NewSchemaExtensionVerification(i *Identity, lifespan time.Duration) *SchemaExtensionVerification { + return &SchemaExtensionVerification{i: i, lifespan: lifespan} +} + +const ( + ChannelTypeEmail = "email" + ChannelTypeSMS = "sms" +) + +func (r *SchemaExtensionVerification) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { + r.l.Lock() + defer r.l.Unlock() + + if s.Verification.Via == "" { + return nil + } + + format, ok := s.RawSchema["format"] + if !ok { + format = "" + } + formatString, ok := format.(string) + if !ok { + return nil + } + + if formatString == "" { + switch s.Verification.Via { + case ChannelTypeEmail: + formatString = "email" + formatter, ok := jsonschema.Formats[formatString] + if !ok { + supportedKeys := slices.Collect(maps.Keys(jsonschema.Formats)) + return ctx.Error("format", "format %q is not supported. Supported formats are [%s]", formatString, strings.Join(supportedKeys, ", ")) + } + + if !formatter(value) { + return ctx.Error("format", "%q is not valid %q", value, formatString) + } + default: + return ctx.Error("format", "no format specified. A format is required if verification is enabled. If this was intentional, please set \"format\" to \"no-validate\"") + } + } + + var normalized string + switch formatString { + case "email", "tel": + var err error + normalized, err = x.NormalizeIdentifier(fmt.Sprintf("%s", value), s.Verification.Via) + if err != nil { + return ctx.Error("format", "%q can not be normalized for format %q", value, formatString) + } + default: + normalized = strings.TrimSpace(fmt.Sprintf("%s", value)) + } + + address := NewVerifiableAddress(normalized, r.i.ID, s.Verification.Via) + r.appendAddress(address) + return nil +} + +func (r *SchemaExtensionVerification) Finish() error { + r.i.VerifiableAddresses = merge(r.v, r.i.VerifiableAddresses) + return nil +} + +// merge merges the base with the overrides through comparison with `has`. It changes the base slice in place. +func merge(base []VerifiableAddress, overrides []VerifiableAddress) []VerifiableAddress { + for i := range base { + if override := has(overrides, &base[i]); override != nil { + base[i] = *override + } + } + + return base +} + +func (r *SchemaExtensionVerification) appendAddress(address *VerifiableAddress) { + if h := has(r.i.VerifiableAddresses, address); h != nil { + if has(r.v, address) == nil { + r.v = append(r.v, *h) + } + return + } + + if has(r.v, address) == nil { + r.v = append(r.v, *address) + } +} + +func has(haystack []VerifiableAddress, needle *VerifiableAddress) *VerifiableAddress { + // Normalize both sides so pre-normalization persisted values still match. + normalizedNeedle := x.GracefulNormalization(needle.Value) + for _, has := range haystack { + if has.Via == needle.Via && x.GracefulNormalization(has.Value) == normalizedNeedle { + return &has + } + } + return nil +} diff --git a/identity/extension_verification_test.go b/identity/extension_verification_test.go new file mode 100644 index 0000000..0e09abc --- /dev/null +++ b/identity/extension_verification_test.go @@ -0,0 +1,540 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "fmt" + "net" + "reflect" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/jsonschema/v3" + _ "github.com/ory/jsonschema/v3/fileloader" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" +) + +const ( + emailSchemaPath = "file://./stub/extension/verify/email.schema.json" + phoneSchemaPath = "file://./stub/extension/verify/phone.schema.json" + missingFormatSchemaPath = "file://./stub/extension/verify/missing-format.schema.json" + legacyEmailMissingFormatSchemaPath = "file://./stub/extension/verify/legacy-email-missing-format.schema.json" + noValidateSchemaPath = "file://./stub/extension/verify/no-validate.schema.json" +) + +func TestSchemaExtensionVerification(t *testing.T) { + net.IP{}.IsPrivate() + t.Run("address verification", func(t *testing.T) { + iid := x.NewUUID() + + for _, tc := range []struct { + name string + schema string + expectErr error + doc string + existing []VerifiableAddress + expect []VerifiableAddress + }{ + { + name: "email:must create new address", + schema: emailSchemaPath, + doc: `{"username":"foo@ory.sh"}`, + expect: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "email:must create new address because new and existing doesn't match", + schema: emailSchemaPath, + doc: `{"username":"foo@ory.sh"}`, + existing: []VerifiableAddress{ + { + Value: "bar@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "email:must find existing address in case of match", + schema: emailSchemaPath, + doc: `{"emails":["baz@ory.sh","foo@ory.sh"]}`, + existing: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "baz@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "email:must return only one address in case of duplication", + schema: emailSchemaPath, + doc: `{"emails":["foo@ory.sh","foo@ory.sh","baz@ory.sh"]}`, + existing: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "baz@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "email:must return error for malformed input", + schema: emailSchemaPath, + doc: `{"emails":["foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, + expectErr: errors.New("I[#/username] S[#/properties/username/format] \"foobar\" is not valid \"email\""), + }, + { + name: "email:must merge email addresses from multiple attributes", + schema: emailSchemaPath, + doc: `{"emails":["foo@ory.sh","bar@ory.sh","bar@ory.sh"], "username": "foobar@ory.sh"}`, + expect: []VerifiableAddress{ + { + Value: "foo@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "bar@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + { + Value: "foobar@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: AddressTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must create new address", + schema: phoneSchemaPath, + doc: `{"username":"+18004444444"}`, + expect: []VerifiableAddress{ + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must create new address because new and existing doesn't match", + schema: phoneSchemaPath, + doc: `{"username":"+18004444444"}`, + existing: []VerifiableAddress{ + { + Value: "+442087599036", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must find existing addresses in case of match", + schema: phoneSchemaPath, + doc: `{"phones":["+18004444444","+442087599036"]}`, + existing: []VerifiableAddress{ + { + Value: "+442087599036", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+380634872774", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "+442087599036", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must return only one address in case of duplication", + schema: phoneSchemaPath, + doc: `{"phones": ["+18004444444","+18004444444","+442087599036"]}`, + existing: []VerifiableAddress{ + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+380634872774", + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + expect: []VerifiableAddress{ + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+442087599036", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must merge phones from multiple attributes", + schema: phoneSchemaPath, + doc: `{"phones": ["+18004444444","+18004444444","+442087599036"], "username": "+380634872774"}`, + expect: []VerifiableAddress{ + { + Value: "+18004444444", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+442087599036", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + { + Value: "+380634872774", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + name: "phone:must return error for malformed input", + schema: phoneSchemaPath, + doc: `{"phones":["+18004444444","+18004444444","12112112"], "username": "+380634872774"}`, + expectErr: errors.New("I[#/phones/2] S[#/properties/phones/items] validation failed\n I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" is not valid \"tel\"\n I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" can not be normalized for format \"tel\""), + }, + { + name: "missing format returns an error", + schema: missingFormatSchemaPath, + doc: `{"phone": "+380634872774"}`, + expectErr: errors.New("I[#/phone] S[#/properties/phone/format] no format specified. A format is required if verification is enabled. If this was intentional, please set \"format\" to \"no-validate\""), + }, + { + name: "missing format works for email if format is missing", + schema: legacyEmailMissingFormatSchemaPath, + doc: `{"email": "user@ory.sh"}`, + expect: []VerifiableAddress{ + { + Value: "user@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "format: no-validate works", + schema: noValidateSchemaPath, + doc: `{"phone": "not a phone number"}`, + expect: []VerifiableAddress{ + { + Value: "not a phone number", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, + { + // 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, + }, + }, + }, + { + name: "phone:should normalize +1 (645) 333-1111 to +16453331111", + schema: phoneSchemaPath, + doc: `{"phones":["+1 (645) 333-1111"]}`, + 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} + + c := jsonschema.NewCompiler() + + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + e := NewSchemaExtensionVerification(id, time.Minute) + runner.AddRunner(e).Register(c) + + err = c.MustCompile(t.Context(), tc.schema).Validate(bytes.NewBufferString(tc.doc)) + if tc.expectErr != nil { + require.EqualError(t, err, tc.expectErr.Error()) + return + } else { + require.NoError(t, err) + } + + require.NoError(t, e.Finish()) + + addresses := id.VerifiableAddresses + require.Len(t, addresses, len(tc.expect)) + + mustContainAddress(t, tc.expect, addresses) + }) + } + }) +} + +func mustContainAddress(t *testing.T, expected, actual []VerifiableAddress) { + for _, act := range actual { + var found bool + for _, expect := range expected { + if reflect.DeepEqual(act, expect) { + found = true + break + } + } + assert.True(t, found, "%+v not in %+v", act, expected) + } +} + +// TestSchemaExtensionVerification_PreservesVerifiedWhenExistingValueIsNotNormalized +// is a regression test for the bug where identities created before phone +// normalization (#11283) lose their verified status on re-validation because +// the persisted VerifiableAddress.Value is in raw form but the extension now +// normalizes the trait to E.164 before comparing. +// +// Invariant we rely on: a persisted phone trait that was verified before must +// stay verified after re-validation, even when the persisted +// VerifiableAddress.Value is not in E.164 form. +func TestSchemaExtensionVerification_PreservesVerifiedWhenExistingValueIsNotNormalized(t *testing.T) { + iid := x.NewUUID() + + // Trait and existing VerifiableAddress both hold the pre-normalization + // raw value (contains an extra leading zero from the national trunk + // prefix). Before #11283, the extension trimmed only, so this value was + // persisted verbatim. After #11283, the same trait normalizes to + // "+4917667111638" -- a value that does not match the persisted + // VerifiableAddress.Value. + rawPhone := "+49 0176 67111638" + id := &Identity{ + ID: iid, + VerifiableAddresses: []VerifiableAddress{{ + Value: rawPhone, + Verified: true, + Status: VerifiableAddressStatusCompleted, + Via: ChannelTypeSMS, + IdentityID: iid, + }}, + } + + c := jsonschema.NewCompiler() + runner, err := schema.NewExtensionRunner(t.Context()) + require.NoError(t, err) + + e := NewSchemaExtensionVerification(id, time.Minute) + runner.AddRunner(e).Register(c) + + err = c.MustCompile(t.Context(), phoneSchemaPath). + Validate(bytes.NewBufferString(fmt.Sprintf(`{"username":%q}`, rawPhone))) + require.NoError(t, err) + require.NoError(t, e.Finish()) + + require.Len(t, id.VerifiableAddresses, 1) + assert.True(t, id.VerifiableAddresses[0].Verified, + "persisted phone must remain verified after re-validation; got %+v", + id.VerifiableAddresses[0]) +} + +func TestMergeVerifiableAddresses(t *testing.T) { + for _, tt := range []struct { + name string + base, overrides, expected []VerifiableAddress + }{ + { + name: "empty base", + base: []VerifiableAddress{}, + overrides: []VerifiableAddress{{ + Value: "override@ory.sh", + Via: "email", + }}, + expected: []VerifiableAddress{}, + }, { + name: "no overlap", + base: []VerifiableAddress{{ + Value: "base@ory.sh", + Via: "email", + }}, + overrides: []VerifiableAddress{{ + Value: "override@ory.sh", + Via: "email", + }}, + expected: []VerifiableAddress{{ + Value: "base@ory.sh", + Via: "email", + }}, + }, { + name: "overrides", + base: []VerifiableAddress{ + {Value: "base@ory.sh", Via: "email"}, + {Value: "common-1-is-overwritten@ory.sh", Via: "email"}, + {Value: "common-2-is-ignored@ory.sh", Via: "no match"}, + }, + overrides: []VerifiableAddress{ + {Value: "common-1-is-overwritten@ory.sh", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted}, + {Value: "common-2-is-ignored@ory.sh", Via: "wrong via"}, + {Value: "override-only-is-ignored@ory.sh", Via: "email"}, + }, + expected: []VerifiableAddress{ + {Value: "base@ory.sh", Via: "email"}, + {Value: "common-1-is-overwritten@ory.sh", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted}, + {Value: "common-2-is-ignored@ory.sh", Via: "no match"}, + }, + }, + } { + t.Run("case="+tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, merge(tt.base, tt.overrides)) + }) + } +} diff --git a/identity/handler.go b/identity/handler.go new file mode 100644 index 0000000..8abcc92 --- /dev/null +++ b/identity/handler.go @@ -0,0 +1,1527 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + "encoding/json" + "io" + "net/http" + "slices" + "strings" + "time" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + + "github.com/gofrs/uuid" + + "github.com/ory/x/crdbx" + "github.com/ory/x/pagination/keysetpagination" + + "github.com/ory/x/pagination/migrationpagination" + "github.com/ory/x/pagination/pagepagination" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/hash" + "github.com/ory/kratos/selfservice/strategy/deviceauthn" + "github.com/ory/kratos/x" + + "github.com/ory/kratos/cipher" + + "github.com/ory/herodot" + + "github.com/pkg/errors" + + "github.com/ory/x/decoderx" + "github.com/ory/x/jsonx" + "github.com/ory/x/openapix" + "github.com/ory/x/region" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" +) + +const ( + RouteCollection = "/identities" + RouteItem = RouteCollection + "/{id}" + RouteCredentialItem = RouteItem + "/credentials/{type}" + + BatchPatchIdentitiesLimit = 1000 + BatchPatchIdentitiesWithPasswordLimit = 200 +) + +type ( + dependencies interface { + PoolProvider + PrivilegedPoolProvider + ManagementProvider + httpx.WriterProvider + config.Provider + nosurfx.CSRFProvider + cipher.Provider + hash.HashProvider + } + HandlerProvider interface { + IdentityHandler() *Handler + } + Handler struct{ r dependencies } +) + +func NewHandler(r dependencies) *Handler { return &Handler{r: r} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.r.CSRFHandler().IgnoreGlobs( + RouteCollection, + RouteCollection+"/*", + RouteCollection+"/*/credentials/*", + httprouterx.AdminPrefix+RouteCollection, + httprouterx.AdminPrefix+RouteCollection+"/*", + httprouterx.AdminPrefix+RouteCollection+"/*/credentials/*", + ) + + public.GET(RouteCollection, redir.RedirectToAdminRoute(h.r)) + public.GET(RouteCollection+"/by/external/{externalID}", redir.RedirectToAdminRoute(h.r)) + public.GET(RouteItem, redir.RedirectToAdminRoute(h.r)) + public.DELETE(RouteItem, redir.RedirectToAdminRoute(h.r)) + public.POST(RouteCollection, redir.RedirectToAdminRoute(h.r)) + public.PUT(RouteItem, redir.RedirectToAdminRoute(h.r)) + public.PATCH(RouteItem, redir.RedirectToAdminRoute(h.r)) + public.DELETE(RouteCredentialItem, redir.RedirectToAdminRoute(h.r)) + + public.GET(httprouterx.AdminPrefix+RouteCollection, redir.RedirectToAdminRoute(h.r)) + public.GET(httprouterx.AdminPrefix+RouteCollection+"/by/external/{externalID}", redir.RedirectToAdminRoute(h.r)) + public.GET(httprouterx.AdminPrefix+RouteItem, redir.RedirectToAdminRoute(h.r)) + public.DELETE(httprouterx.AdminPrefix+RouteItem, redir.RedirectToAdminRoute(h.r)) + public.POST(httprouterx.AdminPrefix+RouteCollection, redir.RedirectToAdminRoute(h.r)) + public.PUT(httprouterx.AdminPrefix+RouteItem, redir.RedirectToAdminRoute(h.r)) + public.PATCH(httprouterx.AdminPrefix+RouteItem, redir.RedirectToAdminRoute(h.r)) + public.DELETE(httprouterx.AdminPrefix+RouteCredentialItem, redir.RedirectToAdminRoute(h.r)) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteCollection, h.list) + admin.GET(RouteItem, h.get) + admin.GET(RouteCollection+"/by/external/{externalID}", h.getByExternalID) + admin.DELETE(RouteItem, h.delete) + admin.PATCH(RouteItem, h.patch) + + admin.POST(RouteCollection, h.create) + admin.PATCH(RouteCollection, h.batchPatchIdentities) + admin.PUT(RouteItem, h.update) + + admin.DELETE(RouteCredentialItem, h.deleteIdentityCredentials) +} + +// Paginated Identity List Response +// +// swagger:response listIdentities +type _ struct { + migrationpagination.ResponseHeaderAnnotation + + // List of identities + // + // in:body + Body []Identity +} + +// Paginated List Identity Parameters +// +// Note: Filters cannot be combined. +// +// swagger:parameters listIdentities +type _ struct { + migrationpagination.RequestParameters + + // Retrieve multiple identities by their IDs. + // + // This parameter has the following limitations: + // + // - Duplicate or non-existent IDs are ignored. + // - The order of returned IDs may be different from the request. + // - This filter does not support pagination. You must implement your own pagination as the maximum number of items returned by this endpoint may not exceed a certain threshold (currently 500). + // + // required: false + // in: query + IdsFilter []string `json:"ids"` + + // CredentialsIdentifier is the identifier (username, email) of the credentials to look up using exact match. + // Only one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used. + // + // required: false + // in: query + CredentialsIdentifier string `json:"credentials_identifier"` + + // This is an EXPERIMENTAL parameter that WILL CHANGE. Do NOT rely on consistent, deterministic behavior. + // THIS PARAMETER WILL BE REMOVED IN AN UPCOMING RELEASE WITHOUT ANY MIGRATION PATH. + // + // CredentialsIdentifierSimilar is the (partial) identifier (username, email) of the credentials to look up using similarity search. + // Only one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used. + // + // required: false + // 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"` + + // List identities that belong to a specific organization. + // + // required: false + // in: query + OrganizationID string `json:"organization_id"` + + crdbx.ConsistencyRequestParameters +} + +func parseListIdentitiesParameters(r *http.Request) (params ListIdentityParameters, err error) { + query := r.URL.Query() + var requestedFilters int + + params.Expand = ExpandDefault + + if ids := query["ids"]; len(ids) > 0 { + requestedFilters++ + for _, v := range ids { + id, err := uuid.FromString(v) + if err != nil { + return params, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Invalid UUID value `%s` for parameter `ids`.", v)) + } + params.IdsFilter = append(params.IdsFilter, id) + } + } + if len(params.IdsFilter) > 500 { + return params, errors.WithStack(herodot.ErrBadRequest().WithReason("The number of ids to filter must not exceed 500.")) + } + + if orgID := query.Get("organization_id"); orgID != "" { + requestedFilters++ + params.OrganizationID, err = uuid.FromString(orgID) + if err != nil { + return params, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Invalid UUID value `%s` for parameter `organization_id`.", orgID)) + } + } + + if identifier := query.Get("credentials_identifier"); identifier != "" { + requestedFilters++ + params.Expand = ExpandEverything + params.CredentialsIdentifier = identifier + } + + if identifier := query.Get("preview_credentials_identifier_similar"); identifier != "" { + requestedFilters++ + params.Expand = ExpandEverything + params.CredentialsIdentifierSimilar = identifier + } + + for _, v := range query["include_credential"] { + params.Expand = ExpandEverything + tc, ok := ParseCredentialsType(v) + if !ok { + return params, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Invalid value `%s` for parameter `include_credential`.", v)) + } + params.DeclassifyCredentials = append(params.DeclassifyCredentials, tc) + } + + if requestedFilters > 1 { + return params, errors.WithStack(herodot.ErrBadRequest().WithReason("You cannot combine multiple filters in this API")) + } + + params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) + if err != nil { + return params, err + } + params.ConsistencyLevel = crdbx.ConsistencyLevelFromRequest(r) + + return params, nil +} + +// swagger:route GET /admin/identities identity listIdentities +// +// # List Identities +// +// Lists all [identities](https://www.ory.com/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: listIdentities +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) list(w http.ResponseWriter, r *http.Request) { + params, err := parseListIdentitiesParameters(r) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + is, nextPage, err := h.r.IdentityPool().ListIdentities(r.Context(), params) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + if params.PagePagination != nil { + total := int64(len(is)) + if params.CredentialsIdentifier == "" { + total, err = h.r.IdentityPool().CountIdentities(r.Context()) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + u := *r.URL + pagepagination.PaginationHeader(w, &u, total, params.PagePagination.Page, params.PagePagination.ItemsPerPage) + } else if nextPage != nil { + u := *r.URL + keysetpagination.Header(w, &u, nextPage) + } + + // Identities using the marshaler for including metadata_admin + isam := make([]WithCredentialsAndAdminMetadataInJSON, len(is)) + for i, identity := range is { + 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) +} + +// Get Identity Parameters +// +// swagger:parameters getIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getIdentity struct { + // ID must be set to the ID of identity you want to get + // + // required: true + // in: path + ID string `json:"id"` + + // 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 []CredentialsType `json:"include_credential"` +} + +// Get Identity By External ID Parameters +// +// swagger:parameters getIdentityByExternalID +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getIdentityByExternalID struct { + // ExternalID must be set to the ID of identity you want to get + // + // required: true + // in: path + ExternalID string `json:"externalID"` + + // 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 []CredentialsType `json:"include_credential"` +} + +// swagger:route GET /admin/identities/{id} identity getIdentity +// +// # Get an Identity +// +// Return an [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) by its ID. You can optionally +// include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: identity +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-high +func (h *Handler) get(w http.ResponseWriter, r *http.Request) { + i, err := h.r.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), x.ParseUUID(r.PathValue("id"))) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + includeCredentials := r.URL.Query()["include_credential"] + 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 + } + } + + emit, err := i.WithDeclassifiedCredentials(r.Context(), h.r, declassify) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + h.r.Writer().Write(w, r, WithCredentialsAndAdminMetadataInJSON(*emit)) +} + +// swagger:route GET /admin/identities/by/external/{externalID} identity getIdentityByExternalID +// +// # Get an Identity by its External ID +// +// Return an [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) by its external ID. You can optionally +// include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: identity +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) getByExternalID(w http.ResponseWriter, r *http.Request) { + externalID := r.PathValue("externalID") + if externalID == "" { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("The external ID must not be empty."))) + return + } + i, err := h.r.PrivilegedIdentityPool().FindIdentityByExternalID(r.Context(), externalID, ExpandEverything) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + includeCredentials := r.URL.Query()["include_credential"] + 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 + } + } + + emit, err := i.WithDeclassifiedCredentials(r.Context(), h.r, declassify) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + h.r.Writer().Write(w, r, WithCredentialsAndAdminMetadataInJSON(*emit)) +} + +// Create Identity Parameters +// +// swagger:parameters createIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createIdentity struct { + // in: body + Body CreateIdentityBody +} + +// Create Identity Body +// +// swagger:model createIdentityBody +type CreateIdentityBody struct { + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + // + // required: true + SchemaID string `json:"schema_id"` + + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits + // in a self-service manner. The input will always be validated against the JSON Schema defined + // in `schema_url`. + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Credentials represents all credentials that can be used for authenticating this identity. + // + // Use this structure to import credentials for a user. + Credentials *IdentityWithCredentials `json:"credentials"` + + // VerifiableAddresses contains all the addresses that can be verified by the user. + // + // Use this structure to import verified addresses for an identity. Please keep in mind + // that the address needs to be represented in the Identity Schema or this field will be overwritten + // on the next identity update. + VerifiableAddresses []VerifiableAddress `json:"verifiable_addresses"` + + // RecoveryAddresses contains all the addresses that can be used to recover an identity. + // + // Use this structure to import recovery addresses for an identity. Please keep in mind + // that the address needs to be represented in the Identity Schema or this field will be overwritten + // on the next identity update. + RecoveryAddresses []RecoveryAddress `json:"recovery_addresses"` + + // Store metadata about the identity which the identity itself can see when calling for example the + // session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic json.RawMessage `json:"metadata_public"` + + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin json.RawMessage `json:"metadata_admin,omitempty"` + + // State is the identity's state. + // + // required: false + State State `json:"state"` + + // OrganizationID is the ID of the organization to which the identity belongs. + // + // required: false + OrganizationID uuid.NullUUID `json:"organization_id"` + + // ExternalID is an optional external ID of the identity. This is used to link + // the identity to an external system. If set, the external ID must be unique + // across all identities. + // + // required: false + ExternalID string `json:"external_id,omitempty"` + + // Region is the Ory Network region this identity will be created in. + // Optional; defaults to the project home region if omitted. Only effective + // on the Ory Network. + // + // required: false + Region region.Region `json:"region,omitempty"` +} + +// Create Identity and Import Credentials +// +// swagger:model identityWithCredentials +type IdentityWithCredentials struct { + // Password if set will import a password credential. + Password *AdminIdentityImportCredentialsPassword `json:"password"` + + // OIDC if set will import an OIDC credential. + OIDC *AdminIdentityImportCredentialsOIDC `json:"oidc"` + + // SAML if set will import a SAML credential. + SAML *AdminIdentityImportCredentialsSAML `json:"saml"` + + // TOTP if set will import a TOTP credential. + TOTP *AdminIdentityImportCredentialsTOTP `json:"totp"` + + // WebAuthn if set will import a WebAuthn credential. + WebAuthn *AdminIdentityImportCredentialsWebAuthn `json:"webauthn"` + + // Passkey if set will import a Passkey credential. + Passkey *AdminIdentityImportCredentialsPasskey `json:"passkey"` + + // LookupSecret if set will import a magic code credential. + LookupSecret *AdminIdentityImportCredentialsLookupSecret `json:"lookup_secret"` +} + +// Create Identity and Import TOTP 2FA Credentials +// +// swagger:model identityWithCredentialsTotp +type AdminIdentityImportCredentialsTOTP struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsTOTPConfig `json:"config"` +} + +// Create Identity and Import TOTP 2FA Credentials Configuration +// +// swagger:model identityWithCredentialsTotpConfig +type AdminIdentityImportCredentialsTOTPConfig struct { + // TOTPURL is the TOTP URL + // + // For more details see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + TOTPURL string `json:"totp_url"` +} + +// Create Identity and Import WebAuthn Credentials +// +// swagger:model identityWithCredentialsWebAuthn +type AdminIdentityImportCredentialsWebAuthn struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsWebAuthnConfig `json:"config"` +} + +// Create Identity and Import WebAuthn Credentials Configuration +// +// swagger:model identityWithCredentialsWebAuthnConfig +type AdminIdentityImportCredentialsWebAuthnConfig struct { + // List of webauthn credentials. + Credentials CredentialsWebAuthn `json:"credentials"` + + // UserHandle is the user handle of the webauthn credential. + UserHandle []byte `json:"user_handle"` +} + +// Create Identity and Import Passkey Credentials +// +// swagger:model identityWithCredentialsPasskey +type AdminIdentityImportCredentialsPasskey struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsPasskeyConfig `json:"config"` +} + +// Create Identity and Import Passkey Credentials Configuration +// +// swagger:model identityWithCredentialsPasskeyConfig +type AdminIdentityImportCredentialsPasskeyConfig struct { + // List of webauthn credentials. + Credentials CredentialsWebAuthn `json:"credentials"` + + // UserHandle is the user handle of the webauthn credential. + UserHandle []byte `json:"user_handle"` +} + +// Create Identity and Import Lookup Secret Credentials +// +// swagger:model identityWithCredentialsLookup Secret +type AdminIdentityImportCredentialsLookupSecret struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsLookupSecretConfig `json:"config"` +} + +// Create Identity and Import Lookup Secret Credentials Configuration +// +// swagger:model identityWithCredentialsLookup SecretConfig +type AdminIdentityImportCredentialsLookupSecretConfig struct { + // Codes is a list of "lookup secret" codes configured for the user. + Codes []RecoveryCode `json:"codes"` +} + +// Create Identity and Import Password Credentials +// +// swagger:model identityWithCredentialsPassword +type AdminIdentityImportCredentialsPassword struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsPasswordConfig `json:"config"` +} + +// Create Identity and Import Password Credentials Configuration +// +// swagger:model identityWithCredentialsPasswordConfig +type AdminIdentityImportCredentialsPasswordConfig struct { + // The hashed password in [PHC format](https://www.ory.com/docs/kratos/manage-identities/import-user-accounts-identities#hashed-passwords) + HashedPassword string `json:"hashed_password"` + + // 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 +// +// swagger:model identityWithCredentialsOidc +type AdminIdentityImportCredentialsOIDC struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsOIDCConfig `json:"config"` +} + +// swagger:model identityWithCredentialsOidcConfig +type AdminIdentityImportCredentialsOIDCConfig struct { + // A list of OpenID Connect Providers + Providers []AdminCreateIdentityImportCredentialsOIDCProvider `json:"providers"` +} + +// Create Identity and Import Social Sign In Credentials Configuration +// +// swagger:model identityWithCredentialsOidcConfigProvider +type AdminCreateIdentityImportCredentialsOIDCProvider struct { + // The subject (`sub`) of the OpenID Connect connection. Usually the `sub` field of the ID Token. + // + // required: true + Subject string `json:"subject"` + + // The OpenID Connect provider to link the subject to. Usually something like `google` or `github`. + // + // required: true + Provider string `json:"provider"` + + // If set, this credential allows the user to sign in using the OpenID Connect provider without setting the subject first. + // + // required: false + UseAutoLink bool `json:"use_auto_link,omitempty"` + + // The organization to assign for the provider. + Organization uuid.NullUUID `json:"organization,omitempty"` +} + +// Payload to import SAML credentials +// +// swagger:model identityWithCredentialsSaml +type AdminIdentityImportCredentialsSAML struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsSAMLConfig `json:"config"` +} + +// Payload of SAML providers +// +// swagger:model identityWithCredentialsSamlConfig +type AdminIdentityImportCredentialsSAMLConfig struct { + // A list of SAML Providers + Providers []AdminCreateIdentityImportCredentialsSAMLProvider `json:"providers"` +} + +// Payload of specific SAML provider +// +// swagger:model identityWithCredentialsSamlConfigProvider +type AdminCreateIdentityImportCredentialsSAMLProvider struct { + // The unique subject of the SAML connection. This value must be immutable at the source. + // + // required: true + Subject string `json:"subject"` + + // The SAML provider to link the subject to. + // + // required: true + Provider string `json:"provider"` + + // The organization to assign for the provider. + Organization uuid.NullUUID `json:"organization"` +} + +// swagger:route POST /admin/identities identity createIdentity +// +// # Create an Identity +// +// Create an [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model). This endpoint can also be used to +// [import credentials](https://www.ory.com/docs/kratos/manage-identities/import-user-accounts-identities) +// for instance passwords, social sign in configurations, or multifactor methods. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 201: identity +// 400: errorGeneric +// 409: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) create(w http.ResponseWriter, r *http.Request) { + var cr CreateIdentityBody + if err := jsonx.NewStrictDecoder(r.Body).Decode(&cr); err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()))) + return + } + + i, err := h.identityFromCreateIdentityBody(r.Context(), &cr) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + if err := h.r.IdentityManager().Create(r.Context(), i); err != nil { + if errors.Is(err, sqlcon.ErrUniqueViolation()) { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrConflict().WithReason("This identity conflicts with another identity that already exists."))) + } else { + h.r.Writer().WriteError(w, r, err) + } + return + } + + h.r.Writer().WriteCreated(w, r, + urlx.AppendPaths( + h.r.Config().SelfAdminURL(r.Context()), + "identities", + i.ID.String(), + ).String(), + WithCredentialsNoConfigAndAdminMetadataInJSON(*i), + ) +} + +func (h *Handler) identityFromCreateIdentityBody(ctx context.Context, cr *CreateIdentityBody) (*Identity, error) { + stateChangedAt := sqlxx.NullTime(time.Now().UTC()) + state := StateActive + if cr.State != "" { + if err := cr.State.IsValid(); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err).WithWrap(err)) + } + state = cr.State + } + + // Validate at the API boundary; the persister re-checks under home_region. + if cr.Region != "" && !cr.Region.Valid() { + return nil, region.NewErrInvalid() + } + + i := &Identity{ + SchemaID: cr.SchemaID, + Traits: []byte(cr.Traits), + State: state, + StateChangedAt: &stateChangedAt, + VerifiableAddresses: cr.VerifiableAddresses, + RecoveryAddresses: cr.RecoveryAddresses, + MetadataAdmin: []byte(cr.MetadataAdmin), + MetadataPublic: []byte(cr.MetadataPublic), + OrganizationID: cr.OrganizationID, + ExternalID: sqlxx.NullString(cr.ExternalID), + Region: cr.Region, + } + // Lowercase all emails, because the schema extension will otherwise not find them. + for k := range i.VerifiableAddresses { + i.VerifiableAddresses[k].Value = strings.ToLower(i.VerifiableAddresses[k].Value) + } + for k := range i.RecoveryAddresses { + i.RecoveryAddresses[k].Value = strings.ToLower(i.RecoveryAddresses[k].Value) + } + + if err := h.importCredentials(ctx, i, cr.Credentials); err != nil { + return nil, err + } + + return i, nil +} + +// swagger:route PATCH /admin/identities identity batchPatchIdentities +// +// # Create multiple identities +// +// Creates multiple [identities](https://www.ory.com/docs/kratos/concepts/identity-user-model). +// +// You can also use this endpoint to [import credentials](https://www.ory.com/docs/kratos/manage-identities/import-user-accounts-identities), +// including passwords, social sign-in settings, and multi-factor authentication methods. +// +// If the patch includes hashed passwords you can import up to 1,000 identities per request. +// +// If the patch includes at least one plaintext password you can import up to 200 identities per request. +// +// Avoid importing large batches with plaintext passwords. They can cause timeouts as the passwords need to be hashed before they are stored. +// +// If at least one identity is imported successfully, the response status is 200 OK. +// If all imports fail, the response is one of the following 4xx errors: +// - 400 Bad Request: The request payload is invalid or improperly formatted. +// - 409 Conflict: Duplicate identities or conflicting data were detected. +// +// If you get a 504 Gateway Timeout: +// - Reduce the batch size +// - Avoid duplicate identities +// - Pre-hash passwords with BCrypt +// +// If the issue persists, contact support. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: batchPatchIdentitiesResponse +// 400: errorGeneric +// 409: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) batchPatchIdentities(w http.ResponseWriter, r *http.Request) { + var ( + req BatchPatchIdentitiesBody + res batchPatchIdentitiesResponse + ) + if err := jsonx.NewStrictDecoder(r.Body).Decode(&req); err != nil { + h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errors.WithStack(err)) + return + } + + if len(req.Identities) > BatchPatchIdentitiesLimit { + h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, + errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "The maximum number of identities per request that can be created or deleted at once is %d.", + BatchPatchIdentitiesLimit))) + return + } + + res.Identities = make([]*BatchIdentityPatchResponse, len(req.Identities)) + // Array to look up the index of the identity in the identities array. + indexInIdentities := make([]*int, len(req.Identities)) + identities := make([]*Identity, 0, len(req.Identities)) + + var withUnHashedPasswordCount int + for i, patch := range req.Identities { + if patch.Create != nil { + res.Identities[i] = &BatchIdentityPatchResponse{ + Action: ActionCreate, + PatchID: patch.ID, + } + identity, err := h.identityFromCreateIdentityBody(r.Context(), patch.Create) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + identities = append(identities, identity) + idx := len(identities) - 1 + indexInIdentities[i] = &idx + + if patch.Create.Credentials != nil && patch.Create.Credentials.Password != nil && + patch.Create.Credentials.Password.Config.Password != "" { + withUnHashedPasswordCount++ + } + } + } + + if withUnHashedPasswordCount > BatchPatchIdentitiesWithPasswordLimit { + h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, + errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "The maximum number of identities per request that can be created with a plaintext password is %d.", + BatchPatchIdentitiesWithPasswordLimit))) + return + } + + 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 { + 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 + } + } + } + + h.r.Writer().Write(w, r, &res) +} + +// Update Identity Parameters +// +// swagger:parameters updateIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateIdentity struct { + // ID must be set to the ID of identity you want to update + // + // required: true + // in: path + ID string `json:"id"` + + // in: body + Body UpdateIdentityBody +} + +// Update Identity Body +// +// swagger:model updateIdentityBody +type UpdateIdentityBody struct { + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set + // will update the Identity's SchemaID. + // + // required: true + SchemaID string `json:"schema_id"` + + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits + // in a self-service manner. The input will always be validated against the JSON Schema defined + // in `schema_id`. + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Credentials represents all credentials that can be used for authenticating this identity. + // + // Use this structure to import credentials for a user. + // Note: this wil override completely identity's credentials. If used incorrectly, this can cause a user to lose + // access to their account! + Credentials *IdentityWithCredentials `json:"credentials"` + + // Store metadata about the identity which the identity itself can see when calling for example the + // session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic json.RawMessage `json:"metadata_public"` + + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin json.RawMessage `json:"metadata_admin,omitempty"` + + // State is the identity's state. + // + // required: true + State State `json:"state"` + + // ExternalID is an optional external ID of the identity. This is used to link + // the identity to an external system. If set, the external ID must be unique + // across all identities. + // + // required: false + ExternalID string `json:"external_id,omitempty"` + + // Region is the Ory Network region this identity is homed in. Optional; + // omit to leave the current region unchanged. + // + // required: false + Region region.Region `json:"region,omitempty"` +} + +// swagger:route PUT /admin/identities/{id} identity updateIdentity +// +// # Update an Identity +// +// This endpoint updates an [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model). The full identity +// payload (except credentials) is expected. +// +// It is possible to update the identity's credentials as well. Using this operation, credentials will not be overwritten +// but instead added to the list. For example, if a user has a social sign in connection set up, updating the credentials +// will keep the social sign in connection and add the new credentials to the list. This prevents accidentally overwriting +// credentials and locking out users. A complete view of all credential types is here: +// +// - `password`: The existing password credential will be completely replaced with the new configuration. You can provide either a hashed password, a plaintext password (which will be hashed), or enable the password migration hook. +// - `oidc`, `saml`: The existing OIDC and SAML credentials will be kept and the new credentials will be added to the list. +// - `totp`: The existing TOTP credentials will be replaced with the new configuration. +// - `lookup_secret`: The existing Lookup Secret codes will be kept and the new codes will be added to the list. +// - `webauthn`, `passkey`: The existing credentials are preserved, new credentials are added, and credentials with matching IDs are updated with new values. If a new `user_handle` is provided, it's added to the identity's identifiers list while preserving previous user handles. +// - `code`: To import code credentials, configure your identity schema to use one of the identity traits as an identifier source (`{"ory.sh/kratos":{"code":{"identifier":true", "via":"email"}}}`). +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: identity +// 400: errorGeneric +// 404: errorGeneric +// 409: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) update(w http.ResponseWriter, r *http.Request) { + var ur UpdateIdentityBody + if err := decoderx.Decode(r, &ur, + decoderx.HTTPJSONDecoder()); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + id := x.ParseUUID(r.PathValue("id")) + identity, err := h.r.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + if ur.SchemaID != "" { + identity.SchemaID = ur.SchemaID + } + + if ur.State != "" && identity.State != ur.State { + if err := ur.State.IsValid(); err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err).WithWrap(err))) + return + } + + stateChangedAt := sqlxx.NullTime(time.Now()) + + identity.State = ur.State + identity.StateChangedAt = &stateChangedAt + } + + identity.Traits = []byte(ur.Traits) + identity.MetadataPublic = []byte(ur.MetadataPublic) + identity.MetadataAdmin = []byte(ur.MetadataAdmin) + identity.ExternalID = sqlxx.NullString(ur.ExternalID) + + // Empty preserves the current region; older clients that pre-date the + // field must not silently wipe it. + if ur.Region != "" { + if !ur.Region.Valid() { + h.r.Writer().WriteError(w, r, region.NewErrInvalid()) + return + } + identity.Region = ur.Region + } + + // Although this is PUT and not PATCH, if the Credentials are not supplied keep the old one + if ur.Credentials != nil { + if err := h.importCredentials(r.Context(), identity, ur.Credentials); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + + if err := h.r.IdentityManager().Update( + r.Context(), + identity, + ManagerAllowWriteProtectedTraits, + ); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, WithCredentialsNoConfigAndAdminMetadataInJSON(*identity)) +} + +// Delete Identity Parameters +// +// swagger:parameters deleteIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type deleteIdentity struct { + // ID is the identity's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route DELETE /admin/identities/{id} identity deleteIdentity +// +// # Delete an Identity +// +// Calling this endpoint irrecoverably and permanently deletes the [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) given its ID. This action can not be undone. +// This endpoint returns 204 when the identity was deleted or 404 if the identity was not found. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 204: emptyResponse +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) delete(w http.ResponseWriter, r *http.Request) { + if err := h.r.PrivilegedIdentityPool().DeleteIdentity(r.Context(), x.ParseUUID(r.PathValue("id"))); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Patch Identity Parameters +// +// swagger:parameters patchIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type patchIdentity struct { + // ID must be set to the ID of identity you want to update + // + // required: true + // in: path + ID string `json:"id"` + + // in: body + Body openapix.JSONPatchDocument +} + +// patchOp is a minimal JSON Patch op used for /region pre-processing. +type patchOp struct { + Op string `json:"op"` + Path string `json:"path"` + From string `json:"from,omitempty"` + Value json.RawMessage `json:"value,omitempty"` +} + +// applyRegionPatchOps applies /region ops to ident.Region and returns the +// patch with those ops removed. Only add/replace are accepted — remove and +// other ops on /region return 400 (callers must always set a region +// explicitly). +func applyRegionPatchOps(body []byte, ident *Identity) ([]byte, error) { + var ops []patchOp + if err := json.Unmarshal(body, &ops); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest(). + WithReason("failed to unmarshal JSON patch"). + WithError(err.Error()). + WithWrap(err)) + } + rest := make([]patchOp, 0, len(ops)) + for _, op := range ops { + if op.Path != "/region" { + rest = append(rest, op) + continue + } + if op.Op != "add" && op.Op != "replace" { + return nil, errors.WithStack(herodot.ErrBadRequest(). + WithReasonf("operation %q is not supported on /region", op.Op)) + } + var v string + if err := json.Unmarshal(op.Value, &v); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest(). + WithReason("failed to unmarshal").WithWrap(err)) + } + r := region.Region(v) + if !r.Valid() { + return nil, region.NewErrInvalid() + } + ident.Region = r + } + if len(rest) == 0 { + return []byte("[]"), nil + } + return json.Marshal(rest) +} + +// swagger:route PATCH /admin/identities/{id} identity patchIdentity +// +// # Patch an Identity +// +// Partially updates an [identity's](https://www.ory.com/docs/kratos/concepts/identity-user-model) field using [JSON Patch](https://jsonpatch.com/). +// The fields `id`, `stateChangedAt` and `credentials` can not be updated using this method. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: identity +// 400: errorGeneric +// 404: errorGeneric +// 409: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) patch(w http.ResponseWriter, r *http.Request) { + requestBody, err := io.ReadAll(r.Body) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + id := x.ParseUUID(r.PathValue("id")) + ident, err := h.r.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // Apply /region ops to ident.Region and strip them from the patch. + // ApplyJSONPatch can't handle them directly because the field is + // omitempty and a "remove" on an absent key would error. + filteredPatch, err := applyRegionPatchOps(requestBody, ident) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + oldState := ident.State + + patchedIdentity, err := jsonx.ApplyJSONPatch(filteredPatch, WithCredentialsAndAdminMetadataInJSON(*ident), "/id", "/stateChangedAt", "/credentials", "/credentials/oidc", "/credentials/oidc/**", "/credentials/saml", "/credentials/saml/**") + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack( + herodot.ErrBadRequest(). + WithReason("failed to apply JSON patch"). + WithError(err.Error()). + WithWrap(err), + )) + return + } + + // Reject patches that desynchronize a credential's "type" field from + // its map key. The patch deny-list lets callers replace whole + // `/credentials/` subtrees or add new keys; a value without a + // "type" field would otherwise either fail deep inside the persister + // with a misleading "code bug" 500 (new key path) or silently + // overwrite the existing credential's config (replace path) — for + // password credentials this destroys `hashed_password`. + // IdentityManager.ValidateIdentity also enforces this as a safeguard, + // but the handler-level check rejects the request before the + // confidential-identity reload and gives a sharper error path. + if err := ValidateCredentialsIntegrity(patchedIdentity.Credentials); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // ApplyJSONPatch can't see omitempty-stripped fields; carry forward. + patchedIdentity.Region = ident.Region + + if oldState != patchedIdentity.State { + // Check if the changed state was actually valid + if err := patchedIdentity.State.IsValid(); err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack( + herodot.ErrBadRequest(). + WithReasonf("The supplied state ('%s') was not valid. Valid states are ('%s', '%s').", string(patchedIdentity.State), StateActive, StateInactive). + WithErrorf("%v", err). + WithWrap(err), + )) + return + } + + // If the state changed, we need to update the timestamp of it + stateChangedAt := sqlxx.NullTime(time.Now()) + patchedIdentity.StateChangedAt = &stateChangedAt + } + + updatedIdentity := Identity(patchedIdentity) + + if err := h.r.IdentityManager().Update( + r.Context(), + &updatedIdentity, + ManagerAllowWriteProtectedTraits, + ); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, WithCredentialsNoConfigAndAdminMetadataInJSON(updatedIdentity)) +} + +// Delete Credential Parameters +// +// swagger:parameters deleteIdentityCredentials +type _ struct { + // ID is the identity's ID. + // + // required: true + // in: path + ID string `json:"id"` + + // Type is the type of credentials to delete. + // + // required: true + // in: path + Type CredentialsType `json:"type"` + + // Identifier is the identifier of the credential to delete. It is required + // for the `oidc`, `saml`, and `deviceauthn` credential types: for `oidc` + // and `saml` it selects the provider link to remove, for `deviceauthn` it + // is the `client_key_id` of the device key to revoke. Find the identifier + // by calling the `GET /admin/identities/{id}?include_credential={type}` + // 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.com/docs/kratos/concepts/identity-user-model) credential by its type. +// You cannot delete passkeys or code auth credentials through this API. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // DeviceAuthn revokes a single key selected by the `identifier` query + // parameter (the key's client_key_id) rather than dropping the whole + // credential. Its read-modify-write runs under an exclusive row lock (see + // deleteDeviceAuthnKey), so it does not go through the shared in-memory + // read-then-update path below. + if CredentialsType(r.PathValue("type")) == CredentialsTypeDeviceAuthn { + if err := h.deleteDeviceAuthnKey(ctx, x.ParseUUID(r.PathValue("id")), r.URL.Query().Get("identifier")); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) + return + } + + identity, err := h.r.PrivilegedIdentityPool().GetIdentityConfidential(ctx, x.ParseUUID(r.PathValue("id"))) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + cred, ok := identity.GetCredentials(CredentialsType(r.PathValue("type"))) + if !ok { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a %s but this user have no %s set up.", r.PathValue("type"), r.PathValue("type")))) + return + } + + switch cred.Type { + case CredentialsTypeLookup, CredentialsTypeTOTP: + identity.DeleteCredentialsType(cred.Type) + case CredentialsTypeWebAuthn: + if err = identity.deleteCredentialWebAuthFromIdentity(); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + case CredentialsTypePassword, CredentialsTypeOIDC, CredentialsTypeSAML: + firstFactor, err := h.r.IdentityManager().CountActiveFirstFactorCredentials(ctx, identity) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + if firstFactor < 2 { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("You cannot remove the last first factor credential."))) + return + } + switch cred.Type { + case CredentialsTypePassword: + if err := identity.deleteCredentialPassword(); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + case CredentialsTypeOIDC, CredentialsTypeSAML: + if err := identity.deleteCredentialOIDCSAMLFromIdentity(cred.Type, r.URL.Query().Get("identifier")); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + default: + // A bunch of credential type deletions are not yet implemented, e.g. passkeys, etc. + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Credentials type %s cannot be deleted.", cred.Type))) + return + } + + if err := h.r.IdentityManager().Update( + ctx, + identity, + ManagerAllowWriteProtectedTraits, + ); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// deleteDeviceAuthnKey revokes the DeviceAuthn key whose client_key_id equals +// identifier. The read-modify-write of the credential config runs under the +// persister's exclusive row lock (SELECT ... FOR UPDATE), so concurrent +// revocations serialize and none is lost; the credential's identifier rows are +// synced to the surviving keys inside the same transaction. When the last key +// is removed the config is left empty (the credential row is not deleted), +// mirroring the DeviceAuthn settings flow. It returns 400 when identifier is +// empty and 404 when the identity has no DeviceAuthn credential or no key +// matches. +func (h *Handler) deleteDeviceAuthnKey(ctx context.Context, id uuid.UUID, identifier string) error { + if identifier == "" { + return errors.WithStack(herodot.ErrBadRequest().WithReason("You must provide an identifier to delete this credential.")) + } + + return h.r.PrivilegedIdentityPool().UpdateCredentialsConfig( + ctx, + id, + CredentialsTypeDeviceAuthn, + UpdateConfig(func(cfg *deviceauthn.CredentialsDeviceAuthnConfig) error { + before := len(cfg.Credentials) + cfg.Credentials = slices.DeleteFunc(cfg.Credentials, func(k deviceauthn.Key) bool { + return k.ClientKeyID == identifier + }) + if len(cfg.Credentials) == before { + return errors.WithStack(herodot.ErrNotFound().WithReasonf("The identifier `%s` was not found among deviceauthn credentials.", identifier)) + } + return nil + }), + // The identifier rows are derived state of the config: sync them to the + // surviving keys' client_key_ids under the same lock. + WithDerivedIdentifiers(func(newConfig []byte) ([]string, error) { + var cfg deviceauthn.CredentialsDeviceAuthnConfig + if err := json.Unmarshal(newConfig, &cfg); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("Unable to decode identity credentials.").WithDebug(err.Error())) + } + ids := make([]string, len(cfg.Credentials)) + for i, k := range cfg.Credentials { + ids[i] = k.ClientKeyID + } + return ids, nil + }), + ) +} diff --git a/identity/handler_import.go b/identity/handler_import.go new file mode 100644 index 0000000..1d8b2b5 --- /dev/null +++ b/identity/handler_import.go @@ -0,0 +1,340 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "context" + "encoding/json" + "slices" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/hash" +) + +func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *IdentityWithCredentials) error { + if creds == nil { + return nil + } + + // 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 + } + } + + if creds.OIDC != nil { + if err := h.importOIDCCredentials(ctx, i, creds.OIDC); err != nil { + return err + } + } + + if creds.SAML != nil { + if err := h.importSAMLCredentials(ctx, i, creds.SAML); err != nil { + return err + } + } + + if creds.WebAuthn != nil { + if err := h.importWebAuthnCredentials(ctx, i, creds.WebAuthn.Config.Credentials, creds.WebAuthn.Config.UserHandle); err != nil { + return err + } + } + + if creds.Passkey != nil { + if err := h.importPasskeyCredentials(ctx, i, creds.Passkey.Config.Credentials, creds.Passkey.Config.UserHandle); err != nil { + return err + } + } + + if creds.LookupSecret != nil { + if err := h.importLookupSecretCredentials(ctx, i, creds.LookupSecret); err != nil { + return err + } + } + + if creds.TOTP != nil { + if err := h.importTOTPCredentials(ctx, i, creds.TOTP); err != nil { + return err + } + } + + return nil +} + +func (h *Handler) importWebAuthnCredentials(_ context.Context, i *Identity, newCredentials CredentialsWebAuthn, userHandle []byte) error { + resultCredentials, previousUserHandle, previousIdentifiers, err := mergeWebAuthnCredentials(i, CredentialsTypeWebAuthn, newCredentials) + if err != nil { + return err + } + + if len(userHandle) == 0 { + userHandle = previousUserHandle + } + + return i.SetCredentialsWithConfig( + CredentialsTypeWebAuthn, + Credentials{ + Identifiers: previousIdentifiers, + }, + CredentialsWebAuthnConfig{ + Credentials: resultCredentials, + UserHandle: userHandle, + }, + ) +} + +// mergeWebAuthnCredentials merges existing WebAuthn credentials with new ones. +// It updates credentials with the same ID and appends new ones. +// Returns the merged credentials, previous user handle, previous identifiers, and any error. +func mergeWebAuthnCredentials( + i *Identity, + credentialType CredentialsType, + newCredentials CredentialsWebAuthn, +) (CredentialsWebAuthn, []byte, []string, error) { + var previousUserHandle []byte + var previousIdentifiers []string + + resultCredentials := newCredentials + + if c, ok := i.GetCredentials(credentialType); ok { + var original CredentialsWebAuthnConfig + if err := json.Unmarshal(c.Config, &original); err != nil { + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) + } + + resultCredentials = make(CredentialsWebAuthn, len(original.Credentials)) + copy(resultCredentials, original.Credentials) + + for _, updated := range newCredentials { + var found bool + for k, originalCredential := range original.Credentials { + if bytes.Equal(originalCredential.ID, updated.ID) { + resultCredentials[k] = updated + found = true + break + } + } + if !found { + resultCredentials = append(resultCredentials, updated) + } + } + + previousUserHandle = original.UserHandle + previousIdentifiers = c.Identifiers + } + + return resultCredentials, previousUserHandle, previousIdentifiers, nil +} + +func (h *Handler) importPasskeyCredentials(_ context.Context, i *Identity, newCredentials CredentialsWebAuthn, userHandle []byte) error { + resultCredentials, previousUserHandle, previousIdentifiers, err := mergeWebAuthnCredentials(i, CredentialsTypePasskey, newCredentials) + if err != nil { + return err + } + + identifiers := previousIdentifiers + if len(userHandle) == 0 { + userHandle = previousUserHandle + } else if !slices.Contains(identifiers, string(userHandle)) { + identifiers = append(identifiers, string(userHandle)) + } + + // The passkey login flow looks the credential up by joining + // identity_credential_identifiers on the userHandle returned by the + // authenticator during the WebAuthn assertion (see + // selfservice/strategy/passkey/passkey_login.go). The userHandle is + // generated at registration time and bound to the physical authenticator, + // so the server cannot reconstruct or default it: an import has to + // supply it, or inherit it from a previously persisted passkey + // credential. Without a userHandle the credential row would be + // non-functional — no assertion can ever resolve to it. Reject the + // import early instead of silently storing a broken credential. See + // https://github.com/ory/kratos/issues/4561. + if len(userHandle) == 0 { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "Passkey credential import requires a non-empty user_handle. " + + "It must match the WebAuthn user handle stored on the authenticator.")) + } + + if len(resultCredentials) == 0 { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "Passkey credential import requires at least one entry in credentials.passkey.config.credentials.")) + } + + return i.SetCredentialsWithConfig( + CredentialsTypePasskey, + Credentials{ + Identifiers: identifiers, + }, + CredentialsWebAuthnConfig{ + Credentials: resultCredentials, + UserHandle: userHandle, + }, + ) +} + +func (h *Handler) importLookupSecretCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsLookupSecret) error { + var codes []RecoveryCode + if c, ok := i.GetCredentials(CredentialsTypeLookup); ok { + var target CredentialsLookupConfig + if err := json.Unmarshal(c.Config, &target); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) + } + codes = target.RecoveryCodes + } + return i.SetCredentialsWithConfig( + CredentialsTypeLookup, + Credentials{}, + CredentialsLookupConfig{ + RecoveryCodes: append(codes, creds.Config.Codes...), + }, + ) +} + +func (h *Handler) importTOTPCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsTOTP) error { + // The TOTP login flow resolves the credential by joining + // identity_credential_identifiers, using the identity ID as the identifier + // (see selfservice/strategy/totp/login.go and settings.go). On imports of + // new identities the identity ID is not yet known here, so we leave + // Identifiers empty and let the persister default it from the persisted + // identity ID. See https://github.com/ory/kratos/issues/4561. + return i.SetCredentialsWithConfig( + CredentialsTypeTOTP, + Credentials{}, + CredentialsTOTPConfig{TOTPURL: creds.Config.TOTPURL}, + ) +} + +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) + if len(creds.Config.Password) > 0 { + // Importing a clear text password + hashed, err = h.r.Hasher(ctx).Generate(ctx, []byte(creds.Config.Password)) + if err != nil { + return err + } + + creds.Config.HashedPassword = string(hashed) + } + + if !(hash.IsValidHashFormat(hashed)) { + return errors.WithStack(herodot.ErrBadRequest().WithReason("The imported password does not match any known hash format.")) + } + + if err := hash.ValidateImportedHash(hashed); err != nil { + if errors.Is(err, hash.ErrHashParametersOutOfBounds) { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason( + "The imported password hash declares cost parameters that exceed the bounds enforced by Ory. " + + "Re-hash the password with a more conservative configuration before importing.")) + } + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason( + "The imported password hash could not be parsed.")) + } + + return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{HashedPassword: string(hashed)}) +} + +func (h *Handler) importOIDCCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsOIDC) error { + var target CredentialsOIDC + c, ok := i.GetCredentials(CredentialsTypeOIDC) + if !ok { + var providers []CredentialsOIDCProvider + var ids []string + for _, p := range creds.Config.Providers { + ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) + provider := CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + UseAutoLink: p.UseAutoLink, + } + if p.Organization.Valid { + provider.Organization = p.Organization.UUID.String() + } + providers = append(providers, provider) + } + + return i.SetCredentialsWithConfig( + CredentialsTypeOIDC, + Credentials{Identifiers: ids}, + CredentialsOIDC{Providers: providers}, + ) + } + + if err := json.Unmarshal(c.Config, &target); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) + } + + for _, p := range creds.Config.Providers { + c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) + provider := CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + UseAutoLink: p.UseAutoLink, + } + if p.Organization.Valid { + provider.Organization = p.Organization.UUID.String() + } + target.Providers = append(target.Providers, provider) + } + return i.SetCredentialsWithConfig(CredentialsTypeOIDC, *c, &target) +} + +func (h *Handler) importSAMLCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsSAML) error { + var target CredentialsOIDC + c, ok := i.GetCredentials(CredentialsTypeSAML) + if !ok { + var providers []CredentialsOIDCProvider + var ids []string + for _, p := range creds.Config.Providers { + ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) + provider := CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + } + if p.Organization.Valid { + provider.Organization = p.Organization.UUID.String() + } + providers = append(providers, provider) + } + + return i.SetCredentialsWithConfig( + CredentialsTypeSAML, + Credentials{Identifiers: ids}, + CredentialsOIDC{Providers: providers}, + ) + } + + if err := json.Unmarshal(c.Config, &target); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) + } + + for _, p := range creds.Config.Providers { + c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) + provider := CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + } + if p.Organization.Valid { + provider.Organization = p.Organization.UUID.String() + } + target.Providers = append(target.Providers, provider) + } + return i.SetCredentialsWithConfig(CredentialsTypeSAML, *c, &target) +} diff --git a/identity/handler_import_registry_test.go b/identity/handler_import_registry_test.go new file mode 100644 index 0000000..f79e27a --- /dev/null +++ b/identity/handler_import_registry_test.go @@ -0,0 +1,144 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/x/snapshotx" +) + +func TestImportCredentialsPassword(t *testing.T) { + t.Parallel() + conf, reg := pkg.NewFastRegistryWithMocks(t) + ctx := context.Background() + + // Create the identity handler + h := reg.IdentityHandler() + + testCases := []struct { + name string + setupIdentity func() *identity.Identity + credentials *identity.AdminIdentityImportCredentialsPassword + verify func(t *testing.T, i *identity.Identity) + }{ + { + name: "import clear text password", + setupIdentity: func() *identity.Identity { + return identity.NewIdentity(conf.DefaultIdentityTraitsSchemaID(ctx)) + }, + credentials: &identity.AdminIdentityImportCredentialsPassword{ + Config: identity.AdminIdentityImportCredentialsPasswordConfig{ + Password: "password123", + }, + }, + verify: func(t *testing.T, i *identity.Identity) { + creds, ok := i.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok, "password credentials should be set") + + var config identity.CredentialsPassword + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.NotEmpty(t, config.HashedPassword) + assert.False(t, config.UsePasswordMigrationHook) + }, + }, + { + name: "import hashed password", + setupIdentity: func() *identity.Identity { + return identity.NewIdentity(conf.DefaultIdentityTraitsSchemaID(ctx)) + }, + credentials: &identity.AdminIdentityImportCredentialsPassword{ + Config: identity.AdminIdentityImportCredentialsPasswordConfig{ + HashedPassword: "$2a$10$JCU0ELjU1TCnFbV2jLi7huEQjVWZG0HzXQq/BWZyO30XR6DJxwN72", + }, + }, + verify: func(t *testing.T, i *identity.Identity) { + creds, ok := i.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok, "password credentials should be set") + + var config identity.CredentialsPassword + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Equal(t, "$2a$10$JCU0ELjU1TCnFbV2jLi7huEQjVWZG0HzXQq/BWZyO30XR6DJxwN72", config.HashedPassword) + assert.False(t, config.UsePasswordMigrationHook) + }, + }, + { + name: "import with password migration hook", + setupIdentity: func() *identity.Identity { + return identity.NewIdentity(conf.DefaultIdentityTraitsSchemaID(ctx)) + }, + credentials: &identity.AdminIdentityImportCredentialsPassword{ + Config: identity.AdminIdentityImportCredentialsPasswordConfig{ + UsePasswordMigrationHook: true, + }, + }, + verify: func(t *testing.T, i *identity.Identity) { + creds, ok := i.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok, "password credentials should be set") + + var config identity.CredentialsPassword + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.True(t, config.UsePasswordMigrationHook) + assert.Empty(t, config.HashedPassword) + }, + }, + { + name: "update existing password credential", + setupIdentity: func() *identity.Identity { + i := identity.NewIdentity(conf.DefaultIdentityTraitsSchemaID(ctx)) + _ = i.SetCredentialsWithConfig( + identity.CredentialsTypePassword, + identity.Credentials{}, + identity.CredentialsPassword{ + HashedPassword: "old-hash", + }, + ) + return i + }, + credentials: &identity.AdminIdentityImportCredentialsPassword{ + Config: identity.AdminIdentityImportCredentialsPasswordConfig{ + Password: "new-password", + }, + }, + verify: func(t *testing.T, i *identity.Identity) { + creds, ok := i.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok, "password credentials should be set") + + var config identity.CredentialsPassword + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.NotEmpty(t, config.HashedPassword) + assert.NotEqual(t, "old-hash", config.HashedPassword) + assert.False(t, config.UsePasswordMigrationHook) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set up a fresh identity for each test + i := tc.setupIdentity() + + // Perform the import + err := h.ImportPasswordCredentials(ctx, i, tc.credentials) + require.NoError(t, err) + + // Verify credential was set correctly + tc.verify(t, i) + + // Take a snapshot of the credentials + snapshotx.SnapshotT(t, i.Credentials, snapshotx.ExceptPaths("password.config.hashed_password")) + }) + } +} diff --git a/identity/handler_import_test.go b/identity/handler_import_test.go new file mode 100644 index 0000000..c3b4681 --- /dev/null +++ b/identity/handler_import_test.go @@ -0,0 +1,898 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/x/snapshotx" +) + +func TestImportCredentialsOIDCSAML(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Setup handler with minimal mock requirements + h := &Handler{} + + testCases := []struct { + name string + setupIdentity func() *Identity + credentials interface{} + credType CredentialsType + }{ + { + name: "OIDC new credential without organization", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsOIDC{ + Config: AdminIdentityImportCredentialsOIDCConfig{ + Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{ + { + Provider: "github", + Subject: "12345", + }, + }, + }, + }, + credType: CredentialsTypeOIDC, + }, + { + name: "OIDC new credential with organization", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsOIDC{ + Config: AdminIdentityImportCredentialsOIDCConfig{ + Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{ + { + Provider: "github", + Subject: "12345", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + }, + }, + }, + credType: CredentialsTypeOIDC, + }, + { + name: "OIDC update credential without organization", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeOIDC, + Credentials{ + Identifiers: []string{OIDCUniqueID("google", "67890")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "google", + Subject: "67890", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsOIDC{ + Config: AdminIdentityImportCredentialsOIDCConfig{ + Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{ + { + Provider: "github", + Subject: "12345", + }, + }, + }, + }, + credType: CredentialsTypeOIDC, + }, + { + name: "OIDC update credential with organization", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeOIDC, + Credentials{ + Identifiers: []string{OIDCUniqueID("google", "67890")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "google", + Subject: "67890", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsOIDC{ + Config: AdminIdentityImportCredentialsOIDCConfig{ + Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{ + { + Provider: "github", + Subject: "12345", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + }, + }, + }, + credType: CredentialsTypeOIDC, + }, + { + name: "OIDC update with multiple providers", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeOIDC, + Credentials{ + Identifiers: []string{OIDCUniqueID("google", "67890")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "google", + Subject: "67890", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsOIDC{ + Config: AdminIdentityImportCredentialsOIDCConfig{ + Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{ + { + Provider: "github", + Subject: "12345", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + { + Provider: "gitlab", + Subject: "abcdef", + }, + }, + }, + }, + credType: CredentialsTypeOIDC, + }, + { + name: "SAML new credential without organization", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsSAML{ + Config: AdminIdentityImportCredentialsSAMLConfig{ + Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{ + { + Provider: "okta", + Subject: "user123", + }, + }, + }, + }, + credType: CredentialsTypeSAML, + }, + { + name: "SAML new credential with organization", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsSAML{ + Config: AdminIdentityImportCredentialsSAMLConfig{ + Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{ + { + Provider: "okta", + Subject: "user123", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + }, + }, + }, + credType: CredentialsTypeSAML, + }, + { + name: "SAML update credential without organization", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeSAML, + Credentials{ + Identifiers: []string{OIDCUniqueID("onelogin", "user456")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "onelogin", + Subject: "user456", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsSAML{ + Config: AdminIdentityImportCredentialsSAMLConfig{ + Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{ + { + Provider: "okta", + Subject: "user123", + }, + }, + }, + }, + credType: CredentialsTypeSAML, + }, + { + name: "SAML update credential with organization", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeSAML, + Credentials{ + Identifiers: []string{OIDCUniqueID("onelogin", "user456")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "onelogin", + Subject: "user456", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsSAML{ + Config: AdminIdentityImportCredentialsSAMLConfig{ + Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{ + { + Provider: "okta", + Subject: "user123", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + }, + }, + }, + credType: CredentialsTypeSAML, + }, + { + name: "SAML update with multiple providers", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeSAML, + Credentials{ + Identifiers: []string{OIDCUniqueID("onelogin", "user456")}, + }, + CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Provider: "onelogin", + Subject: "user456", + }, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsSAML{ + Config: AdminIdentityImportCredentialsSAMLConfig{ + Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{ + { + Provider: "okta", + Subject: "user123", + Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true}, + }, + { + Provider: "auth0", + Subject: "user789", + }, + }, + }, + }, + credType: CredentialsTypeSAML, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set up a fresh identity for each test + i := tc.setupIdentity() + var err error + + // Perform the import based on a credential type + switch tc.credType { + case CredentialsTypeOIDC: + err = h.importOIDCCredentials(ctx, i, tc.credentials.(*AdminIdentityImportCredentialsOIDC)) + case CredentialsTypeSAML: + err = h.importSAMLCredentials(ctx, i, tc.credentials.(*AdminIdentityImportCredentialsSAML)) + } + + require.NoError(t, err) + + // Verify credential was set correctly + creds, ok := i.GetCredentials(tc.credType) + require.True(t, ok, "credentials should be set") + + // Verify the credentials contain proper identifiers and config + assert.NotEmpty(t, creds.Identifiers) + assert.NotEmpty(t, creds.Config) + + // Take a snapshot of the credentials + snapshotx.SnapshotT(t, creds) + + // Additional checks based on credential type + switch tc.credType { + case CredentialsTypeOIDC: + oidcCreds := tc.credentials.(*AdminIdentityImportCredentialsOIDC) + for _, p := range oidcCreds.Config.Providers { + id := OIDCUniqueID(p.Provider, p.Subject) + assert.Contains(t, creds.Identifiers, id) + } + case CredentialsTypeSAML: + samlCreds := tc.credentials.(*AdminIdentityImportCredentialsSAML) + for _, p := range samlCreds.Config.Providers { + id := OIDCUniqueID(p.Provider, p.Subject) + assert.Contains(t, creds.Identifiers, id) + } + } + }) + } +} + +func TestImportLookupSecretCredentials(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Setup handler with minimal mock requirements + h := &Handler{} + + testCases := []struct { + name string + setupIdentity func() *Identity + credentials *AdminIdentityImportCredentialsLookupSecret + expectedCodes int + }{ + { + name: "new lookup secret credential", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsLookupSecret{ + Config: AdminIdentityImportCredentialsLookupSecretConfig{ + Codes: []RecoveryCode{ + {Code: "code1"}, + {Code: "code2"}, + }, + }, + }, + expectedCodes: 2, + }, + { + name: "update existing lookup secret credential", + setupIdentity: func() *Identity { + i := &Identity{} + _ = i.SetCredentialsWithConfig( + CredentialsTypeLookup, + Credentials{}, + CredentialsLookupConfig{ + RecoveryCodes: []RecoveryCode{ + {Code: "existing-code"}, + }, + }, + ) + return i + }, + credentials: &AdminIdentityImportCredentialsLookupSecret{ + Config: AdminIdentityImportCredentialsLookupSecretConfig{ + Codes: []RecoveryCode{ + {Code: "new-code1"}, + {Code: "new-code2"}, + }, + }, + }, + expectedCodes: 3, // 1 existing + 2 new + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set up a fresh identity for each test + i := tc.setupIdentity() + + // Perform the import + err := h.importLookupSecretCredentials(ctx, i, tc.credentials) + require.NoError(t, err) + + // Verify credential was set correctly + creds, ok := i.GetCredentials(CredentialsTypeLookup) + require.True(t, ok, "credentials should be set") + + // Parse the config to check the recovery codes + var config CredentialsLookupConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + // Verify the expected number of codes + assert.Len(t, config.RecoveryCodes, tc.expectedCodes) + + // Take a snapshot of the credentials + snapshotx.SnapshotT(t, creds) + + // Verify specific codes based on a test case + if tc.name == "new lookup secret credential" { + assert.Contains(t, getCodeValues(config.RecoveryCodes), "code1") + assert.Contains(t, getCodeValues(config.RecoveryCodes), "code2") + } else { + assert.Contains(t, getCodeValues(config.RecoveryCodes), "existing-code") + assert.Contains(t, getCodeValues(config.RecoveryCodes), "new-code1") + assert.Contains(t, getCodeValues(config.RecoveryCodes), "new-code2") + } + }) + } +} + +func TestImportPasskeyCredentials(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Setup handler + h := &Handler{} + + // Create test credentials + cred1 := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1")} + cred2 := CredentialWebAuthn{ID: []byte("cred2"), PublicKey: []byte("pk2")} + cred1Updated := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1-updated")} + cred3 := CredentialWebAuthn{ID: []byte("cred3"), PublicKey: []byte("pk3")} + + testCases := []struct { + name string + setupIdentity func() *Identity + credentials CredentialsWebAuthn + userHandle []byte + verify func(t *testing.T, i *Identity) + }{ + { + name: "new passkey credentials", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: CredentialsWebAuthn{cred1, cred2}, + userHandle: []byte("user1"), + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypePasskey) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Len(t, config.Credentials, 2) + assert.Equal(t, []byte("user1"), config.UserHandle) + assert.Contains(t, creds.Identifiers, "user1") + }, + }, + { + name: "update existing credentials - add new, update existing", + setupIdentity: func() *Identity { + i := &Identity{} + err := i.SetCredentialsWithConfig( + CredentialsTypePasskey, + Credentials{ + Identifiers: []string{"existingUser"}, + }, + CredentialsWebAuthnConfig{ + Credentials: CredentialsWebAuthn{cred1, cred2}, + UserHandle: []byte("existingUser"), + }, + ) + require.NoError(t, err) + return i + }, + credentials: CredentialsWebAuthn{cred1Updated, cred3}, + userHandle: []byte{}, // Empty to test reusing existing + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypePasskey) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + // Should have updated cred1, kept cred2, and added cred3 + assert.Len(t, config.Credentials, 3) + assert.Equal(t, []byte("existingUser"), config.UserHandle) + + // Find the updated credential + var found bool + for _, c := range config.Credentials { + if bytes.Equal(c.ID, []byte("cred1")) { + assert.Equal(t, []byte("pk1-updated"), c.PublicKey) + found = true + break + } + } + assert.True(t, found, "Updated credential should be present") + + // Verify cred3 was added + var foundNew bool + for _, c := range config.Credentials { + if bytes.Equal(c.ID, []byte("cred3")) { + foundNew = true + break + } + } + assert.True(t, foundNew, "New credential should be added") + }, + }, + { + name: "new user handle added to identifiers", + setupIdentity: func() *Identity { + i := &Identity{} + err := i.SetCredentialsWithConfig( + CredentialsTypePasskey, + Credentials{ + Identifiers: []string{"existingUser"}, + }, + CredentialsWebAuthnConfig{ + Credentials: CredentialsWebAuthn{cred1}, + UserHandle: []byte("existingUser"), + }, + ) + require.NoError(t, err) + return i + }, + credentials: CredentialsWebAuthn{cred2}, + userHandle: []byte("newUser"), + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypePasskey) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Equal(t, []byte("newUser"), config.UserHandle) + assert.Contains(t, creds.Identifiers, "existingUser") + assert.Contains(t, creds.Identifiers, "newUser") + assert.Len(t, creds.Identifiers, 2) + assert.Len(t, config.Credentials, 2) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + i := tc.setupIdentity() + err := h.importPasskeyCredentials(ctx, i, tc.credentials, tc.userHandle) + require.NoError(t, err) + tc.verify(t, i) + snapshotx.SnapshotT(t, i.Credentials) + }) + } + + // Negative cases — input that would store a non-functional credential + // must be rejected up front. See + // https://github.com/ory/kratos/issues/4561. + // + // herodot wraps the user-facing detail in Reason; err.Error() returns + // the canonical status message ("The request was malformed or + // contained invalid parameters"), so unwrap to a *herodot.DefaultError + // and assert against Reason() instead. + requireBadRequestReason := func(t *testing.T, err error, mustContain string) { + t.Helper() + require.Error(t, err) + var herr *herodot.DefaultError + require.True(t, errors.As(err, &herr), "expected a *herodot.DefaultError, got %T: %v", err, err) + assert.Equal(t, herodot.ErrBadRequest().StatusCode(), herr.StatusCode()) + assert.Contains(t, herr.Reason(), mustContain) + } + + t.Run("case=rejects import without user_handle on a new credential", func(t *testing.T) { + i := &Identity{} + err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{cred1}, nil) + requireBadRequestReason(t, err, "user_handle") + _, ok := i.GetCredentials(CredentialsTypePasskey) + assert.False(t, ok, "no passkey credential should be persisted on the identity") + }) + + t.Run("case=rejects import without user_handle when previous credential also lacks one", func(t *testing.T) { + i := &Identity{} + require.NoError(t, i.SetCredentialsWithConfig( + CredentialsTypePasskey, + Credentials{}, + CredentialsWebAuthnConfig{Credentials: CredentialsWebAuthn{cred1}}, + )) + err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{cred2}, nil) + requireBadRequestReason(t, err, "user_handle") + }) + + t.Run("case=rejects import without any credentials", func(t *testing.T) { + i := &Identity{} + err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{}, []byte("user1")) + requireBadRequestReason(t, err, "credentials") + }) +} + +func TestImportWebAuthnCredentials(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Setup handler + h := &Handler{} + + // Create test credentials + cred1 := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1")} + cred2 := CredentialWebAuthn{ID: []byte("cred2"), PublicKey: []byte("pk2")} + cred1Updated := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1-updated")} + cred3 := CredentialWebAuthn{ID: []byte("cred3"), PublicKey: []byte("pk3")} + + testCases := []struct { + name string + setupIdentity func() *Identity + credentials CredentialsWebAuthn + userHandle []byte + verify func(t *testing.T, i *Identity) + }{ + { + name: "new webauthn credentials", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: CredentialsWebAuthn{cred1, cred2}, + userHandle: []byte("user1"), + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypeWebAuthn) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Len(t, config.Credentials, 2) + assert.Equal(t, []byte("user1"), config.UserHandle) + }, + }, + { + name: "update existing credentials - add new, update existing", + setupIdentity: func() *Identity { + i := &Identity{} + err := i.SetCredentialsWithConfig( + CredentialsTypeWebAuthn, + Credentials{}, + CredentialsWebAuthnConfig{ + Credentials: CredentialsWebAuthn{cred1, cred2}, + UserHandle: []byte("existingUser"), + }, + ) + require.NoError(t, err) + return i + }, + credentials: CredentialsWebAuthn{cred1Updated, cred3}, + userHandle: []byte{}, // Empty to test reusing existing + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypeWebAuthn) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + // Should have updated cred1, kept cred2, and added cred3 + assert.Len(t, config.Credentials, 3) + assert.Equal(t, []byte("existingUser"), config.UserHandle) + + // Find the updated credential + var found bool + for _, c := range config.Credentials { + if bytes.Equal(c.ID, []byte("cred1")) { + assert.Equal(t, []byte("pk1-updated"), c.PublicKey) + found = true + break + } + } + assert.True(t, found, "Updated credential should be present") + + // Verify cred3 was added + var foundNew bool + for _, c := range config.Credentials { + if bytes.Equal(c.ID, []byte("cred3")) { + foundNew = true + break + } + } + assert.True(t, foundNew, "New credential should be added") + }, + }, + { + name: "override existing user handle", + setupIdentity: func() *Identity { + i := &Identity{} + err := i.SetCredentialsWithConfig( + CredentialsTypeWebAuthn, + Credentials{}, + CredentialsWebAuthnConfig{ + Credentials: CredentialsWebAuthn{cred1}, + UserHandle: []byte("existingUser"), + }, + ) + require.NoError(t, err) + return i + }, + credentials: CredentialsWebAuthn{cred2}, + userHandle: []byte("newUser"), + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypeWebAuthn) + require.True(t, ok) + + var config CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Equal(t, []byte("newUser"), config.UserHandle) + assert.Len(t, config.Credentials, 2) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + i := tc.setupIdentity() + err := h.importWebAuthnCredentials(ctx, i, tc.credentials, tc.userHandle) + require.NoError(t, err) + tc.verify(t, i) + snapshotx.SnapshotT(t, i.Credentials) + }) + } +} + +func TestImportTOTPCredentials(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Setup handler + h := &Handler{} + + testCases := []struct { + name string + setupIdentity func() *Identity + credentials *AdminIdentityImportCredentialsTOTP + verify func(t *testing.T, i *Identity) + }{ + { + name: "new totp credential", + setupIdentity: func() *Identity { + return &Identity{} + }, + credentials: &AdminIdentityImportCredentialsTOTP{ + Config: AdminIdentityImportCredentialsTOTPConfig{ + TOTPURL: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", + }, + }, + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypeTOTP) + require.True(t, ok) + + var config CredentialsTOTPConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + assert.Equal(t, "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", config.TOTPURL) + // The importer leaves Identifiers empty because the identity ID + // is not yet known. The persister defaults it to the persisted + // identity ID at credential-create time. See + // https://github.com/ory/kratos/issues/4561. + assert.Empty(t, creds.Identifiers) + }, + }, + { + name: "update existing totp credential", + setupIdentity: func() *Identity { + i := &Identity{} + err := i.SetCredentialsWithConfig( + CredentialsTypeTOTP, + Credentials{}, + CredentialsTOTPConfig{ + TOTPURL: "otpauth://totp/Example:alice@example.com?secret=OLDSECRET&issuer=Example", + }, + ) + require.NoError(t, err) + return i + }, + credentials: &AdminIdentityImportCredentialsTOTP{ + Config: AdminIdentityImportCredentialsTOTPConfig{ + TOTPURL: "otpauth://totp/Example:alice@example.com?secret=NEWSECRET&issuer=Example", + }, + }, + verify: func(t *testing.T, i *Identity) { + creds, ok := i.GetCredentials(CredentialsTypeTOTP) + require.True(t, ok) + + var config CredentialsTOTPConfig + require.NoError(t, json.Unmarshal(creds.Config, &config)) + + // Should have the new TOTP URL + assert.Equal(t, "otpauth://totp/Example:alice@example.com?secret=NEWSECRET&issuer=Example", config.TOTPURL) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + i := tc.setupIdentity() + err := h.importTOTPCredentials(ctx, i, tc.credentials) + require.NoError(t, err) + tc.verify(t, i) + snapshotx.SnapshotT(t, i.Credentials) + }) + } +} + +// Helper function to extract code values for easier assertions +func getCodeValues(codes []RecoveryCode) []string { + var values []string + for _, code := range codes { + values = append(values, code.Code) + } + return values +} + +// TestBatchImport_RegionWireThrough verifies that identityFromCreateIdentityBody +// correctly propagates the Region field from CreateIdentityBody to the resulting +// Identity for each entry in a batch import. This ensures the multi-region +// persister receives identities with the correct region so it can route INSERTs +// to the right CockroachDB region. +func TestBatchImport_RegionWireThrough(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // A minimal handler is sufficient: importCredentials is a no-op when + // Credentials is nil, so no registry wiring is needed. + h := &Handler{} + + testCases := []struct { + name string + body *CreateIdentityBody + expectedRegion string + }{ + { + name: "region eu-central is propagated", + body: &CreateIdentityBody{ + SchemaID: "default", + Traits: json.RawMessage(`{"bar":"baz"}`), + Region: "eu-central", + }, + expectedRegion: "eu-central", + }, + { + name: "region us-west is propagated", + body: &CreateIdentityBody{ + SchemaID: "default", + Traits: json.RawMessage(`{"bar":"baz"}`), + Region: "us-west", + }, + expectedRegion: "us-west", + }, + { + name: "empty region is preserved as empty", + body: &CreateIdentityBody{ + SchemaID: "default", + Traits: json.RawMessage(`{"bar":"baz"}`), + }, + expectedRegion: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ident, err := h.identityFromCreateIdentityBody(ctx, tc.body) + require.NoError(t, err) + assert.Equal(t, tc.expectedRegion, string(ident.Region)) + }) + } +} diff --git a/identity/identity.go b/identity/identity.go new file mode 100644 index 0000000..448f5eb --- /dev/null +++ b/identity/identity.go @@ -0,0 +1,757 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + "database/sql/driver" + "encoding/json" + "fmt" + "slices" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/tidwall/gjson" + "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/region" + "github.com/ory/x/sqlxx" +) + +// An Identity's State +// +// The state can either be `active` or `inactive`. +// +// swagger:enum State +type State string + +const ( + StateActive State = "active" + StateInactive State = "inactive" +) + +func (lt State) IsValid() error { + switch lt { + case StateActive, StateInactive: + return nil + } + return errors.New("identity state is not valid") +} + +// Identity represents an Ory Kratos identity +// +// An [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) represents a (human) user in Ory. +// +// swagger:model identity +type Identity struct { + // ID is the identity's unique identifier. + // + // The Identity ID can not be changed and can not be chosen. This ensures future + // compatibility and optimization for distributed stores such as CockroachDB. + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id"` + + // ExternalID is an optional external ID of the identity. This is used to link + // the identity to an external system. If set, the external ID must be unique + // across all identities. + // + // required: false + ExternalID sqlxx.NullString `json:"external_id,omitempty" faker:"-" db:"external_id"` + + // Credentials represents all credentials that can be used for authenticating this identity. + Credentials map[CredentialsType]Credentials `json:"credentials,omitempty" faker:"-" db:"-"` + + // 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:"-"` + + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + // + // required: true + SchemaID string `json:"schema_id" faker:"-" db:"schema_id"` + + // SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from. + // + // format: url + // required: true + SchemaURL string `json:"schema_url" faker:"-" db:"-"` + + // State is the identity's state. + // + // This value has currently no effect. + State State `json:"state" faker:"-" db:"state"` + + // StateChangedAt contains the last time when the identity's state changed. + StateChangedAt *sqlxx.NullTime `json:"state_changed_at,omitempty" faker:"-" db:"state_changed_at"` + + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits + // in a self-service manner. The input will always be validated against the JSON Schema defined + // in `schema_url`. + // + // required: true + Traits Traits `json:"traits" faker:"-" db:"traits"` + + // VerifiableAddresses contains all the addresses that can be verified by the user. + // + // Extensions: + // --- + // x-omitempty: true + // --- + VerifiableAddresses []VerifiableAddress `json:"verifiable_addresses,omitempty" faker:"-" has_many:"identity_verifiable_addresses" fk_id:"identity_id" order_by:"id asc"` + + // RecoveryAddresses contains all the addresses that can be used to recover an identity. + // + // Extensions: + // --- + // x-omitempty: true + // --- + RecoveryAddresses []RecoveryAddress `json:"recovery_addresses,omitempty" faker:"-" has_many:"identity_recovery_addresses" fk_id:"identity_id" order_by:"id asc"` + + // Store metadata about the identity which the identity itself can see when calling for example the + // session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic sqlxx.NullJSONRawMessage `json:"metadata_public" faker:"-" db:"metadata_public"` + + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin sqlxx.NullJSONRawMessage `json:"metadata_admin,omitempty" faker:"-" db:"metadata_admin"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"` + + // Region is the Ory Network region this identity is homed in. Set by + // the multi-region persister; empty on OSS and single-region deployments. + Region region.Region `json:"region,omitempty" db:"-"` +} + +func (i *Identity) PageToken() keysetpagination.PageToken { + return keysetpagination.StringPageToken(i.ID.String()) +} + +func DefaultPageToken() keysetpagination.PageToken { + return keysetpagination.StringPageToken(uuid.Nil.String()) +} + +// Traits represent an identity's traits. The identity is able to create, modify, and delete traits +// in a self-service manner. The input will always be validated against the JSON Schema defined +// in `schema_url`. +// +// swagger:model identityTraits +type Traits json.RawMessage + +func (t *Traits) Scan(value any) error { + return sqlxx.JSONScan(t, value) +} + +func (t Traits) Value() (driver.Value, error) { + return string(t), nil +} + +func (t *Traits) String() string { + return string(*t) +} + +// MarshalJSON returns m as the JSON encoding of m. +func (t Traits) MarshalJSON() ([]byte, error) { + if t == nil { + return []byte("null"), nil + } + return t, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (t *Traits) UnmarshalJSON(data []byte) error { + if t == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *t = append((*t)[0:0], data...) + return nil +} + +func (i Identity) TableName(context.Context) string { + return "identities" +} + +func (i *Identity) IsActive() bool { + return i.State == StateActive +} + +func (i *Identity) SetCredentials(t CredentialsType, c Credentials) { + if i.Credentials == nil { + i.Credentials = make(map[CredentialsType]Credentials) + } + + c.Type = t + c.IdentityID = i.ID + i.Credentials[t] = c +} + +func (i *Identity) SetCredentialsWithConfig(t CredentialsType, c Credentials, conf any) (err error) { + if i.Credentials == nil { + i.Credentials = make(map[CredentialsType]Credentials) + } + + c.Config, err = json.Marshal(conf) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode %s credentials to JSON: %s", t, err)) + } + + c.Type = t + i.Credentials[t] = c + return nil +} + +func (i *Identity) DeleteCredentialsType(t CredentialsType) { + if i.Credentials == nil { + return + } + + delete(i.Credentials, t) +} + +// 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 fallback + } + return c +} + +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 + c.Version = version + + i.SetCredentials(t, *c) +} + +func (i *Identity) GetCredentials(t CredentialsType) (*Credentials, bool) { + if c, ok := i.Credentials[t]; ok { + return &c, true + } + + return nil, false +} + +func (i *Identity) ParseCredentials(t CredentialsType, config any) (*Credentials, error) { + if c, ok := i.Credentials[t]; ok { + if err := json.Unmarshal(c.Config, config); err != nil { + return nil, errors.WithStack(err) + } + return &c, nil + } + + return nil, herodot.ErrNotFound().WithReasonf("identity does not have credential type %s", t) +} + +func (i *Identity) CopyWithoutCredentials() *Identity { + ii := *i + ii.Credentials = nil + return &ii +} + +// MergeOIDCCredentials merges the new credentials into the existing credentials. +// If the provider already exists, the provider is replace and the identifier is +// updated. This function requires the new credentials to have exactly one +// provider and one identifier, as returned by identity.NewCredentialsOIDC. +func (i *Identity) MergeOIDCCredentials(t CredentialsType, newCreds Credentials) (err error) { + creds, ok := i.Credentials[t] + if !ok { + i.SetCredentials(t, newCreds) + return nil + } + + var conf CredentialsOIDC + if err = json.Unmarshal(creds.Config, &conf); err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode old %s credentials from JSON: %s", creds.Config, err)) + } + + var newConf CredentialsOIDC + if err = json.Unmarshal(newCreds.Config, &newConf); err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode new %s credentials from JSON: %s", newCreds.Config, err)) + } + + if len(newConf.Providers) != 1 { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected exactly one provider to merge credentials.")) + } + newProvider := newConf.Providers[0] + + // The identifier should have been set already, but we set it here just in case. + if len(newCreds.Identifiers) != 1 { + newCreds.Identifiers = []string{OIDCUniqueID(newProvider.Provider, newProvider.Subject)} + } + + // Delete `use_auto_link` providers and their identifiers + var obsoleteIdentifiers []string + conf.Providers = slices.DeleteFunc(conf.Providers, func(p CredentialsOIDCProvider) bool { + if p.Provider == newProvider.Provider && p.UseAutoLink { + obsoleteIdentifiers = append(obsoleteIdentifiers, OIDCUniqueID(p.Provider, p.Subject)) + return true + } + return false + }) + creds.Identifiers = slices.DeleteFunc(creds.Identifiers, func(identifier string) bool { + return slices.Contains(obsoleteIdentifiers, identifier) + }) + + creds.Identifiers = append(creds.Identifiers, newCreds.Identifiers...) + conf.Providers = append(conf.Providers, newProvider) + + return i.SetCredentialsWithConfig(t, creds, conf) +} + +func NewIdentity(traitsSchemaID string) *Identity { + if traitsSchemaID == "" { + traitsSchemaID = config.DefaultIdentityTraitsSchemaID + } + + stateChangedAt := sqlxx.NullTime(time.Now().UTC()) + return &Identity{ + ID: uuid.Nil, + Credentials: map[CredentialsType]Credentials{}, + Traits: Traits("{}"), + SchemaID: traitsSchemaID, + VerifiableAddresses: []VerifiableAddress{}, + State: StateActive, + StateChangedAt: &stateChangedAt, + } +} + +func (i Identity) GetID() uuid.UUID { + return i.ID +} + +func (i Identity) MarshalJSON() ([]byte, error) { + type localIdentity Identity + i.Credentials = nil + i.MetadataAdmin = nil + result, err := json.Marshal(localIdentity(i)) + if err != nil { + return nil, err + } + return result, nil +} + +func (i *Identity) UnmarshalJSON(b []byte) error { + type localIdentity Identity + err := json.Unmarshal(b, (*localIdentity)(i)) + i.Credentials = nil + i.MetadataAdmin = nil + 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) { + if c, err := m.CountActiveMultiFactorCredentials(ctx, i); err != nil { + return err + } else if c > 0 { + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel2) + return nil + } + + if c, err := m.CountActiveFirstFactorCredentials(ctx, i); err != nil { + return err + } else if c > 0 { + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel1) + return nil + } + + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(NoAuthenticatorAssuranceLevel) + return nil +} + +type WithAdminMetadataInJSON Identity + +func (i WithAdminMetadataInJSON) MarshalJSON() ([]byte, error) { + type localIdentity Identity + i.Credentials = nil + return json.Marshal(localIdentity(i)) +} + +type WithCredentialsAndAdminMetadataInJSON Identity + +func (i WithCredentialsAndAdminMetadataInJSON) MarshalJSON() ([]byte, error) { + type localIdentity Identity + return json.Marshal(localIdentity(i)) +} + +type WithCredentialsNoConfigAndAdminMetadataInJSON Identity + +func (i WithCredentialsNoConfigAndAdminMetadataInJSON) MarshalJSON() ([]byte, error) { + type localIdentity Identity + for k, v := range i.Credentials { + v.Config = nil + i.Credentials[k] = v + } + return json.Marshal(localIdentity(i)) +} + +func (i *Identity) Validate() error { + expected := i.NID + if expected == uuid.Nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReason("Received empty nid.")) + } + + i.RecoveryAddresses = lo.Filter(i.RecoveryAddresses, func(v RecoveryAddress, key int) bool { + return v.NID == expected + }) + + i.VerifiableAddresses = lo.Filter(i.VerifiableAddresses, func(v VerifiableAddress, key int) bool { + return v.NID == expected + }) + + for k := range i.Credentials { + c := i.Credentials[k] + if c.NID != expected { + delete(i.Credentials, k) + continue + } + } + + return nil +} + +// CollectVerifiableAddresses returns a slice of all verifiable addresses of the given identities. +func CollectVerifiableAddresses(i []*Identity) (res []VerifiableAddress) { + res = make([]VerifiableAddress, 0, len(i)) + for _, id := range i { + res = append(res, id.VerifiableAddresses...) + } + + return res +} + +// CollectRecoveryAddresses returns a slice of all recovery addresses of the given identities. +func CollectRecoveryAddresses(i []*Identity) (res []RecoveryAddress) { + res = make([]RecoveryAddress, 0, len(i)) + for _, id := range i { + res = append(res, id.RecoveryAddresses...) + } + + return res +} + +func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Provider, includeCredentials []CredentialsType) (*Identity, error) { + credsToPublish := make(map[CredentialsType]Credentials, len(i.Credentials)) + + for ct, original := range i.Credentials { + if !slices.Contains(includeCredentials, ct) { + toPublish := original + toPublish.Config = []byte{} + credsToPublish[ct] = toPublish + continue + } + + switch ct { + case CredentialsTypeOIDC, CredentialsTypeSAML: + toPublish := original + toPublish.Config = []byte{} + + var i int + var err error + gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { + if ct == CredentialsTypeOIDC { + // Don't expose these for SAML + 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() + + plaintext, decryptErr := c.Cipher(ctx).Decrypt(ctx, ciphertext) + if decryptErr != nil { + 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 + } + + if org := v.Get("organization").String(); org != "" { + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), org) + if err != nil { + return false + } + } + + if useAutoLink := v.Get("use_auto_link").Bool(); useAutoLink { + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.use_auto_link", i), useAutoLink) + if err != nil { + return false + } + } + + i++ + return true + }) + + if err != nil { + return nil, err + } + + credsToPublish[ct] = toPublish + case CredentialsTypeDeviceAuthn: + // The deviceauthn config stores per-key PIN state, including + // `pin_secret`, the at-rest HPKE ciphertext of the user's PIN + // secret. Strip the whole `pin` object from every key before + // publishing so the admin include_credential path never leaks it. + toPublish := original + + var err error + gjson.GetBytes(original.Config, "credentials").ForEach(func(key, _ gjson.Result) bool { + toPublish.Config, err = sjson.DeleteBytes(toPublish.Config, fmt.Sprintf("credentials.%d.pin", key.Int())) + return err == nil + }) + if err != nil { + return nil, err + } + + credsToPublish[ct] = toPublish + default: + credsToPublish[ct] = original + } + } + + ii := *i + ii.Credentials = credsToPublish + return &ii, nil +} + +func (i *Identity) deleteCredentialPassword() error { + cred, ok := i.GetCredentials(CredentialsTypePassword) + if !ok { + return errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a password credential but this user has no such credential set up.")) + } + cred.Config = []byte("{}") + i.SetCredentials(CredentialsTypePassword, *cred) + return 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) deleteCredentialOIDCSAMLFromIdentity(ct CredentialsType, identifierToDelete string) error { + switch ct { + case CredentialsTypeOIDC, CredentialsTypeSAML: + // ok + default: + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unexpected credential type encountered: got %q, expected [%s, %s]", ct, CredentialsTypeOIDC, CredentialsTypeSAML)) + } + if identifierToDelete == "" { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("You must provide an identifier to delete this credential.")) + } + _, hasOIDC := i.GetCredentials(ct) + if !hasOIDC { + return errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a %s credential but this user has no such credential set up.", ct)) + } + var oidcConfig CredentialsOIDC + creds, err := i.ParseCredentials(ct, &oidcConfig) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updatedIdentifiers := make([]string, 0, len(oidcConfig.Providers)) + updatedProviders := make([]CredentialsOIDCProvider, 0, len(oidcConfig.Providers)) + 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[ct] = *creds + return nil +} + +// Patch Identities Parameters +// +// swagger:parameters batchPatchIdentities +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type batchPatchIdentitites struct { + // in: body + Body BatchPatchIdentitiesBody +} + +// Patch Identities Body +// +// swagger:model patchIdentitiesBody +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type BatchPatchIdentitiesBody struct { + // Identities holds the list of patches to apply + // + // required + Identities []*BatchIdentityPatch `json:"identities"` + + // Future fields: + // RemotePatchesURL string + // Async bool +} + +// Payload for patching an identity +// +// swagger:model identityPatch +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type BatchIdentityPatch struct { + // The identity to create. + Create *CreateIdentityBody `json:"create"` + + // The ID of this patch. + // + // The patch ID is optional. If specified, the ID will be returned in the + // response, so consumers of this API can correlate the response with the + // patch. + ID *uuid.UUID `json:"patch_id"` +} + +// swagger:enum BatchPatchAction +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type BatchPatchAction string + +const ( + // Create this identity. + ActionCreate BatchPatchAction = "create" + + // Error indicates that the patch failed. + ActionError BatchPatchAction = "error" + + // Future actions: + // + // Delete this identity. + // ActionDelete BatchPatchAction = "delete" + // + // ActionUpdate BatchPatchAction = "update" +) + +// Patch identities response +// +// swagger:model batchPatchIdentitiesResponse +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type batchPatchIdentitiesResponse struct { + // The patch responses for the individual identities. + Identities []*BatchIdentityPatchResponse `json:"identities"` +} + +// Response for a single identity patch +// +// swagger:model identityPatchResponse +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type BatchIdentityPatchResponse struct { + // The action for this specific patch + Action BatchPatchAction `json:"action"` + + // The identity ID payload of this patch + IdentityID *uuid.UUID `json:"identity,omitempty"` + + // 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_recovery.go b/identity/identity_recovery.go new file mode 100644 index 0000000..6e4dcc5 --- /dev/null +++ b/identity/identity_recovery.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "slices" + "time" + + "github.com/gofrs/uuid" +) + +type ( + // RecoveryAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema. + RecoveryAddressStatus string + + // swagger:model recoveryIdentityAddress + RecoveryAddress struct { + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // required: true + Value string `json:"value" db:"value"` + + // required: true + Via string `json:"via" db:"via"` + + // BreakGlassForOrganization, when set to an organization ID, allows this + // recovery address to bypass SSO enforcement for that organization. This + // enables designated users to recover their account via email when the + // SSO provider is unavailable. + BreakGlassForOrganization uuid.NullUUID `json:"break_glass_for_organization,omitzero" db:"break_glass_for_organization"` + + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" faker:"-" db:"updated_at"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + } +) + +func (a RecoveryAddress) TableName() string { return "identity_recovery_addresses" } +func (a RecoveryAddress) GetID() uuid.UUID { return a.ID } + +// Signature returns a unique string representation for the recovery address. +func (a RecoveryAddress) Signature() string { + return fmt.Sprintf("%v|%v|%v|%v|%v", a.Value, a.Via, a.BreakGlassForOrganization, a.IdentityID, a.NID) +} + +// RecoveryAddressesEqual reports whether two sets of recovery addresses are +// equal, comparing by value (via Signature) and ignoring order. It mirrors +// VerifiableAddressesEqual and is used by the privileged-session gate so that +// changing a recovery address requires a privileged session. +func RecoveryAddressesEqual(original, updated []RecoveryAddress) bool { + if len(original) != len(updated) { + return false + } + originalSignatures := make([]string, len(original)) + for i, a := range original { + originalSignatures[i] = a.Signature() + } + updatedSignatures := make([]string, len(updated)) + for i, a := range updated { + updatedSignatures[i] = a.Signature() + } + slices.Sort(originalSignatures) + slices.Sort(updatedSignatures) + return slices.Equal(originalSignatures, updatedSignatures) +} + +func NewRecoveryEmailAddress( + value string, + identity uuid.UUID, +) *RecoveryAddress { + return &RecoveryAddress{ + Value: value, + Via: AddressTypeEmail, + IdentityID: identity, + } +} + +func NewRecoverySMSAddress( + value string, + identity uuid.UUID, +) *RecoveryAddress { + return &RecoveryAddress{ + Value: value, + Via: AddressTypeSMS, + IdentityID: identity, + } +} diff --git a/identity/identity_recovery_test.go b/identity/identity_recovery_test.go new file mode 100644 index 0000000..bef7168 --- /dev/null +++ b/identity/identity_recovery_test.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/x" +) + +func TestNewRecoveryEmailAddress(t *testing.T) { + iid := x.NewUUID() + a := NewRecoveryEmailAddress("foo@ory.sh", iid) + + assert.Equal(t, a.Value, "foo@ory.sh") + assert.Equal(t, a.Via, AddressTypeEmail) + assert.Equal(t, iid, a.IdentityID) + assert.Equal(t, uuid.Nil, a.ID) +} + +// TestRecoveryAddress_Hash tests that the hash considers all fields that are +// written to the database (ignoring some well-known fields like the ID or +// timestamps). +func TestRecoveryAddress_Hash(t *testing.T) { + cases := []struct { + name string + a RecoveryAddress + }{ + { + name: "full fields", + a: RecoveryAddress{ + ID: x.NewUUID(), + Value: "foo@bar.me", + Via: AddressTypeEmail, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + IdentityID: x.NewUUID(), + NID: x.NewUUID(), + }, + }, + { + name: "empty fields", + a: RecoveryAddress{}, + }, + { + name: "email constructor", + a: *NewRecoveryEmailAddress("foo@ory.sh", x.NewUUID()), + }, + { + name: "full fields", + a: RecoveryAddress{ + ID: x.NewUUID(), + Value: "6502530000", + Via: AddressTypeSMS, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + IdentityID: x.NewUUID(), + NID: x.NewUUID(), + }, + }, + { + name: "SMS constructor", + a: *NewRecoverySMSAddress("6502530000", x.NewUUID()), + }, + { + name: "break glass for organization", + a: RecoveryAddress{ + ID: x.NewUUID(), + Value: "breakglass@ory.sh", + Via: AddressTypeEmail, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + IdentityID: x.NewUUID(), + NID: x.NewUUID(), + BreakGlassForOrganization: uuid.NullUUID{UUID: x.NewUUID(), Valid: true}, + }, + }, + } + + for _, tc := range cases { + t.Run("case="+tc.name, func(t *testing.T) { + assert.Equal(t, + reflectiveHash(tc.a), + tc.a.Signature(), + ) + }) + } +} diff --git a/identity/identity_region_test.go b/identity/identity_region_test.go new file mode 100644 index 0000000..b0c33de --- /dev/null +++ b/identity/identity_region_test.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "encoding/json" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/x/region" +) + +func TestIdentity_RegionJSONRoundTrip(t *testing.T) { + i := &identity.Identity{ID: uuid.Must(uuid.NewV4()), SchemaID: "default", Region: region.EUCentral} + b, err := json.Marshal(i) + require.NoError(t, err) + assert.Contains(t, string(b), `"region":"eu-central"`) + + var decoded identity.Identity + require.NoError(t, json.Unmarshal(b, &decoded)) + assert.Equal(t, region.EUCentral, decoded.Region) +} + +func TestIdentity_RegionJSONOmitEmpty(t *testing.T) { + i := &identity.Identity{ID: uuid.Must(uuid.NewV4()), SchemaID: "default"} + b, err := json.Marshal(i) + require.NoError(t, err) + assert.NotContains(t, string(b), `"region"`) +} diff --git a/identity/identity_test.go b/identity/identity_test.go new file mode 100644 index 0000000..55a87cf --- /dev/null +++ b/identity/identity_test.go @@ -0,0 +1,729 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +func TestNewIdentity(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + assert.Equal(t, uuid.Nil, i.ID) + assert.NotEmpty(t, i.Traits) + assert.NotNil(t, i.Credentials) + assert.True(t, i.IsActive()) +} + +func TestIdentityCredentialsOr(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = nil + + expected := &Credentials{ID: x.NewUUID(), Type: CredentialsTypePassword} + assert.Equal(t, expected, i.GetCredentialsOr(CredentialsTypePassword, expected)) + + expected = &Credentials{ID: x.NewUUID(), Type: CredentialsTypeWebAuthn} + i.SetCredentials(CredentialsTypeWebAuthn, *expected) + + assert.Equal(t, expected, i.GetCredentialsOr(CredentialsTypeWebAuthn, nil)) +} + +func TestIdentityCredentialsOrCreate(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = nil + + expected := &Credentials{Config: []byte("true"), IdentityID: i.ID, Type: CredentialsTypePassword} + i.UpsertCredentialsConfig(CredentialsTypePassword, []byte("true"), 0) + actual, ok := i.GetCredentials(CredentialsTypePassword) + assert.True(t, ok) + assert.Equal(t, expected, actual) +} + +func TestIdentityCredentials(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = nil + + // Shouldn't error if map is nil + i.DeleteCredentialsType(CredentialsTypeTOTP) + + expectedTOTP := Credentials{ID: x.NewUUID(), Type: CredentialsTypeTOTP} + i.SetCredentials(CredentialsTypeTOTP, expectedTOTP) + actual, found := i.GetCredentials(CredentialsTypeTOTP) + assert.True(t, found, "should set and find the credential if map was nil") + assert.Equal(t, &expectedTOTP, actual) + + expectedPassword := Credentials{ID: x.NewUUID(), Type: CredentialsTypePassword} + i.SetCredentials(CredentialsTypePassword, expectedPassword) + actual, found = i.GetCredentials(CredentialsTypePassword) + assert.True(t, found, "should set and find the credential if map was not nil") + assert.Equal(t, &expectedPassword, actual) + + expectedOIDC := Credentials{ID: x.NewUUID()} + i.SetCredentials(CredentialsTypeOIDC, expectedOIDC) + actual, found = i.GetCredentials(CredentialsTypeOIDC) + assert.True(t, found) + assert.Equal(t, expectedOIDC.ID, actual.ID) + assert.Equal(t, CredentialsTypeOIDC, actual.Type, "should set the type if we forgot to set it in the credentials struct") + + i.DeleteCredentialsType(CredentialsTypePassword) + _, found = i.GetCredentials(CredentialsTypePassword) + assert.False(t, found, "should delete a credential properly") + + actual, found = i.GetCredentials(CredentialsTypeTOTP) + assert.True(t, found, "but not alter other credentials") + assert.Equal(t, &expectedTOTP, actual) +} + +func TestMarshalExcludesCredentials(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = map[CredentialsType]Credentials{ + CredentialsTypePassword: { + ID: uuid.UUID{}, + }, + } + + rawJSON, err := json.Marshal(i) + require.NoError(t, err) + + creds := gjson.GetBytes(rawJSON, "credentials") + assert.Falsef(t, creds.Exists(), "Credentials should not be rendered to JSON, but got: %q", creds.Raw) + + // To ensure the original identity is not changed / Unmarshal has no side effects: + require.NotEmpty(t, i.Credentials) +} + +func TestMarshalExcludesCredentialsByReference(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = map[CredentialsType]Credentials{ + CredentialsTypePassword: { + ID: uuid.UUID{}, + }, + } + + var b bytes.Buffer + require.Nil(t, json.NewEncoder(&b).Encode(&i)) + + assert.False(t, gjson.Get(b.String(), "credentials").Exists(), "Credentials should not be rendered to json") + + // To ensure the original identity is not changed / Unmarshal has no side effects: + require.NotEmpty(t, i.Credentials) +} + +func TestMarshalIgnoresAdminMetadata(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.MetadataAdmin = []byte(`{"admin":"bar"}`) + i.MetadataPublic = []byte(`{"public":"bar"}`) + + var b bytes.Buffer + require.Nil(t, json.NewEncoder(&b).Encode(&i)) + + assert.False(t, gjson.Get(b.String(), "metadata_admin.admin").Exists(), "Admin metadata should not be rendered to json but got: %s", b.String()) + assert.Equal(t, "bar", gjson.Get(b.String(), "metadata_public.public").String(), "Public metadata should be rendered to json") + + // To ensure the original identity is not changed / Unmarshal has no side effects: + require.NotEmpty(t, i.MetadataAdmin) + require.NotEmpty(t, i.MetadataPublic) +} + +func TestUnMarshallIgnoresCredentials(t *testing.T) { + t.Parallel() + + jsonText := `{"id":"3234ad11-49c6-49e2-bfac-537f3e06cd85","schema_id":"default","schema_url":"","traits":{}, "credentials" : {"password":{"type":"","identifiers":null,"config":null,"updatedAt":"0001-01-01T00:00:00Z"}}}` + var i Identity + err := json.Unmarshal([]byte(jsonText), &i) + assert.Nil(t, err) + + assert.Nil(t, i.Credentials) + assert.Equal(t, "3234ad11-49c6-49e2-bfac-537f3e06cd85", i.ID.String()) +} + +func TestUnMarshallIgnoresAdminMetadata(t *testing.T) { + t.Parallel() + + jsonText := `{"id":"3234ad11-49c6-49e2-bfac-537f3e06cd85","schema_id":"default","schema_url":"","traits":{}, "admin_metadata" : {"foo":"bar"}}` + var i Identity + err := json.Unmarshal([]byte(jsonText), &i) + assert.Nil(t, err) + + assert.Nil(t, i.MetadataAdmin) +} + +func TestMarshalIdentityWithCredentialsWhenCredentialsNil(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Credentials = nil + + var b bytes.Buffer + require.Nil(t, json.NewEncoder(&b).Encode(WithCredentialsNoConfigAndAdminMetadataInJSON(*i))) + + assert.False(t, gjson.Get(b.String(), "credentials").Exists()) +} + +func TestMarshalIdentityWithAdminMetadata(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.MetadataAdmin = []byte(`{"some":"metadata"}`) + + var b bytes.Buffer + require.Nil(t, json.NewEncoder(&b).Encode(WithAdminMetadataInJSON(*i))) + assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original metadata_admin should not be touched by marshalling") +} + +func TestMarshalIdentityWithCredentialsNoConfig(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage(`{"some":"secret"}`), + }, + } + i.Credentials = credentials + i.MetadataAdmin = []byte(`{"some":"metadata"}`) + + rawJSON, err := json.Marshal((*WithCredentialsNoConfigAndAdminMetadataInJSON)(i)) + require.NoError(t, err) + + credentialsInJSON := gjson.GetBytes(rawJSON, "credentials") + assert.Truef(t, credentialsInJSON.Exists(), "Credentials should be rendered to JSON, but got: %q", credentialsInJSON.Raw) + + assert.JSONEq(t, `{"password":{"type":"password","identifiers":null,"updated_at":"0001-01-01T00:00:00Z","created_at":"0001-01-01T00:00:00Z","version":0}}`, credentialsInJSON.Raw) + assert.Equal(t, credentials, i.Credentials, "Original credentials should not be touched by marshalling") + assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original metadata_admin should not be touched by marshalling") +} + +func TestMarshalIdentityWithAll(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + } + i.Credentials = credentials + i.MetadataAdmin = []byte(`{"some":"metadata"}`) + + var b bytes.Buffer + require.Nil(t, json.NewEncoder(&b).Encode(WithCredentialsAndAdminMetadataInJSON(*i))) + + credentialsInJSON := gjson.Get(b.String(), "credentials") + assert.True(t, credentialsInJSON.Exists()) + + snapshotx.SnapshotT(t, json.RawMessage(credentialsInJSON.Raw)) + assert.Equal(t, credentials, i.Credentials, "Original credentials should not be touched by marshalling") + assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original credentials should not be touched by marshalling") +} + +func TestValidateNID(t *testing.T) { + t.Parallel() + + nid := x.NewUUID() + for k, tc := range []struct { + i *Identity + expect *Identity + expectedErr bool + }{ + {i: &Identity{}, expectedErr: true}, + {i: &Identity{NID: nid}}, + { + i: &Identity{NID: nid, RecoveryAddresses: []RecoveryAddress{{NID: x.NewUUID()}}}, + expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}}, + }, + { + i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: x.NewUUID()}}}, + expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}}, + }, + { + i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: x.NewUUID()}}}, + expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{}, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}}, + }, + { + i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: x.NewUUID()}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}}, + expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}}, + }, + { + i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: x.NewUUID()}}}, + expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{}}, + }, + { + i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}}, + expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}}, + }, + { + i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: x.NewUUID()}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}}, + expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}}, + }, + { + i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}}, + expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}}, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + err := tc.i.Validate() + if tc.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + if tc.expect != nil { + assert.EqualValues(t, tc.expect, tc.i) + } + } + }) + } +} + +// TestRecoveryAddresses tests the CollectRecoveryAddresses are collected from all identities. +func TestRecoveryAddresses(t *testing.T) { + t.Parallel() + + var addresses []RecoveryAddress + + for i := range 10 { + addresses = append(addresses, RecoveryAddress{ + Value: fmt.Sprintf("address-%d", i), + }) + } + + id1 := &Identity{RecoveryAddresses: addresses[:5]} + id2 := &Identity{} + id3 := &Identity{RecoveryAddresses: addresses[5:]} + + assert.Equal(t, addresses, CollectRecoveryAddresses([]*Identity{id1, id2, id3})) +} + +// TestVerifiableAddresses tests the VerfifableAddresses are collected from all identities. +func TestVerifiableAddresses(t *testing.T) { + t.Parallel() + + var addresses []VerifiableAddress + + for i := 0; i < 10; i++ { + addresses = append(addresses, VerifiableAddress{ + Value: fmt.Sprintf("address-%d", i), + }) + } + + id1 := &Identity{VerifiableAddresses: addresses[:5]} + id2 := &Identity{} + id3 := &Identity{VerifiableAddresses: addresses[5:]} + + assert.Equal(t, addresses, CollectVerifiableAddresses([]*Identity{id1, id2, id3})) +} + +type cipherProvider struct{} + +func (c *cipherProvider) Cipher(context.Context) cipher.Cipher { + return cipher.NewNoop() +} + +func TestWithDeclassifiedCredentials(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"zab", "bar"}, + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage(`{"some": "secret"}`), + }, + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"bar", "baz"}, + // hint: + // echo '666f6f' | xxd -r -p + Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"bar","provider":"oidc1","initial_id_token":"666f6f"}]}`), + }, + CredentialsTypeSAML: { + Type: CredentialsTypeSAML, + Identifiers: []string{"qux", "quz"}, + // hint: + // echo 'this should not appear in output' | xxd -ps -c 0 + Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"qux","provider":"saml1","initial_id_token":"746869732073686f756c64206e6f742061707065617220696e206f75747075740a"}]}`), + }, + CredentialsTypeWebAuthn: { + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"foo", "bar"}, + Config: sqlxx.JSONRawMessage(`{"some": "secret"}`), + }, + } + i.Credentials = credentials + + t.Run("case=no-include", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, nil) + 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=include-webauthn", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn}) + 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=include-multi", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &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(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeOIDC}) + 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=saml", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeSAML}) + 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=deviceauthn", func(t *testing.T) { + di := NewIdentity(config.DefaultIdentityTraitsSchemaID) + di.Credentials = map[CredentialsType]Credentials{ + CredentialsTypeDeviceAuthn: { + Type: CredentialsTypeDeviceAuthn, + Identifiers: []string{di.ID.String()}, + Config: sqlxx.JSONRawMessage(`{"credentials":[` + + `{"client_key_id":"key-with-pin","device_name":"Pixel","state":"confirmed","pin":{"pin_secret":"deadbeefciphertext","failed_attempts":1}},` + + `{"client_key_id":"key-without-pin","device_name":"iPhone","state":"confirmed"}` + + `]}`), + }, + } + + actualIdentity, err := di.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeDeviceAuthn}) + require.NoError(t, err) + + actual, ok := actualIdentity.Credentials[CredentialsTypeDeviceAuthn] + require.True(t, ok) + + // The PIN secret (and the whole pin object) must never reach the admin API. + assert.NotContains(t, string(actual.Config), "pin_secret") + assert.NotContains(t, string(actual.Config), "deadbeefciphertext") + assert.False(t, gjson.GetBytes(actual.Config, "credentials.0.pin").Exists(), "pin object must be stripped") + + // Non-secret fields must be preserved. + assert.Equal(t, "key-with-pin", gjson.GetBytes(actual.Config, "credentials.0.client_key_id").String()) + assert.Equal(t, "Pixel", gjson.GetBytes(actual.Config, "credentials.0.device_name").String()) + assert.Equal(t, "confirmed", gjson.GetBytes(actual.Config, "credentials.0.state").String()) + assert.Equal(t, "key-without-pin", gjson.GetBytes(actual.Config, "credentials.1.client_key_id").String()) + assert.Equal(t, "iPhone", gjson.GetBytes(actual.Config, "credentials.1.device_name").String()) + }) +} + +func TestDeleteCredentialOIDCSAMLFromIdentity(t *testing.T) { + t.Parallel() + + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + + err := i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "") + assert.Error(t, err) + err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "does-not-exist") + assert.Error(t, err) + err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "") + assert.Error(t, err) + err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "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"}]}`), + }, + CredentialsTypeSAML: { + Type: CredentialsTypeSAML, + 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.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "zab") + assert.Error(t, err) + err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "foo") + assert.Error(t, err) + err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "bar") + assert.Error(t, err, "matches multiple OIDC credentials") + + require.NoError(t, i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "bar:1234")) + + assert.Len(t, i.Credentials, 4) + + 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, CredentialsTypeSAML) + assert.EqualValues(t, i.Credentials[CredentialsTypeSAML].Identifiers, []string{"bar:1234", "baz:5678"}) + + 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) + + require.NoError(t, i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "baz:5678")) + + assert.Len(t, i.Credentials, 4) + assert.Contains(t, i.Credentials, CredentialsTypeOIDC) + assert.EqualValues(t, i.Credentials[CredentialsTypeOIDC].Identifiers, []string{"baz:5678"}) + + assert.Contains(t, i.Credentials, CredentialsTypeSAML) + saml, ok := i.GetCredentials(CredentialsTypeSAML) + require.True(t, ok) + assert.EqualValues(t, saml.Identifiers, []string{"bar:1234"}) + var samlCfg CredentialsOIDC + _, err = i.ParseCredentials(CredentialsTypeSAML, &samlCfg) + require.NoError(t, err) + assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "bar", Subject: "1234"}}}, samlCfg) +} + +func TestMergeOIDCCredentials(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + identity *Identity + newCredentials Credentials + + expectedIdentity *Identity + assertErr assert.ErrorAssertionFunc + }{ + { + name: "adds OIDC credential if not exists", + identity: &Identity{}, + newCredentials: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"oidc","subject":"1234"}]}`), + }, + + expectedIdentity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"oidc","subject":"1234"}]}`), + }, + }, + }, + }, + { + name: "merges OIDC credential if exists", + identity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Type: CredentialsTypePassword, + Identifiers: []string{"user@example.com"}, + }, + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"foo", "replace:1234", "bar", "baz", "replace:abc", "replace:dont-replace"}, + Config: sqlxx.JSONRawMessage(`{"providers": [ + {"provider": "replace", "subject": "1234", "use_auto_link": true}, + {"provider": "dont-touch", "subject": "foo"}, + {"provider": "replace", "subject": "abc", "use_auto_link": true}, + {"provider": "also-dont-touch", "subject": "bar", "use_auto_link": true}, + {"provider": "replace", "subject": "dont-replace", "use_auto_link": false} +]}`), + }, + }, + }, + newCredentials: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "replace", "subject": "new-subject"}]}`), + }, + + expectedIdentity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"foo", "bar", "baz", "replace:dont-replace", "replace:new-subject"}, + Config: sqlxx.JSONRawMessage(`{ + "providers" : [ { + "subject" : "foo", + "provider" : "dont-touch", + "initial_id_token" : "", + "initial_access_token" : "", + "initial_refresh_token" : "" + }, { + "subject" : "bar", + "provider" : "also-dont-touch", + "initial_id_token" : "", + "initial_access_token" : "", + "initial_refresh_token" : "", + "use_auto_link": true + }, { + "subject" : "dont-replace", + "provider" : "replace", + "initial_id_token" : "", + "initial_access_token" : "", + "initial_refresh_token" : "" + }, { + "subject" : "new-subject", + "provider" : "replace", + "initial_id_token" : "", + "initial_access_token" : "", + "initial_refresh_token" : "" + } ] +}`), + }, + CredentialsTypePassword: { + Type: CredentialsTypePassword, + Identifiers: []string{"user@example.com"}, + }, + }, + }, + }, + { + name: "errs if new credential has no provider", + identity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "oidc", "subject": "1234"}]}`), + }, + }, + }, + newCredentials: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers": []}`), + }, + + assertErr: assert.Error, + }, + { + name: "errs if identity credentials are invalid", + identity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage("invalid"), + }, + }, + }, + newCredentials: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "replace", "subject": "new-subject"}]}`), + }, + + assertErr: assert.Error, + }, + { + name: "errs if new credential config is invalid", + identity: &Identity{ + Credentials: map[CredentialsType]Credentials{ + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "oidc", "subject": "1234"}]}`), + }, + }, + }, + newCredentials: Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{"oidc:1234"}, + Config: sqlxx.JSONRawMessage(`invalid`), + }, + + assertErr: assert.Error, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + err := tc.identity.MergeOIDCCredentials(CredentialsTypeOIDC, tc.newCredentials) + + if tc.assertErr != nil { + tc.assertErr(t, err) + } else { + require.NoError(t, err) + } + + if tc.expectedIdentity != nil { + var buf bytes.Buffer + require.NoError(t, json.Compact(&buf, tc.expectedIdentity.Credentials[CredentialsTypeOIDC].Config)) + tc.expectedIdentity.UpsertCredentialsConfig(CredentialsTypeOIDC, buf.Bytes(), 0) + assert.EqualExportedValues(t, tc.expectedIdentity, tc.identity) + } + }) + } +} diff --git a/identity/identity_verification.go b/identity/identity_verification.go new file mode 100644 index 0000000..849ff1a --- /dev/null +++ b/identity/identity_verification.go @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "slices" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlxx" +) + +const ( + VerifiableAddressStatusPending VerifiableAddressStatus = "pending" + VerifiableAddressStatusSent VerifiableAddressStatus = "sent" + VerifiableAddressStatusCompleted VerifiableAddressStatus = "completed" +) + +// VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema +// +// swagger:model identityVerifiableAddressStatus +type VerifiableAddressStatus string + +// VerifiableAddress is an identity's verifiable address +// +// swagger:model verifiableIdentityAddress +type VerifiableAddress struct { + // The ID + // + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // The address value + // + // example foo@user.com + // required: true + Value string `json:"value" db:"value"` + + // Indicates if the address has already been verified + // + // example: true + // required: true + Verified bool `json:"verified" db:"verified"` + + // The delivery method + // + // enum: email,sms + // example: email + // required: true + Via string `json:"via" db:"via"` + + // The verified address status + // + // enum: pending,sent,completed + // example: sent + // required: true + Status VerifiableAddressStatus `json:"status" db:"status"` + + // When the address was verified + // + // example: 2014-01-01T23:28:56.782Z + // required: false + VerifiedAt *sqlxx.NullTime `json:"verified_at,omitempty" faker:"-" db:"verified_at"` + + // When this entry was created + // + // example: 2014-01-01T23:28:56.782Z + CreatedAt time.Time `json:"created_at" faker:"-" db:"created_at"` + + // When this entry was last updated + // + // example: 2014-01-01T23:28:56.782Z + UpdatedAt time.Time `json:"updated_at" faker:"-" db:"updated_at"` + + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (a VerifiableAddress) TableName() string { + return "identity_verifiable_addresses" +} + +func NewVerifiableEmailAddress(value string, identity uuid.UUID) *VerifiableAddress { + return NewVerifiableAddress(value, identity, AddressTypeEmail) +} + +func NewVerifiableAddress(value string, identity uuid.UUID, channel string) *VerifiableAddress { + return &VerifiableAddress{ + Value: value, + Verified: false, + Status: VerifiableAddressStatusPending, + Via: channel, + IdentityID: identity, + } +} + +func (a VerifiableAddress) GetID() uuid.UUID { + return a.ID +} + +// Signature returns a unique string representation for the recovery address. +func (a VerifiableAddress) Signature() string { + return fmt.Sprintf("%v|%v|%v|%v|%v|%v|%v", a.Value, a.Verified, a.Via, a.Status, a.VerifiedAt, a.IdentityID, a.NID) +} + +func VerifiableAddressesEqual(original, updated []VerifiableAddress) bool { + return slices.EqualFunc(original, updated, func(a, b VerifiableAddress) bool { + return a.Signature() == b.Signature() + }) +} + +type VerifiableAddressLike interface { + // ToPersistable returns the verifiable address to be persisted. + // Returns nil when the change is pending (e.g., via PendingTraitsChange). + ToPersistable() (va *VerifiableAddress, isPersistable bool) + // Address returns the address value + Address() string + // Via returns the delivery method + DeliveryVia() string +} + +var _ VerifiableAddressLike = &VerifiableAddress{} + +func (va *VerifiableAddress) ToPersistable() (*VerifiableAddress, bool) { + return va, true +} + +func (va VerifiableAddress) Address() string { + return va.Value +} + +func (va VerifiableAddress) DeliveryVia() string { + return va.Via +} diff --git a/identity/identity_verification_test.go b/identity/identity_verification_test.go new file mode 100644 index 0000000..8a59445 --- /dev/null +++ b/identity/identity_verification_test.go @@ -0,0 +1,105 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "reflect" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" + + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/x" +) + +func TestNewVerifiableEmailAddress(t *testing.T) { + iid := x.NewUUID() + a := NewVerifiableEmailAddress("foo@ory.sh", iid) + var nullTime *sqlxx.NullTime + + assert.Equal(t, a.Value, "foo@ory.sh") + assert.Equal(t, a.Via, AddressTypeEmail) + assert.Equal(t, a.Status, VerifiableAddressStatusPending) + assert.Equal(t, a.Verified, false) + assert.EqualValues(t, nullTime, a.VerifiedAt) + assert.Equal(t, iid, a.IdentityID) + assert.Equal(t, uuid.Nil, a.ID) +} + +var tagsIgnoredForHashing = map[string]struct{}{ + "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 { + var ( + val = reflect.ValueOf(record) + typ = reflect.TypeOf(record) + values = []string{} + ) + for i := 0; i < val.NumField(); i++ { + dbTag, ok := typ.Field(i).Tag.Lookup("db") + if !ok { + continue + } + if _, ignore := tagsIgnoredForHashing[dbTag]; ignore { + continue + } + if !val.Field(i).CanInterface() { + continue + } + values = append(values, fmt.Sprintf("%v", val.Field(i).Interface())) + } + return strings.Join(values, "|") +} + +// TestVerifiableAddress_Hash tests that the hash considers all fields that are +// written to the database (ignoring some well-known fields like the ID or +// timestamps). +func TestVerifiableAddress_Hash(t *testing.T) { + now := sqlxx.NullTime(time.Now()) + cases := []struct { + name string + a VerifiableAddress + }{ + { + name: "full fields", + a: VerifiableAddress{ + ID: x.NewUUID(), + Value: "foo@bar.me", + Verified: false, + Via: AddressTypeEmail, + Status: VerifiableAddressStatusPending, + VerifiedAt: &now, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + IdentityID: x.NewUUID(), + NID: x.NewUUID(), + }, + }, { + name: "empty fields", + a: VerifiableAddress{}, + }, { + name: "constructor", + a: *NewVerifiableEmailAddress("foo@ory.sh", x.NewUUID()), + }, + } + + for _, tc := range cases { + t.Run("case="+tc.name, func(t *testing.T) { + assert.Equal(t, + reflectiveHash(tc.a), + tc.a.Signature(), + ) + }) + } +} diff --git a/identity/manager.go b/identity/manager.go new file mode 100644 index 0000000..3081c74 --- /dev/null +++ b/identity/manager.go @@ -0,0 +1,810 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "slices" + "sort" + "strings" + "time" + + stderrors "errors" + + "github.com/gofrs/uuid" + "github.com/mohae/deepcopy" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/transaction" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" +) + +func ErrProtectedFieldModified() *herodot.DefaultError { + return herodot.ErrForbidden(). + WithReasonf(`A field was modified that updates one or more credentials-related settings. This action was blocked because an unprivileged method was used to execute the update. This is either a configuration issue or a bug and should be reported to the system administrator.`) +} + +type ( + managerDependencies interface { + config.Provider + PoolProvider + PrivilegedPoolProvider + otelx.Provider + courier.Provider + ValidationProvider + ActiveCredentialsCounterStrategyProvider + logrusx.Provider + transaction.PersistenceProvider + PendingTraitsChangePersistenceProvider + template.Dependencies + } + ManagementProvider interface { + IdentityManager() *Manager + } + Manager struct { + r managerDependencies + } + + ManagerOptions struct { + ExposeValidationErrors bool + AllowWriteProtectedTraits bool + ExcludedCredentialTypes []CredentialsType + } + + ManagerOption func(*ManagerOptions) +) + +func NewManager(r managerDependencies) *Manager { + return &Manager{r: r} +} + +func ManagerExposeValidationErrorsForInternalTypeAssertion(options *ManagerOptions) { + options.ExposeValidationErrors = true +} + +func ManagerAllowWriteProtectedTraits(options *ManagerOptions) { + options.AllowWriteProtectedTraits = true +} + +// ManagerWithoutCredentialTypes excludes the given credential types from the +// identity persist: Update leaves their rows untouched. Strategies that +// persist those credentials themselves (e.g. through the locked +// UpdateCredentialsConfig path) set this so the persist cannot clobber a +// concurrent locked write. +func ManagerWithoutCredentialTypes(cts ...CredentialsType) ManagerOption { + return func(options *ManagerOptions) { + options.ExcludedCredentialTypes = append(options.ExcludedCredentialTypes, cts...) + } +} + +func newManagerOptions(opts []ManagerOption) *ManagerOptions { + var o ManagerOptions + for _, f := range opts { + f(&o) + } + return &o +} + +func (m *Manager) Create(ctx context.Context, i *Identity, opts ...ManagerOption) (err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.Create") + defer otelx.End(span, &err) + + if i.SchemaID == "" { + i.SchemaID = m.r.Config().DefaultIdentityTraitsSchemaID(ctx) + } + + o := newManagerOptions(opts) + if err := m.ValidateIdentity(ctx, i, o); 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) + } + return err + } + + return nil +} + +// maxParallelConflictLookups bounds how many conflict lookups run against the +// database concurrently. +const maxParallelConflictLookups = 8 + +// A match either carries the conflicting identity directly (credential lookup) +// or just its ID (address lookup). +type conflictMatch struct { + found *Identity + identityID uuid.UUID + address string + addressType string +} + +type conflictLookup func(ctx context.Context) (*conflictMatch, error) + +// findFirstConflict returns the match of the lowest-index lookup, or +// sqlcon.ErrNoRows if no lookup matches. Outside a transaction all lookups run +// concurrently, bounded by maxParallelConflictLookups. +func findFirstConflict(ctx context.Context, lookups []conflictLookup) (*conflictMatch, error) { + // Inside a transaction the single connection must not run concurrent queries, so the lookups run + // sequentially and stop at the first match. + if popx.InTransaction(ctx) { + for _, lookup := range lookups { + match, err := lookup(ctx) + if errors.Is(err, sqlcon.ErrNoRows()) { + continue + } else if err != nil { + return nil, err + } else if match == nil { + continue + } + return match, nil + } + return nil, sqlcon.ErrNoRows() + } + + // Each goroutine records its own outcome (match and error) into a distinct + // slice element, then always returns nil so the group never cancels a + // still-running higher-priority lookup. After Wait the outcomes are resolved + // in priority order, matching the sequential path: a lower-priority error + // can neither mask a higher-priority match nor be returned + // non-deterministically. + type outcome struct { + match *conflictMatch + err error + } + results := make([]outcome, len(lookups)) + eg := new(errgroup.Group) + eg.SetLimit(maxParallelConflictLookups) + for i, lookup := range lookups { + eg.Go(func() error { + match, err := lookup(ctx) + if errors.Is(err, sqlcon.ErrNoRows()) { + return nil + } + results[i] = outcome{match: match, err: err} + return nil + }) + } + if err := eg.Wait(); err != nil { + return nil, err + } + for _, r := range results { + if r.err != nil { + return nil, r.err + } + if r.match != nil { + return r.match, nil + } + } + return nil, sqlcon.ErrNoRows() +} + +func (m *Manager) ConflictingIdentity(ctx context.Context, i *Identity) (found *Identity, foundConflictAddress string, conflictAddressType string, err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.ConflictingIdentity") + defer otelx.End(span, &err) + + // Lookups are ordered by priority: credential identifiers first, then + // verifiable addresses, then recovery addresses. The lowest-index match + // wins, so the reported conflict is deterministic. + var lookups []conflictLookup + for _, ct := range slices.Sorted(maps.Keys(i.Credentials)) { + for _, identifier := range i.Credentials[ct].Identifiers { + lookups = append(lookups, func(ctx context.Context) (*conflictMatch, error) { + conflicting, _, err := m.r.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, ct, identifier) + if err != nil { + return nil, err + } + return &conflictMatch{found: conflicting, address: identifier, addressType: ct.String()}, nil + }) + } + } + for _, va := range i.VerifiableAddresses { + lookups = append(lookups, func(ctx context.Context) (*conflictMatch, error) { + conflictingAddress, err := m.r.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, va.Via, va.Value) + if err != nil { + return nil, err + } + return &conflictMatch{identityID: conflictingAddress.IdentityID, address: conflictingAddress.Value, addressType: va.Via}, nil + }) + } + for _, ra := range i.RecoveryAddresses { + lookups = append(lookups, func(ctx context.Context) (*conflictMatch, error) { + conflictingAddress, err := m.r.PrivilegedIdentityPool().FindRecoveryAddressByValue(ctx, ra.Via, ra.Value) + if err != nil { + return nil, err + } + return &conflictMatch{identityID: conflictingAddress.IdentityID, address: conflictingAddress.Value, addressType: ra.Via}, nil + }) + } + + // Fanning out separate lookups (instead of a single UNION query in the + // persister) composes with both the OSS and the multi-region persister, + // which overrides FindByCredentialsIdentifier with region-aware logic. + match, err := findFirstConflict(ctx, lookups) + if err != nil { + return nil, "", "", err + } + + if match.found != nil { + // FindByCredentialsIdentifier does not expand identity credentials. + if err := m.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, match.found, ExpandCredentials); err != nil { + return nil, "", "", err + } + return match.found, match.address, match.addressType, nil + } + conflicting, err := m.r.PrivilegedIdentityPool().GetIdentity(ctx, match.identityID, ExpandCredentials) + if err != nil { + return nil, "", "", err + } + return conflicting, match.address, match.addressType, nil +} + +func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identity) (err error) { + if !m.r.Config().SelfServiceFlowRegistrationLoginHints(ctx) { + return &ErrDuplicateCredentials{error: e} + } + + found, foundConflictAddress, conflictingAddressType, err := m.ConflictingIdentity(ctx, i) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + return &ErrDuplicateCredentials{error: e} + } + return err + } + + // We need to sort the credentials for the error message to be deterministic. + var creds []Credentials + for _, cred := range found.Credentials { + creds = append(creds, cred) + } + sort.Slice(creds, func(i, j int) bool { + return creds[i].Type < creds[j].Type + }) + + duplicateCredErr := &ErrDuplicateCredentials{error: e} + // OIDC credentials are not email addresses but the sub claim from the OIDC provider. + // This is useless for the user, so in that case, we don't set the identifier hint. + if conflictingAddressType != CredentialsTypeOIDC.String() { + duplicateCredErr.SetIdentifierHint(strings.Trim(foundConflictAddress, " ")) + } + + for _, cred := range creds { + if cred.Config == nil { + continue + } + + // Basically, we only have password, oidc, and webauthn as first factor credentials. + // We don't care about second factor, because they don't help the user understand how to sign + // in to the first factor (obviously). + switch cred.Type { + case CredentialsTypePassword: + if duplicateCredErr.IdentifierHint() == "" && len(cred.Identifiers) == 1 { + duplicateCredErr.SetIdentifierHint(cred.Identifiers[0]) + } + + 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: + duplicateCredErr.AddCredentialsType(cred.Type) + case CredentialsTypeOIDC: + var cfg CredentialsOIDC + 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)) + } + + available := make([]string, 0, len(cfg.Providers)) + for _, provider := range cfg.Providers { + available = append(available, provider.Provider) + } + + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.availableOIDCProviders = available + case CredentialsTypeWebAuthn: + 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)) + } + + if duplicateCredErr.IdentifierHint() == "" && len(cred.Identifiers) == 1 { + duplicateCredErr.SetIdentifierHint(cred.Identifiers[0]) + } + for _, webauthn := range cfg.Credentials { + if webauthn.IsPasswordless { + duplicateCredErr.AddCredentialsType(cred.Type) + break + } + } + + // DeviceAuthn is currently not to be used as a first factor. + case CredentialsTypeDeviceAuthn: + continue + + 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)) + } + + if duplicateCredErr.IdentifierHint() == "" && len(cred.Identifiers) == 1 { + duplicateCredErr.SetIdentifierHint(cred.Identifiers[0]) + } + for _, webauthn := range cfg.Credentials { + if webauthn.IsPasswordless { + duplicateCredErr.AddCredentialsType(cred.Type) + break + } + } + } + } + + return duplicateCredErr +} + +type ErrDuplicateCredentials struct { + error + + availableCredentials []CredentialsType + availableOIDCProviders []string + identifierHint string +} + +var _ schema.DuplicateCredentialsHinter = (*ErrDuplicateCredentials)(nil) + +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 NewCreateIdentitiesError(capacity int) *CreateIdentitiesError { + return &CreateIdentitiesError{ + failedIdentities: make(map[*Identity]*herodot.DefaultError, capacity), + } +} + +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 e == nil || 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) + + createIdentitiesError := NewCreateIdentitiesError(len(identities)) + 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, ident, o); err != nil { + reason := err.Error() + if e, ok := stderrors.AsType[*herodot.DefaultError](err); ok { + reason = e.Reason() + } + createIdentitiesError.AddFailedIdentity(ident, herodot.ErrBadRequest().WithReason(reason).WithWrap(err)) + continue + } + validIdentities = append(validIdentities, ident) + } + + 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 createIdentitiesError.ErrOrNil() +} + +func (m *Manager) requiresPrivilegedAccess(ctx context.Context, original, updated *Identity, o *ManagerOptions) (err error) { + _, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.requiresPrivilegedAccess") + defer otelx.End(span, &err) + + switch { + case o.AllowWriteProtectedTraits: + return nil + + case !CredentialsEqual(updated.Credentials, original.Credentials), + !VerifiableAddressesEqual(updated.VerifiableAddresses, original.VerifiableAddresses), + !RecoveryAddressesEqual(updated.RecoveryAddresses, original.RecoveryAddresses): + // reset the identity + *updated = *original + return errors.WithStack(ErrProtectedFieldModified()) + } + + return nil +} + +func (m *Manager) Update(ctx context.Context, updated *Identity, opts ...ManagerOption) (err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.Update") + defer otelx.End(span, &err) + + o := newManagerOptions(opts) + if err := m.ValidateIdentity(ctx, updated, o); err != nil { + return err + } + + original, err := m.r.PrivilegedIdentityPool().GetIdentityConfidential(ctx, updated.ID) + if err != nil { + return err + } + + if err := m.requiresPrivilegedAccess(ctx, original, updated, o); err != nil { + return err + } + + mods := []UpdateIdentityModifier{DiffAgainst(original)} + if len(o.ExcludedCredentialTypes) > 0 { + mods = append(mods, WithoutCredentialTypes(o.ExcludedCredentialTypes...)) + } + return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, updated, mods...) +} + +func (m *Manager) UpdateSchemaID(ctx context.Context, id uuid.UUID, schemaID string, opts ...ManagerOption) (err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.UpdateSchemaID") + defer otelx.End(span, &err) + + o := newManagerOptions(opts) + original, err := m.r.PrivilegedIdentityPool().GetIdentityConfidential(ctx, id) + if err != nil { + return err + } + + if !o.AllowWriteProtectedTraits && original.SchemaID != schemaID { + return errors.WithStack(ErrProtectedFieldModified()) + } + + original.SchemaID = schemaID + if err := m.ValidateIdentity(ctx, original, o); err != nil { + return err + } + + return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, original) +} + +func (m *Manager) SetTraits(ctx context.Context, id uuid.UUID, traits Traits, opts ...ManagerOption) (_ *Identity, err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.SetTraits") + defer otelx.End(span, &err) + + o := newManagerOptions(opts) + original, err := m.r.PrivilegedIdentityPool().GetIdentityConfidential(ctx, id) + if err != nil { + return nil, err + } + + // original is used to check whether protected traits were modified + updated := deepcopy.Copy(original).(*Identity) + updated.Traits = traits + if err := m.ValidateIdentity(ctx, updated, o); err != nil { + return nil, err + } + + if err := m.requiresPrivilegedAccess(ctx, original, updated, o); err != nil { + return nil, err + } + + 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) + + updated, err := m.SetTraits(ctx, id, traits, opts...) + if err != nil { + return err + } + + return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, updated) +} + +func (m *Manager) ValidateIdentity(ctx context.Context, i *Identity, o *ManagerOptions) (err error) { + // Safeguard against callers that write to i.Credentials directly with a + // Type field that does not match its map key. Must run before + // IdentityValidator.Validate, because the schema-extension pass calls + // SetCredentials and would silently repair Type — masking the bug + // while still allowing the malformed Config to overwrite the existing + // credential row. + if err := ValidateCredentialsIntegrity(i.Credentials); err != nil { + return err + } + + if err := m.r.IdentityValidator().Validate(ctx, i); err != nil { + 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 +} + +func (m *Manager) CountActiveFirstFactorCredentials(ctx context.Context, i *Identity) (count int, 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.CountActiveFirstFactorCredentials") + // defer otelx.End(span, &err) + + for _, strategy := range m.r.ActiveCredentialsCounterStrategies(ctx) { + current, err := strategy.CountActiveFirstFactorCredentials(ctx, i.Credentials) + if err != nil { + return 0, err + } + + count += current + } + return count, nil +} + +func (m *Manager) CountActiveMultiFactorCredentials(ctx context.Context, i *Identity) (count int, 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.CountActiveMultiFactorCredentials") + // defer otelx.End(span, &err) + + for _, strategy := range m.r.ActiveCredentialsCounterStrategies(ctx) { + current, err := strategy.CountActiveMultiFactorCredentials(ctx, i.Credentials) + if err != nil { + return 0, err + } + + count += current + } + return count, nil +} + +var ErrConcurrentModification = stderrors.New("concurrent modification detected") + +// AddressRef identifies a verifiable address by its value and channel. +// Used by the notify_previous_addresses hook to persist the set of +// addresses that should receive a change notification. +type AddressRef struct { + Value string `json:"value"` + Via string `json:"via"` +} + +// sendIdentityNotifications queues a notification template to each target via +// the appropriate courier channel, sharing the courier, identity-model, and +// error-collection plumbing across the concrete notification types. buildEmail +// and buildSMS construct the channel-specific template for a single recipient +// from the identity model and a shared RFC3339 timestamp. Errors from individual +// targets are collected and returned as a joined error but do not short-circuit +// the batch — a failure to notify one recipient must not prevent others from +// being notified. +func (m *Manager) sendIdentityNotifications( + ctx context.Context, + spanName string, + targets []AddressRef, + i *Identity, + buildEmail func(to string, identity map[string]any, at string) courier.EmailTemplate, + buildSMS func(to string, identity map[string]any, at string) courier.SMSTemplate, +) (err error) { + ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, spanName) + defer otelx.End(span, &err) + + if len(targets) == 0 { + return nil + } + + c, err := m.r.Courier(ctx) + if err != nil { + return err + } + + model, err := x.StructToMap(i) + if err != nil { + return errors.WithStack(err) + } + at := time.Now().UTC().Format(time.RFC3339) + + var errs []error + for _, t := range targets { + switch t.Via { + case AddressTypeEmail: + if _, qerr := c.QueueEmail(ctx, buildEmail(t.Value, model, at)); qerr != nil { + m.r.Logger().WithError(qerr). + WithField("via", t.Via). + Warn("Failed to queue identity notification email.") + errs = append(errs, qerr) + } + case AddressTypeSMS: + if _, qerr := c.QueueSMS(ctx, buildSMS(t.Value, model, at)); qerr != nil { + m.r.Logger().WithError(qerr). + WithField("via", t.Via). + Warn("Failed to queue identity notification SMS.") + errs = append(errs, qerr) + } + default: + m.r.Logger(). + WithField("via", t.Via). + Warn("Skipping identity notification target with unsupported Via.") + } + } + + return stderrors.Join(errs...) +} + +// SendVerifiableAddressChangedNotifications queues a change notification to +// each target via the appropriate courier channel. Errors from individual +// targets are collected and returned as a joined error but do not +// short-circuit the batch — a failure to notify one recipient should not +// prevent others from receiving their notification. +func (m *Manager) SendVerifiableAddressChangedNotifications(ctx context.Context, targets []AddressRef, i *Identity) error { + return m.sendIdentityNotifications(ctx, "identity.Manager.SendVerifiableAddressChangedNotifications", targets, i, + func(to string, identity map[string]any, at string) courier.EmailTemplate { + return email.NewVerifiableAddressChanged(m.r, &email.VerifiableAddressChangedModel{To: to, Identity: identity, ChangedAt: at}) + }, + func(to string, identity map[string]any, at string) courier.SMSTemplate { + return sms.NewVerifiableAddressChanged(m.r, &sms.VerifiableAddressChangedModel{To: to, Identity: identity, ChangedAt: at}) + }, + ) +} + +// SendAuthenticatorKeyAddedNotifications queues a security notification to each +// target via the appropriate courier channel after a new authenticator key was +// enrolled or an existing key's secret was rotated. Errors from individual +// targets are collected and returned as a joined error but do not short-circuit +// the batch — callers must never fail the enrollment/rotate flow on a courier +// error; they should log and continue. +func (m *Manager) SendAuthenticatorKeyAddedNotifications(ctx context.Context, targets []AddressRef, i *Identity) error { + return m.sendIdentityNotifications(ctx, "identity.Manager.SendAuthenticatorKeyAddedNotifications", targets, i, + func(to string, identity map[string]any, at string) courier.EmailTemplate { + return email.NewAuthenticatorKeyAdded(m.r, &email.AuthenticatorKeyAddedModel{To: to, Identity: identity, AddedAt: at}) + }, + func(to string, identity map[string]any, at string) courier.SMSTemplate { + return sms.NewAuthenticatorKeyAdded(m.r, &sms.AuthenticatorKeyAddedModel{To: to, Identity: identity, AddedAt: at}) + }, + ) +} diff --git a/identity/manager_test.go b/identity/manager_test.go new file mode 100644 index 0000000..9b80119 --- /dev/null +++ b/identity/manager_test.go @@ -0,0 +1,1182 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/ory/herodot" + "github.com/ory/x/configx" + "github.com/ory/x/sqlcon" + + _ "embed" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/x" +) + +//go:embed stub/aal.json +var refreshAALStubs []byte + +func TestManager(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]interface{}{ + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRegistrationLoginHints: true, + config.ViperKeyDefaultIdentitySchemaID: "default", + }), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": "file://./stub/manager.schema.json", + "extension": "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(t.Context(), i)) + }) + + newTraits := func(email string, unprotected string) identity.Traits { + return identity.Traits(fmt.Sprintf(`{"email":"%[1]s","email_verify":"%[1]s","email_recovery":"%[1]s","email_creds":"%[1]s","unprotected": "%[2]s"}`, email, unprotected)) + } + + checkExtensionFields := func(i *identity.Identity, expected string) func(*testing.T) { + return func(t *testing.T) { + require.Len(t, i.VerifiableAddresses, 1) + assert.EqualValues(t, expected, i.VerifiableAddresses[0].Value) + assert.EqualValues(t, identity.AddressTypeEmail, i.VerifiableAddresses[0].Via) + + require.Len(t, i.RecoveryAddresses, 1) + assert.EqualValues(t, expected, i.RecoveryAddresses[0].Value) + assert.EqualValues(t, identity.AddressTypeEmail, i.RecoveryAddresses[0].Via) + + require.NotNil(t, i.Credentials[identity.CredentialsTypePassword]) + assert.Equal(t, []string{expected}, i.Credentials[identity.CredentialsTypePassword].Identifiers) + } + } + + checkExtensionFieldsForIdentities := func(t *testing.T, expected string, original *identity.Identity) { + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + identities := []identity.Identity{*original, *fromStore} + for k := range identities { + t.Run(fmt.Sprintf("identity=%d", k), checkExtensionFields(&identities[k], expected)) + } + } + + 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(t.Context(), []*identity.Identity{original})) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentity(t.Context(), 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(t.Context(), original)) + checkExtensionFieldsForIdentities(t, email, original) + got, ok := original.InternalAvailableAAL.ToAAL() + require.True(t, ok) + assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) + }) + + t.Run("case=correctly set AAL", func(t *testing.T) { + t.Run("case=should set AAL to 0 if no credentials are available", 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(t.Context(), original)) + got, ok := original.InternalAvailableAAL.ToAAL() + require.True(t, ok) + assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) + }) + + t.Run("case=should set AAL to 1 if password 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, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + got, ok := original.InternalAvailableAAL.ToAAL() + require.True(t, ok) + assert.Equal(t, identity.AuthenticatorAssuranceLevel1, got) + }) + + 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, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + identity.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), + }, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + got, ok := original.InternalAvailableAAL.ToAAL() + require.True(t, ok) + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) + }) + + 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, "") + original.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), + }, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + got, ok := original.InternalAvailableAAL.ToAAL() + require.True(t, ok) + 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(t.Context(), original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) + require.Error(t, err) + assert.Contains(t, err.Error(), "\"not an email\" is not valid \"email\"") + }) + + 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(t.Context(), original) + require.Error(t, err) + assert.NotContains(t, err.Error(), "\"not an email\" is not valid \"email\"") + }) + + t.Run("case=should correctly hint at the duplicate credential", func(t *testing.T) { + createIdentity := func(email string, field string, creds map[identity.CredentialsType]identity.Credentials) *identity.Identity { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(fmt.Sprintf(`{"%s":"%s"}`, field, email)) + i.Credentials = creds + return i + } + + t.Run("case=credential identifier duplicate", func(t *testing.T) { + t.Run("type=password", 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."}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(t.Context(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(t.Context(), second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.EqualValues(t, []string{identity.CredentialsTypePassword.String()}, verr.AvailableCredentials()) + assert.Len(t, verr.AvailableOIDCProviders(), 0) + assert.Equal(t, verr.IdentifierHint(), email) + }) + + t.Run("type=webauthn", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"credentials": [{"is_passwordless":true}]}`), + }, + } + + first := createIdentity(email, "email_webauthn", creds) + require.NoError(t, reg.IdentityManager().Create(t.Context(), first)) + + second := createIdentity(email, "email_webauthn", nil) + err := reg.IdentityManager().Create(t.Context(), second) + require.Error(t, err) + + 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(t.Context(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(t.Context(), 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()) + // The conflicting identifier is the oidc subject, which is not useful for the user + 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(t.Context(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(t.Context(), 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(t.Context(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(t.Context(), 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) { + t.Run("case=password duplicate", func(t *testing.T) { + // This test mimics a case where an existing user with email + password exists, and the + // new user tries to sign up with a verification email (NOT email + password) that matches + // this existing record. Here, the end result is that we want to show the + // user: "Sign up with email foo@bar.com and your password instead." + 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."}`), + }, + } + + first := createIdentity(email, field, creds) + require.NoError(t, reg.IdentityManager().Create(t.Context(), first)) + + second := createIdentity(email, field, nil) + err := reg.IdentityManager().Create(t.Context(), second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.EqualValues(t, []string{identity.CredentialsTypePassword.String()}, verr.AvailableCredentials()) + assert.Len(t, verr.AvailableOIDCProviders(), 0) + assert.Equal(t, verr.IdentifierHint(), email) + }) + + t.Run("case=OIDC duplicate", func(t *testing.T) { + // This test mimics a case where user signed up using Social Sign In exists, and the + // new user tries to sign up with a verification email (NOT email + password) that matches + // this existing record (for example by using another social sign in provider. + // Here, the end result is that we want to show "Sign in using google instead". + 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, field, creds) + require.NoError(t, reg.IdentityManager().Create(t.Context(), first)) + + second := createIdentity(email, field, nil) + err := reg.IdentityManager().Create(t.Context(), second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.EqualValues(t, []string{identity.CredentialsTypeOIDC.String()}, verr.AvailableCredentials()) + assert.EqualValues(t, verr.AvailableOIDCProviders(), []string{"github", "google"}) + assert.Equal(t, verr.IdentifierHint(), email) + }) + } + + t.Run("case=verifiable address", func(t *testing.T) { + runAddress(t, "email_verify") + }) + + t.Run("case=recovery address", func(t *testing.T) { + runAddress(t, "email_recovery") + }) + }) + }) + + t.Run("method=Update", func(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(t.Context(), original)) + + original.Traits = newTraits("bar@ory.sh", "") + require.NoError(t, reg.IdentityManager().Update(t.Context(), original, identity.ManagerAllowWriteProtectedTraits)) + + checkExtensionFieldsForIdentities(t, "bar@ory.sh", original) + }) + + t.Run("case=should reject credentials whose type does not match the map key", func(t *testing.T) { + // Safeguards the invariant from any caller that bypasses + // Identity.SetCredentials and writes to i.Credentials directly. + // The PATCH /admin/identities/{id} handler already enforces + // this; the Manager check is defense in depth. + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(email, "") + original.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }) + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + // Bypass SetCredentials and write a malformed entry whose + // Type does not match the map key. This is what the patch + // handler effectively does when it decodes a JSON Patch + // straight into the Credentials map. + original.Credentials[identity.CredentialsTypePassword] = identity.Credentials{ + Type: "", + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{}`), + } + + err := reg.IdentityManager().Update(t.Context(), original, identity.ManagerAllowWriteProtectedTraits) + require.Error(t, err) + var herr *herodot.DefaultError + require.True(t, errors.As(err, &herr), "expected *herodot.DefaultError, got %T: %v", err, err) + assert.Equal(t, http.StatusBadRequest, herr.CodeField) + assert.Equal(t, `credentials.password.type must equal "password", got ""`, herr.Reason()) + + // Confirm the persisted credential row is byte-identical. + reloaded, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + cred := reloaded.Credentials[identity.CredentialsTypePassword] + assert.Equal(t, identity.CredentialsTypePassword, cred.Type) + assert.Equal(t, []string{email}, cred.Identifiers) + assert.JSONEq(t, `{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`, string(cred.Config)) + }) + + t.Run("case=should not touch credentials excluded from the update", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(email, "") + original.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }) + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + fromDB, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + dbCred := fromDB.Credentials[identity.CredentialsTypePassword] + + // The in-memory password config diverges from the database, as if a + // concurrent locked credential-config write had won. The excluded + // type must survive the persist. + original.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"stale-in-memory"}`), + Version: dbCred.Version, + }) + original.Traits = newTraits(email, "updated") + + require.NoError(t, reg.IdentityManager().Update(t.Context(), original, + identity.ManagerAllowWriteProtectedTraits, + identity.ManagerWithoutCredentialTypes(identity.CredentialsTypePassword))) + + reloaded, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + cred := reloaded.Credentials[identity.CredentialsTypePassword] + assert.Equal(t, dbCred.ID, cred.ID, "excluded credential must not be recreated") + assert.JSONEq(t, string(dbCred.Config), string(cred.Config), "excluded credential config must not change") + assert.JSONEq(t, string(newTraits(email, "updated")), string(reloaded.Traits), "traits must still update") + // The identity now carries the database state of the excluded type. + assert.JSONEq(t, string(dbCred.Config), string(original.Credentials[identity.CredentialsTypePassword].Config)) + }) + + t.Run("case=should set AAL to 1 if password 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(t.Context(), original)) + original.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + } + require.NoError(t, reg.IdentityManager().Update(t.Context(), 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) { + 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, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.InternalAvailableAAL.String) + require.NoError(t, reg.IdentityManager().Update(t.Context(), 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(t.Context(), original, identity.ManagerAllowWriteProtectedTraits)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.InternalAvailableAAL.String) + }) + + 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(t.Context(), original)) + original.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), + }, + } + require.NoError(t, reg.IdentityManager().Update(t.Context(), 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(t.Context(), original)) + + original.Traits = newTraits("email-update-2@ory.sh", "") + err := reg.IdentityManager().Update(t.Context(), original) + require.Error(t, err) + assert.Equal(t, identity.ErrProtectedFieldModified(), errors.Cause(err)) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), 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. + checkExtensionFields(fromStore, "email-update-1@ory.sh")(t) + }) + + t.Run("case=should update unprotected traits with multiple credential identifiers", func(t *testing.T) { + original := identity.NewIdentity("extension") + original.Traits = identity.Traits(`{"email": "email-update-ewisdfuja@ory.sh", "names": ["username1", "username2"], "age": 30}`) + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + assert.Len(t, original.Credentials[identity.CredentialsTypePassword].Identifiers, 3) + + original.Traits = identity.Traits(`{"email": "email-update-ewisdfuja@ory.sh", "names": ["username1", "username2"], "age": 31}`) + require.NoError(t, reg.IdentityManager().Update(t.Context(), original)) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + assert.JSONEq(t, string(original.Traits), string(fromStore.Traits)) + }) + + t.Run("case=should update unprotected traits with verified user", func(t *testing.T) { + email := x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(email, "initial") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + // mock successful verification process + addr := original.VerifiableAddresses[0] + addr.Verified = true + addr.VerifiedAt = new(sqlxx.NullTime(time.Now().UTC())) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(t.Context(), &addr)) + + // reload to properly set the verified address + var err error + original, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + + original.Traits = newTraits(email, "updated") + require.NoError(t, reg.IdentityManager().Update(t.Context(), original)) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + assert.JSONEq(t, string(original.Traits), string(fromStore.Traits)) + }) + + t.Run("case=changing recovery address removes it from the store", func(t *testing.T) { + originalEmail := x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(originalEmail, "") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), 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(t.Context(), original, identity.ManagerAllowWriteProtectedTraits)) + + fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + checkExtensionFields(fromStore, newEmail)(t) + + recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(t.Context(), 0, 500) + require.NoError(t, err) + + var foundRecoveryAddress bool + for _, a := range recoveryAddresses { + assert.NotEqual(t, a.Value, originalEmail) + if a.Value == newEmail { + foundRecoveryAddress = true + } + } + require.True(t, foundRecoveryAddress) + + verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(t.Context(), 0, 500) + require.NoError(t, err) + var foundVerifiableAddress bool + for _, a := range verifiableAddresses { + assert.NotEqual(t, a.Value, originalEmail) + if a.Value == newEmail { + foundVerifiableAddress = true + } + } + require.True(t, foundVerifiableAddress) + }) + }) + + t.Run("method=CountActiveFirstFactorCredentials", func(t *testing.T) { + id := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + count, err := reg.IdentityManager().CountActiveFirstFactorCredentials(t.Context(), id) + require.NoError(t, err) + assert.Equal(t, 0, count) + + id.Credentials[identity.CredentialsTypePassword] = identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"foo"}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + } + + count, err = reg.IdentityManager().CountActiveFirstFactorCredentials(t.Context(), id) + require.NoError(t, err) + assert.Equal(t, 1, count) + }) + + t.Run("method=CountActiveMultiFactorCredentials", func(t *testing.T) { + id := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + count, err := reg.IdentityManager().CountActiveMultiFactorCredentials(t.Context(), id) + require.NoError(t, err) + assert.Equal(t, 0, count) + + id.Credentials[identity.CredentialsTypePassword] = identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"foo"}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + } + + count, err = reg.IdentityManager().CountActiveMultiFactorCredentials(t.Context(), id) + require.NoError(t, err) + assert.Equal(t, 0, count) + + id.Credentials[identity.CredentialsTypeWebAuthn] = identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{"foo"}, + Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), + } + + count, err = reg.IdentityManager().CountActiveMultiFactorCredentials(t.Context(), id) + require.NoError(t, err) + assert.Equal(t, 1, count) + }) + + t.Run("method=UpdateTraits", func(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(t.Context(), original)) + + require.NoError(t, reg.IdentityManager().UpdateTraits( + t.Context(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""), + identity.ManagerAllowWriteProtectedTraits)) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), 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. + checkExtensionFields(fromStore, "email-updatetraits-2@ory.sh")(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(t.Context(), original)) + + // These should all fail because they modify existing keys + require.Error(t, reg.IdentityManager().UpdateTraits(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), 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)) + }) + + 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(t.Context(), original)) + + err := reg.IdentityManager().UpdateTraits( + t.Context(), 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(t.Context(), 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. + checkExtensionFields(fromStore, "email-updatetraits-1@ory.sh")(t) + }) + + t.Run("case=should always update updated_at field", func(t *testing.T) { + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits("email-updatetraits-3@ory.sh", "") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + time.Sleep(time.Millisecond) + + require.NoError(t, reg.IdentityManager().UpdateTraits( + t.Context(), original.ID, newTraits("email-updatetraits-4@ory.sh", ""), + identity.ManagerAllowWriteProtectedTraits)) + + updated, err := reg.IdentityPool().GetIdentity(t.Context(), original.ID, identity.ExpandNothing) + require.NoError(t, err) + assert.NotEqual(t, original.UpdatedAt, updated.UpdatedAt, "UpdatedAt field should be updated") + }) + + t.Run("case=should update unprotected traits with verified user", func(t *testing.T) { + email := x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(email, "initial") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + // mock successful verification so VerifiedAt is non-nil + addr := original.VerifiableAddresses[0] + addr.Verified = true + addr.VerifiedAt = new(sqlxx.NullTime(time.Now().UTC())) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(t.Context(), &addr)) + + // UpdateTraits -> SetTraits -> deepcopy.Copy is the path that triggered the bug + require.NoError(t, reg.IdentityManager().UpdateTraits( + t.Context(), original.ID, newTraits(email, "updated"))) + + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), original.ID) + require.NoError(t, err) + assert.JSONEq(t, string(newTraits(email, "updated")), string(fromStore.Traits)) + }) + }) + + 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(t.Context(), 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(t.Context(), 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(t.Context(), id.ID, expand) + require.NoError(t, err) + require.NoError(t, reg.IdentityManager().RefreshAvailableAAL(t.Context(), actual)) + assert.NotEmpty(t, actual.Credentials) + assert.EqualValues(t, tc.Expected, actual.InternalAvailableAAL.String) + }) + } + }) + + t.Run("method=ConflictingIdentity", func(t *testing.T) { + conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(t.Context(), conflicOnIdentifier)) + + conflicOnVerifiableAddress := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnVerifiableAddress.Traits = identity.Traits(`{"email":"user-va@example.com", "email_verify":"conflict-on-va@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(t.Context(), conflicOnVerifiableAddress)) + + conflicOnRecoveryAddress := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnRecoveryAddress.Traits = identity.Traits(`{"email":"user-ra@example.com", "email_recovery":"conflict-on-ra@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(t.Context(), conflicOnRecoveryAddress)) + + t.Run("case=returns not found if no conflict", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"no-conflict@example.com"}}, + }, + }) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + assert.Nil(t, found) + assert.Empty(t, foundConflictAddress) + assert.Empty(t, addressType) + }) + + t.Run("case=conflict on identifier", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"conflict-on-identifier@example.com"}}, + }, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnIdentifier.ID, found.ID) + assert.Equal(t, "conflict-on-identifier@example.com", foundConflictAddress) + assert.EqualValues(t, string(identity.CredentialsTypePassword), addressType) + }) + + t.Run("case=conflict on verifiable address", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{{ + Value: "conflict-on-va@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnVerifiableAddress.ID, found.ID) + assert.Equal(t, "conflict-on-va@example.com", foundConflictAddress) + assert.Equal(t, "email", addressType) + }) + t.Run("case=conflict on recovery address", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + RecoveryAddresses: []identity.RecoveryAddress{{ + Value: "conflict-on-ra@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnRecoveryAddress.ID, found.ID) + assert.Equal(t, "conflict-on-ra@example.com", foundConflictAddress) + assert.Equal(t, "email", addressType) + }) + + t.Run("case=identifier match wins over verifiable and recovery address conflicts", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"conflict-on-identifier@example.com"}}, + }, + VerifiableAddresses: []identity.VerifiableAddress{{ + Value: "conflict-on-va@example.com", + Via: "email", + }}, + RecoveryAddresses: []identity.RecoveryAddress{{ + Value: "conflict-on-ra@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnIdentifier.ID, found.ID) + assert.Equal(t, "conflict-on-identifier@example.com", foundConflictAddress) + assert.EqualValues(t, string(identity.CredentialsTypePassword), addressType) + }) + + t.Run("case=verifiable address wins over recovery address", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{{ + Value: "conflict-on-va@example.com", + Via: "email", + }}, + RecoveryAddresses: []identity.RecoveryAddress{{ + Value: "conflict-on-ra@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnVerifiableAddress.ID, found.ID) + assert.Equal(t, "conflict-on-va@example.com", foundConflictAddress) + assert.Equal(t, "email", addressType) + }) + + t.Run("case=credential type order is deterministic", func(t *testing.T) { + oidcSubject := "google:" + x.NewUUID().String() + conflictOnOIDC := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflictOnOIDC.Traits = identity.Traits(`{"email":"conflict-on-oidc@example.com"}`) + conflictOnOIDC.SetCredentials(identity.CredentialsTypeOIDC, identity.Credentials{ + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{oidcSubject}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"google"}]}`), + }) + require.NoError(t, reg.IdentityManager().Create(t.Context(), conflictOnOIDC)) + + conflictOnPassword := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflictOnPassword.Traits = identity.Traits(`{"email":"conflict-on-password@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(t.Context(), conflictOnPassword)) + + // Both credentials match a different identity. The lookup must + // check credential types in sorted order, so "oidc" wins over + // "password" on every run. + for range 10 { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: {Identifiers: []string{oidcSubject}}, + identity.CredentialsTypePassword: {Identifiers: []string{"conflict-on-password@example.com"}}, + }, + }) + require.NoError(t, err) + assert.Equal(t, conflictOnOIDC.ID, found.ID) + assert.Equal(t, oidcSubject, foundConflictAddress) + assert.EqualValues(t, string(identity.CredentialsTypeOIDC), addressType) + } + }) + + t.Run("case=second identifier within one credential type matches", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"no-conflict@example.com", "conflict-on-identifier@example.com"}}, + }, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnIdentifier.ID, found.ID) + assert.Equal(t, "conflict-on-identifier@example.com", foundConflictAddress) + assert.EqualValues(t, string(identity.CredentialsTypePassword), addressType) + }) + + t.Run("case=empty identity returns no rows", func(t *testing.T) { + found, foundConflictAddress, addressType, err := reg.IdentityManager().ConflictingIdentity(t.Context(), &identity.Identity{}) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + assert.Nil(t, found) + assert.Empty(t, foundConflictAddress) + assert.Empty(t, addressType) + }) + + t.Run("case=canceled context propagates", func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + cancel() + _, _, _, err := reg.IdentityManager().ConflictingIdentity(ctx, &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"conflict-on-identifier@example.com"}}, + }, + }) + assert.ErrorIs(t, err, context.Canceled) + assert.NotErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) +} + +func TestManagerNoDefaultNamedSchema(t *testing.T) { + _, reg := pkg.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.com/", + })) + + t.Run("case=should create identity with default schema", func(t *testing.T) { + stateChangedAt := sqlxx.NullTime(time.Now().UTC()) + original := &identity.Identity{ + SchemaID: "", + Traits: []byte(identity.Traits(`{"email":"foo@ory.sh"}`)), + State: identity.StateActive, + StateChangedAt: &stateChangedAt, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + }) +} + +func TestManager_SendVerifiableAddressChangedNotifications(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]interface{}{ + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeyDefaultIdentitySchemaID: "default", + }), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": "file://./stub/manager.schema.json", + })), + ) + + t.Run("case=queues email and sms for supported targets", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"new@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + targets := []identity.AddressRef{ + {Value: "old@example.com", Via: identity.AddressTypeEmail}, + {Value: "+15551234567", Via: identity.AddressTypeSMS}, + } + + require.NoError(t, reg.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, targets, i)) + + messages, err := reg.CourierPersister().NextMessages(ctx, 10) + require.NoError(t, err) + require.Len(t, messages, 2) + + var gotEmail, gotSMS bool + for _, m := range messages { + switch m.Type { + case courier.MessageTypeEmail: + assert.Equal(t, "old@example.com", m.Recipient) + gotEmail = true + case courier.MessageTypeSMS: + assert.Equal(t, "+15551234567", m.Recipient) + gotSMS = true + } + } + assert.True(t, gotEmail, "expected an email message") + assert.True(t, gotSMS, "expected an SMS message") + }) + + t.Run("case=unsupported Via skipped", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"skip@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + require.NoError(t, reg.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, []identity.AddressRef{ + {Value: "fax:+123", Via: "fax"}, + }, i)) + + messages, err := reg.CourierPersister().NextMessages(ctx, 10) + if err == nil { + for _, m := range messages { + assert.NotEqual(t, "fax:+123", m.Recipient, "fax target must not be queued") + } + } + }) + + t.Run("case=empty targets is noop", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"noop@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + require.NoError(t, reg.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, nil, i)) + }) +} + +func TestManager_SendAuthenticatorKeyAddedNotifications(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]interface{}{ + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeyDefaultIdentitySchemaID: "default", + }), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": "file://./stub/manager.schema.json", + })), + ) + + t.Run("case=queues email and sms for supported targets", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"key-added@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + targets := []identity.AddressRef{ + {Value: "owner@example.com", Via: identity.AddressTypeEmail}, + {Value: "+15557654321", Via: identity.AddressTypeSMS}, + } + + require.NoError(t, reg.IdentityManager().SendAuthenticatorKeyAddedNotifications(ctx, targets, i)) + + messages, err := reg.CourierPersister().NextMessages(ctx, 10) + require.NoError(t, err) + require.Len(t, messages, 2) + + var gotEmail, gotSMS bool + for _, m := range messages { + assert.Equal(t, template.TypeAuthenticatorKeyAdded, m.TemplateType) + switch m.Type { + case courier.MessageTypeEmail: + assert.Equal(t, "owner@example.com", m.Recipient) + gotEmail = true + case courier.MessageTypeSMS: + assert.Equal(t, "+15557654321", m.Recipient) + gotSMS = true + } + } + assert.True(t, gotEmail, "expected an email message") + assert.True(t, gotSMS, "expected an SMS message") + }) + + t.Run("case=unsupported Via skipped", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"skip-key@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + require.NoError(t, reg.IdentityManager().SendAuthenticatorKeyAddedNotifications(ctx, []identity.AddressRef{ + {Value: "fax:+123", Via: "fax"}, + }, i)) + + messages, err := reg.CourierPersister().NextMessages(ctx, 10) + if err == nil { + for _, m := range messages { + assert.NotEqual(t, "fax:+123", m.Recipient, "fax target must not be queued") + } + } + }) + + t.Run("case=empty targets is noop", func(t *testing.T) { + ctx := t.Context() + i := identity.NewIdentity("default") + i.Traits = identity.Traits(`{"email":"noop-key@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + require.NoError(t, reg.IdentityManager().SendAuthenticatorKeyAddedNotifications(ctx, nil, i)) + }) +} diff --git a/identity/pending_traits_change.go b/identity/pending_traits_change.go new file mode 100644 index 0000000..f9f1ea2 --- /dev/null +++ b/identity/pending_traits_change.go @@ -0,0 +1,120 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/x/region" +) + +const ( + PendingTraitsChangeStatusPending = "pending" + PendingTraitsChangeStatusCompleted = "completed" +) + +// PendingTraitsChange stores a deferred traits update that is waiting +// for the new address to be verified before being applied to the identity. +// +// swagger:ignore +type PendingTraitsChange struct { + // ID is the unique identifier of this pending change. + ID uuid.UUID `json:"id" db:"id"` + + // IdentityID is the identity this change belongs to. + IdentityID uuid.UUID `json:"identity_id" db:"identity_id"` + + // IdentityRegion optionally carries the already-known region of the + // identity the change belongs to, so persisters can use it without + // re-reading the identity row. It is never persisted. + IdentityRegion region.Region `json:"-" db:"-" faker:"-"` + + // NID is the network ID (multi-tenant discriminator). + NID uuid.UUID `json:"-" db:"nid"` + + // NewAddressValue is the new address value being verified (e.g., "new@example.com"). + NewAddressValue string `json:"new_address_value" db:"new_address_value"` + + // NewAddressVia is the delivery channel for the new address ("email" or "sms"). + NewAddressVia string `json:"new_address_via" db:"new_address_via"` + + // OriginalTraitsHash is the SHA-256 hash of the identity's traits at the time + // the pending change was created. Used to detect concurrent modifications. + OriginalTraitsHash string `json:"original_traits_hash" db:"original_traits_hash"` + + // ProposedTraits is the full traits JSON snapshot to apply on successful verification. + ProposedTraits json.RawMessage `json:"proposed_traits" db:"proposed_traits"` + + // VerificationFlowID links this pending change to the verification flow that must complete. + VerificationFlowID uuid.UUID `json:"verification_flow_id" db:"verification_flow_id"` + + // Status is the current state: "pending" or "completed". + Status string `json:"status" db:"status"` + + // SessionID is the session that created this pending change. Used at apply + // time to verify the session is still active and match it to the current NID. + // Nullable: ON DELETE SET NULL in the schema — a NULL value means the + // session was revoked or deleted and the pending change must be rejected. + SessionID uuid.NullUUID `json:"session_id,omitempty" db:"session_id"` + + // OriginSettingsFlowID is the settings flow that produced this pending + // change. Used at apply time to load the real flow for the + // settings-post-persist webhook chain. Non-nullable in practice: the + // FK is declared ON DELETE CASCADE, so a missing flow deletes the PTC. + // A NULL value here means the FK was never set by a malformed writer + // and the pending change must be rejected. + OriginSettingsFlowID uuid.NullUUID `json:"origin_settings_flow_id,omitempty" db:"origin_settings_flow_id"` + + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +func (PendingTraitsChange) TableName() string { + return "identity_pending_traits_changes" +} + +func (p *PendingTraitsChange) ToPersistable() (*VerifiableAddress, bool) { + return nil, false +} + +func (p *PendingTraitsChange) Address() string { + return p.NewAddressValue +} + +func (p *PendingTraitsChange) DeliveryVia() string { + return p.NewAddressVia +} + +// HashTraits returns a hex-encoded SHA-256 hash of the JSON-compacted traits. +// Used to detect concurrent identity modifications between pending change +// creation and application. +func HashTraits(traits json.RawMessage) string { + var buf bytes.Buffer + if err := json.Compact(&buf, traits); err != nil { + h := sha256.Sum256(traits) + return hex.EncodeToString(h[:]) + } + h := sha256.Sum256(buf.Bytes()) + return hex.EncodeToString(h[:]) +} + +// PendingTraitsChangePersister handles CRUD for pending traits changes. +type PendingTraitsChangePersister interface { + CreatePendingTraitsChange(ctx context.Context, p *PendingTraitsChange) error + GetPendingTraitsChangeByVerificationFlow(ctx context.Context, flowID uuid.UUID) (*PendingTraitsChange, error) + DeletePendingTraitsChangesByIdentity(ctx context.Context, identityID uuid.UUID) error + UpdatePendingTraitsChange(ctx context.Context, p *PendingTraitsChange) error +} + +// PendingTraitsChangePersistenceProvider provides access to the persister. +type PendingTraitsChangePersistenceProvider interface { + PendingTraitsChangePersister() PendingTraitsChangePersister +} diff --git a/identity/pool.go b/identity/pool.go new file mode 100644 index 0000000..cef4dc0 --- /dev/null +++ b/identity/pool.go @@ -0,0 +1,271 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" + "github.com/ory/x/crdbx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/sqlxx" + + "github.com/gofrs/uuid" +) + +func NewUpdateIdentityOptions(opts []UpdateIdentityModifier) UpdateIdentityOptions { + var o UpdateIdentityOptions + for _, opt := range opts { + opt(&o) + } + return o +} + +// NewCreateIdentitiesOptions parses CreateIdentities modifiers. +func NewCreateIdentitiesOptions(opts []CreateIdentitiesModifier) *CreateIdentitiesOptions { + o := &CreateIdentitiesOptions{} + for _, opt := range opts { + opt(o) + } + return o +} + +// WithExtraColumns appends fixed (key, value) columns to a batch insert. +func WithExtraColumns(cols []ExtraColumn) CreateIdentitiesModifier { + return func(o *CreateIdentitiesOptions) { + o.ExtraColumns = append(o.ExtraColumns, cols...) + } +} + +// NewUpdateCredentialsConfigOptions parses UpdateCredentialsConfig modifiers. +func NewUpdateCredentialsConfigOptions(opts []UpdateCredentialsConfigModifier) *UpdateCredentialsConfigOptions { + o := &UpdateCredentialsConfigOptions{} + for _, opt := range opts { + opt(o) + } + return o +} + +// WithDerivedIdentifiers syncs the credential's identifier rows to the set +// derived from the post-mutation config, inside the same locked transaction. +// Like mutate, derive must be pure: it may run more than once on database +// retries. +func WithDerivedIdentifiers(derive func(newConfig []byte) ([]string, error)) UpdateCredentialsConfigModifier { + return func(o *UpdateCredentialsConfigOptions) { + o.DeriveIdentifiers = derive + } +} + +// DiffAgainst instructs UpdateIdentity to attempt a minimal update of the +// identity's data in the database by computing a diff against `existing` and +// only updating what is necessary, rather than bulk-replacing everything. Use +// with caution. If `existing` is different from what is stored in the database +// at the time of the update, the results are undefined. An error is returned if +// `existing` has a mismatching IdentityID or NID. +func DiffAgainst(existing *Identity) UpdateIdentityModifier { + return func(o *UpdateIdentityOptions) { + o.fromDatabase = existing + } +} + +func (o UpdateIdentityOptions) FromDatabase() *Identity { + return o.fromDatabase +} + +// WithoutCredentialTypes excludes the given credential types from the +// credential-association diff: their rows are neither deleted, recreated, +// nor updated by UpdateIdentity, and the returned identity carries their +// database state instead of the in-memory one. Use it for credential types +// whose rows are persisted through UpdateCredentialsConfig. +func WithoutCredentialTypes(cts ...CredentialsType) UpdateIdentityModifier { + return func(o *UpdateIdentityOptions) { + o.excludedCredentialTypes = append(o.excludedCredentialTypes, cts...) + } +} + +func (o UpdateIdentityOptions) ExcludedCredentialTypes() []CredentialsType { + return o.excludedCredentialTypes +} + +// WithUpdateExtraColumns appends fixed (key, value) columns to the inserts +// that UpdateIdentity performs for associated rows (addresses, credentials). +func WithUpdateExtraColumns(cols []ExtraColumn) UpdateIdentityModifier { + return func(o *UpdateIdentityOptions) { + o.extraColumns = append(o.extraColumns, cols...) + } +} + +func (o UpdateIdentityOptions) ExtraColumns() []ExtraColumn { + return o.extraColumns +} + +type ( + ListIdentityParameters struct { + Expand Expandables + IdsFilter []uuid.UUID + CredentialsIdentifier string + CredentialsIdentifierSimilar string + DeclassifyCredentials []CredentialsType + KeySetPagination []keysetpagination.Option + OrganizationID uuid.UUID + ConsistencyLevel crdbx.ConsistencyLevel + StatementTransformer func(string) string + + // ColumnsTransformer rewrites the SELECT column list to add extra + // columns the persister scans. Must be set together with RowScanner; + // the persister rejects the call if only one is provided. + ColumnsTransformer func(string) string + + // RowScanner replaces the default scan into []Identity to consume + // the extra columns added by ColumnsTransformer. Must be set together + // with ColumnsTransformer. + RowScanner func(con *pop.Connection, query string, args []any) ([]Identity, error) + + // DEPRECATED + PagePagination *x.Page + } + + UpdateIdentityModifier func(*UpdateIdentityOptions) + UpdateIdentityOptions struct { + fromDatabase *Identity + excludedCredentialTypes []CredentialsType + extraColumns []ExtraColumn + } + + // ExtraColumn carries a (key, value) pair for an extra SQL column on a + // batch insert (e.g. crdb_region) without coupling Identity to it. + ExtraColumn struct { + K string + V any + } + + CreateIdentitiesOptions struct { + ExtraColumns []ExtraColumn + } + + CreateIdentitiesModifier func(*CreateIdentitiesOptions) + + UpdateCredentialsConfigOptions struct { + ExtraColumns []ExtraColumn + DeriveIdentifiers func(newConfig []byte) ([]string, error) + } + + UpdateCredentialsConfigModifier func(*UpdateCredentialsConfigOptions) + + Pool interface { + // ListIdentities lists all identities in the store given the page and itemsPerPage. + ListIdentities(ctx context.Context, params ListIdentityParameters) ([]Identity, *keysetpagination.Paginator, error) + + // CountIdentities counts the number of identities in the store. + CountIdentities(ctx context.Context) (int64, error) + + // GetIdentity returns an identity by its id. Will return an error if the identity does not exist or backend + // connectivity is broken. + GetIdentity(context.Context, uuid.UUID, sqlxx.Expandables) (*Identity, error) + + // FindVerifiableAddressByValue returns a matching address or sql.ErrNoRows if no address could be found. + FindVerifiableAddressByValue(ctx context.Context, via, address string) (*VerifiableAddress, error) + + // FindRecoveryAddressByValue returns a matching address or sql.ErrNoRows if no address could be found. + FindRecoveryAddressByValue(ctx context.Context, via, address string) (*RecoveryAddress, error) + + // FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue finds the values of all recovery addresses for an identity if at least one of its recovery addresses matches the provided value. + FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) ([]string, error) + } + + PoolProvider interface { + IdentityPool() Pool + } + + PrivilegedPoolProvider interface { + PrivilegedIdentityPool() PrivilegedPool + } + + PrivilegedPool interface { + Pool + + // FindByCredentialsIdentifier returns an identity by querying for it's credential identifiers. + FindByCredentialsIdentifier(ctx context.Context, ct CredentialsType, match string) (*Identity, *Credentials, error) + + // DeleteIdentity removes an identity by its id. Will return an error + // 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, updateColumns ...string) error + + // CreateIdentity creates an identity. It is capable of setting credentials without encoding. Will return an error + // if identity exists, backend connectivity is broken, or trait validation fails. + CreateIdentity(context.Context, *Identity) error + + // CreateIdentities creates multiple identities. It is capable of setting credentials without encoding. Will return an error + // if identity exists, backend connectivity is broken, or trait validation fails. + CreateIdentities(ctx context.Context, identities []*Identity, opts ...CreateIdentitiesModifier) error + + // UpdateIdentity updates an identity including its confidential / privileged / protected data. + UpdateIdentity(context.Context, *Identity, ...UpdateIdentityModifier) error + + // UpdateIdentityColumns updates targeted columns of an identity. + UpdateIdentityColumns(ctx context.Context, i *Identity, columns ...string) error + + // UpdateCredentialsConfig atomically read-modify-writes a single + // identity_credentials row's config under an exclusive row lock: + // concurrent updates serialize and mutate always observes the latest + // committed config. Use it for credential-content decisions that must + // be mutually exclusive (PIN lockout counter, webauthn clone counter); + // wrap mutate with UpdateConfig for typed configs. + // mutate must be pure (it may run more than once on database retries); + // a structurally unchanged result skips the write; the version column + // (the config's schema version) is untouched. Calls inside a + // surrounding transaction are rejected. + // opts may narrow the row set with ExtraColumns (the cloud + // multi-region persister pins crdb_region) and sync the credential's + // identifier rows to the post-mutation config with + // WithDerivedIdentifiers. + UpdateCredentialsConfig(ctx context.Context, identityID uuid.UUID, ct CredentialsType, mutate func(config []byte) ([]byte, error), opts ...UpdateCredentialsConfigModifier) 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 + // or not. + ListVerifiableAddresses(ctx context.Context, page, itemsPerPage int) ([]VerifiableAddress, error) + + // ListRecoveryAddresses lists all tracked recovery addresses. + 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 + + // FindIdentityByCredentialIdentifier returns an identity by matching the identifier to any of the identity's credentials. + FindIdentityByCredentialIdentifier(ctx context.Context, identifier string, caseSensitive bool, expandables Expandables) (*Identity, error) + + // FindIdentityByWebauthnUserHandle returns an identity matching a webauthn user handle. + FindIdentityByWebauthnUserHandle(ctx context.Context, userHandle []byte) (*Identity, error) + + // FindIdentityByCredentialsIdentifier returns an identity by its external ID. + FindIdentityByExternalID(ctx context.Context, externalID string, expand sqlxx.Expandables) (*Identity, error) + } +) + +func (p ListIdentityParameters) TransformStatement(statement string) string { + if p.StatementTransformer != nil { + return p.StatementTransformer(statement) + } + return statement +} diff --git a/identity/pool_test.go b/identity/pool_test.go new file mode 100644 index 0000000..c9afc0a --- /dev/null +++ b/identity/pool_test.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" +) + +func TestCreateIdentitiesOptions_WithExtraColumns(t *testing.T) { + o := identity.NewCreateIdentitiesOptions([]identity.CreateIdentitiesModifier{ + identity.WithExtraColumns([]identity.ExtraColumn{{K: "crdb_region", V: "gcp-europe-west3"}}), + }) + got := o.ExtraColumns + require.Len(t, got, 1) + assert.Equal(t, "crdb_region", got[0].K) + assert.Equal(t, "gcp-europe-west3", got[0].V) +} diff --git a/identity/recovery_address_privileged_test.go b/identity/recovery_address_privileged_test.go new file mode 100644 index 0000000..81513e4 --- /dev/null +++ b/identity/recovery_address_privileged_test.go @@ -0,0 +1,143 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "fmt" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" +) + +// TestRecoveryAddressChangeRequiresPrivilegedSession reproduces HackerOne +// #3786482: the Settings-flow privileged-session gate compares only credentials +// and verifiable addresses, never recovery addresses. A non-privileged (stale) +// session could therefore change a victim's recovery address without re-auth, +// then drive account recovery to take over the account. +// +// The fix adds RecoveryAddressesEqual to identity.Manager.requiresPrivilegedAccess +// so that changing only a recovery address on a non-privileged session is +// rejected with identity.ErrProtectedFieldModified — the same protected-field +// path that already guards credentials and verifiable addresses. +// +// The "default" schema here maps email_recovery to a recovery address ONLY (no +// verification extension), so changing email_recovery changes the recovery +// address without touching the verifiable address. This isolates the recovery +// address as the single changed protected field. +func TestRecoveryAddressChangeRequiresPrivilegedSession(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeyDefaultIdentitySchemaID: "default", + }), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": "file://./stub/manager.schema.json", + })), + ) + + // traits builds a trait set where the recovery address (email_recovery) can + // be varied independently of the verifiable address (email_verify) and the + // login identifiers (email, email_creds). + traits := func(identifier, recovery string) identity.Traits { + return identity.Traits(fmt.Sprintf( + `{"email":%[1]q,"email_creds":%[1]q,"email_verify":%[1]q,"email_recovery":%[2]q}`, + identifier, recovery, + )) + } + + recoveryAddressValues := func(t *testing.T, i *identity.Identity) []string { + t.Helper() + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), i.ID) + require.NoError(t, err) + values := make([]string, 0, len(fromStore.RecoveryAddresses)) + for _, a := range fromStore.RecoveryAddresses { + values = append(values, a.Value) + } + return values + } + + t.Run("case=non-privileged session must not change a recovery address", func(t *testing.T) { + identifier := "victim-" + x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = traits(identifier, "original-recovery@ory.sh") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + require.Equal(t, []string{"original-recovery@ory.sh"}, recoveryAddressValues(t, original)) + + // Attacker (non-privileged session) changes ONLY the recovery address. + // The login identifier and verifiable address are unchanged. + original.Traits = traits(identifier, "attacker-recovery@ory.sh") + err := reg.IdentityManager().Update(t.Context(), original) + require.Error(t, err, "changing a recovery address without a privileged session must be rejected") + assert.Equal(t, identity.ErrProtectedFieldModified(), errors.Cause(err)) + + // The stored recovery address must be unchanged. + assert.Equal(t, []string{"original-recovery@ory.sh"}, recoveryAddressValues(t, original), + "the recovery address must not have been modified by a non-privileged session") + }) + + t.Run("case=privileged session may change a recovery address", func(t *testing.T) { + identifier := "owner-" + x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = traits(identifier, "owner-recovery@ory.sh") + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + original.Traits = traits(identifier, "new-owner-recovery@ory.sh") + require.NoError(t, reg.IdentityManager().Update(t.Context(), original, identity.ManagerAllowWriteProtectedTraits)) + + assert.Equal(t, []string{"new-owner-recovery@ory.sh"}, recoveryAddressValues(t, original)) + }) + + t.Run("case=non-privileged session may still update unprotected traits", func(t *testing.T) { + // Guard against the fix over-blocking: an update that does not touch the + // recovery address (or any protected field) must continue to succeed on a + // non-privileged session. + identifier := "stable-" + x.NewUUID().String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = identity.Traits(fmt.Sprintf( + `{"email":%[1]q,"email_recovery":%[1]q,"unprotected":"before"}`, identifier)) + require.NoError(t, reg.IdentityManager().Create(t.Context(), original)) + + original.Traits = identity.Traits(fmt.Sprintf( + `{"email":%[1]q,"email_recovery":%[1]q,"unprotected":"after"}`, identifier)) + require.NoError(t, reg.IdentityManager().Update(t.Context(), original), + "updating an unprotected trait must not require a privileged session") + }) +} + +// TestRecoveryAddressesEqual covers the new order-independent comparison used by +// the privileged-access gate, mirroring VerifiableAddressesEqual. +func TestRecoveryAddressesEqual(t *testing.T) { + t.Parallel() + + id := x.NewUUID() + a := identity.RecoveryAddress{Value: "a@ory.sh", Via: identity.AddressTypeEmail, IdentityID: id} + b := identity.RecoveryAddress{Value: "b@ory.sh", Via: identity.AddressTypeEmail, IdentityID: id} + + for _, tc := range []struct { + name string + original, update []identity.RecoveryAddress + equal bool + }{ + {name: "both empty", original: nil, update: nil, equal: true}, + {name: "identical single", original: []identity.RecoveryAddress{a}, update: []identity.RecoveryAddress{a}, equal: true}, + {name: "different value", original: []identity.RecoveryAddress{a}, update: []identity.RecoveryAddress{b}, equal: false}, + {name: "added address", original: []identity.RecoveryAddress{a}, update: []identity.RecoveryAddress{a, b}, equal: false}, + {name: "removed address", original: []identity.RecoveryAddress{a, b}, update: []identity.RecoveryAddress{a}, equal: false}, + {name: "different via", original: []identity.RecoveryAddress{{Value: "a@ory.sh", Via: identity.AddressTypeEmail, IdentityID: id}}, update: []identity.RecoveryAddress{{Value: "a@ory.sh", Via: identity.AddressTypeSMS, IdentityID: id}}, equal: false}, + } { + t.Run("case="+tc.name, func(t *testing.T) { + assert.Equal(t, tc.equal, identity.RecoveryAddressesEqual(tc.original, tc.update)) + }) + } +} diff --git a/identity/registry.go b/identity/registry.go new file mode 100644 index 0000000..766674b --- /dev/null +++ b/identity/registry.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "net/url" +) + +type Registry interface { + IdentityPool() Pool +} + +type Configuration interface { + SelfAdminURL() *url.URL + DefaultIdentityTraitsSchemaURL() *url.URL +} diff --git a/identity/stub/aal.json b/identity/stub/aal.json new file mode 100644 index 0000000..bb206c5 --- /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/expand.schema.json b/identity/stub/expand.schema.json new file mode 100644 index 0000000..5cb8a8e --- /dev/null +++ b/identity/stub/expand.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "traits": { + "additionalProperties": false, + "properties": { + "email": { + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + }, + "title": "Email address", + "type": "string", + "maxLength": 320 + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "email", + "name" + ], + "type": "object" + } + }, + "title": "Person", + "type": "object" +} diff --git a/identity/stub/extension.schema.json b/identity/stub/extension.schema.json new file mode 100644 index 0000000..512854c --- /dev/null +++ b/identity/stub/extension.schema.json @@ -0,0 +1,49 @@ +{ + "$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": { + "password": { + "identifier": true + } + } + } + }, + "names": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "email" + ] + } + }, + "required": [ + "traits" + ], + "additionalProperties": false +} diff --git a/identity/stub/extension/credentials/code-no-validate.schema.json b/identity/stub/extension/credentials/code-no-validate.schema.json new file mode 100644 index 0000000..d33dbc5 --- /dev/null +++ b/identity/stub/extension/credentials/code-no-validate.schema.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "no-validate", + "pattern": "^(?:[a-zA-Z0-9!#$%&'*+\\=?^_`{|}~.\\-]+@(?:docomo\\.ne\\.jp|softbank\\.ne\\.jp)|[a-zA-Z0-9!#$%&'*+\\=?^_`{|}~\\-]+(?:\\.[a-zA-Z0-9!#$%&'*+\\=?^_`{|}~\\-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)$", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } +} 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 0000000..4e51a5a --- /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/code.schema.json b/identity/stub/extension/credentials/code.schema.json new file mode 100644 index 0000000..bef244b --- /dev/null +++ b/identity/stub/extension/credentials/code.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } +} diff --git a/identity/stub/extension/credentials/email.schema.json b/identity/stub/extension/credentials/email.schema.json new file mode 100644 index 0000000..848bfe5 --- /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/stub/extension/credentials/multi.schema.json b/identity/stub/extension/credentials/multi.schema.json new file mode 100644 index 0000000..43ba04c --- /dev/null +++ b/identity/stub/extension/credentials/multi.schema.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } +} diff --git a/identity/stub/extension/credentials/schema.json b/identity/stub/extension/credentials/schema.json new file mode 100644 index 0000000..07069a7 --- /dev/null +++ b/identity/stub/extension/credentials/schema.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } +} \ No newline at end of file diff --git a/identity/stub/extension/credentials/webauthn.schema.json b/identity/stub/extension/credentials/webauthn.schema.json new file mode 100644 index 0000000..63e0759 --- /dev/null +++ b/identity/stub/extension/credentials/webauthn.schema.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + } +} diff --git a/identity/stub/extension/recovery/email.schema.json b/identity/stub/extension/recovery/email.schema.json new file mode 100644 index 0000000..044aece --- /dev/null +++ b/identity/stub/extension/recovery/email.schema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/recovery/no-validate.schema.json b/identity/stub/extension/recovery/no-validate.schema.json new file mode 100644 index 0000000..f3c523a --- /dev/null +++ b/identity/stub/extension/recovery/no-validate.schema.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "no-validate", + "pattern": "^[a-zA-Z0-9!#$%&'*+\\=?^_`{|}~.\\-]+@docomo\\.ne\\.jp$", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/recovery/schema.json b/identity/stub/extension/recovery/schema.json new file mode 100644 index 0000000..044aece --- /dev/null +++ b/identity/stub/extension/recovery/schema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/recovery/sms.schema.json b/identity/stub/extension/recovery/sms.schema.json new file mode 100644 index 0000000..b57e243 --- /dev/null +++ b/identity/stub/extension/recovery/sms.schema.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "telephoneNumber": { + "type": "string", + "format": "tel", + "title": "Telephone Number", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "sms" + } + }, + "verification": { + "via": "sms" + }, + "recovery": { + "via": "sms" + } + } + } + }, + "required": [ + "telephoneNumber" + ], + "additionalProperties": false +} diff --git a/identity/stub/extension/verify/email.schema.json b/identity/stub/extension/verify/email.schema.json new file mode 100644 index 0000000..9906957 --- /dev/null +++ b/identity/stub/extension/verify/email.schema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/verify/legacy-email-missing-format.schema.json b/identity/stub/extension/verify/legacy-email-missing-format.schema.json new file mode 100644 index 0000000..50d7c5f --- /dev/null +++ b/identity/stub/extension/verify/legacy-email-missing-format.schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/verify/missing-format.schema.json b/identity/stub/extension/verify/missing-format.schema.json new file mode 100644 index 0000000..b371612 --- /dev/null +++ b/identity/stub/extension/verify/missing-format.schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "phone": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } +} diff --git a/identity/stub/extension/verify/no-validate.schema.json b/identity/stub/extension/verify/no-validate.schema.json new file mode 100644 index 0000000..d92bb04 --- /dev/null +++ b/identity/stub/extension/verify/no-validate.schema.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "noformat", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } +} diff --git a/identity/stub/extension/verify/phone.schema.json b/identity/stub/extension/verify/phone.schema.json new file mode 100644 index 0000000..6b273dc --- /dev/null +++ b/identity/stub/extension/verify/phone.schema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "phones": { + "type": "array", + "items": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + }, + "username": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } +} diff --git a/identity/stub/handler/customer.schema.json b/identity/stub/handler/customer.schema.json new file mode 100644 index 0000000..73a0e8a --- /dev/null +++ b/identity/stub/handler/customer.schema.json @@ -0,0 +1,33 @@ +{ + "$id": "https://example.com/customer.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "additionalProperties": false, + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/identity/stub/handler/employee.schema.json b/identity/stub/handler/employee.schema.json new file mode 100644 index 0000000..667962a --- /dev/null +++ b/identity/stub/handler/employee.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://example.com/employee.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "department": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/identity/stub/handler/multiple_emails.schema.json b/identity/stub/handler/multiple_emails.schema.json new file mode 100644 index 0000000..5905f36 --- /dev/null +++ b/identity/stub/handler/multiple_emails.schema.json @@ -0,0 +1,40 @@ +{ + "$id": "https://example.com/customer.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/identity/stub/identity-2.schema.json b/identity/stub/identity-2.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/identity/stub/identity-2.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/identity/stub/identity.schema.json b/identity/stub/identity.schema.json new file mode 100644 index 0000000..0337894 --- /dev/null +++ b/identity/stub/identity.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/identity/stub/localhost-ref.schema.json b/identity/stub/localhost-ref.schema.json new file mode 100644 index 0000000..bc8eaa3 --- /dev/null +++ b/identity/stub/localhost-ref.schema.json @@ -0,0 +1,19 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "$ref": "http://192.168.178.1:1234/" + } + } + } + } +} diff --git a/identity/stub/manager.schema.json b/identity/stub/manager.schema.json new file mode 100644 index 0000000..2b7b6ef --- /dev/null +++ b/identity/stub/manager.schema.json @@ -0,0 +1,70 @@ +{ + "$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": { + "password": { + "identifier": true + } + } + } + }, + "email_creds": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "email_webauthn": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "webauthn": { + "identifier": true + } + } + } + }, + "email_verify": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + }, + "email_recovery": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "recovery": { + "via": "email" + } + } + }, + "unprotected": { + "type": "string" + } + }, + "required": [ + ] + } + }, + "additionalProperties": false +} diff --git a/identity/stub/webauthn/v0.json b/identity/stub/webauthn/v0.json new file mode 100644 index 0000000..4017734 --- /dev/null +++ b/identity/stub/webauthn/v0.json @@ -0,0 +1,28 @@ +{ + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z" + }, + { + "id": "1Q4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z" + } + ] +} diff --git a/identity/stub/webauthn/v1.json b/identity/stub/webauthn/v1.json new file mode 100644 index 0000000..cdb8720 --- /dev/null +++ b/identity/stub/webauthn/v1.json @@ -0,0 +1,18 @@ +{ + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": true + } + ], + "user_handle":"2gZaSs9fTEeGmsBlC4gfgg==" +} diff --git a/identity/validator.go b/identity/validator.go new file mode 100644 index 0000000..feb9746 --- /dev/null +++ b/identity/validator.go @@ -0,0 +1,265 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "context" + "maps" + "slices" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + "github.com/ory/x/otelx" +) + +type ( + validatorDependencies interface { + schema.IdentitySchemaProvider + config.Provider + } + Validator struct { + v *schema.Validator + d validatorDependencies + } + ValidationProvider interface { + IdentityValidator() *Validator + } +) + +func NewValidator(d validatorDependencies) *Validator { + return &Validator{v: schema.NewValidator(), d: d} +} + +func (v *Validator) ValidateWithRunner(ctx context.Context, i *Identity, runners ...schema.ValidateExtension) error { + runner, err := schema.NewExtensionRunner(ctx, schema.WithValidateRunners(runners...)) + if err != nil { + return err + } + + ss, err := v.d.IdentityTraitsSchemas(ctx) + if err != nil { + return err + } + + s, err := ss.GetByID(i.SchemaID) + if err != nil { + return err + } + + if len(i.Traits) == 0 { + i.Traits = []byte(`{}`) + } + + if err := v.normalizePhoneTraits(ctx, i, s.URL.String()); err != nil { + return err + } + + traits, err := sjson.SetRawBytes([]byte(`{}`), "traits", i.Traits) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithError(err.Error())) + } + + return v.v.Validate(ctx, s.URL.String(), traits, + schema.WithExtensionRunner(runner), + schema.WithDisallowRefs(v.d.Config().SecurityDisallowRefInIdentitySchemas(ctx)), + ) +} + +// normalizePhoneTraits rewrites trait values used as phone-channel +// identifiers (code+sms credential identifiers, recovery via sms, or +// verification via sms) into the E.164 form Kratos stores in its side +// tables. Without this, webhook payloads templated against +// `identity.traits` see the raw user input while Kratos keys on the +// normalized identifier, producing inconsistent values across systems. +// +// Email-channel identifiers preserve case per the regression test for +// https://github.com/ory/kratos/issues/3187. +// +// Schema-load and compile errors are swallowed; the subsequent +// `v.v.Validate` call uses the same loader and compiler wiring and +// re-surfaces the same error with full herodot context. Context +// cancellation, however, must propagate so callers see the cancellation +// instead of a generic schema error from the second compile. +// +// WORKAROUND: this compiles the schema a second time and walks it from +// scratch because github.com/ory/jsonschema/v3 does not expose the JSON +// pointer of the value being validated to extension hooks. Once we move +// to santhosh-tekuri/jsonschema/v6, which provides +// ValidatorContext.ValueLocation(), this and walkPhoneTraits collapse +// into a single ValidateExtension that writes back to i.Traits inline. +// Tracking: ~/.claude/docs/cloud/plans/jsonschema-v6-migration.md. +func (v *Validator) normalizePhoneTraits(ctx context.Context, i *Identity, schemaURL string) error { + swallow := func(err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + return nil + } + + compiler, err := schema.NewCompilerWithURL(ctx, schemaURL, + v.d.Config().SecurityDisallowRefInIdentitySchemas(ctx)) + if err != nil { + return swallow(err) + } + // Register populates each compiled *Schema's Extensions map with the + // parsed *ExtensionConfig that walkPhoneTraits reads. Without this + // call, the walk sees nil extensions and never normalizes. + runner, err := schema.NewExtensionRunner(ctx) + if err != nil { + return swallow(err) + } + runner.Register(compiler) + + compiled, err := compiler.Compile(ctx, schemaURL) + if err != nil { + return swallow(err) + } + + i.Traits = walkPhoneTraits(i.Traits, compiled.Properties["traits"], "", map[*jsonschema.Schema]struct{}{}) + return nil +} + +// walkPhoneTraits traverses the compiled schema tree and rewrites every +// trait leaf marked as a phone-channel identifier to its E.164 form. +// +// Properties extend the path with the property name; array items extend +// it with the index. Combinators (allOf, anyOf, oneOf, if/then/else, +// not, $ref) sit at the same path. PatternProperties and contains are +// not walked: identity schemas in practice keep credential identifiers +// on named or indexed positions. +// +// `stack` holds the schemas currently on the recursion path so true +// cycles short-circuit without preventing legitimate DAG reuse — +// e.g. two trait properties that share a `$ref` definition each get +// walked because the shared node is removed from the stack when the +// first walk returns. Pure-$ref cycles are already rejected by +// schema/prevalidate.go, but cycles with intermediate validation +// (a $ref back to an ancestor that has `properties`) reach the walk +// and need this guard. +func walkPhoneTraits(traits Traits, node *jsonschema.Schema, path string, stack map[*jsonschema.Schema]struct{}) Traits { + if node == nil { + return traits + } + if _, onStack := stack[node]; onStack { + return traits + } + stack[node] = struct{}{} + defer delete(stack, node) + + if isPhoneIdentifier(node) { + return normalizePhoneAt(traits, path) + } + + // Combinators evaluated at the same path. + for _, sub := range slices.Concat(node.AllOf, node.AnyOf, node.OneOf, + []*jsonschema.Schema{node.Not, node.If, node.Then, node.Else, node.Ref}) { + traits = walkPhoneTraits(traits, sub, path, stack) + } + + // Properties extend the path. Iterate in sorted order so the + // rewrite sequence is deterministic and tests are reproducible. + for _, name := range slices.Sorted(maps.Keys(node.Properties)) { + next := escapePathSegment(name) + if path != "" { + next = path + "." + next + } + traits = walkPhoneTraits(traits, node.Properties[name], next, stack) + } + + // Array items. `Items` is nil, *Schema (homogeneous — every element + // validates against the same schema), or []*Schema (tuple — index N + // validates against the Nth schema). For homogeneous items we walk + // against the actual data because schema length is unbounded; for + // tuple items the schema length defines the slots to walk. + switch items := node.Items.(type) { + case *jsonschema.Schema: + index := 0 + gjson.GetBytes(traits, path).ForEach(func(_, _ gjson.Result) bool { + next := strconv.Itoa(index) + if path != "" { + next = path + "." + next + } + traits = walkPhoneTraits(traits, items, next, stack) + index++ + return true + }) + case []*jsonschema.Schema: + for i, item := range items { + next := strconv.Itoa(i) + if path != "" { + next = path + "." + next + } + traits = walkPhoneTraits(traits, item, next, stack) + } + } + return traits +} + +// normalizePhoneAt rewrites the string trait at `path` to its E.164 +// form when present and parseable. It returns the input unchanged on +// any miss so the caller can chain it through a walk. +func normalizePhoneAt(traits Traits, path string) Traits { + value := gjson.GetBytes(traits, path) + if value.Type != gjson.String { + return traits + } + normalized, err := x.NormalizeIdentifier(value.String(), "sms") + if err != nil || normalized == value.String() { + return traits + } + if updated, err := sjson.SetBytes(traits, path, normalized); err == nil { + return updated + } + return traits +} + +// escapePathSegment escapes characters that gjson/sjson treat as path +// metacharacters so a property like "phone.number" is addressed as the +// single key "phone.number" rather than as nested `phone` → `number`. +func escapePathSegment(segment string) string { + if !strings.ContainsAny(segment, `.*?\`) { + return segment + } + var b strings.Builder + b.Grow(len(segment) + 4) + for _, r := range segment { + switch r { + case '.', '*', '?', '\\': + b.WriteByte('\\') + } + b.WriteRune(r) + } + return b.String() +} + +// isPhoneIdentifier reports whether any identity-extension hook would +// normalize the value at this schema node as an E.164 phone number. +func isPhoneIdentifier(node *jsonschema.Schema) bool { + cfg, _ := node.Extensions[schema.ExtensionName].(*schema.ExtensionConfig) + if cfg == nil { + return false + } + return (cfg.Credentials.Code.Identifier && cfg.Credentials.Code.Via == "sms") || + cfg.Recovery.Via == "sms" || + cfg.Verification.Via == "sms" +} + +func (v *Validator) Validate(ctx context.Context, i *Identity) error { + return otelx.WithSpan(ctx, "identity.Validator.Validate", func(ctx context.Context) error { + return v.ValidateWithRunner(ctx, i, + NewSchemaExtensionCredentials(i), + NewSchemaExtensionVerification(i, v.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx)), + NewSchemaExtensionRecovery(i), + ) + }) +} diff --git a/identity/validator_test.go b/identity/validator_test.go new file mode 100644 index 0000000..f99e020 --- /dev/null +++ b/identity/validator_test.go @@ -0,0 +1,566 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3/httploader" + "github.com/ory/kratos/driver/config" + . "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/x/configx" + "github.com/ory/x/httpx" +) + +func TestSchemaValidatorDisallowsInternalNetworkRequests(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "localhost": "https://localhost/schema/whatever", + "privateRef": "file://stub/localhost-ref.schema.json", + "inlineRef": "base64://" + base64.StdEncoding.EncodeToString([]byte(`{"traits": {}}`)), + })), + configx.WithValue(config.ViperKeyClientHTTPNoPrivateIPRanges, true), + ) + + v := NewValidator(reg) + + for id, expectedErr := range map[string]string{ + "localhost": "no such host", + "privateRef": "no route to host", + "inlineRef": "", + } { + t.Run(id, func(t *testing.T) { + t.Parallel() + + i := &Identity{ + SchemaID: id, + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + } + ctx := context.WithValue(t.Context(), httploader.ContextKey, reg.HTTPClient(t.Context())) + err := v.Validate(ctx, i) + if expectedErr == "" { + assert.NoError(t, err) + return + } + + var hErr *herodot.DefaultError + require.ErrorAs(t, err, &hErr) + assert.Contains(t, hErr.Debug(), expectedErr) + }) + } +} + +func TestSchemaValidator(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + router := http.NewServeMux() + router.HandleFunc("GET /schema/{name}", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "` + r.PathValue("name") + `": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +}`)) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": ts.URL + "/schema/firstName", + "whatever": ts.URL + "/schema/whatever", + "unreachable-url": ts.URL + "/404-not-found", + })), + ) + v := NewValidator(reg) + + for k, tc := range []struct { + i *Identity + err string + }{ + { + i: &Identity{ + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + }, + }, + { + i: &Identity{ + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": -1 }`), + }, + err: "I[#/traits/age] S[#/properties/traits/properties/age/minimum] must be >= 1 but found -1", + }, + { + i: &Identity{ + Traits: Traits(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + }, + err: `I[#/traits] S[#/properties/traits/additionalProperties] additionalProperties "whatever" not allowed`, + }, + { + i: &Identity{ + SchemaID: "whatever", + Traits: Traits(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + }, + }, + { + i: &Identity{ + SchemaID: "whatever", + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + }, + err: `I[#/traits] S[#/properties/traits/additionalProperties] additionalProperties "firstName" not allowed`, + }, + { + i: &Identity{ + SchemaID: "unreachable-url", + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + }, + err: "Invalid configuration", + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctx := context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + err := v.Validate(ctx, tc.i) + if tc.err == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.err) + } + }) + } +} + +// TestSchemaValidatorNormalizesIdentifierTraits verifies that trait values +// used as credential, recovery, or verification identifiers are normalized +// to the same canonical form Kratos stores in the side tables. This keeps +// webhook payloads consistent with the value Kratos uses internally. +// +// Regression test for https://github.com/ory-corp/cloud/issues/11739. +func TestSchemaValidatorNormalizesIdentifierTraits(t *testing.T) { + t.Parallel() + + router := http.NewServeMux() + router.HandleFunc("GET /schema/code-sms", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-sms.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/recovery-sms", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/recovery-sms.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "my.phone.number": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "recovery": { + "via": "sms" + } + } + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/verification-sms", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/verification-sms.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/verification-sms-array", func(w http.ResponseWriter, _ *http.Request) { + // Array of phones, each element a verification+sms identifier. + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/verification-sms-array.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phones": { + "type": "array", + "items": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/code-sms-shared-ref", func(w http.ResponseWriter, _ *http.Request) { + // Two trait properties share the same $defs/PhoneIdentifier + // definition. With recursion-stack semantics both must + // normalize; with a one-shot visited-set only the first would. + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-sms-shared-ref.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "PhoneIdentifier": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + }, + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "primary_phone": { "$ref": "#/$defs/PhoneIdentifier" }, + "secondary_phone": { "$ref": "#/$defs/PhoneIdentifier" } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/code-email", func(w http.ResponseWriter, _ *http.Request) { + // Email-channel identifier: trait must NOT be normalized + // (case-preservation is asserted by ory/kratos#3187). + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-email.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/code-sms-recursive", func(w http.ResponseWriter, _ *http.Request) { + // Self-referential schema: `Person` has a `spouse` property + // pointing back to `Person`. Without the recursion-stack guard + // in walkPhoneTraits, the walk would loop forever; with it the + // walk visits `phone` and `spouse.phone`, then stops. + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-sms-recursive.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "Person": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + }, + "spouse": { "$ref": "#/$defs/Person" } + } + } + }, + "type": "object", + "properties": { + "traits": { "$ref": "#/$defs/Person" } + } +}`)) + }) + router.HandleFunc("GET /schema/code-sms-anyof", func(w http.ResponseWriter, _ *http.Request) { + // Sibling combinator coverage: the production code lumps + // anyOf/oneOf/allOf into the same slices.Concat, but only + // allOf is otherwise exercised. Drop the wrong key from the + // concat and this test catches it. + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-sms-anyof.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "anyOf": [ + { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + ] + } + } + } + } +}`)) + }) + router.HandleFunc("GET /schema/code-sms-allof", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{ + "$id": "https://example.com/code-sms-allof.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "allOf": [ + { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + ] + } + } + } + } +}`)) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "code-sms": ts.URL + "/schema/code-sms", + "recovery-sms": ts.URL + "/schema/recovery-sms", + "verification-sms": ts.URL + "/schema/verification-sms", + "verification-sms-array": ts.URL + "/schema/verification-sms-array", + "code-sms-allof": ts.URL + "/schema/code-sms-allof", + "code-sms-anyof": ts.URL + "/schema/code-sms-anyof", + "code-sms-recursive": ts.URL + "/schema/code-sms-recursive", + "code-sms-shared-ref": ts.URL + "/schema/code-sms-shared-ref", + "code-email": ts.URL + "/schema/code-email", + })), + ) + v := NewValidator(reg) + + for _, tc := range []struct { + name string + schemaID string + input string + wantTraits string + wantErr string + }{ + { + name: "credential identifier code+sms normalizes phone in traits", + schemaID: "code-sms", + input: `{"phone":"+49 176 671 11 638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + name: "credential identifier code+sms normalizes already E.164 phone idempotently", + schemaID: "code-sms", + input: `{"phone":"+4917667111638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + name: "recovery via sms normalizes phone in traits", + schemaID: "recovery-sms", + input: `{"my.phone.number":"+49 176 671 11 638"}`, + wantTraits: `{"my.phone.number":"+4917667111638"}`, + }, + { + name: "verification via sms normalizes phone in traits", + schemaID: "verification-sms", + input: `{"phone":"+49 176 671 11 638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + name: "phone identifier nested under allOf is normalized", + schemaID: "code-sms-allof", + input: `{"phone":"+49 176 671 11 638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + name: "phone identifier nested under anyOf is normalized", + schemaID: "code-sms-anyof", + input: `{"phone":"+49 176 671 11 638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + // Cycle guard: a self-referential schema (Person.spouse + // → Person) must not send walkPhoneTraits into infinite + // recursion. The top-level phone still normalizes; the + // schema walk hits the spouse → Person cycle and + // short-circuits without exploding the stack. A hang or + // stack overflow here means the recursion guard regressed. + name: "recursive schema terminates and normalizes the top-level phone", + schemaID: "code-sms-recursive", + input: `{"phone":"+49 176 671 11 638"}`, + wantTraits: `{"phone":"+4917667111638"}`, + }, + { + // Array elements at `phones[*]` are configured as + // verification+sms identifiers and must be normalized. + name: "verification via sms normalizes phones inside an array", + schemaID: "verification-sms-array", + input: `{"phones":["+49 176 671 11 638","+44 20 7946 0958"]}`, + wantTraits: `{"phones":["+4917667111638","+442079460958"]}`, + }, + { + // Two trait properties share the same $ref definition. + // The walk must visit both, not skip the second because + // the shared schema node was seen during the first walk. + name: "shared $ref between two trait paths normalizes both", + schemaID: "code-sms-shared-ref", + input: `{"primary_phone":"+49 176 671 11 638","secondary_phone":"+44 20 7946 0958"}`, + wantTraits: `{"primary_phone":"+4917667111638","secondary_phone":"+442079460958"}`, + }, + { + // Regression guard for ory/kratos#3187: email-channel + // identifiers must keep the user-supplied case in traits. + name: "email-channel identifier preserves trait case verbatim", + schemaID: "code-email", + input: `{"email":"UpperCased@Ory.SH"}`, + wantTraits: `{"email":"UpperCased@Ory.SH"}`, + }, + { + // When the trait value is not a parseable phone number, + // validation should fail and traits should be untouched. + name: "unparseable phone leaves traits untouched and surfaces validation error", + schemaID: "code-sms", + input: `{"phone":"not-a-phone-number"}`, + wantTraits: `{"phone":"not-a-phone-number"}`, + wantErr: "is not valid", + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + i := &Identity{ + SchemaID: tc.schemaID, + Traits: Traits(tc.input), + } + err := v.Validate(ctx, i) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + } else { + require.NoError(t, err) + } + assert.JSONEq(t, tc.wantTraits, string(i.Traits), + "trait JSON must match the normalized identifier form so webhook payloads are consistent with what Kratos stores") + }) + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4948125 --- /dev/null +++ b/install.sh @@ -0,0 +1,408 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2020-07-08T20:40:41Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null < set FOO=bar + > set BAZ=bar + > ` + name + ` ... + +Windows (powershell): + > $env:FOO = "bar" + > $env:BAZ = "bar" + > ` + name + ` +` +} diff --git a/oryx/cmdx/helper.go b/oryx/cmdx/helper.go new file mode 100644 index 0000000..5cf65e2 --- /dev/null +++ b/oryx/cmdx/helper.go @@ -0,0 +1,247 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "testing" + + "golang.org/x/sync/errgroup" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + + "github.com/pkg/errors" +) + +var ( + // ErrNoPrintButFail is returned to detect a failure state that was already reported to the user in some way + ErrNoPrintButFail = fmt.Errorf("this error should never be printed") + + debugStdout, debugStderr = io.Discard, io.Discard +) + +func init() { + if os.Getenv("DEBUG") != "" { + debugStdout = os.Stdout + debugStderr = os.Stderr + } +} + +// FailSilently is supposed to be used within a commands RunE function. +// It silences cobras error handling and returns the ErrNoPrintButFail error. +func FailSilently(cmd *cobra.Command) error { + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return errors.WithStack(ErrNoPrintButFail) +} + +// Must fatals with the optional message if err is not nil. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func Must(err error, message string, args ...interface{}) { + if err == nil { + return + } + + _, _ = fmt.Fprintf(os.Stderr, message+"\n", args...) + os.Exit(1) +} + +// CheckResponse fatals if err is nil or the response.StatusCode does not match the expectedStatusCode +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func CheckResponse(err error, expectedStatusCode int, response *http.Response) { + Must(err, "Command failed because error occurred: %s", err) + + if response.StatusCode != expectedStatusCode { + out, err := io.ReadAll(response.Body) + if err != nil { + out = []byte{} + } + pretty, err := json.MarshalIndent(json.RawMessage(out), "", "\t") + if err == nil { + out = pretty + } + + Fatalf( + `Command failed because status code %d was expected but code %d was received. + +Response payload: + +%s`, + expectedStatusCode, + response.StatusCode, + out, + ) + } +} + +// FormatResponse takes an object and prints a json.MarshalIdent version of it or fatals. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func FormatResponse(o interface{}) string { + out, err := json.MarshalIndent(o, "", "\t") + Must(err, `Command failed because an error occurred while prettifying output: %s`, err) + return string(out) +} + +// Fatalf prints to os.Stderr and exists with code 1. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func Fatalf(message string, args ...interface{}) { + if len(args) > 0 { + _, _ = fmt.Fprintf(os.Stderr, message+"\n", args...) + } else { + _, _ = fmt.Fprintln(os.Stderr, message) + } + os.Exit(1) +} + +// CallbackWriter will execute each callback once the message is received. +// The full matched message is passed to the callback. An error returned from the callback is returned by Write. +type CallbackWriter struct { + Callbacks map[string]func([]byte) error + buf bytes.Buffer +} + +func (c *CallbackWriter) Write(msg []byte) (int, error) { + for p, cb := range c.Callbacks { + if bytes.Contains(msg, []byte(p)) { + if err := cb(msg); err != nil { + return 0, err + } + } + } + return c.buf.Write(msg) +} + +func (c *CallbackWriter) String() string { + return c.buf.String() +} + +var _ io.Writer = (*CallbackWriter)(nil) + +func prepareCmd(cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args []string) { + cmd.SetIn(stdIn) + outs := []io.Writer{debugStdout} + if stdOut != nil { + outs = append(outs, stdOut) + } + cmd.SetOut(io.MultiWriter(outs...)) + errs := []io.Writer{debugStderr} + if stdErr != nil { + errs = append(errs, stdErr) + } + cmd.SetErr(io.MultiWriter(errs...)) + + if args == nil { + args = []string{} + } + cmd.SetArgs(args) +} + +// ExecBackgroundCtx runs the cobra command in the background. +func ExecBackgroundCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group { + prepareCmd(cmd, stdIn, stdOut, stdErr, args) + + eg := &errgroup.Group{} + eg.Go(func() error { + defer cmd.SetIn(nil) + return cmd.ExecuteContext(ctx) + }) + + return eg +} + +// Exec runs the provided cobra command with the given reader as STD_IN and the given args. +// Returns STD_OUT, STD_ERR and the error from the execution. +func Exec(t testing.TB, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) { + return ExecCtx(t.Context(), cmd, stdIn, args...) +} + +func ExecCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) { + stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} + + prepareCmd(cmd, stdIn, stdOut, stdErr, args) + + // needs to be on a separate line to ensure that the output buffers are read AFTER the command ran + err := cmd.ExecuteContext(ctx) + + return stdOut.String(), stdErr.String(), err +} + +// ExecNoErr is a helper that assumes a successful run from Exec. +// Returns STD_OUT. +func ExecNoErr(t testing.TB, cmd *cobra.Command, args ...string) string { + return ExecNoErrCtx(t.Context(), t, cmd, args...) +} + +func ExecNoErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string { + stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...) + if err == nil { + require.Len(t, stdErr, 0, "std_out: %s\nstd_err: %s", stdOut, stdErr) + } else { + require.ErrorIsf(t, err, context.Canceled, "std_out: %s\nstd_err: %s", stdOut, stdErr) + } + return stdOut +} + +// ExecExpectedErr is a helper that assumes a failing run from Exec returning ErrNoPrintButFail +// Returns STD_ERR. +func ExecExpectedErr(t testing.TB, cmd *cobra.Command, args ...string) string { + return ExecExpectedErrCtx(t.Context(), t, cmd, args...) +} + +func ExecExpectedErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string { + stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...) + require.True(t, errors.Is(err, ErrNoPrintButFail), "std_out: %s\nstd_err: %s", stdOut, stdErr) + require.Len(t, stdOut, 0, stdErr) + return stdErr +} + +type CommandExecuter struct { + New func() *cobra.Command + Ctx context.Context + PersistentArgs []string +} + +func (c *CommandExecuter) Exec(stdin io.Reader, args ...string) (string, string, error) { + return ExecCtx(c.Ctx, c.New(), stdin, append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecBackground(stdin io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group { + return ExecBackgroundCtx(c.Ctx, c.New(), stdin, stdOut, stdErr, append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecNoErr(t require.TestingT, args ...string) string { + return ExecNoErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecExpectedErr(t require.TestingT, args ...string) string { + return ExecExpectedErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...) +} + +type URL struct { + url.URL +} + +var _ pflag.Value = (*URL)(nil) + +func (u *URL) Set(s string) error { + uu, err := url.Parse(s) + if err != nil { + return err + } + u.URL = *uu + return nil +} + +func (*URL) Type() string { + return "url" +} diff --git a/oryx/cmdx/http.go b/oryx/cmdx/http.go new file mode 100644 index 0000000..890884c --- /dev/null +++ b/oryx/cmdx/http.go @@ -0,0 +1,125 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/x/httpx" +) + +const ( + envKeyEndpoint = "ORY_SDK_URL" + FlagEndpoint = "endpoint" + FlagSkipTLSVerify = "skip-tls-verify" + FlagHeaders = "http-header" +) + +// Remote returns the remote endpoint for the given command. +func Remote(cmd *cobra.Command) (string, error) { + endpoint, err := cmd.Flags().GetString(FlagEndpoint) + if err != nil { + return "", errors.WithStack(err) + } + + if endpoint != "" { + return strings.TrimRight(endpoint, "/"), nil + } else if endpoint := os.Getenv("ORY_SDK_URL"); endpoint != "" { + return strings.TrimRight(endpoint, "/"), nil + } + + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To execute this command, the endpoint URL must point to the URL where Ory is located. To set the endpoint URL, use flag `--endpoint` or environment variable `ORY_SDK_URL`.") + return "", FailSilently(cmd) +} + +// RemoteURI returns the remote URI for the given command. +func RemoteURI(cmd *cobra.Command) (*url.URL, error) { + remote, err := Remote(cmd) + if err != nil { + return nil, err + } + + endpoint, err := url.ParseRequestURI(remote) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse endpoint URL: %s", err) + return nil, err + } + + return endpoint, nil +} + +// NewClient creates a new HTTP client. +func NewClient(cmd *cobra.Command) (*http.Client, *url.URL, error) { + endpoint, err := cmd.Flags().GetString(FlagEndpoint) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + if endpoint == "" { + endpoint = os.Getenv(envKeyEndpoint) + } + + if endpoint == "" { + return nil, nil, errors.Errorf("you have to set the remote endpoint, try --help for details") + } + + u, err := url.Parse(strings.TrimRight(endpoint, "/")) + if err != nil { + return nil, nil, errors.Wrapf(err, `could not parse the endpoint URL "%s"`, endpoint) + } + + hc := retryablehttp.NewClient().StandardClient() + hc.Timeout = time.Second * 10 + + rawHeaders, err := cmd.Flags().GetStringSlice(FlagHeaders) + if err != nil { + return nil, nil, errors.WithStack(err) + } + header := http.Header{} + for _, h := range rawHeaders { + parts := strings.Split(h, ":") + if len(parts) != 2 { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Unable to parse `--http-header` flag. Format of flag value is a `: ` delimited string like `--http-header 'Some-Header: some-values; other values`. Received: %v", rawHeaders) + return nil, nil, FailSilently(cmd) + } + + for k := range parts { + parts[k] = strings.TrimSpace(parts[k]) + } + + header.Add(parts[0], parts[1]) + } + + skipVerify, err := cmd.Flags().GetBool(FlagSkipTLSVerify) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + rt := httpx.NewTransportWithHeader(header) + rt.RoundTripper = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipVerify, //nolint:gosec // This is a false positive + }, + } + hc.Transport = rt + return hc, u, nil +} + +// RegisterHTTPClientFlags registers HTTP client configuration flags. +func RegisterHTTPClientFlags(flags *pflag.FlagSet) { + flags.StringP(FlagEndpoint, FlagEndpoint[:1], "", fmt.Sprintf("The API URL this command should target. Alternatively set using the %s environmental variable.", envKeyEndpoint)) + flags.Bool(FlagSkipTLSVerify, false, "Do not verify TLS certificates. Useful when dealing with self-signed certificates. Do not use in production!") + flags.StringSliceP(FlagHeaders, "H", []string{}, "A list of additional HTTP headers to set. HTTP headers is separated by a `: `, for example: `-H 'Authorization: bearer some-token'`.") +} diff --git a/oryx/cmdx/noise_printer.go b/oryx/cmdx/noise_printer.go new file mode 100644 index 0000000..fbd46c9 --- /dev/null +++ b/oryx/cmdx/noise_printer.go @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type ConditionalPrinter struct { + w io.Writer + print bool +} + +const ( + FlagQuiet = "quiet" +) + +func RegisterNoiseFlags(flags *pflag.FlagSet) { + flags.BoolP(FlagQuiet, FlagQuiet[:1], false, "Be quiet with output printing.") +} + +// NewLoudOutPrinter returns a ConditionalPrinter that +// only prints to cmd.OutOrStdout when --quiet is not set +func NewLoudOutPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.OutOrStdout(), + print: !quiet, + } +} + +// NewQuietOutPrinter returns a ConditionalPrinter that +// only prints to cmd.OutOrStdout when --quiet is set +func NewQuietOutPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.OutOrStdout(), + print: quiet, + } +} + +// NewLoudErrPrinter returns a ConditionalPrinter that +// only prints to cmd.ErrOrStderr when --quiet is not set +func NewLoudErrPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.ErrOrStderr(), + print: !quiet, + } +} + +// NewQuietErrPrinter returns a ConditionalPrinter that +// only prints to cmd.ErrOrStderr when --quiet is set +func NewQuietErrPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.ErrOrStderr(), + print: quiet, + } +} + +// NewLoudPrinter returns a ConditionalPrinter that +// only prints to w when --quiet is not set +func NewLoudPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: w, + print: !quiet, + } +} + +// NewQuietPrinter returns a ConditionalPrinter that +// only prints to w when --quiet is set +func NewQuietPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: w, + print: quiet, + } +} + +func NewConditionalPrinter(w io.Writer, print bool) *ConditionalPrinter { + return &ConditionalPrinter{ + w: w, + print: print, + } +} + +func (p *ConditionalPrinter) Println(a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprintln(p.w, a...) + } + return +} + +func (p *ConditionalPrinter) Print(a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprint(p.w, a...) + } + return +} + +func (p *ConditionalPrinter) Printf(format string, a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprintf(p.w, format, a...) + } + return +} diff --git a/oryx/cmdx/output.go b/oryx/cmdx/output.go new file mode 100644 index 0000000..b17d46b --- /dev/null +++ b/oryx/cmdx/output.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import "strconv" + +type ( + // OutputIder outputs an ID + OutputIder string + // OutputIderCollection outputs a list of IDs + OutputIderCollection struct { + Items []OutputIder + } +) + +func (OutputIder) Header() []string { + return []string{"ID"} +} + +func (i OutputIder) Columns() []string { + return []string{string(i)} +} + +func (i OutputIder) Interface() interface{} { + return i +} + +func (OutputIderCollection) Header() []string { + return []string{"ID"} +} + +func (c OutputIderCollection) Table() [][]string { + rows := make([][]string, len(c.Items)) + for i, ident := range c.Items { + rows[i] = []string{string(ident)} + } + return rows +} + +func (c OutputIderCollection) Interface() interface{} { + return c.Items +} + +func (c OutputIderCollection) Len() int { + return len(c.Items) +} + +type PaginatedList struct { + Collection interface { + Table + IDs() []string + } `json:"-"` + Items []interface{} `json:"items"` + NextPageToken string `json:"next_page_token"` + IsLastPage bool `json:"is_last_page"` +} + +func (r *PaginatedList) Header() []string { + return r.Collection.Header() +} + +func (r *PaginatedList) Table() [][]string { + return append( + r.Collection.Table(), + []string{}, + []string{"NEXT PAGE TOKEN", r.NextPageToken}, + []string{"IS LAST PAGE", strconv.FormatBool(r.IsLastPage)}, + ) +} + +func (r *PaginatedList) Interface() interface{} { + return r +} + +func (r *PaginatedList) Len() int { + return r.Collection.Len() + 3 +} + +func (r *PaginatedList) IDs() []string { + return r.Collection.IDs() +} + +var _ Table = (*PaginatedList)(nil) diff --git a/oryx/cmdx/pagination.go b/oryx/cmdx/pagination.go new file mode 100644 index 0000000..b5c721a --- /dev/null +++ b/oryx/cmdx/pagination.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +const ( + FlagPageSize = "page-size" + FlagPageToken = "page-token" +) + +func RegisterTokenPaginationFlags(cmd *cobra.Command) (pageSize int, pageToken string) { + cmd.Flags().StringVar(&pageToken, FlagPageToken, "", "page token acquired from a previous response") + cmd.Flags().IntVar(&pageSize, FlagPageSize, 100, "maximum number of items to return") + return +} + +// ParsePaginationArgs parses pagination arguments from the command line. +func ParsePaginationArgs(cmd *cobra.Command, pageArg, perPageArg string) (page, perPage int64, err error) { + if len(pageArg+perPageArg) > 0 { + page, err = strconv.ParseInt(pageArg, 0, 64) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse page argument\"%s\": %s", pageArg, err) + return 0, 0, FailSilently(cmd) + } + + perPage, err = strconv.ParseInt(perPageArg, 0, 64) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse per-page argument\"%s\": %s", perPageArg, err) + return 0, 0, FailSilently(cmd) + } + } + return +} + +// ParseTokenPaginationArgs parses token-based pagination arguments from the command line. +func ParseTokenPaginationArgs(cmd *cobra.Command) (page string, perPage int, err error) { + pageArg, err := cmd.Flags().GetString(FlagPageToken) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%s\": %s", FlagPageToken, pageArg, err) + return "", 0, FailSilently(cmd) + } + + perPageArg, err := cmd.Flags().GetInt(FlagPageSize) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%d\": %s", FlagPageSize, perPageArg, err) + return "", 0, FailSilently(cmd) + } + + return pageArg, perPageArg, nil +} diff --git a/oryx/cmdx/printing.go b/oryx/cmdx/printing.go new file mode 100644 index 0000000..b686d7d --- /dev/null +++ b/oryx/cmdx/printing.go @@ -0,0 +1,355 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/go-openapi/jsonpointer" + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/tidwall/gjson" +) + +type ( + TableHeader interface { + Header() []string + } + TableRow interface { + TableHeader + Columns() []string + Interface() interface{} + } + Table interface { + TableHeader + Table() [][]string + Interface() interface{} + Len() int + } + Nil struct{} + + Format string +) + +const ( + FormatQuiet Format = "quiet" + FormatTable Format = "table" + FormatJSON Format = "json" + FormatJSONPath Format = "jsonpath" + FormatJSONPointer Format = "jsonpointer" + FormatJSONPretty Format = "json-pretty" + FormatYAML Format = "yaml" + FormatDefault Format = "default" + + FlagFormat = "format" + + None = "" +) + +func (Nil) String() string { + return "null" +} + +func (Nil) Interface() interface{} { + return nil +} + +type printOptions struct { + format string +} + +type PrintOption func(*printOptions) + +func WithFormat(v string) PrintOption { + return func(o *printOptions) { + o.format = v + } +} + +func PrintErrors(cmd *cobra.Command, errs map[string]error) { + for src, err := range errs { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: %s\n", src, err.Error()) + } +} + +func PrintRow(cmd *cobra.Command, row TableRow) { + PrintRowf(cmd.OutOrStdout(), row, WithFormat(getFormatValue(cmd))) +} + +func PrintRowf(w io.Writer, row TableRow, opts ...PrintOption) { + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + switch parseFormat(o.format) { + case FormatQuiet: + if idAble, ok := row.(interface{ ID() string }); ok { + _, _ = fmt.Fprintln(w, idAble.ID()) + break + } + _, _ = fmt.Fprintln(w, row.Columns()[0]) + case FormatJSON: + printJSON(w, row.Interface(), false, "") + case FormatYAML: + printYAML(w, row.Interface()) + case FormatJSONPretty: + printJSON(w, row.Interface(), true, "") + case FormatJSONPath: + printJSON(w, row.Interface(), true, getPath(o.format)) + case FormatJSONPointer: + printJSON(w, filterJSONPointer(o.format, row.Interface()), true, "") + case FormatTable, FormatDefault: + w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0) + + fields := row.Columns() + for i, h := range row.Header() { + _, _ = fmt.Fprintf(w, "%s\t%s\t\n", h, fields[i]) + } + + _ = w.Flush() + } +} + +func filterJSONPointer(f string, data any) any { + _, jsonptr, found := strings.Cut(f, "=") + if !found { + _, _ = fmt.Fprintf(os.Stderr, + "Format %s is missing a JSON pointer, e.g., --%s=%s=. The path syntax is described at https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-07.", + f, FlagFormat, f) + os.Exit(1) + } + ptr, err := jsonpointer.New(jsonptr) + Must(err, "invalid JSON pointer: %s", err) + + result, _, err := ptr.Get(data) + Must(err, "failed to apply JSON pointer: %s", err) + + return result +} + +func PrintTable(cmd *cobra.Command, table Table) { + PrintTablef(cmd.OutOrStdout(), table, WithFormat(getFormatValue(cmd))) +} + +func PrintTablef(w io.Writer, table Table, opts ...PrintOption) { + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + switch parseFormat(o.format) { + case FormatQuiet: + if table.Len() == 0 { + fmt.Fprintln(w) + } + + if idAble, ok := table.(interface{ IDs() []string }); ok { + for _, row := range idAble.IDs() { + fmt.Fprintln(w, row) + } + break + } + + for _, row := range table.Table() { + fmt.Fprintln(w, row[0]) + } + case FormatJSON: + printJSON(w, table.Interface(), false, "") + case FormatJSONPretty: + printJSON(w, table.Interface(), true, "") + case FormatJSONPath: + printJSON(w, table.Interface(), true, getPath(o.format)) + case FormatJSONPointer: + printJSON(w, filterJSONPointer(o.format, table.Interface()), true, "") + case FormatYAML: + printYAML(w, table.Interface()) + default: + w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0) + + for _, h := range table.Header() { + fmt.Fprintf(w, "%s\t", h) + } + fmt.Fprintln(w) + + for _, row := range table.Table() { + fmt.Fprintln(w, strings.Join(row, "\t")+"\t") + } + + _ = w.Flush() + } +} + +type interfacer interface{ Interface() interface{} } + +func PrintJSONAble(cmd *cobra.Command, d interface{ String() string }) { + PrintJSONAblef(cmd.OutOrStdout(), d, WithFormat(getFormatValue(cmd))) +} + +func PrintJSONAblef(w io.Writer, d interface{ String() string }, opts ...PrintOption) { + if d == nil { + d = Nil{} + } + + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + var path string + switch parseFormat(o.format) { + default: + _, _ = fmt.Fprint(w, d.String()) + case FormatJSON: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, v, false, "") + case FormatJSONPath: + path = getPath(o.format) + fallthrough + case FormatJSONPretty: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, v, true, path) + case FormatJSONPointer: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, filterJSONPointer(o.format, v), true, "") + case FormatYAML: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printYAML(w, v) + } +} + +func getQuiet(cmd *cobra.Command) bool { + // ignore the error here as we use this function also when the flag might not be registered + q, _ := cmd.Flags().GetBool(FlagQuiet) + return q +} + +func getFormatValue(cmd *cobra.Command) string { + if getQuiet(cmd) { + return string(FormatQuiet) + } + + f, _ := cmd.Flags().GetString(FlagFormat) + return f +} + +func parseFormat(f string) Format { + switch { + case f == string(FormatQuiet): + return FormatQuiet + case f == string(FormatTable): + return FormatTable + case f == string(FormatJSON): + return FormatJSON + case strings.HasPrefix(f, string(FormatJSONPath)): + return FormatJSONPath + case strings.HasPrefix(f, string(FormatJSONPointer)): + return FormatJSONPointer + case f == string(FormatJSONPretty): + return FormatJSONPretty + case f == string(FormatYAML): + return FormatYAML + default: + return FormatDefault + } +} + +func getPath(f string) string { + _, path, found := strings.Cut(f, "=") + if !found { + _, _ = fmt.Fprintf(os.Stderr, + "Format %s is missing a path, e.g., --%s=%s=. The path syntax is described at https://github.com/tidwall/gjson/blob/master/SYNTAX.md", + f, FlagFormat, f) + os.Exit(1) + } + + return path +} + +func printJSON(w io.Writer, v interface{}, pretty bool, path string) { + if path != "" { + temp, err := json.Marshal(v) + Must(err, "Error encoding JSON: %s", err) + v = gjson.GetBytes(temp, path).Value() + } + + e := json.NewEncoder(w) + if pretty { + e.SetIndent("", " ") + } + err := e.Encode(v) + // unexpected error + Must(err, "Error encoding JSON: %s", err) +} + +func printYAML(w io.Writer, v interface{}) { + j, err := json.Marshal(v) + Must(err, "Error encoding JSON: %s", err) + e, err := yaml.JSONToYAML(j) + Must(err, "Error encoding YAML: %s", err) + _, _ = w.Write(e) +} + +func RegisterJSONFormatFlags(flags *pflag.FlagSet) { + flags.String(FlagFormat, string(FormatDefault), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatDefault, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer)) +} + +func RegisterFormatFlags(flags *pflag.FlagSet) { + RegisterNoiseFlags(flags) + flags.String(FlagFormat, string(FormatTable), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatTable, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer)) +} + +func PrintOpenAPIError(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + + var be interface { + Body() []byte + } + if !errors.As(err, &be) { + return err + } + + body := be.Body() + didPrettyPrint := false + if message := gjson.GetBytes(body, "error.message"); message.Exists() { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", message.String()) + didPrettyPrint = true + } + if reason := gjson.GetBytes(body, "error.reason"); reason.Exists() { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", reason.String()) + didPrettyPrint = true + } + + if didPrettyPrint { + return FailSilently(cmd) + } + + if body, err := json.MarshalIndent(json.RawMessage(body), "", " "); err == nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\nFailed to execute API request, see error above.\n", body) + return FailSilently(cmd) + } + + return err +} diff --git a/oryx/cmdx/usage.go b/oryx/cmdx/usage.go new file mode 100644 index 0000000..891b8cd --- /dev/null +++ b/oryx/cmdx/usage.go @@ -0,0 +1,122 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bytes" + "maps" + "sync" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + usageTemplateFuncs = sprig.TxtFuncMap() + usageTemplateFuncsMux sync.RWMutex + enableUsageTemplatingOnce sync.Once +) + +// AddUsageTemplateFunc adds a template function to the usage template. +func AddUsageTemplateFunc(name string, f interface{}) { + usageTemplateFuncsMux.Lock() + usageTemplateFuncs[name] = f + usageTemplateFuncsMux.Unlock() +} + +const ( + helpTemplate = `{{insertTemplate . (or .Long .Short) | trimTrailingWhitespaces}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` + usageTemplate = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{insertTemplate . .Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` +) + +// EnableUsageTemplating enables gotemplates for usage strings, i.e. cmd.Short, cmd.Long, and cmd.Example. +// The data for the template is the command itself. Especially useful are `.Root.Name` and `.CommandPath`. +// This will be inherited by all subcommands, so enabling it on the root command is sufficient. +func EnableUsageTemplating(cmds ...*cobra.Command) { + enableUsageTemplatingOnce.Do(func() { + cobra.AddTemplateFunc("insertTemplate", TemplateCommandField) + }) + for _, cmd := range cmds { + cmd.SetHelpTemplate(helpTemplate) + cmd.SetUsageTemplate(usageTemplate) + } +} + +func TemplateCommandField(cmd *cobra.Command, field string) (string, error) { + t := template.New("") + usageTemplateFuncsMux.RLock() + t.Funcs(maps.Clone(usageTemplateFuncs)) + usageTemplateFuncsMux.RUnlock() + t, err := t.Parse(field) + if err != nil { + return "", err + } + var out bytes.Buffer + if err := t.Execute(&out, cmd); err != nil { + return "", err + } + return out.String(), nil +} + +// DisableUsageTemplating resets the commands usage template to the default. +// This can be used to undo the effects of EnableUsageTemplating, specifically for a subcommand. +func DisableUsageTemplating(cmds ...*cobra.Command) { + defaultCmd := new(cobra.Command) + for _, cmd := range cmds { + cmd.SetHelpTemplate(defaultCmd.HelpTemplate()) + cmd.SetUsageTemplate(defaultCmd.UsageTemplate()) + } +} + +// AssertUsageTemplates asserts that the usage string of the commands are properly templated. +func AssertUsageTemplates(t require.TestingT, cmd *cobra.Command) { + var usage, help string + require.NotPanics(t, func() { + usage = cmd.UsageString() + + out, err := cmd.OutOrStdout(), cmd.ErrOrStderr() + bb := new(bytes.Buffer) + + cmd.SetOut(bb) + cmd.SetErr(bb) + require.NoError(t, cmd.Help()) + help = bb.String() + + cmd.SetOut(out) + cmd.SetErr(err) + }) + assert.NotContains(t, usage, "{{") + assert.NotContains(t, help, "{{") + for _, child := range cmd.Commands() { + AssertUsageTemplates(t, child) + } +} diff --git a/oryx/cmdx/user_input.go b/oryx/cmdx/user_input.go new file mode 100644 index 0000000..1659d29 --- /dev/null +++ b/oryx/cmdx/user_input.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/pkg/errors" +) + +// asks for confirmation with the question string s and reads the answer +// pass nil to use os.Stdin and os.Stdout +func AskForConfirmation(s string, stdin io.Reader, stdout io.Writer) bool { + if stdin == nil { + stdin = os.Stdin + } + if stdout == nil { + stdout = os.Stdout + } + + ok, err := AskScannerForConfirmation(s, bufio.NewReader(stdin), stdout) + if err != nil { + Must(err, "Unable to confirm: %s", err) + } + + return ok +} + +func AskScannerForConfirmation(s string, reader *bufio.Reader, stdout io.Writer) (bool, error) { + if stdout == nil { + stdout = os.Stdout + } + + for { + _, err := fmt.Fprintf(stdout, "%s [y/n]: ", s) + if err != nil { + return false, errors.Wrap(err, "unable to print to stdout") + } + + response, err := reader.ReadString('\n') + if err != nil { + return false, errors.Wrap(err, "unable to read from stdin") + } + + response = strings.ToLower(strings.TrimSpace(response)) + if response == "y" || response == "yes" { + return true, nil + } else if response == "n" || response == "no" { + return false, nil + } + } +} diff --git a/oryx/cmdx/version.go b/oryx/cmdx/version.go new file mode 100644 index 0000000..8864161 --- /dev/null +++ b/oryx/cmdx/version.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// Version returns a *cobra.Command that handles the `version` command. +func Version(gitTag, gitHash, buildTime *string) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show the build version, build time, and git hash", + Run: func(cmd *cobra.Command, args []string) { + if len(*gitTag) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine version because the build process did not properly configure it.") + } else { + fmt.Printf("Version: %s\n", *gitTag) + } + + if len(*gitHash) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine build commit because the build process did not properly configure it.") + } else { + fmt.Printf("Build Commit: %s\n", *gitHash) + } + + if len(*buildTime) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine build timestamp because the build process did not properly configure it.") + } else { + fmt.Printf("Build Timestamp: %s\n", *buildTime) + } + }, + } +} diff --git a/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json b/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json @@ -0,0 +1 @@ +{} diff --git a/oryx/configx/context.go b/oryx/configx/context.go new file mode 100644 index 0000000..a465d36 --- /dev/null +++ b/oryx/configx/context.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import "context" + +type contextKey int + +const configContextKey contextKey = iota + 1 + +func ContextWithConfigOptions(ctx context.Context, opts ...OptionModifier) context.Context { + return context.WithValue(ctx, configContextKey, opts) +} + +func ConfigOptionsFromContext(ctx context.Context) []OptionModifier { + opts, ok := ctx.Value(configContextKey).([]OptionModifier) + if !ok { + return []OptionModifier{} + } + return opts +} diff --git a/oryx/configx/cors.go b/oryx/configx/cors.go new file mode 100644 index 0000000..cc9528d --- /dev/null +++ b/oryx/configx/cors.go @@ -0,0 +1,41 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + _ "embed" + "net/http" + + "github.com/rs/cors" + + "github.com/ory/x/corsx" +) + +const CORSConfigSchemaID = "ory://cors-config" + +//go:embed cors.schema.json +var CORSConfigSchema []byte + +func (p *Provider) CORS(prefix string, defaults cors.Options) (cors.Options, bool) { + prefix = cleanPrefix(prefix) + allowedOrigins := p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins) + allowInsecureOrigins := p.BoolF("feature_flags.legacy_allow_insecure_origins", false) + return cors.Options{ + // Populated even though rs/cors ignores it once AllowOriginVaryRequestFunc + // is set: some consumers read AllowedOrigins directly — notably hydra's + // oauth2cors.Middleware (which builds its own matcher and treats an empty + // list as "allow all", a CORS bypass) and oathkeeper's address detection. + AllowedOrigins: p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins), + AllowedMethods: p.StringsF(prefix+"cors.allowed_methods", defaults.AllowedMethods), + AllowedHeaders: p.StringsF(prefix+"cors.allowed_headers", defaults.AllowedHeaders), + ExposedHeaders: p.StringsF(prefix+"cors.exposed_headers", defaults.ExposedHeaders), + AllowCredentials: p.BoolF(prefix+"cors.allow_credentials", defaults.AllowCredentials), + OptionsPassthrough: p.BoolF(prefix+"cors.options_passthrough", defaults.OptionsPassthrough), + MaxAge: p.IntF(prefix+"cors.max_age", defaults.MaxAge), + Debug: p.BoolF(prefix+"cors.debug", defaults.Debug), + AllowOriginVaryRequestFunc: func(_ *http.Request, origin string) (bool, []string) { + return corsx.CheckOrigin(allowedOrigins, origin, allowInsecureOrigins), nil + }, + }, p.Bool(prefix + "cors.enabled") +} diff --git a/oryx/configx/cors.schema.json b/oryx/configx/cors.schema.json new file mode 100644 index 0000000..e65559a --- /dev/null +++ b/oryx/configx/cors.schema.json @@ -0,0 +1,106 @@ +{ + "$id": "https://github.com/ory/x/configx/cors.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "CORS", + "description": "Configures Cross Origin Resource Sharing for this endpoint.", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: https://*.example.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "matches all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non-simple headers the client is allowed to use with cross-domain requests.", + "examples": [ + [ + "Authorization", + "Content-Type", + "Max-Age", + "X-Session-Token", + "X-XSRF-TOKEN", + "X-CSRF-TOKEN" + ] + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug CORS issues.", + "default": false + } + } +} diff --git a/oryx/configx/error.go b/oryx/configx/error.go new file mode 100644 index 0000000..d705092 --- /dev/null +++ b/oryx/configx/error.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "fmt" + + "github.com/pkg/errors" +) + +type ImmutableError struct { + From interface{} + To interface{} + Key string + error +} + +func NewImmutableError(key string, from, to interface{}) error { + return &ImmutableError{ + From: from, + To: to, + Key: key, + error: errors.Errorf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", key, from, to), + } +} + +func (e *ImmutableError) Error() string { + return fmt.Sprintf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", e.Key, e.From, e.To) +} diff --git a/oryx/configx/helpers.go b/oryx/configx/helpers.go new file mode 100644 index 0000000..8b9fc80 --- /dev/null +++ b/oryx/configx/helpers.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/spf13/pflag" +) + +// RegisterFlags registers the config file flag. +func RegisterFlags(flags *pflag.FlagSet) { + flags.StringSliceP("config", "c", []string{}, "Path to one or more .json, .yaml, .yml, .toml config files. Values are loaded in the order provided, meaning that the last config file overwrites values from the previous config file.") +} + +// host = unix:/path/to/socket => port is discarded, otherwise format as host:port +func GetAddress(host string, port int) string { + if strings.HasPrefix(host, "unix:") { + return host + } + return fmt.Sprintf("%s:%d", host, port) +} + +func (s *Serve) GetAddress() string { + return GetAddress(s.Host, s.Port) +} + +// AddSchemaResources adds the config schema partials to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddSchemaResources(c interface { + AddResource(url string, r io.Reader) error +}) error { + if err := c.AddResource(TLSConfigSchemaID, bytes.NewReader(TLSConfigSchema)); err != nil { + return err + } + if err := c.AddResource(ServeConfigSchemaID, bytes.NewReader(ServeConfigSchema)); err != nil { + return err + } + return c.AddResource(CORSConfigSchemaID, bytes.NewReader(CORSConfigSchema)) +} + +func cleanPrefix(prefix string) string { + if len(prefix) > 0 { + prefix = strings.TrimRight(prefix, ".") + "." + } + return prefix +} diff --git a/oryx/configx/koanf_confmap.go b/oryx/configx/koanf_confmap.go new file mode 100644 index 0000000..4824598 --- /dev/null +++ b/oryx/configx/koanf_confmap.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "encoding/json" + "errors" + + "github.com/knadh/koanf/maps" + "github.com/tidwall/gjson" +) + +// KoanfConfmap implements a raw map[string]interface{} provider. +type KoanfConfmap struct { + tuples []tuple +} + +// Provider returns a confmap Provider that takes a flat or nested +// map[string]interface{}. If a delim is provided, it indicates that the +// keys are flat and the map needs to be unflatted by delim. +func NewKoanfConfmap(tuples []tuple) *KoanfConfmap { + return &KoanfConfmap{tuples: jsonify(tuples)} +} + +func jsonify(tuples []tuple) []tuple { + for k, t := range tuples { + var parsed interface{} + switch vt := t.Value.(type) { + case string: + if gjson.Valid(vt) && json.NewDecoder(bytes.NewBufferString(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + case []byte: + if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + case json.RawMessage: + if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + } + } + return tuples +} + +// ReadBytes is not supported by the env provider. +func (e *KoanfConfmap) ReadBytes() ([]byte, error) { + return nil, errors.New("confmap provider does not support this method") +} + +// Read returns the loaded map[string]interface{}. +func (e *KoanfConfmap) Read() (map[string]interface{}, error) { + values := map[string]interface{}{} + for _, t := range e.tuples { + values[t.Key] = t.Value + } + + // Ensure any nested values are properly converted as well + cp := maps.Copy(values) + maps.IntfaceKeysToStrings(cp) + cp = maps.Unflatten(cp, Delimiter) + + return cp, nil +} diff --git a/oryx/configx/koanf_env.go b/oryx/configx/koanf_env.go new file mode 100644 index 0000000..0e97725 --- /dev/null +++ b/oryx/configx/koanf_env.go @@ -0,0 +1,185 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "encoding/json" + "os" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/jsonschema/v3" + + "github.com/spf13/cast" + "github.com/tidwall/gjson" + + "github.com/ory/x/castx" + "github.com/ory/x/jsonschemax" +) + +var isNumRegex = regexp.MustCompile("^[0-9]+$") + +func NewKoanfEnv(prefix string, rawSchema []byte, schema *jsonschema.Schema) (*Env, error) { + paths, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + + return &Env{ + paths: paths, + prefix: prefix, + }, nil +} + +// Env implements an environment variables provider. +type Env struct { + prefix string + paths []jsonschemax.Path +} + +// ReadBytes is not supported by the env provider. +func (e *Env) ReadBytes() ([]byte, error) { + return nil, errors.New("env provider does not support this method") +} + +// Read reads all available environment variables into a key:value map +// and returns it. +func (e *Env) Read() (map[string]interface{}, error) { + // Collect the environment variable keys. + var keys []string + for _, k := range os.Environ() { + if e.prefix != "" { + if strings.HasPrefix(k, e.prefix) { + keys = append(keys, k) + } + } else { + keys = append(keys, k) + } + } + + raw := "{}" + var err error + for _, k := range keys { + parts := strings.SplitN(k, "=", 2) + + key, value := e.extract(parts[0], parts[1]) + // If the callback blanked the key, it should be omitted + if key == "" { + continue + } + + raw, err = sjson.Set(raw, key, value) + if err != nil { + return nil, errors.WithStack(err) + } + } + + var m map[string]interface{} + if err := json.Unmarshal([]byte(raw), &m); err != nil { + return nil, errors.WithStack(err) + } + + return m, nil +} + +// Watch is not supported. +func (e *Env) Watch(cb func(event interface{}, err error)) error { + return errors.New("env provider does not support this method") +} + +func (e *Env) extract(key string, value string) (string, interface{}) { + key = strings.Replace(strings.ToLower(strings.TrimPrefix(key, e.prefix)), "_", ".", -1) + + for _, path := range e.paths { + normalized := strings.Replace(path.Name, "_", ".", -1) + name := path.Name + + // Crazy hack to get arrays working. + var indices []string + searchParts := strings.Split(normalized, ".") + keyParts := strings.Split(key, ".") + if len(searchParts) == len(keyParts) { + for k, search := range searchParts { + if search != keyParts[k] { + indices = nil + } + + if search != "#" { + continue + } + + if !isNumRegex.MatchString(keyParts[k]) { + continue + } + + searchParts[k] = keyParts[k] + indices = append(indices, keyParts[k]) + } + } + + if len(indices) > 0 { + normalized = strings.Join(searchParts, ".") + for _, index := range indices { + name = strings.Replace(name, "#", index, 1) + } + } + + if normalized == key { + switch path.TypeHint { + case jsonschemax.String: + return name, cast.ToString(value) + case jsonschemax.Float: + return name, cast.ToFloat64(value) + case jsonschemax.Int: + return name, cast.ToInt64(value) + case jsonschemax.Bool: + return name, cast.ToBool(value) + case jsonschemax.Nil: + return name, nil + case jsonschemax.BoolSlice: + if !gjson.Valid(value) { + return name, cast.ToBoolSlice(value) + } + fallthrough + case jsonschemax.StringSlice: + if !gjson.Valid(value) { + return name, castx.ToStringSlice(value) + } + fallthrough + case jsonschemax.IntSlice: + if !gjson.Valid(value) { + return name, cast.ToIntSlice(value) + } + fallthrough + case jsonschemax.FloatSlice: + if !gjson.Valid(value) { + return name, castx.ToFloatSlice(value) + } + fallthrough + case jsonschemax.JSON: + return name, decode(value) + default: + return name, value + } + } + } + + return "", nil +} + +func decode(value string) (v interface{}) { + b := []byte(value) + var arr []interface{} + if err := json.Unmarshal(b, &arr); err == nil { + return &arr + } + h := map[string]interface{}{} + if err := json.Unmarshal(b, &h); err == nil { + return &h + } + return nil +} diff --git a/oryx/configx/koanf_file.go b/oryx/configx/koanf_file.go new file mode 100644 index 0000000..a862df8 --- /dev/null +++ b/oryx/configx/koanf_file.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/v2" + + "github.com/pkg/errors" + + "github.com/ory/x/watcherx" +) + +// KoanfFile implements a KoanfFile provider. +type KoanfFile struct { + subKey string + path string + parser koanf.Parser +} + +// NewKoanfFile returns a file provider. +func NewKoanfFile(path string) (*KoanfFile, error) { + return NewKoanfFileSubKey(path, "") +} + +func NewKoanfFileSubKey(path, subKey string) (*KoanfFile, error) { + kf := &KoanfFile{ + path: filepath.Clean(path), + subKey: subKey, + } + + switch e := filepath.Ext(path); e { + case ".toml": + kf.parser = toml.Parser() + case ".json": + kf.parser = json.Parser() + case ".yaml", ".yml": + kf.parser = yaml.Parser() + default: + return nil, errors.Errorf("unknown config file extension: %s", e) + } + + return kf, nil +} + +// ReadBytes is not supported by KoanfFile. +func (f *KoanfFile) ReadBytes() ([]byte, error) { + return nil, errors.New("file provider does not support this method") +} + +// Read reads the file and returns the parsed configuration. +func (f *KoanfFile) Read() (map[string]interface{}, error) { + //#nosec G304 -- false positive + fc, err := os.ReadFile(f.path) + if err != nil { + return nil, errors.WithStack(err) + } + + v, err := f.parser.Unmarshal(fc) + if err != nil { + return nil, errors.WithStack(err) + } + + if f.subKey == "" { + return v, nil + } + + path := strings.Split(f.subKey, Delimiter) + for i := range path { + v = map[string]interface{}{ + path[len(path)-1-i]: v, + } + } + + return v, nil +} + +// WatchChannel watches the file and triggers a callback when it changes. It is a +// blocking function that internally spawns a goroutine to watch for changes. +func (f *KoanfFile) WatchChannel(ctx context.Context, c watcherx.EventChannel) (watcherx.Watcher, error) { + return watcherx.WatchFile(ctx, f.path, c) +} diff --git a/oryx/configx/koanf_full_merge.go b/oryx/configx/koanf_full_merge.go new file mode 100644 index 0000000..dc25868 --- /dev/null +++ b/oryx/configx/koanf_full_merge.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/x/jsonx" +) + +func MergeAllTypes(src, dst map[string]interface{}) error { + rawSrc, err := json.Marshal(src) + if err != nil { + return errors.WithStack(err) + } + + dstSrc, err := json.Marshal(dst) + if err != nil { + return errors.WithStack(err) + } + + keys := jsonx.Flatten(rawSrc) + for key, value := range keys { + dstSrc, err = sjson.SetBytes(dstSrc, key, value) + if err != nil { + return errors.WithStack(err) + } + } + + return errors.WithStack(json.Unmarshal(dstSrc, &dst)) +} diff --git a/oryx/configx/koanf_memory.go b/oryx/configx/koanf_memory.go new file mode 100644 index 0000000..2eff963 --- /dev/null +++ b/oryx/configx/koanf_memory.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/v2" + + "github.com/pkg/errors" + + stdjson "encoding/json" +) + +// KoanfMemory implements a KoanfMemory provider. +type KoanfMemory struct { + doc stdjson.RawMessage + parser koanf.Parser +} + +// NewKoanfMemory returns a file provider. +func NewKoanfMemory(doc stdjson.RawMessage) *KoanfMemory { + return &KoanfMemory{ + doc: doc, + parser: json.Parser(), + } +} + +func (f *KoanfMemory) SetDoc(doc stdjson.RawMessage) { + f.doc = doc +} + +// ReadBytes reads the contents of a file on disk and returns the bytes. +func (f *KoanfMemory) ReadBytes() ([]byte, error) { + return nil, errors.New("file provider does not support this method") +} + +// Read is not supported by the file provider. +func (f *KoanfMemory) Read() (map[string]interface{}, error) { + v, err := f.parser.Unmarshal(f.doc) + if err != nil { + return nil, errors.WithStack(err) + } + + return v, nil +} diff --git a/oryx/configx/koanf_schema_defaults.go b/oryx/configx/koanf_schema_defaults.go new file mode 100644 index 0000000..6ed6607 --- /dev/null +++ b/oryx/configx/koanf_schema_defaults.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "strings" + + "github.com/knadh/koanf/maps" + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +type KoanfSchemaDefaults struct { + keys []jsonschemax.Path +} + +func NewKoanfSchemaDefaults(rawSchema []byte, schema *jsonschema.Schema) (*KoanfSchemaDefaults, error) { + keys, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + + return &KoanfSchemaDefaults{keys: keys}, nil +} + +func (k *KoanfSchemaDefaults) ReadBytes() ([]byte, error) { + return nil, errors.New("schema defaults provider does not support this method") +} + +func (k *KoanfSchemaDefaults) Read() (map[string]interface{}, error) { + values := map[string]interface{}{} + for _, key := range k.keys { + // It's an array! + if strings.Contains(key.Name, "#") { + continue + } + + if key.Default != nil { + values[key.Name] = key.Default + } + } + + // Deep-copy: getSchemaPaths caches Path entries by schema hash, so + // key.Default may be a map/slice shared across all callers. koanf.Merge + // retains references when merging, and recursively writes into shared + // sub-maps when a later provider overlaps the default's path — + // without this copy, concurrent configx.New calls race on the shared + // default value. + return maps.Unflatten(maps.Copy(values), "."), nil +} diff --git a/oryx/configx/options.go b/oryx/configx/options.go new file mode 100644 index 0000000..6a51797 --- /dev/null +++ b/oryx/configx/options.go @@ -0,0 +1,159 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/logrusx" + + "github.com/knadh/koanf/v2" + + "github.com/ory/x/watcherx" +) + +type ( + OptionModifier func(p *Provider) +) + +func WithContext(ctx context.Context) OptionModifier { + return func(p *Provider) { + for _, o := range ConfigOptionsFromContext(ctx) { + o(p) + } + } +} + +func WithConfigFiles(files ...string) OptionModifier { + return func(p *Provider) { + p.files = append(p.files, files...) + } +} + +func WithImmutables(immutables ...string) OptionModifier { + return func(p *Provider) { + p.immutables = append(p.immutables, immutables...) + } +} + +func WithExceptImmutables(exceptImmutables ...string) OptionModifier { + return func(p *Provider) { + p.exceptImmutables = append(p.exceptImmutables, exceptImmutables...) + } +} + +func WithFlags(flags *pflag.FlagSet) OptionModifier { + return func(p *Provider) { + p.flags = flags + } +} + +func WithLogger(l *logrusx.Logger) OptionModifier { + return func(p *Provider) { + p.logger = l + } +} + +func SkipValidation() OptionModifier { + return func(p *Provider) { + p.skipValidation = true + } +} + +func DisableEnvLoading() OptionModifier { + return func(p *Provider) { + p.disableEnvLoading = true + } +} + +func WithValue(key string, value interface{}) OptionModifier { + return func(p *Provider) { + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + } +} + +func WithValues(values map[string]interface{}) OptionModifier { + return func(p *Provider) { + for key, value := range values { + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + } + } +} + +func WithBaseValues(values map[string]interface{}) OptionModifier { + return func(p *Provider) { + for key, value := range values { + p.baseValues = append(p.baseValues, tuple{Key: key, Value: value}) + } + } +} + +func WithUserProviders(providers ...koanf.Provider) OptionModifier { + return func(p *Provider) { + p.userProviders = providers + } +} + +// DEPRECATED without replacement. This option is a no-op. +func OmitKeysFromTracing(keys ...string) OptionModifier { + return func(*Provider) {} +} + +func AttachWatcher(watcher func(event watcherx.Event, err error)) OptionModifier { + return func(p *Provider) { + p.onChanges = append(p.onChanges, watcher) + } +} + +func WithLogrusWatcher(l *logrusx.Logger) OptionModifier { + return AttachWatcher(LogrusWatcher(l)) +} + +func LogrusWatcher(l *logrusx.Logger) func(e watcherx.Event, err error) { + return func(e watcherx.Event, err error) { + l.WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("A change to a configuration file was detected.") + + if et := new(jsonschema.ValidationError); errors.As(err, &et) { + l.WithField("event", fmt.Sprintf("%#v", et)). + Errorf("The changed configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.") + } else if et := new(ImmutableError); errors.As(err, &et) { + l.WithError(err). + WithField("key", et.Key). + WithField("old_value", fmt.Sprintf("%v", et.From)). + WithField("new_value", fmt.Sprintf("%v", et.To)). + Errorf("A configuration value marked as immutable has changed. Rolling back to the last working configuration revision. To reload the values please restart the process.") + } else if err != nil { + l.WithError(err).Errorf("An error occurred while watching config file %s", e.Source()) + } else { + l.WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("Configuration change processed successfully.") + } + } +} + +func WithStderrValidationReporter() OptionModifier { + return func(p *Provider) { + p.onValidationError = func(k *koanf.Koanf, err error) { + p.printHumanReadableValidationErrors(k, os.Stderr, err) + } + } +} + +func WithStandardValidationReporter(w io.Writer) OptionModifier { + return func(p *Provider) { + p.onValidationError = func(k *koanf.Koanf, err error) { + p.printHumanReadableValidationErrors(k, w, err) + } + } +} diff --git a/oryx/configx/permission.go b/oryx/configx/permission.go new file mode 100644 index 0000000..51be0a5 --- /dev/null +++ b/oryx/configx/permission.go @@ -0,0 +1,56 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "os" + "os/user" + "strconv" +) + +type UnixPermission struct { + Owner string + Group string + Mode os.FileMode +} + +func (p *UnixPermission) SetPermission(file string) error { + var e error + e = os.Chmod(file, p.Mode) + if e != nil { + return e + } + + gid := -1 + uid := -1 + + if p.Owner != "" { + var userObj *user.User + userObj, e = user.Lookup(p.Owner) + if e != nil { + return e + } + uid, e = strconv.Atoi(userObj.Uid) + if e != nil { + return e + } + } + if p.Group != "" { + var group *user.Group + group, e := user.LookupGroup(p.Group) + if e != nil { + return e + } + gid, e = strconv.Atoi(group.Gid) + if e != nil { + return e + } + } + + e = os.Chown(file, uid, gid) + if e != nil { + return e + } + return nil +} diff --git a/oryx/configx/pflag.go b/oryx/configx/pflag.go new file mode 100644 index 0000000..9362a54 --- /dev/null +++ b/oryx/configx/pflag.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "strings" + + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/pkg/errors" + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +type PFlagProvider struct { + p *posflag.Posflag + paths []jsonschemax.Path +} + +func NewPFlagProvider(rawSchema []byte, schema *jsonschema.Schema, f *pflag.FlagSet, k *koanf.Koanf) (*PFlagProvider, error) { + paths, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + return &PFlagProvider{ + p: posflag.Provider(f, ".", k), + paths: paths, + }, nil +} + +func (p *PFlagProvider) ReadBytes() ([]byte, error) { + return nil, errors.New("pflag provider does not support this method") +} + +func (p *PFlagProvider) Read() (map[string]interface{}, error) { + all, err := p.p.Read() + if err != nil { + return nil, errors.WithStack(err) + } + knownFlags := make(map[string]interface{}, len(all)) + for k, v := range all { + k = strings.ReplaceAll(k, ".", "-") + for _, path := range p.paths { + normalized := strings.ReplaceAll(path.Name, ".", "-") + if k == normalized { + knownFlags[k] = v + break + } + } + } + return knownFlags, nil +} + +var _ koanf.Provider = (*PFlagProvider)(nil) diff --git a/oryx/configx/provider.go b/oryx/configx/provider.go new file mode 100644 index 0000000..efd71ed --- /dev/null +++ b/oryx/configx/provider.go @@ -0,0 +1,561 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "context" + "fmt" + "io" + "net/url" + "os" + "reflect" + "sync" + "time" + + "github.com/inhies/go-bytesize" + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/watcherx" +) + +type tuple struct { + Key string + Value interface{} +} + +type Provider struct { + l sync.RWMutex + *koanf.Koanf + immutables, exceptImmutables []string + + schema []byte + flags *pflag.FlagSet + validator *jsonschema.Schema + onChanges []func(watcherx.Event, error) + onValidationError func(k *koanf.Koanf, err error) + + forcedValues []tuple + baseValues []tuple + files []string + + skipValidation bool + disableEnvLoading bool + + logger *logrusx.Logger + + providers []koanf.Provider + userProviders []koanf.Provider +} + +const ( + FlagConfig = "config" + Delimiter = "." +) + +// RegisterConfigFlag registers the "--config" flag on pflag.FlagSet. +func RegisterConfigFlag(flags *pflag.FlagSet, fallback []string) { + flags.StringSliceP(FlagConfig, "c", fallback, "Config files to load, overwriting in the order specified.") +} + +// New creates a new provider instance or errors. +// Configuration values are loaded in the following order: +// +// 1. Defaults from the JSON Schema +// 2. Config files (yaml, yml, toml, json) +// 3. Command line flags +// 4. Environment variables +// +// There will also be file-watchers started for all config files. To cancel the +// watchers, cancel the context. +func New(ctx context.Context, schema []byte, modifiers ...OptionModifier) (*Provider, error) { + validator, err := getSchema(ctx, schema) + if err != nil { + return nil, err + } + + l := logrus.New() + l.Out = io.Discard + + p := &Provider{ + schema: schema, + validator: validator, + onValidationError: func(k *koanf.Koanf, err error) {}, + logger: logrusx.New("discarding config logger", "", logrusx.UseLogger(l)), + Koanf: koanf.NewWithConf(koanf.Conf{Delim: Delimiter, StrictMerge: true}), + } + + for _, m := range modifiers { + m(p) + } + + providers, err := p.createProviders(ctx) + if err != nil { + return nil, err + } + + p.providers = providers + + k, err := p.newKoanf() + if err != nil { + return nil, err + } + + p.replaceKoanf(k) + return p, nil +} + +func (p *Provider) SkipValidation() bool { + return p.skipValidation +} + +func (p *Provider) createProviders(ctx context.Context) (providers []koanf.Provider, err error) { + defaultsProvider, err := NewKoanfSchemaDefaults(p.schema, p.validator) + if err != nil { + return nil, err + } + providers = append(providers, defaultsProvider) + + // Workaround for https://github.com/knadh/koanf/pull/47 + for _, t := range p.baseValues { + providers = append(providers, NewKoanfConfmap([]tuple{t})) + } + + paths := p.files + if p.flags != nil { + p, _ := p.flags.GetStringSlice(FlagConfig) + paths = append(paths, p...) + } + + p.logger.WithField("files", paths).Debug("Adding config files.") + + c := make(watcherx.EventChannel) + + defer func() { + if err == nil && len(paths) > 0 { + go p.watchForFileChanges(ctx, c) + } + }() + for _, path := range paths { + fp, err := NewKoanfFile(path) + if err != nil { + return nil, err + } + + if _, err := fp.WatchChannel(ctx, c); err != nil { + return nil, err + } + + providers = append(providers, fp) + } + + providers = append(providers, p.userProviders...) + + if p.flags != nil { + pp, err := NewPFlagProvider(p.schema, p.validator, p.flags, p.Koanf) + if err != nil { + return nil, err + } + providers = append(providers, pp) + } + + if !p.disableEnvLoading { + envProvider, err := NewKoanfEnv("", p.schema, p.validator) + if err != nil { + return nil, err + } + providers = append(providers, envProvider) + } + + // Workaround for https://github.com/knadh/koanf/pull/47 + for _, t := range p.forcedValues { + providers = append(providers, NewKoanfConfmap([]tuple{t})) + } + + return providers, nil +} + +func (p *Provider) replaceKoanf(k *koanf.Koanf) { + p.Koanf = k +} + +func (p *Provider) validate(k *koanf.Koanf) error { + if p.skipValidation { + return nil + } + + out, err := k.Marshal(json.Parser()) + if err != nil { + return errors.WithStack(err) + } + if err := p.validator.Validate(bytes.NewReader(out)); err != nil { + p.onValidationError(k, err) + return err + } + + return nil +} + +// newKoanf creates a new koanf instance with all the updated config +// +// This is unfortunately required due to several limitations / bugs in koanf: +// +// - https://github.com/knadh/koanf/issues/77 +// - https://github.com/knadh/koanf/pull/47 +func (p *Provider) newKoanf() (_ *koanf.Koanf, err error) { + k := koanf.New(Delimiter) + + for _, provider := range p.providers { + // posflag.Posflag requires access to Koanf instance so we recreate the provider here which is a workaround + // for posflag.Provider's API. + if _, ok := provider.(*posflag.Posflag); ok { + provider = posflag.Provider(p.flags, ".", k) + } + + var opts []koanf.Option + if _, ok := provider.(*Env); ok { + opts = append(opts, koanf.WithMergeFunc(MergeAllTypes)) + } + + if err := k.Load(provider, nil, opts...); err != nil { + return nil, err + } + } + + if err := p.validate(k); err != nil { + return nil, err + } + + return k, nil +} + +// SetTracer does nothing. DEPRECATED without replacement. +func (p *Provider) SetTracer(_ context.Context, _ *otelx.Tracer) { +} + +func (p *Provider) runOnChanges(e watcherx.Event, err error) { + for k := range p.onChanges { + p.onChanges[k](e, err) + } +} + +func deleteOtherKeys(k *koanf.Koanf, keys []string) { +outer: + for _, key := range k.Keys() { + for _, ik := range keys { + if key == ik { + continue outer + } + } + k.Delete(key) + } +} + +func (p *Provider) reload(e watcherx.Event) { + p.l.Lock() + + var err error + defer func() { + // we first want to unlock and then runOnChanges, so that the callbacks can actually use the Provider + p.l.Unlock() + p.runOnChanges(e, err) + }() + + nk, err := p.newKoanf() + if err != nil { + return // unlocks & runs changes in defer + } + + oldImmutables, newImmutables := p.Koanf.Copy(), nk.Copy() + deleteOtherKeys(oldImmutables, p.immutables) + deleteOtherKeys(newImmutables, p.immutables) + + for _, key := range p.exceptImmutables { + oldImmutables.Delete(key) + newImmutables.Delete(key) + } + if !reflect.DeepEqual(oldImmutables.Raw(), newImmutables.Raw()) { + for _, key := range p.immutables { + if !reflect.DeepEqual(oldImmutables.Get(key), newImmutables.Get(key)) { + err = NewImmutableError(key, fmt.Sprintf("%v", p.Koanf.Get(key)), fmt.Sprintf("%v", nk.Get(key))) + return // unlocks & runs changes in defer + } + } + } + + p.replaceKoanf(nk) + + // unlocks & runs changes in defer +} + +func (p *Provider) watchForFileChanges(ctx context.Context, c watcherx.EventChannel) { + for { + select { + case <-ctx.Done(): + return + case e := <-c: + switch et := e.(type) { + case *watcherx.ErrorEvent: + p.runOnChanges(e, et) + default: + p.reload(e) + } + } + } +} + +// DirtyPatch patches individual config keys without reloading the full config +// +// WARNING! This method is only useful to override existing keys in string or number +// format. DO NOT use this method to override arrays, maps, or other complex types. +// +// This method DOES NOT validate the config against the config JSON schema. If you +// need to validate the config, use the Set method instead. +// +// This method can not be used to remove keys from the config as that is not +// possible without reloading the full config. +func (p *Provider) DirtyPatch(key string, value any) error { + p.l.Lock() + defer p.l.Unlock() + + t := tuple{Key: key, Value: value} + kc := NewKoanfConfmap([]tuple{t}) + + p.forcedValues = append(p.forcedValues, t) + p.providers = append(p.providers, kc) + + if err := p.Koanf.Load(kc, nil, []koanf.Option{}...); err != nil { + return err + } + + return nil +} + +func (p *Provider) Set(key string, value interface{}) error { + p.l.Lock() + defer p.l.Unlock() + + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + p.providers = append(p.providers, NewKoanfConfmap([]tuple{{Key: key, Value: value}})) + + k, err := p.newKoanf() + if err != nil { + return err + } + + p.replaceKoanf(k) + return nil +} + +// ConfigFiles returns all config file paths configured on this provider, +// including paths set via WithConfigFiles and via the --config flag. +func (p *Provider) ConfigFiles() []string { + paths := make([]string, len(p.files)) + copy(paths, p.files) + if p.flags != nil { + flagPaths, _ := p.flags.GetStringSlice(FlagConfig) + paths = append(paths, flagPaths...) + } + return paths +} + +func (p *Provider) BoolF(key string, fallback bool) bool { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Bool(key) +} + +func (p *Provider) StringF(key string, fallback string) string { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.String(key) +} + +func (p *Provider) StringsF(key string, fallback []string) (val []string) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Strings(key) +} + +func (p *Provider) IntF(key string, fallback int) (val int) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Int(key) +} + +func (p *Provider) Float64F(key string, fallback float64) (val float64) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Float64(key) +} + +func (p *Provider) DurationF(key string, fallback time.Duration) (val time.Duration) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Duration(key) +} + +func (p *Provider) ByteSizeF(key string, fallback bytesize.ByteSize) bytesize.ByteSize { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + switch v := p.Koanf.Get(key).(type) { + case string: + // this type usually comes from user input + dec, err := bytesize.Parse(v) + if err != nil { + p.logger.WithField("key", key).WithField("raw_value", v).WithError(err).Warnf("error parsing byte size value, using fallback of %s", fallback) + return fallback + } + return dec + case float64: + // this type comes from json.Unmarshal + return bytesize.ByteSize(v) + case bytesize.ByteSize: + return v + default: + p.logger.WithField("key", key).WithField("raw_type", fmt.Sprintf("%T", v)).WithField("raw_value", fmt.Sprintf("%+v", v)).Errorf("error converting byte size value because of unknown type, using fallback of %s", fallback) + return fallback + } +} + +func (p *Provider) GetF(key string, fallback interface{}) (val interface{}) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Exists(key) { + return fallback + } + + return p.Get(key) +} + +func (p *Provider) TracingConfig(serviceName string) *otelx.Config { + return &otelx.Config{ + ServiceName: p.StringF("tracing.service_name", serviceName), + DeploymentEnvironment: p.StringF("tracing.deployment_environment", ""), + Provider: p.String("tracing.provider"), + Providers: otelx.ProvidersConfig{ + Jaeger: otelx.JaegerConfig{ + Sampling: otelx.JaegerSampling{ + ServerURL: p.String("tracing.providers.jaeger.sampling.server_url"), + TraceIDRatio: p.Float64F("tracing.providers.jaeger.sampling.trace_id_ratio", 1), + }, + LocalAgentAddress: p.String("tracing.providers.jaeger.local_agent_address"), + }, + Zipkin: otelx.ZipkinConfig{ + ServerURL: p.String("tracing.providers.zipkin.server_url"), + Sampling: otelx.ZipkinSampling{ + SamplingRatio: p.Float64("tracing.providers.zipkin.sampling.sampling_ratio"), + }, + }, + OTLP: otelx.OTLPConfig{ + ServerURL: p.String("tracing.providers.otlp.server_url"), + Insecure: p.Bool("tracing.providers.otlp.insecure"), + Sampling: otelx.OTLPSampling{ + SamplingRatio: p.Float64F("tracing.providers.otlp.sampling.sampling_ratio", 1), + }, + AuthorizationHeader: p.String("tracing.providers.otlp.authorization_header"), + }, + }, + } +} + +func (p *Provider) RequestURIF(path string, fallback *url.URL) *url.URL { + p.l.RLock() + defer p.l.RUnlock() + + switch t := p.Get(path).(type) { + case *url.URL: + return t + case url.URL: + return &t + case string: + if parsed, err := url.ParseRequestURI(t); err == nil { + return parsed + } + } + + return fallback +} + +func (p *Provider) URIF(path string, fallback *url.URL) *url.URL { + p.l.RLock() + defer p.l.RUnlock() + + switch t := p.Get(path).(type) { + case *url.URL: + return t + case url.URL: + return &t + case string: + if parsed, err := url.Parse(t); err == nil { + return parsed + } + } + + return fallback +} + +// PrintHumanReadableValidationErrors prints human readable validation errors. Duh. +func (p *Provider) PrintHumanReadableValidationErrors(w io.Writer, err error) { + p.printHumanReadableValidationErrors(p.Koanf, w, err) +} + +func (p *Provider) printHumanReadableValidationErrors(k *koanf.Koanf, w io.Writer, err error) { + if err == nil { + return + } + + _, _ = fmt.Fprintln(os.Stderr, "") + conf, innerErr := k.Marshal(json.Parser()) + if innerErr != nil { + _, _ = fmt.Fprintf(w, "Unable to unmarshal configuration: %+v", innerErr) + } + + jsonschemax.FormatValidationErrorForCLI(w, conf, err) +} diff --git a/oryx/configx/schema.go b/oryx/configx/schema.go new file mode 100644 index 0000000..9ee1381 --- /dev/null +++ b/oryx/configx/schema.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "fmt" + + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" +) + +func newCompiler(schema []byte) (string, *jsonschema.Compiler, error) { + id := gjson.GetBytes(schema, "$id").String() + if id == "" { + id = fmt.Sprintf("%s.json", uuid.Must(uuid.NewV4()).String()) + } + + compiler := jsonschema.NewCompiler() + if err := compiler.AddResource(id, bytes.NewReader(schema)); err != nil { + return "", nil, errors.WithStack(err) + } + + // DO NOT REMOVE THIS + compiler.ExtractAnnotations = true + + if err := otelx.AddConfigSchema(compiler); err != nil { + return "", nil, err + } + if err := logrusx.AddConfigSchema(compiler); err != nil { + return "", nil, err + } + if err := AddSchemaResources(compiler); err != nil { + return "", nil, err + } + + return id, compiler, nil +} diff --git a/oryx/configx/schema_cache.go b/oryx/configx/schema_cache.go new file mode 100644 index 0000000..023fb18 --- /dev/null +++ b/oryx/configx/schema_cache.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "crypto/sha256" + + "github.com/dgraph-io/ristretto/v2" + + "github.com/ory/jsonschema/v3" +) + +var schemaCacheConfig = &ristretto.Config[[]byte, *jsonschema.Schema]{ + // Hold up to 25 schemas in cache. Usually we only need one. + MaxCost: 25, + NumCounters: 250, + BufferItems: 64, + Metrics: false, + IgnoreInternalCost: true, + Cost: func(*jsonschema.Schema) int64 { + return 1 + }, +} + +var schemaCache, _ = ristretto.NewCache(schemaCacheConfig) + +func getSchema(ctx context.Context, schema []byte) (*jsonschema.Schema, error) { + key := sha256.Sum256(schema) + if val, found := schemaCache.Get(key[:]); found { + return val, nil + } + + schemaID, comp, err := newCompiler(schema) + if err != nil { + return nil, err + } + + validator, err := comp.Compile(ctx, schemaID) + if err != nil { + return nil, err + } + + schemaCache.Set(key[:], validator, 1) + schemaCache.Wait() + return validator, nil +} diff --git a/oryx/configx/schema_path_cache.go b/oryx/configx/schema_path_cache.go new file mode 100644 index 0000000..3b3d5f5 --- /dev/null +++ b/oryx/configx/schema_path_cache.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "crypto/sha256" + + "github.com/ory/x/jsonschemax" + + "github.com/dgraph-io/ristretto/v2" + + "github.com/ory/jsonschema/v3" +) + +var schemaPathCacheConfig = &ristretto.Config[[]byte, []jsonschemax.Path]{ + // Hold up to 25 schemas in cache. Usually we only need one. + MaxCost: 250, + NumCounters: 2500, + BufferItems: 64, + Metrics: false, + IgnoreInternalCost: true, +} + +var schemaPathCache, _ = ristretto.NewCache(schemaPathCacheConfig) + +func getSchemaPaths(rawSchema []byte, schema *jsonschema.Schema) ([]jsonschemax.Path, error) { + key := sha256.Sum256(rawSchema) + if val, found := schemaPathCache.Get(key[:]); found { + return val, nil + } + + keys, err := jsonschemax.ListPathsWithInitializedSchemaAndArraysIncluded(schema) + if err != nil { + return nil, err + } + + schemaPathCache.Set(key[:], keys, 1) + schemaPathCache.Wait() + return keys, nil +} diff --git a/oryx/configx/serve.go b/oryx/configx/serve.go new file mode 100644 index 0000000..0249ccf --- /dev/null +++ b/oryx/configx/serve.go @@ -0,0 +1,137 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "cmp" + "context" + "crypto/tls" + _ "embed" + "fmt" + "net/url" + "os" + + "github.com/ory/x/logrusx" + "github.com/ory/x/tlsx" +) + +const ( + ServeConfigSchemaID = "ory://serve-config" + TLSConfigSchemaID = "ory://tls-config" +) + +//go:embed serve.schema.json +var ServeConfigSchema []byte + +//go:embed tls.schema.json +var TLSConfigSchema []byte + +type ( + Serve struct { + Host, WriteListenFile string + Port int + BaseURL *url.URL + Socket UnixPermission + TLS TLS + RequestLog ServeRequestLog + } + TLS struct { + Enabled bool + AllowTerminationFrom []string + CertBase64, KeyBase64, CertPath, KeyPath string + } + ServeRequestLog struct { + DisableHealth bool + } +) + +func (p *Provider) Serve(prefix string, isDev bool, defaults Serve) *Serve { + prefix = cleanPrefix(prefix) + + defaults.Socket.Mode = cmp.Or(defaults.Socket.Mode, 0o755) + + serve := Serve{ + Host: p.StringF(prefix+"host", defaults.Host), + Port: p.IntF(prefix+"port", defaults.Port), + WriteListenFile: p.StringF(prefix+"write_listen_file", defaults.WriteListenFile), + BaseURL: p.URIF(prefix+"base_url", defaults.BaseURL), + Socket: UnixPermission{ + Owner: p.StringF(prefix+"socket.owner", defaults.Socket.Owner), + Group: p.StringF(prefix+"socket.group", defaults.Socket.Group), + Mode: os.FileMode(p.IntF(prefix+"socket.mode", int(defaults.Socket.Mode))), + }, + TLS: p.TLS(prefix+"tls", defaults.TLS), + RequestLog: ServeRequestLog{ + DisableHealth: p.BoolF(prefix+"request_log.disable_for_health", defaults.RequestLog.DisableHealth), + }, + } + + if serve.BaseURL == nil { + serve.BaseURL = &url.URL{ + Scheme: "http", + Path: "/", + } + if !isDev || serve.TLS.Enabled { + serve.BaseURL.Scheme = "https" + } + host := serve.Host + if host == "0.0.0.0" || host == "" { + var err error + host, err = os.Hostname() + if err != nil { + p.logger.WithError(err).Warn("Unable to get hostname from system, falling back to 127.0.0.1.") + host = "127.0.0.1" + } + } + serve.BaseURL.Host = fmt.Sprintf("%s:%d", host, serve.Port) + } + + return &serve +} + +func (p *Provider) TLS(prefix string, defaults TLS) TLS { + prefix = cleanPrefix(prefix) + + return TLS{ + Enabled: p.BoolF(prefix+"enabled", defaults.Enabled), + AllowTerminationFrom: p.StringsF(prefix+"allow_termination_from", defaults.AllowTerminationFrom), + CertBase64: p.StringF(prefix+"cert.base64", defaults.CertBase64), + KeyBase64: p.StringF(prefix+"key.base64", defaults.KeyBase64), + CertPath: p.StringF(prefix+"cert.path", defaults.CertPath), + KeyPath: p.StringF(prefix+"key.path", defaults.KeyPath), + } +} + +func (t *TLS) GetCertFunc(ctx context.Context, l *logrusx.Logger, ifaceName string) (tlsx.CertFunc, error) { + switch { + case t.CertBase64 != "" && t.KeyBase64 != "": + cert, err := tlsx.CertificateFromBase64(t.CertBase64, t.KeyBase64) + if err != nil { + return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err) + } + l.Infof("Setting up HTTPS for %s", ifaceName) + return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return &cert, nil }, nil + case t.CertPath != "" && t.KeyPath != "": + errs := make(chan error, 1) + getCert, err := tlsx.GetCertificate(ctx, t.CertPath, t.KeyPath, errs) + if err != nil { + return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err) + } + go func() { + for { + select { + case <-ctx.Done(): + return + case err := <-errs: + l.WithError(err).Error("Failed to reload TLS certificates, using previous certificates") + } + } + }() + l.Infof("Setting up HTTPS for %s (automatic certificate reloading active)", ifaceName) + return getCert, nil + default: + l.Infof("TLS has not been configured for %s, skipping", ifaceName) + } + return nil, nil +} diff --git a/oryx/configx/serve.schema.json b/oryx/configx/serve.schema.json new file mode 100644 index 0000000..1f1df38 --- /dev/null +++ b/oryx/configx/serve.schema.json @@ -0,0 +1,70 @@ +{ + "$id": "https://github.com/ory/x/configx/serve.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "request_log": { + "type": "object", + "properties": { + "disable_for_health": { + "title": "Disable health endpoints request logging", + "description": "Disable request logging for /health/alive and /health/ready endpoints", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "base_url": { + "title": "Base URL", + "description": "The URL where the endpoint is exposed at. This domain is used to generate redirects, form URLs, and more.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/", + "https://my-app.com/.ory/kratos/public", + "https://auth.my-app.com/hydra" + ] + }, + "host": { + "title": "Host", + "description": "The host (interface) that the endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Port", + "description": "The port that the endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "socket": { + "type": "object", + "additionalProperties": false, + "description": "Sets the permissions of the unix socket", + "properties": { + "owner": { + "type": "string", + "description": "Owner of unix socket. If empty, the owner will be the user running the service.", + "default": "" + }, + "group": { + "type": "string", + "description": "Group of unix socket. If empty, the group will be the primary group of the user running the service.", + "default": "" + }, + "mode": { + "type": "integer", + "description": "Mode of unix socket in numeric form", + "default": 493, + "minimum": 0, + "maximum": 511 + } + } + }, + "tls": { + "$ref": "ory://tls-config" + } + } +} diff --git a/oryx/configx/span.go b/oryx/configx/span.go new file mode 100644 index 0000000..2f2471e --- /dev/null +++ b/oryx/configx/span.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +const ( + LoadSpanOpName = "config-load" + UpdatedSpanOpName = "config-update" + SnapshotSpanOpName = "config-snapshot" +) diff --git a/oryx/configx/stub/benchmark/benchmark.yaml b/oryx/configx/stub/benchmark/benchmark.yaml new file mode 100644 index 0000000..4723b37 --- /dev/null +++ b/oryx/configx/stub/benchmark/benchmark.yaml @@ -0,0 +1,312 @@ +# Please find the documentation for this file at +# https://www.ory.com/oathkeeper/docs/configuration + +log: + level: debug + format: json + +profiling: cpu + +serve: + proxy: + port: 1234 + host: 127.0.0.1 + + timeout: + read: 1s + write: 2s + idle: 3s + + cors: + enabled: true + allowed_origins: + - https://example.com + - https://*.example.com + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + max_age: 10 + debug: true + tls: + key: + path: /path/to/key.pem + base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3... + cert: + path: /path/to/cert.pem + base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr... + + api: + port: 1235 + host: 127.0.0.2 + + timeout: + read: 1s + write: 2s + idle: 3s + + cors: + enabled: true + allowed_origins: + - https://example.org + - https://*.example.org + allowed_methods: + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + max_age: 10 + debug: true + tls: + key: + path: /path/to/key.pem + base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3... + cert: + path: /path/to/cert.pem + base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr... + + prometheus: + port: 9000 + host: localhost + metrics_path: /metrics + collapse_request_paths: true + +# Configures Access Rules +access_rules: + # Locations (list of URLs) where access rules should be fetched from on boot. + # It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules. + repositories: + # If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be + # fetched from the local file system. + - file://path/to/rules.json + # If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) + # are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{"id":"foo-rule","authenticators":[....]}]`)): + - inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d + # If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be + # fetched from the provided HTTP(s) location. + - https://path-to-my-rules/rules.json + # Optional fields describing matching strategy, defaults to "regexp". + matching_strategy: glob + +errors: + fallback: + - json + handlers: + redirect: + enabled: true + config: + to: http://path-to/redirect + json: + enabled: true + config: + verbose: true + when: + - error: + - unauthorized + - forbidden + - internal_server_error + request: + header: + content_type: + - application/json + accept: + - application/json + cidr: + - 127.0.0.0/24 + +# All authenticators can be configured under this configuration key +authenticators: + # Configures the anonymous authenticator + anonymous: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # Sets the anonymous username. Defaults to "anonymous". Common names include "guest", "anon", "anonymous", "unknown". + subject: guest + + # Configures the cookie session authenticator + cookie_session: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # Sets the origin to proxy requests to. If the response is a 200 with body `{ "subject": "...", "extra": {} }` + # The request will pass the subject through successfully, otherwise it will be marked as unauthorized + check_session_url: https://session-store-host + + # Sets a list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if + # none of the passed cookies are set on the request + only: + - sessionid + + # Configures the jwt authenticator + jwt: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The URL where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web + # Token. Usually something like "https://my-keys.com/.well-known/jwks.json". The response of that endpoint must + # return a JSON Web Key Set (JWKS). + jwks_urls: + - https://my-website.com/.well-known/jwks.json + - https://my-other-website.com/.well-known/jwks.json + - file://path/to/local/jwks.json + + # Sets the strategy to be used to validate/match the scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults + # to "none". + scope_strategy: wildcard + + # Configures the noop authenticator + noop: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + # Configures the oauth2_client_credentials authenticator + oauth2_client_credentials: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint that will be used to validate the client credentials. + token_url: https://my-website.com/oauth2/token + + # Configures the oauth2_introspection authenticator + oauth2_introspection: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The OAuth 2.0 Token Introspection endpoint. + introspection_url: https://my-website.com/oauth2/introspection + + # Sets the strategy to be used to validate/match the token scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults + # to "none". + scope_strategy: exact + + # Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer + # Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant. + pre_authorization: + # Enable pre-authorization. Defaults to false. + enabled: true + + # REQUIRED IF ENABLED - The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant. + client_id: some_id + + # REQUIRED IF ENABLED - The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant. + client_secret: some_secret + + # The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant. + scope: + - foo + - bar + + # REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed. + token_url: https://my-website.com/oauth2/token + + # Configures the unauthorized authenticator + unauthorized: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + +# All authorizers can be configured under this configuration key +authorizers: + # Configures the allow authorizer + allow: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + # Configures the deny authorizer + deny: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + # Configures the keto_engine_acp_ory authorizer + keto_engine_acp_ory: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The base URL of ORY Keto, typically something like http(s)://[:]/ + base_url: http://my-keto/ + required_action: unknown + required_resource: unknown + + # Configures the remote authorizer + remote: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + remote: https://host/path + headers: {} + + # Configures the remote_json authorizer + remote_json: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + remote: https://host/path + payload: "{}" + +# All mutators can be configured under this configuration key +mutators: + header: + enabled: true + config: + headers: + foo: bar + + # Configures the cookie mutator + cookie: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + config: + cookies: + foo: bar + + # Configures the hydrator mutator + hydrator: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + + config: + api: + url: https://some-url/ + + # Configures the id_token mutator + id_token: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + config: + # REQUIRED IF ENABLED - Sets the "iss" value of the ID Token. + issuer_url: https://my-oathkeeper/ + # REQUIRED IF ENABLED - Sets the URL where keys should be fetched from. Supports remote locations (http, https) as + # well as local filesystem paths. + jwks_url: https://fetch-keys/from/this/location.json + # jwks_url: file:///from/this/absolute/location.json + # jwks_url: file://../from/this/relative/location.json + + # Sets the time-to-live of the ID token. Defaults to one minute. Valid time units are: s (second), m (minute), h (hour). + ttl: 1h + + # Configures the noop mutator + noop: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true diff --git a/oryx/configx/stub/benchmark/schema.config.json b/oryx/configx/stub/benchmark/schema.config.json new file mode 100644 index 0000000..08d9faf --- /dev/null +++ b/oryx/configx/stub/benchmark/schema.config.json @@ -0,0 +1,204 @@ +{ + "log": { + "level": "debug", + "format": "json" + }, + "profiling": "cpu", + "serve": { + "proxy": { + "port": 1234, + "host": "127.0.0.1", + "timeout": { + "read": "1s", + "write": "2s", + "idle": "3s" + }, + "cors": { + "enabled": true, + "allowed_origins": ["https://example.com", "https://*.example.com"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_headers": ["Authorization", "Content-Type"], + "exposed_headers": ["Content-Type"], + "allow_credentials": true, + "max_age": 10, + "debug": true + }, + "tls": { + "key": { + "path": "/path/to/key.pem", + "base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..." + }, + "cert": { + "path": "/path/to/cert.pem", + "base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + } + } + }, + "api": { + "port": 1235, + "host": "127.0.0.2", + "cors": { + "enabled": true, + "allowed_origins": ["https://example.org", "https://*.example.org"], + "allowed_methods": ["GET", "PUT", "PATCH", "DELETE"], + "allowed_headers": ["Authorization", "Content-Type"], + "exposed_headers": ["Content-Type"], + "allow_credentials": true, + "max_age": 10, + "debug": true + }, + "tls": { + "key": { + "path": "/path/to/key.pem", + "base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..." + }, + "cert": { + "path": "/path/to/cert.pem", + "base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + } + } + } + }, + "access_rules": { + "repositories": [ + "file://path/to/rules.json", + "inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d", + "https://path-to-my-rules/rules.json" + ], + "matching_strategy": "glob" + }, + "errors": { + "fallback": ["json"], + "handlers": { + "redirect": { + "enabled": true, + "config": { + "to": "http://path-to/redirect" + } + }, + "json": { + "enabled": true, + "config": { + "verbose": true, + "when": [ + { + "error": ["unauthorized", "forbidden", "internal_server_error"], + "request": { + "header": { + "content_type": ["application/json"], + "accept": ["application/json"] + }, + "cidr": ["127.0.0.0/24"] + } + } + ] + } + } + } + }, + "authenticators": { + "anonymous": { + "enabled": true, + "config": { + "subject": "guest" + } + }, + "cookie_session": { + "enabled": true, + "config": { + "check_session_url": "https://session-store-host", + "only": ["sessionid"] + } + }, + "jwt": { + "enabled": true, + "config": { + "jwks_urls": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ], + "scope_strategy": "wildcard" + } + }, + "noop": { + "enabled": true + }, + "oauth2_client_credentials": { + "enabled": true, + "config": { + "token_url": "https://my-website.com/oauth2/token" + } + }, + "oauth2_introspection": { + "enabled": true, + "config": { + "introspection_url": "https://my-website.com/oauth2/introspection", + "scope_strategy": "exact", + "pre_authorization": { + "enabled": true, + "client_id": "some_id", + "client_secret": "some_secret", + "scope": ["foo", "bar"], + "token_url": "https://my-website.com/oauth2/token" + } + } + }, + "unauthorized": { + "enabled": true + } + }, + "authorizers": { + "allow": { + "enabled": true + }, + "deny": { + "enabled": true + }, + "keto_engine_acp_ory": { + "enabled": true, + "config": { + "base_url": "http://my-keto/", + "required_action": "unknown", + "required_resource": "unknown" + } + } + }, + "mutators": { + "header": { + "enabled": false, + "config": { + "headers": { + "foo": "bar" + } + } + }, + "cookie": { + "enabled": true, + "config": { + "cookies": { + "foo": "bar" + } + } + }, + "hydrator": { + "enabled": true, + "config": { + "api": { + "url": "https://some-url/" + } + } + }, + "id_token": { + "enabled": true, + "config": { + "issuer_url": "https://my-oathkeeper/", + "jwks_url": "https://fetch-keys/from/this/location.json", + "ttl": "1h" + } + }, + "noop": { + "enabled": true + } + } +} diff --git a/oryx/configx/stub/domain-aliases/config.schema.json b/oryx/configx/stub/domain-aliases/config.schema.json new file mode 100644 index 0000000..7900791 --- /dev/null +++ b/oryx/configx/stub/domain-aliases/config.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "domain_aliases": { + "title": "Domain Aliases", + "description": "Adds an alias domain. If a request with the hostname (FQDN) matching the hostname in the alias is found, that URL is used as the base URL.", + "type": "array", + "items": [ + { + "additionalProperties": false, + "type": "object", + "required": ["match_domain", "base_path", "scheme"], + "properties": { + "match_domain": { + "minLength": 1, + "title": "Matching Domain", + "description": "Sets the matching domain. If the domain matches with this entry, the accompanying base_url will be used.", + "type": "string", + "examples": ["localhost", "my-domain.com"] + }, + "scheme": { + "title": "Scheme", + "description": "Sets the scheme, for example https or http.", + "type": "string", + "enum": ["http", "https"] + }, + "base_path": { + "minLength": 1, + "title": "Base Path", + "description": "Sets the base path for the matched domain.", + "type": "string", + "default": "/", + "pattern": "^/.*$", + "examples": ["/", "/.ory/kratos"] + } + } + } + ] + } + } +} diff --git a/oryx/configx/stub/from-files/a.yaml b/oryx/configx/stub/from-files/a.yaml new file mode 100644 index 0000000..f3e1808 --- /dev/null +++ b/oryx/configx/stub/from-files/a.yaml @@ -0,0 +1,27 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings diff --git a/oryx/configx/stub/from-files/b.yaml b/oryx/configx/stub/from-files/b.yaml new file mode 100644 index 0000000..1d48989 --- /dev/null +++ b/oryx/configx/stub/from-files/b.yaml @@ -0,0 +1,54 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/from-files/config.schema.json b/oryx/configx/stub/from-files/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/from-files/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "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/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/from-files/expected.json b/oryx/configx/stub/from-files/expected.json new file mode 100644 index 0000000..2fe5dd1 --- /dev/null +++ b/oryx/configx/stub/from-files/expected.json @@ -0,0 +1,124 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": false + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/hydra/config.schema.json b/oryx/configx/stub/hydra/config.schema.json new file mode 100644 index 0000000..e2ce4af --- /dev/null +++ b/oryx/configx/stub/hydra/config.schema.json @@ -0,0 +1,792 @@ +{ + "$id": "https://github.com/ory/hydra/docs/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Hydra Configuration", + "type": "object", + "definitions": { + "http_method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + }, + "port_number": { + "type": "integer", + "description": "The port to listen on.", + "minimum": 1, + "maximum": 65535 + }, + "socket": { + "type": "object", + "additionalProperties": false, + "description": "Sets the permissions of the unix socket", + "properties": { + "owner": { + "type": "string", + "description": "Owner of unix socket. If empty, the owner will be the user running hydra.", + "default": "" + }, + "group": { + "type": "string", + "description": "Group of unix socket. If empty, the group will be the primary group of the user running hydra.", + "default": "" + }, + "mode": { + "type": "integer", + "description": "Mode of unix socket in numeric form", + "default": 493, + "minimum": 0, + "maximum": 511 + } + } + }, + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": ["Authorization", "Content-Type"], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "pem_file": { + "type": "object", + "oneOf": [ + { + "properties": { + "path": { + "type": "string", + "description": "The path to the pem file.", + "examples": ["/path/to/file.pem"] + } + }, + "additionalProperties": false, + "required": ["path"] + }, + { + "properties": { + "base64": { + "type": "string", + "description": "The base64 encoded string (without padding).", + "contentEncoding": "base64", + "contentMediaType": "application/x-pem-file", + "examples": ["b3J5IGh5ZHJhIGlzIGF3ZXNvbWUK"] + } + }, + "additionalProperties": false, + "required": ["base64"] + } + ] + }, + "duration": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "examples": ["1h"] + } + }, + "properties": { + "log": { + "type": "object", + "additionalProperties": false, + "description": "Configures the logger", + "properties": { + "level": { + "type": "string", + "description": "Sets the log level.", + "enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"], + "default": "info" + }, + "leak_sensitive_values": { + "type": "boolean", + "description": "Logs sensitive values such as cookie and URL parameter.", + "default": false + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "description": "Sets the log format.", + "enum": ["json", "json_pretty", "text"], + "default": "text" + } + } + }, + "serve": { + "type": "object", + "additionalProperties": false, + "description": "Controls the configuration for the http(s) daemon(s).", + "properties": { + "public": { + "type": "object", + "additionalProperties": false, + "description": "Controls the public daemon serving public API endpoints like /oauth2/auth, /oauth2/token, /.well-known/jwks.json", + "properties": { + "port": { + "default": 4444, + "allOf": [ + { + "$ref": "#/definitions/port_number" + } + ] + }, + "host": { + "type": "string", + "description": "The interface or unix socket ORY Hydra should listen and handle public API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.", + "default": "", + "examples": ["localhost"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "access_log": { + "type": "object", + "additionalProperties": false, + "description": "Access Log configuration for public server.", + "properties": { + "disable_for_health": { + "type": "boolean", + "description": "Disable access log for health endpoints.", + "default": false + } + } + } + } + }, + "admin": { + "type": "object", + "additionalProperties": false, + "properties": { + "port": { + "default": 4445, + "allOf": [ + { + "$ref": "#/definitions/port_number" + } + ] + }, + "host": { + "type": "string", + "description": "The interface or unix socket ORY Hydra should listen and handle administrative API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.", + "default": "", + "examples": ["localhost"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "access_log": { + "type": "object", + "additionalProperties": false, + "description": "Access Log configuration for admin server.", + "properties": { + "disable_for_health": { + "type": "boolean", + "description": "Disable access log for health endpoints.", + "default": false + } + } + } + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "description": "Configures HTTPS (HTTP over TLS). If configured, the server automatically supports HTTP/2.", + "properties": { + "key": { + "description": "Configures the private key (pem encoded).", + "allOf": [ + { + "$ref": "#/definitions/pem_file" + } + ] + }, + "cert": { + "description": "Configures the private key (pem encoded).", + "allOf": [ + { + "$ref": "#/definitions/pem_file" + } + ] + }, + "allow_termination_from": { + "type": "array", + "description": "Whitelist one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. Hydra serves http instead of https when this option is set.", + "items": { + "type": "string", + "oneOf": [ + { + "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + { + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" + } + ], + "examples": ["127.0.0.1/32"] + } + } + } + }, + "cookies": { + "type": "object", + "additionalProperties": false, + "properties": { + "same_site_mode": { + "type": "string", + "description": "Specify the SameSite mode that cookies should be sent with.", + "enum": ["Strict", "Lax", "None"], + "default": "None" + }, + "same_site_legacy_workaround": { + "type": "boolean", + "description": "Some older browser versions don’t work with SameSite=None. This option enables the workaround defined in https://web.dev/samesite-cookie-recipes/ which essentially stores a second cookie without SameSite as a fallback.", + "default": false, + "examples": [true] + } + } + } + } + }, + "dsn": { + "type": "string", + "description": "Sets the data source name. This configures the backend where ORY Hydra persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.sh/docs/hydra/dependencies-environment#sql" + }, + "webfinger": { + "type": "object", + "additionalProperties": false, + "description": "Configures ./well-known/ settings.", + "properties": { + "jwks": { + "type": "object", + "additionalProperties": false, + "description": "Configures the /.well-known/jwks.json endpoint.", + "properties": { + "broadcast_keys": { + "type": "array", + "description": "A list of JSON Web Keys that should be exposed at that endpoint. This is usually the public key for verifying OpenID Connect ID Tokens. However, you might want to add additional keys here as well.", + "items": { + "type": "string" + }, + "default": ["hydra.openid.id-token"], + "examples": ["hydra.jwt.access-token"] + } + } + }, + "oidc_discovery": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect Discovery (/.well-known/openid-configuration).", + "properties": { + "jwks_url": { + "type": "string", + "description": "Overwrites the JWKS URL", + "format": "uri", + "examples": ["https://my-service.com/.well-known/jwks.json"] + }, + "token_url": { + "type": "string", + "description": "Overwrites the OAuth2 Token URL", + "format": "uri", + "examples": ["https://my-service.com/oauth2/token"] + }, + "auth_url": { + "type": "string", + "description": "Overwrites the OAuth2 Auth URL", + "format": "uri", + "examples": ["https://my-service.com/oauth2/auth"] + }, + "client_registration_url": { + "description": "Sets the OpenID Connect Dynamic Client Registration Endpoint", + "type": "string", + "format": "uri", + "examples": ["https://my-service.com/clients"] + }, + "supported_claims": { + "type": "array", + "description": "A list of supported claims to be broadcasted. Claim \"sub\" is always included.", + "items": { + "type": "string" + }, + "examples": [["email", "username"]] + }, + "supported_scope": { + "type": "array", + "description": "The scope OAuth 2.0 Clients may request. Scope `offline`, `offline_access`, and `openid` are always included.", + "items": { + "type": "string" + }, + "examples": [["email", "whatever", "read.photos"]] + }, + "userinfo_url": { + "type": "string", + "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to ORY Hydra's userinfo endpoint at /userinfo. Set this value if you want to handle this endpoint yourself.", + "format": "uri", + "examples": ["https://example.org/my-custom-userinfo-endpoint"] + } + } + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect features.", + "properties": { + "subject_identifiers": { + "type": "object", + "additionalProperties": false, + "description": "Configures the Subject Identifier algorithm. For more information please head over to the documentation: https://www.ory.sh/docs/hydra/advanced#subject-identifier-algorithms", + "properties": { + "enabled": { + "type": "array", + "description": "A list of algorithms to enable.", + "items": { + "type": "string", + "enum": ["public", "pairwise"] + } + }, + "pairwise": { + "type": "object", + "additionalProperties": false, + "description": "Configures the pairwise algorithm.", + "properties": { + "salt": { + "type": "string" + } + }, + "required": ["salt"] + } + }, + "if": { + "properties": { + "enabled": { + "contains": { + "const": "pairwise" + } + } + } + }, + "then": { + "required": ["pairwise"] + }, + "else": { + "properties": { + "pairwise": { + "$comment": "This enforces pairwise to not be set if 'enabled' does not contain 'pairwise'", + "not": {} + } + } + }, + "examples": [ + { + "enabled": ["public", "pairwise"], + "pairwise": { + "salt": "some-random-salt" + } + } + ] + }, + "dynamic_client_registration": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect Dynamic Client Registration (exposed as admin endpoints /clients/...).", + "properties": { + "default_scope": { + "type": "array", + "description": "The OpenID Connect Dynamic Client Registration specification has no concept of whitelisting OAuth 2.0 Scope. If you want to expose Dynamic Client Registration, you should set the default scope enabled for newly registered clients. Keep in mind that users can overwrite this default by setting the \"scope\" key in the registration payload, effectively disabling the concept of whitelisted scopes.", + "items": { + "type": "string" + }, + "examples": [["openid", "offline", "offline_access"]] + } + } + } + } + }, + "urls": { + "type": "object", + "additionalProperties": false, + "properties": { + "self": { + "type": "object", + "additionalProperties": false, + "properties": { + "issuer": { + "type": "string", + "description": "This value will be used as the \"issuer\" in access and ID tokens. It must be specified and using HTTPS protocol, unless --dangerous-force-http is set. This should typically be equal to the public value.", + "format": "uri", + "examples": ["https://localhost:4444/"] + }, + "public": { + "type": "string", + "description": "This is the base location of the public endpoints of your ORY Hydra installation. This should typically be equal to the issuer value. If left unspecified, it falls back to the issuer value.", + "format": "uri", + "examples": ["https://localhost:4444/"] + } + } + }, + "login": { + "type": "string", + "description": "Sets the login endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-login.app/login"] + }, + "consent": { + "type": "string", + "description": "Sets the consent endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-consent.app/consent"] + }, + "logout": { + "type": "string", + "description": "Sets the logout endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-logout.app/logout"] + }, + "error": { + "type": "string", + "description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-error.app/error"] + }, + "post_logout_redirect": { + "type": "string", + "description": "When a user agent requests to logout, it will be redirected to this url afterwards per default.", + "format": "uri", + "examples": ["https://my-example.app/logout-successful"] + } + } + }, + "strategies": { + "type": "object", + "additionalProperties": false, + "properties": { + "scope": { + "type": "string", + "description": "Defines how scopes are matched. For more details have a look at https://github.com/ory/fosite#scopes", + "enum": [ + "exact", + "wildcard", + "DEPRECATED_HIERARCHICAL_SCOPE_STRATEGY" + ], + "default": "wildcard" + }, + "access_token": { + "type": "string", + "description": "Defines access token type. jwt is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens", + "enum": ["opaque", "jwt"] + } + } + }, + "ttl": { + "type": "object", + "additionalProperties": false, + "description": "Configures time to live.", + "properties": { + "login_consent_request": { + "description": "Configures how long a user login and consent flow may take.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "access_token": { + "description": "Configures how long access tokens are valid.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "refresh_token": { + "description": "Configures how long refresh tokens are valid. Set to -1 for refresh tokens to never expire.", + "default": "720h", + "oneOf": [ + { + "$ref": "#/definitions/duration" + }, + { + "enum": ["-1", -1] + } + ] + }, + "id_token": { + "description": "Configures how long id tokens are valid.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "auth_code": { + "description": "Configures how long auth codes are valid.", + "default": "10m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + } + }, + "oauth2": { + "type": "object", + "additionalProperties": false, + "properties": { + "expose_internal_errors": { + "type": "boolean", + "description": "Set this to true if you want to share error debugging information with your OAuth 2.0 clients. Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error codes and similar errors.", + "default": false, + "examples": [true] + }, + "session": { + "type": "object", + "properties": { + "encrypt_at_rest": { + "type": "boolean", + "default": true, + "title": "Encrypt OAuth2 Session", + "description": "If set to true (default) ORY Hydra encrypt OAuth2 and OpenID Connect session data using AES-GCM and the system secret before persisting it in the database." + } + } + }, + "include_legacy_error_fields": { + "type": "boolean", + "description": "Set this to true if you want to include the `error_hint` and `error_debug` legacy fields in error responses. We recommend to set this to `false` unless you have clients using these fields.", + "default": false, + "examples": [true] + }, + "hashers": { + "type": "object", + "additionalProperties": false, + "description": "Configures hashing algorithms. Supports only BCrypt at the moment.", + "properties": { + "bcrypt": { + "type": "object", + "additionalProperties": false, + "description": "Configures the BCrypt hashing algorithm used for hashing Client Secrets.", + "properties": { + "cost": { + "type": "integer", + "description": "Sets the BCrypt cost. The higher the value, the more CPU time is being used to generate hashes.", + "default": 10, + "minimum": 4, + "maximum": 31 + } + } + } + } + }, + "pkce": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforced": { + "type": "boolean", + "description": "Sets whether PKCE should be enforced for all clients.", + "examples": [true] + }, + "enforced_for_public_clients": { + "type": "boolean", + "description": "Sets whether PKCE should be enforced for public clients.", + "examples": [true] + } + } + }, + "client_credentials": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_grant_allowed_scope": { + "type": "boolean", + "description": "Defines how scopes are added if the request doesn't contains any scope", + "examples": [false] + } + } + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "description": "The secrets section configures secrets used for encryption and signing of several systems. All secrets can be rotated, for more information on this topic go to: https://www.ory.sh/docs/hydra/advanced#rotation-of-hmac-token-signing-and-database-and-cookie-encryption-keys", + "properties": { + "system": { + "description": "The system secret must be at least 16 characters long. If none is provided, one will be generated. They key is used to encrypt sensitive data using AES-GCM (256 bit) and validate HMAC signatures. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.", + "type": "array", + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "this-is-the-primary-secret", + "this-is-an-old-secret", + "this-is-another-old-secret" + ] + ] + }, + "cookie": { + "type": "array", + "description": "A secret that is used to encrypt cookie sessions. Defaults to secrets.system. It is recommended to use a separate secret in production. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.", + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "this-is-the-primary-secret", + "this-is-an-old-secret", + "this-is-another-old-secret" + ] + ] + } + } + }, + "profiling": { + "type": "string", + "description": "Enables profiling if set. For more details on profiling, head over to: https://blog.golang.org/profiling-go-programs", + "enum": ["cpu", "mem"], + "examples": ["cpu"] + }, + "tracing": { + "$ref": "ory://tracing-config" + }, + "version": { + "type": "string", + "title": "The Hydra version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "cgroups": { + "type": "object", + "additionalProperties": false, + "description": "ORY Hydra can respect Linux container CPU quota", + "properties": { + "v1": { + "type": "object", + "additionalProperties": false, + "description": "Configures parameters using cgroups v1 hierarchy", + "properties": { + "auto_max_procs_enabled": { + "type": "boolean", + "description": "Set GOMAXPROCS automatically according to cgroups limits", + "default": false, + "examples": [true] + } + } + } + } + } + }, + "required": ["dsn"] +} diff --git a/oryx/configx/stub/hydra/expected.json b/oryx/configx/stub/hydra/expected.json new file mode 100644 index 0000000..d35faf4 --- /dev/null +++ b/oryx/configx/stub/hydra/expected.json @@ -0,0 +1,122 @@ +{ + "cgroups": { + "v1": { + "auto_max_procs_enabled": false + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "log": { + "format": "text", + "leak_sensitive_values": false, + "level": "info" + }, + "oauth2": { + "expose_internal_errors": false, + "hashers": { + "bcrypt": { + "cost": 10 + } + }, + "include_legacy_error_fields": false, + "session": { + "encrypt_at_rest": true + } + }, + "oidc": { + "subject_identifiers": { + "enabled": ["pairwise", "public"], + "pairwise": { + "salt": "youReallyNeedToChangeThis" + } + } + }, + "secrets": { + "system": ["youReallyNeedToChangeThis"] + }, + "serve": { + "admin": { + "access_log": { + "disable_for_health": false + }, + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": false, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "", + "port": 4445, + "socket": { + "group": "", + "mode": 493, + "owner": "" + } + }, + "cookies": { + "same_site_legacy_workaround": false, + "same_site_mode": "Lax" + }, + "public": { + "access_log": { + "disable_for_health": false + }, + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": false, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "", + "port": 4444, + "socket": { + "group": "", + "mode": 493, + "owner": "" + } + } + }, + "strategies": { + "scope": "wildcard" + }, + "tracing": { + "provider": "jaeger", + "providers": { + "jaeger": { + "local_agent_address": "jaeger:6831", + "sampling": { + "server_url": "http://jaeger:5778/sampling" + } + } + } + }, + "ttl": { + "access_token": "1h", + "auth_code": "10m", + "id_token": "1h", + "login_consent_request": "1h", + "refresh_token": "720h" + }, + "urls": { + "consent": "http://127.0.0.1:3000/consent", + "login": "http://127.0.0.1:3000/login", + "logout": "http://127.0.0.1:3000/logout", + "self": { + "issuer": "http://127.0.0.1:4444" + } + }, + "webfinger": { + "jwks": { + "broadcast_keys": ["hydra.openid.id-token"] + } + } +} diff --git a/oryx/configx/stub/hydra/hydra.yaml b/oryx/configx/stub/hydra/hydra.yaml new file mode 100644 index 0000000..441dfc9 --- /dev/null +++ b/oryx/configx/stub/hydra/hydra.yaml @@ -0,0 +1,22 @@ +serve: + cookies: + same_site_mode: Lax + +urls: + self: + issuer: http://127.0.0.1:4444 + consent: http://127.0.0.1:3000/consent + login: http://127.0.0.1:3000/login + logout: http://127.0.0.1:3000/logout + +secrets: + system: + - youReallyNeedToChangeThis + +oidc: + subject_identifiers: + enabled: + - pairwise + - public + pairwise: + salt: youReallyNeedToChangeThis diff --git a/oryx/configx/stub/kratos/config.schema.json b/oryx/configx/stub/kratos/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/kratos/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "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/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/kratos/expected.json b/oryx/configx/stub/kratos/expected.json new file mode 100644 index 0000000..e9b5d45 --- /dev/null +++ b/oryx/configx/stub/kratos/expected.json @@ -0,0 +1,135 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": true, + "config": { + "providers": [ + { + "id": "google", + "provider": "google", + "mapper_url": "file:///etc/config/kratos/oidc.google.jsonnet", + "client_id": "client@example.com", + "client_secret": "secret" + } + ] + } + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/kratos/kratos.yaml b/oryx/configx/stub/kratos/kratos.yaml new file mode 100644 index 0000000..0d74f19 --- /dev/null +++ b/oryx/configx/stub/kratos/kratos.yaml @@ -0,0 +1,76 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + oidc: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/multi/a.yaml b/oryx/configx/stub/multi/a.yaml new file mode 100644 index 0000000..f3e1808 --- /dev/null +++ b/oryx/configx/stub/multi/a.yaml @@ -0,0 +1,27 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings diff --git a/oryx/configx/stub/multi/b.yaml b/oryx/configx/stub/multi/b.yaml new file mode 100644 index 0000000..1d48989 --- /dev/null +++ b/oryx/configx/stub/multi/b.yaml @@ -0,0 +1,54 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/multi/config.schema.json b/oryx/configx/stub/multi/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/multi/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "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/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/multi/expected.json b/oryx/configx/stub/multi/expected.json new file mode 100644 index 0000000..2fe5dd1 --- /dev/null +++ b/oryx/configx/stub/multi/expected.json @@ -0,0 +1,124 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": false + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/nested-array/config.schema.json b/oryx/configx/stub/nested-array/config.schema.json new file mode 100644 index 0000000..b70c935 --- /dev/null +++ b/oryx/configx/stub/nested-array/config.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + } + }, + "additionalProperties": false, + "required": [], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + } + } + } +} diff --git a/oryx/configx/stub/nested-array/expected.json b/oryx/configx/stub/nested-array/expected.json new file mode 100644 index 0000000..e8609d2 --- /dev/null +++ b/oryx/configx/stub/nested-array/expected.json @@ -0,0 +1,11 @@ +{ + "providers": [ + { + "id": "google", + "client_id": "client@example.com" + }, + { + "client_id": "some@example.com" + } + ] +} diff --git a/oryx/configx/stub/nested-array/kratos.yaml b/oryx/configx/stub/nested-array/kratos.yaml new file mode 100644 index 0000000..ac667ff --- /dev/null +++ b/oryx/configx/stub/nested-array/kratos.yaml @@ -0,0 +1,2 @@ +providers: + - id: google diff --git a/oryx/configx/stub/watch/config.schema.json b/oryx/configx/stub/watch/config.schema.json new file mode 100644 index 0000000..80382f4 --- /dev/null +++ b/oryx/configx/stub/watch/config.schema.json @@ -0,0 +1,19 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "config", + "type": "object", + "properties": { + "dsn": { + "type": "string" + }, + "foo": { + "const": "bar" + }, + "bar": { + "type": "string", + "enum": ["foo", "bar", "baz"] + } + }, + "required": ["dsn"] +} diff --git a/oryx/configx/tls.schema.json b/oryx/configx/tls.schema.json new file mode 100644 index 0000000..832f679 --- /dev/null +++ b/oryx/configx/tls.schema.json @@ -0,0 +1,68 @@ +{ + "$id": "https://github.com/ory/x/tlsx/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "key": { + "title": "Private Key (PEM)", + "$ref": "#/definitions/source" + }, + "cert": { + "title": "TLS Certificate (PEM)", + "$ref": "#/definitions/source" + }, + "allow_termination_from": { + "type": "array", + "description": "Allow-list one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. The service serves http instead of https when this option is set.", + "items": { + "description": "CIDR address range.", + "type": "string", + "oneOf": [ + { + "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + { + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" + } + ], + "examples": ["127.0.0.1/32"] + } + } + }, + "definitions": { + "source": { + "type": "object", + "oneOf": [ + { + "properties": { + "path": { + "title": "Path to PEM-encoded File", + "type": "string", + "examples": ["path/to/file.pem"] + } + }, + "additionalProperties": false + }, + { + "properties": { + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/oryx/contextx/contextual.go b/oryx/contextx/contextual.go new file mode 100644 index 0000000..e7d7446 --- /dev/null +++ b/oryx/contextx/contextual.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/ory/x/configx" + + "github.com/gofrs/uuid" +) + +type ( + Contextualizer interface { + // Network returns the network id for the given context. + Network(ctx context.Context, network uuid.UUID) uuid.UUID + + // Config returns the config for the given context. + Config(ctx context.Context, config *configx.Provider) *configx.Provider + } + Provider interface { + Contextualizer() Contextualizer + } + Static struct { + NID uuid.UUID + C *configx.Provider + } +) + +func (d *Static) Network(_ context.Context, nid uuid.UUID) uuid.UUID { + if d.NID == uuid.Nil { + return nid + } + return d.NID +} + +func (d *Static) Config(_ context.Context, c *configx.Provider) *configx.Provider { + if d.C == nil { + return c + } + return d.C +} diff --git a/oryx/contextx/contextual_mock.go b/oryx/contextx/contextual_mock.go new file mode 100644 index 0000000..ef75861 --- /dev/null +++ b/oryx/contextx/contextual_mock.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/ory/x/configx" + + "github.com/gofrs/uuid" +) + +// TestContextualizer is a mock implementation of the Contextualizer interface. +type TestContextualizer struct{} + +type contextKeyFake int + +// fakeNIDContext is a test key for NID. +const fakeNIDContext contextKeyFake = 1 + +// SetNIDContext sets the nid for the given context. +func SetNIDContext(ctx context.Context, nid uuid.UUID) context.Context { + return context.WithValue(ctx, fakeNIDContext, nid) //nolint:staticcheck +} + +// Network returns the network id for the given context. +func (d *TestContextualizer) Network(ctx context.Context, network uuid.UUID) uuid.UUID { + nid, ok := ctx.Value(fakeNIDContext).(uuid.UUID) + if !ok { + return network + } + return nid +} + +// Config returns the config for the given context. +func (d *TestContextualizer) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + return config +} diff --git a/oryx/contextx/default.go b/oryx/contextx/default.go new file mode 100644 index 0000000..5733807 --- /dev/null +++ b/oryx/contextx/default.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/configx" +) + +type Default struct{} + +var _ Contextualizer = (*Default)(nil) + +func (d *Default) Network(_ context.Context, network uuid.UUID) uuid.UUID { + if network == uuid.Nil { + panic("nid must be not nil") + } + return network +} + +func (d *Default) Config(_ context.Context, config *configx.Provider) *configx.Provider { + return config +} diff --git a/oryx/contextx/testhelpers.go b/oryx/contextx/testhelpers.go new file mode 100644 index 0000000..98e835b --- /dev/null +++ b/oryx/contextx/testhelpers.go @@ -0,0 +1,158 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + "net/http" + "net/http/httptest" + + "github.com/gofrs/uuid" + + "github.com/ory/x/configx" +) + +type ( + TestConfigProvider struct { + ConfigSchema []byte + Options []configx.OptionModifier + } + contextKey int +) + +func NewTestConfigProvider(schema []byte, opts ...configx.OptionModifier) *TestConfigProvider { + return &TestConfigProvider{ + ConfigSchema: schema, + Options: opts, + } +} + +func (t *TestConfigProvider) Network(ctx context.Context, network uuid.UUID) uuid.UUID { + return (&Default{}).Network(ctx, network) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + return config + } + + opts := make([]configx.OptionModifier, 1, 1+len(values)) + opts[0] = configx.WithValues(config.All()) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := configx.New(ctx, t.ConfigSchema, append(t.Options, 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 ( + _ 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/oryx/contextx/tree.go b/oryx/contextx/tree.go new file mode 100644 index 0000000..125e8a3 --- /dev/null +++ b/oryx/contextx/tree.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + "testing" +) + +type ContextKey int + +const ( + ValidContextKey ContextKey = iota + 1 +) + +var RootContext = context.WithValue(context.Background(), ValidContextKey, true) + +func TestRootContext(t testing.TB) context.Context { + return context.WithValue(t.Context(), ValidContextKey, true) +} + +func IsRootContext(ctx context.Context) bool { + is, ok := ctx.Value(ValidContextKey).(bool) + return is && ok +} diff --git a/oryx/cookiex/cookiex.go b/oryx/cookiex/cookiex.go new file mode 100644 index 0000000..3ebcf07 --- /dev/null +++ b/oryx/cookiex/cookiex.go @@ -0,0 +1,266 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package cookiex encodes authenticated, encrypted HTTP cookies. Values are +// sealed with the Ory-wide AEAD (see ory/x/aead) under keys derived from the +// configured secrets with HKDF-SHA256, and bound to their purpose and cookie +// name. The package replaces the gorilla/securecookie-based cookie stores; a +// decode-only fallback reads cookies minted by those stores until they have +// expired. +package cookiex + +import ( + "crypto/hkdf" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/aead" +) + +const ( + // formatPrefix marks the versioned wire format. A dot is not part of the + // base64url alphabet, so legacy securecookie values can never collide + // with the prefix. + formatPrefix = "v1." + // kdfInfo domain-separates the HKDF key derivation. + kdfInfo = "ory/x/cookiex/v1" + // aadPrefix domain-separates the additional authenticated data. + aadPrefix = "ory/x/cookiex/v1" + // defaultMaxAge matches the gorilla/securecookie default that the + // previous cookie stores relied on. + defaultMaxAge = 30 * 24 * time.Hour + // maxCookieValueLength is the browser limit that securecookie also + // enforced. + maxCookieValueLength = 4096 +) + +// ErrInvalidCookie is returned when a cookie with the requested name is +// present but none of its values could be decoded (or none matched). +var ErrInvalidCookie = errors.New("cookiex: cookie could not be decoded") + +type ( + // Option configures a Codec. + Option func(*config) + + config struct { + maxAge time.Duration + legacyKeyPairs [][]byte + legacyEncode bool + } +) + +// WithMaxAge overrides how old a cookie may be before decoding rejects it. +// The default is 30 days; zero disables the check. The value must not be +// negative. +func WithMaxAge(d time.Duration) Option { + return func(c *config) { c.maxAge = d } +} + +// Codec seals and opens cookie values of type T. T is serialized as JSON. +// A Codec is safe for concurrent use. +type Codec[T any] struct { + purpose string + keys [][32]byte + maxAge time.Duration + legacy legacyState + now func() time.Time +} + +// New returns a codec for the given purpose. The purpose is bound into the +// ciphertext and used as the metric label; it must be a short constant like +// "kratos/session". Because the purpose is embedded in the additional +// authenticated data, it must be non-empty and must not contain a pipe +// character. The codec seals with a key derived from the first secret +// and opens with keys derived from any of them, so secrets rotate by +// prepending a new one. +func New[T any](purpose string, secrets [][]byte, opts ...Option) (*Codec[T], error) { + if purpose == "" || strings.Contains(purpose, "|") { + return nil, errors.New("cookiex: purpose must be non-empty and must not contain a pipe character") + } + if len(secrets) == 0 { + return nil, errors.New("cookiex: at least one secret is required") + } + cfg := config{maxAge: defaultMaxAge} + for _, opt := range opts { + opt(&cfg) + } + if cfg.maxAge < 0 { + return nil, errors.New("cookiex: max age must not be negative") + } + if cfg.legacyEncode && len(cfg.legacyKeyPairs) == 0 { + return nil, errors.New("cookiex: legacy encode requires legacy key pairs") + } + keys := make([][32]byte, len(secrets)) + for i, secret := range secrets { + key, err := hkdf.Key(sha256.New, secret, nil, kdfInfo, 32) + if err != nil { + return nil, errors.Wrap(err, "cookiex: cannot derive key") + } + keys[i] = [32]byte(key) + } + return &Codec[T]{ + purpose: purpose, + keys: keys, + maxAge: cfg.maxAge, + legacy: newLegacyState(cfg, cfg.maxAge), + now: time.Now, + }, nil +} + +// envelope wraps the JSON payload with the seal time so decoding can enforce +// the max age. +type envelope struct { + IssuedAt int64 `json:"iat"` + Values json.RawMessage `json:"v"` +} + +// aad binds a ciphertext to this codec's purpose and the cookie name, so a +// sealed value cannot be replayed as a different cookie or in a different +// context, even under the same key. +func (c *Codec[T]) aad(name string) []byte { + return []byte(aadPrefix + "|" + c.purpose + "|" + name) +} + +func (c *Codec[T]) seal(name string, value T) (string, error) { + payload, err := json.Marshal(value) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal cookie value") + } + plaintext, err := json.Marshal(envelope{IssuedAt: c.now().Unix(), Values: payload}) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal envelope") + } + a, err := aead.New(c.keys[0]) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot create AEAD") + } + // The nonce is prepended to the ciphertext. AEADs that manage the nonce + // internally report a nonce size of zero, so this also covers them. + nonce := make([]byte, a.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", errors.Wrap(err, "cookiex: cannot generate nonce") + } + sealed := a.Seal(nonce, nonce, plaintext, c.aad(name)) + return formatPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil +} + +func (c *Codec[T]) open(name, value string) (T, error) { + var zero T + raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(value, formatPrefix)) + if err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + for _, key := range c.keys { + a, err := aead.New(key) + if err != nil { + return zero, errors.Wrap(err, "cookiex: cannot create AEAD") + } + if len(raw) < a.NonceSize() { + continue + } + plaintext, err := a.Open(nil, raw[:a.NonceSize()], raw[a.NonceSize():], c.aad(name)) + if err != nil { + continue + } + var env envelope + if err := json.Unmarshal(plaintext, &env); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + if c.maxAge > 0 && c.now().Sub(time.Unix(env.IssuedAt, 0)) > c.maxAge { + return zero, errors.WithStack(ErrInvalidCookie) + } + var out T + if err := json.Unmarshal(env.Values, &out); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + return out, nil + } + return zero, errors.WithStack(ErrInvalidCookie) +} + +// Set seals value into cookie.Value and writes the cookie to w. All other +// attributes (name, path, domain, max-age, secure, http-only, same-site) must +// be set by the caller on the cookie. +func (c *Codec[T]) Set(w http.ResponseWriter, cookie *http.Cookie, value T) error { + var encoded string + var err error + if c.legacy.encode { + encoded, err = c.sealLegacy(cookie.Name, value) + } else { + encoded, err = c.seal(cookie.Name, value) + } + if err != nil { + return err + } + if len(encoded) > maxCookieValueLength { + return errors.Errorf("cookiex: encoded cookie %q exceeds %d bytes", cookie.Name, maxCookieValueLength) + } + cookie.Value = encoded + http.SetCookie(w, cookie) + return nil +} + +// Get returns the value of the first cookie named name that decodes. It +// returns http.ErrNoCookie when no cookie with that name is present, and +// ErrInvalidCookie when none of the present values decode. +func (c *Codec[T]) Get(r *http.Request, name string) (T, error) { + return c.GetMatching(r, name, nil) +} + +// GetMatching returns the value of the first cookie named name that decodes +// and matches. Requests can carry multiple cookies with the same name, for +// example one from a parent and one from a sub domain; match selects among +// them. A nil match matches any value. It returns http.ErrNoCookie when no +// cookie with that name is present, and ErrInvalidCookie when none of the +// present values decode and match. +func (c *Codec[T]) GetMatching(r *http.Request, name string, match func(T) bool) (T, error) { + if match == nil { + match = func(T) bool { return true } + } + var zero T + found := false + for _, ck := range r.Cookies() { + if ck.Name != name { + continue + } + found = true + // Refuse oversized values before doing any decode work, matching the + // limit Set enforces and securecookie's own decode-side check. + if len(ck.Value) > maxCookieValueLength { + continue + } + var value T + var err error + if strings.HasPrefix(ck.Value, formatPrefix) { + value, err = c.open(name, ck.Value) + } else { + value, err = c.openLegacy(name, ck.Value) + } + if err != nil { + continue + } + if match(value) { + return value, nil + } + } + if !found { + return zero, errors.WithStack(http.ErrNoCookie) + } + return zero, errors.WithStack(ErrInvalidCookie) +} + +// Expire overwrites the cookie with an expired empty one, deleting it from +// the client. +func (c *Codec[T]) Expire(w http.ResponseWriter, cookie *http.Cookie) { + cookie.Value = "" + cookie.MaxAge = -1 + cookie.Expires = time.Unix(1, 0) + http.SetCookie(w, cookie) +} diff --git a/oryx/cookiex/legacy_securecookie.go b/oryx/cookiex/legacy_securecookie.go new file mode 100644 index 0000000..b4549e4 --- /dev/null +++ b/oryx/cookiex/legacy_securecookie.go @@ -0,0 +1,136 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cookiex + +// This file isolates the gorilla/securecookie dependency. It exists so that +// cookies sealed by the previous securecookie-based stores keep decoding +// during the migration, and (behind WithLegacyEncode) so that freshly minted +// cookies stay readable by old pods during the stage-1 rollout. Delete it, +// and the securecookie dependency, once ory_legacy_cookie_decodes_total has +// flatlined fleet-wide for a full max-cookie-lifespan window. + +import ( + "crypto/sha256" + "encoding/json" + "time" + + "github.com/gorilla/securecookie" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// legacyDecodes counts cookies that were successfully decoded via the legacy +// securecookie fallback. It is registered on the Prometheus default +// registerer, which Kratos and Hydra expose on /metrics/prometheus. +var legacyDecodes = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "ory_legacy_cookie_decodes_total", + Help: "Number of cookies successfully decoded via the legacy securecookie fallback, by codec purpose.", +}, []string{"purpose"}) + +// LegacySecureCookieKeyPairs derives the (hashKey, blockKey) pairs the +// securecookie-based cookie stores used: each secret signs, and its SHA-256 +// digest encrypts. +func LegacySecureCookieKeyPairs(secrets [][]byte) [][]byte { + pairs := make([][]byte, 0, 2*len(secrets)) + for _, secret := range secrets { + block := sha256.Sum256(secret) + pairs = append(pairs, secret, block[:]) + } + return pairs +} + +// WithLegacyKeyPairs enables decoding cookies in the legacy securecookie +// format, sealed under the given (hashKey, blockKey) pairs. While this option +// is in use, T must be unmarshalable from a flat JSON object with string +// values; WithLegacyEncode additionally requires T to marshal to one. Remove +// once all cookies sealed in the legacy format have expired. +func WithLegacyKeyPairs(pairs ...[]byte) Option { + return func(c *config) { c.legacyKeyPairs = pairs } +} + +type legacyState struct { + codecs []securecookie.Codec + encode bool +} + +func newLegacyState(cfg config, maxAge time.Duration) legacyState { + codecs := securecookie.CodecsFromPairs(cfg.legacyKeyPairs...) + for _, codec := range codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(int(maxAge / time.Second)) + } + } + return legacyState{codecs: codecs, encode: cfg.legacyEncode} +} + +// openLegacy decodes a securecookie-sealed value and bridges it into T via +// its JSON representation: legacy payloads are flat string-to-string maps, so +// string, time.Time, and UUID fields all round-trip through their JSON string +// forms. +func (c *Codec[T]) openLegacy(name, value string) (T, error) { + var zero T + if len(c.legacy.codecs) == 0 { + return zero, errors.WithStack(ErrInvalidCookie) + } + values := make(map[any]any) + if err := securecookie.DecodeMulti(name, value, &values, c.legacy.codecs...); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + flat := make(map[string]string, len(values)) + for k, v := range values { + key, keyOK := k.(string) + val, valOK := v.(string) + if !keyOK || !valOK { + return zero, errors.WithStack(ErrInvalidCookie) + } + flat[key] = val + } + bridge, err := json.Marshal(flat) + if err != nil { + return zero, errors.Wrap(err, "cookiex: cannot bridge legacy cookie") + } + var out T + if err := json.Unmarshal(bridge, &out); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + legacyDecodes.WithLabelValues(c.purpose).Inc() + return out, nil +} + +// WithLegacyEncode makes Set seal in the legacy securecookie format under the +// first legacy key pair, so pods that only understand the legacy format can +// read freshly minted cookies during a rolling deploy. Requires +// WithLegacyKeyPairs. This is stage 1 of the rollout; a follow-up removes the +// option, flipping encoding to the v1 format. JSON null values inside the +// payload are coerced to empty strings by the bridge; do not use pointer-typed +// fields while legacy encode is enabled. +func WithLegacyEncode() Option { + return func(c *config) { c.legacyEncode = true } +} + +// sealLegacy bridges T through its JSON representation into the flat +// string-to-string map that the securecookie stores used. +func (c *Codec[T]) sealLegacy(name string, value T) (string, error) { + buf, err := json.Marshal(value) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal cookie value") + } + var flat map[string]string + if err := json.Unmarshal(buf, &flat); err != nil { + return "", errors.Wrap(err, "cookiex: payload must be a flat JSON object with string values while legacy encode is enabled") + } + if flat == nil { + return "", errors.New("cookiex: payload must be a flat JSON object with string values while legacy encode is enabled") + } + values := make(map[any]any, len(flat)) + for k, v := range flat { + values[k] = v + } + encoded, err := securecookie.EncodeMulti(name, values, c.legacy.codecs[0]) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot encode legacy cookie") + } + return encoded, nil +} diff --git a/oryx/corsx/check_origin.go b/oryx/corsx/check_origin.go new file mode 100644 index 0000000..8022dfb --- /dev/null +++ b/oryx/corsx/check_origin.go @@ -0,0 +1,141 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import ( + "strings" + + "golang.org/x/net/publicsuffix" +) + +// CheckOrigin is a function that can be used well with cors.Options.AllowOriginRequestFunc. +// It checks whether the origin is allowed following the same behavior as github.com/rs/cors. +// +// When legacyAllowInsecureOrigins is false (the default), wildcard patterns are +// only honored when ClassifyOrigin reports them as bounded at a registrable +// domain. Pass true to opt into legacy (trusting) behavior for unbounded +// wildcards. +// +// TODO: legacyAllowInsecureOrigins grandfathers a fixed set of projects through +// a time-boxed migration window (feature_flags.legacy_allow_insecure_origins). +// Once those projects move to bounded wildcards and the entitlement is revoked, +// drop this parameter and always enforce the boundary. +// +// Recommended usage for hot-reloadable origins: +// +// func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { +// opts, enabled := p.GetProvider(ctx).CORS(prefix, cors.Options{ +// AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, +// AllowedHeaders: []string{"Authorization", "Content-Type", "Cookie"}, +// ExposedHeaders: []string{"Content-Type", "Set-Cookie"}, +// AllowCredentials: true, +// }) +// opts.AllowOriginRequestFunc = func(r *http.Request, origin string) bool { +// // load the origins from the config on every request to allow hot-reloading +// allowedOrigins := p.GetProvider(r.Context()).Strings(prefix + ".cors.allowed_origins") +// return corsx.CheckOrigin(allowedOrigins, origin, false) +// } +// return opts, enabled +// } +func CheckOrigin(allowedOrigins []string, origin string, legacyAllowInsecureOrigins bool) bool { + if len(allowedOrigins) == 0 { + return true + } + for _, o := range allowedOrigins { + if o == "*" { + // allow all origins + return true + } + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may be error-prone, we chose to ignore the spec here. + // https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/cors.go#L132-L133 + o = strings.ToLower(o) + prefix, suffix, found := strings.Cut(o, "*") + if !found { + // not a pattern, check for equality + if o == origin { + return true + } + continue + } + // Only honor wildcards bounded at a registrable domain unless the caller + // explicitly opts into legacy insecure matching. See ClassifyOrigin. + if !legacyAllowInsecureOrigins && ClassifyOrigin(o).IsUnsafeWildcard() { + continue + } + // inspired by https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/utils.go#L15 + if len(origin) >= len(prefix)+len(suffix) && strings.HasPrefix(origin, prefix) && strings.HasSuffix(origin, suffix) { + return true + } + } + return false +} + +// OriginPattern describes a CORS origin or return-URL host pattern: whether it +// uses a wildcard and, if so, whether that wildcard is safely bounded at a +// registrable domain. +type OriginPattern struct { + // HasWildcard reports whether the pattern contains a "*". + HasWildcard bool + + // BoundedWildcard reports whether the "*" is confined to a subdomain label + // and the fixed domain that follows it is a registrable domain (an eTLD+1, + // e.g. "example.com" or "example.co.uk"). Every host the pattern can match + // then shares that one customer-owned registrable domain, so an attacker + // cannot register a matching host. Always false when HasWildcard is false. + BoundedWildcard bool + + // Base is the fixed domain that follows the wildcard label — "example.com" + // for "*.example.com", "com" for "*.com". It is empty for non-wildcards and + // for bare or trailing wildcards where no domain follows the "*". When + // BoundedWildcard is false, Base names the offending suffix, which is the + // actionable signal for reporting why a wildcard was rejected. + Base string +} + +// IsUnsafeWildcard reports whether the pattern is a wildcard that is NOT bounded +// at a registrable domain. Such a wildcard would match an attacker-registrable +// host (e.g. "https://*foo.com" matches "https://evilfoo.com"), so it must be +// rejected unless the caller explicitly opts into legacy insecure matching. This +// is the dominant question at call sites that gate, drop, or reject wildcard +// origins and return URLs. +func (p OriginPattern) IsUnsafeWildcard() bool { + return p.HasWildcard && !p.BoundedWildcard +} + +// ClassifyOrigin inspects a CORS origin or bare host pattern and reports whether +// it is a wildcard and, if so, whether the wildcard is safely bounded at a +// registrable domain. Only the text from the last "*" onward is inspected, so +// the result is identical whether pattern carries a scheme or is a bare host; a +// trailing ":port" is ignored. Examples: +// +// - "https://*.example.com" → {HasWildcard: true, BoundedWildcard: true, Base: "example.com"} +// - "https://*foo.com" → {HasWildcard: true, Base: "com"} (dot-less; base is a public suffix) +// - "https://*.com" → {HasWildcard: true, Base: "com"} (public suffix, not registrable) +// - "https://www.ory.*" → {HasWildcard: true} (trailing; no domain follows) +// - "https://exact.foo.com" → {} (no wildcard) +func ClassifyOrigin(pattern string) OriginPattern { + i := strings.LastIndexByte(pattern, '*') + if i < 0 { + return OriginPattern{} + } + p := OriginPattern{HasWildcard: true} + // The fixed domain is everything after the first "." that follows the last + // "*" — i.e. the label containing the wildcard is dropped. Without such a dot + // the wildcard is trailing or bare, so no registrable domain follows. + _, base, found := strings.Cut(pattern[i:], ".") + if !found { + return p + } + base = strings.ToLower(base) + if host, _, found := strings.Cut(base, ":"); found { + base = host // Drop a trailing ":port". + } + p.Base = base + // EffectiveTLDPlusOne returns an error when base is itself a public suffix + // (e.g. "com", "co.uk", "vercel.app") or otherwise has no registrable domain. + _, err := publicsuffix.EffectiveTLDPlusOne(base) + p.BoundedWildcard = err == nil + return p +} diff --git a/oryx/corsx/cmd.go b/oryx/corsx/cmd.go new file mode 100644 index 0000000..b475201 --- /dev/null +++ b/oryx/corsx/cmd.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +// HelpMessage returns a string containing information on setting up this CORS middleware. +func HelpMessage() string { + return `- CORS_ENABLED: Switch CORS support on (true) or off (false). Default is off (false). + + Example: CORS_ENABLED=true + +- CORS_ALLOWED_ORIGINS: A list of origins (comma separated values) a cross-domain request can be executed from. + If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) + to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. + Only one wildcard can be used per origin. The default value is *. + + Example: CORS_ALLOWED_ORIGINS=http://*.domain.com,http://*.domain2.com + +- CORS_ALLOWED_METHODS: A list of methods (comma separated values) the client is allowed to use with cross-domain + requests. Default value is simple methods (GET and POST). + + Example: CORS_ALLOWED_METHODS=POST,GET,PUT + +- CORS_ALLOWED_CREDENTIALS: Indicates whether the request can include user credentials like cookies, HTTP authentication + or client side SSL certificates. + + Default: CORS_ALLOWED_CREDENTIALS=false + Example: CORS_ALLOWED_CREDENTIALS=true + +- CORS_DEBUG: Debugging flag adds additional output to debug server side CORS issues. + + Default: CORS_DEBUG=false + Example: CORS_DEBUG=true + +- CORS_MAX_AGE: Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 + which stands for no max age. + + Default: CORS_MAX_AGE=0 + Example: CORS_MAX_AGE=10 + +- CORS_ALLOWED_HEADERS: A list of non simple headers (comma separated values) the client is allowed to use with + cross-domain requests. + +- CORS_EXPOSED_HEADERS: Indicates which headers (comma separated values) are safe to expose to the API of a + CORS API specification.` +} diff --git a/oryx/corsx/defaults.go b/oryx/corsx/defaults.go new file mode 100644 index 0000000..136bdec --- /dev/null +++ b/oryx/corsx/defaults.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +// CORSRequestHeadersSafelist We add the safe list cors accept headers +// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header +var CORSRequestHeadersSafelist = []string{"Accept", "Content-Type", "Content-Length", "Accept-Language", "Content-Language"} + +// CORSResponseHeadersSafelist We add the safe list cors expose headers +// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header +var CORSResponseHeadersSafelist = []string{"Set-Cookie", "Cache-Control", "Expires", "Last-Modified", "Pragma", "Content-Length", "Content-Language", "Content-Type"} + +// CORSDefaultAllowedMethods Default allowed methods +var CORSDefaultAllowedMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE"} + +// CORSRequestHeadersExtended Extended list of request headers +// these will be concatenated with the safelist +var CORSRequestHeadersExtended = []string{"Authorization", "X-CSRF-TOKEN"} + +// CORSResponseHeadersExtended Extended list of response headers +// these will be concatenated with the safelist +var CORSResponseHeadersExtended = []string{} + +// CORSDefaultMaxAge max age for cache of preflight request result +// default is 5 seconds +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age +var CORSDefaultMaxAge = 5 + +// CORSAllowCredentials default value for allow credentials +// this is required for cookies to be sent by the browser +// we always want this since we are using cookies for authentication most of the time +var CORSAllowCredentials = true diff --git a/oryx/corsx/middleware.go b/oryx/corsx/middleware.go new file mode 100644 index 0000000..a6ab0b8 --- /dev/null +++ b/oryx/corsx/middleware.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import ( + "context" + "net/http" + + "github.com/rs/cors" + "github.com/urfave/negroni" +) + +// ContextualizedMiddleware is a context-aware CORS middleware. It allows hot-reloading CORS configuration using +// the HTTP request context. +// +// n := negroni.New() +// n.UseFunc(ContextualizedMiddleware(func(context.Context) (opts cors.Options, enabled bool) { +// panic("implement me") +// }) +// // ... +// +// Deprecated: because this is not really practical to use, you should use CheckOrigin as the cors.Options.AllowOriginRequestFunc instead. +func ContextualizedMiddleware(provider func(context.Context) (opts cors.Options, enabled bool)) negroni.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + options, enabled := provider(r.Context()) + if !enabled { + next(rw, r) + return + } + + cors.New(options).Handler(next).ServeHTTP(rw, r) + } +} diff --git a/oryx/corsx/normalize.go b/oryx/corsx/normalize.go new file mode 100644 index 0000000..609b124 --- /dev/null +++ b/oryx/corsx/normalize.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import "net/url" + +// NormalizeOrigins normalizes the CORS origins. +func NormalizeOrigins(origins []url.URL) []string { + results := make([]string, len(origins)) + for k, o := range origins { + results[k] = o.Scheme + "://" + o.Host + } + return results +} + +// NormalizeOriginStrings normalizes the CORS origins from string representation +func NormalizeOriginStrings(origins []string) ([]string, error) { + results := make([]string, len(origins)) + for k, o := range origins { + u, err := url.ParseRequestURI(o) + if err != nil { + return nil, err + } + results[k] = u.Scheme + "://" + u.Host + } + return results, nil +} diff --git a/oryx/crdbx/isolation.go b/oryx/crdbx/isolation.go new file mode 100644 index 0000000..f714ecd --- /dev/null +++ b/oryx/crdbx/isolation.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/sqlcon" +) + +// SetTransactionReadCommitted lowers the isolation of the current transaction +// to READ COMMITTED on CockroachDB. Multi-statement read transactions at +// SERIALIZABLE must refresh their whole read set when a concurrent write +// bumps the transaction timestamp, and surface RETRY_SERIALIZABLE to the +// client when the refresh fails; under READ COMMITTED every statement reads +// at its own timestamp and those conflicts are absorbed server-side. Must be +// called before the transaction runs its first query. Postgres already runs +// statements at READ COMMITTED by default, and MySQL's SET TRANSACTION +// applies to the next transaction rather than the current one, so every +// other dialect is a no-op. YugabyteDB maps READ COMMITTED to snapshot +// isolation unless yb_enable_read_committed_isolation is enabled, so it stays +// on the normal transaction retry path instead of using this CockroachDB-only +// override. +func SetTransactionReadCommitted(c *pop.Connection) error { + if c.Dialect.Name() != dbal.DriverCockroachDB { + // Only CockroachDB supports and needs this. + return nil + } + + return sqlcon.HandleError(c.RawQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED").Exec()) +} diff --git a/oryx/crdbx/readonly.go b/oryx/crdbx/readonly.go new file mode 100644 index 0000000..595060a --- /dev/null +++ b/oryx/crdbx/readonly.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" + "github.com/ory/x/sqlcon" +) + +// SetTransactionReadOnly sets the current transaction to read only for +// CockroachDB, PostgreSQL, and YugabyteDB, which all let the server skip +// write-path bookkeeping and reject accidental writes. Must be called before the +// transaction runs its first query. MySQL's SET TRANSACTION applies to the +// next transaction rather than the current one, and SQLite has no +// equivalent, so every other dialect is a no-op. +func SetTransactionReadOnly(c *pop.Connection) error { + if dbal.IsPostgresCompatible(c.Dialect.Name()) { + return sqlcon.HandleError(c.RawQuery("SET TRANSACTION READ ONLY").Exec()) + } + return nil +} diff --git a/oryx/crdbx/staleness.go b/oryx/crdbx/staleness.go new file mode 100644 index 0000000..f915884 --- /dev/null +++ b/oryx/crdbx/staleness.go @@ -0,0 +1,110 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "net/http" + + "github.com/ory/x/dbal" + + "github.com/ory/pop/v6" + + "github.com/ory/x/sqlcon" +) + +// Control API consistency guarantees +// +// swagger:model consistencyRequestParameters +type ConsistencyRequestParameters struct { + // Read Consistency Level (preview) + // + // The read consistency level determines the consistency guarantee for reads: + // + // - strong (slow): The read is guaranteed to return the most recent data committed at the start of the read. + // - eventual (very fast): The result will return data that is about 4.8 seconds old. + // + // The default consistency guarantee can be changed in the Ory Network Console or using the Ory CLI with + // `ory patch project --replace '/previews/default_read_consistency_level="strong"'`. + // + // Setting the default consistency level to `eventual` may cause regressions in the future as we add consistency + // controls to more APIs. Currently, the following APIs will be affected by this setting: + // + // - `GET /admin/identities` + // + // This feature is in preview and only available in Ory Network. + // + // required: false + // in: query + Consistency ConsistencyLevel `json:"consistency"` +} + +// ConsistencyLevel is the consistency level. +// swagger:enum ConsistencyLevel +type ConsistencyLevel string + +const ( + // ConsistencyLevelUnset is the unset / default consistency level. + ConsistencyLevelUnset ConsistencyLevel = "" + // ConsistencyLevelStrong is the strong consistency level. + ConsistencyLevelStrong ConsistencyLevel = "strong" + // ConsistencyLevelEventual is the eventual consistency level using follower read timestamps. + ConsistencyLevelEventual ConsistencyLevel = "eventual" +) + +// ConsistencyLevelFromRequest extracts the consistency level from a request. +func ConsistencyLevelFromRequest(r *http.Request) ConsistencyLevel { + return ConsistencyLevelFromString(r.URL.Query().Get("consistency")) +} + +// ConsistencyLevelFromString converts a string to a ConsistencyLevel. +// If the string is not recognized or unset, ConsistencyLevelStrong is returned. +func ConsistencyLevelFromString(in string) ConsistencyLevel { + switch in { + case string(ConsistencyLevelStrong): + return ConsistencyLevelStrong + case string(ConsistencyLevelEventual): + return ConsistencyLevelEventual + case string(ConsistencyLevelUnset): + return ConsistencyLevelUnset + } + return ConsistencyLevelStrong +} + +// SetTransactionConsistency sets the transaction consistency level for CockroachDB. +func SetTransactionConsistency(c *pop.Connection, level ConsistencyLevel, fallback ConsistencyLevel) error { + q := getTransactionConsistencyQuery(c.Dialect.Name(), level, fallback) + if len(q) == 0 { + return nil + } + + return sqlcon.HandleError(c.RawQuery(q).Exec()) +} + +const transactionFollowerReadTimestamp = "SET TRANSACTION AS OF SYSTEM TIME follower_read_timestamp()" + +func getTransactionConsistencyQuery(dialect string, level ConsistencyLevel, fallback ConsistencyLevel) string { + if dialect != dbal.DriverCockroachDB { + // Only CockroachDB supports this. + return "" + } + + switch level { + case ConsistencyLevelStrong: + // Nothing to do + return "" + case ConsistencyLevelEventual: + // Jumps to end of function + case ConsistencyLevelUnset: + fallthrough + default: + if fallback != ConsistencyLevelEventual { + // Nothing to do + return "" + } + + // Jumps to end of function + } + + return transactionFollowerReadTimestamp +} diff --git a/oryx/dbal/canonicalize.go b/oryx/dbal/canonicalize.go new file mode 100644 index 0000000..672fada --- /dev/null +++ b/oryx/dbal/canonicalize.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +const ( + DriverMySQL = "mysql" + DriverCockroachDB = "cockroach" + DriverPostgreSQL = "postgres" + // DriverYugabyteDB is the dialect name YugabyteDB connections report. It is + // a commercial (Ory Enterprise License) dialect that speaks the PostgreSQL + // wire protocol and SQL dialect, so most SQL paths treat it like postgres. + DriverYugabyteDB = "yugabyte" +) + +// IsPostgresCompatible reports whether dialect speaks the PostgreSQL SQL +// dialect. It does not imply support for CockroachDB-specific syntax or +// features. +func IsPostgresCompatible(dialect string) bool { + switch dialect { + case DriverPostgreSQL, DriverCockroachDB, DriverYugabyteDB: + return true + default: + return false + } +} diff --git a/oryx/dbal/driver.go b/oryx/dbal/driver.go new file mode 100644 index 0000000..ae6ed33 --- /dev/null +++ b/oryx/dbal/driver.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "strings" +) + +// IsSQLite returns true if the connection is a SQLite string. +func IsSQLite(dsn string) bool { + scheme := strings.Split(dsn, "://")[0] + return scheme == "sqlite" || scheme == "sqlite3" +} diff --git a/oryx/dbal/dsn.go b/oryx/dbal/dsn.go new file mode 100644 index 0000000..d9c3c85 --- /dev/null +++ b/oryx/dbal/dsn.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +var sqliteMemoryRegexp = regexp.MustCompile(`^sqlite://file:.+\?.*&?mode=memory($|&.*)|sqlite://(file:)?:memory:\?.*$|^(:memory:|memory)$`) + +// IsMemorySQLite returns true if a given DSN string is pointing to a SQLite database. +// +// SQLite can be written in different styles depending on the use case +// - just in memory +// - shared connection +// - shared but unique in the same process +// see: https://sqlite.org/inmemorydb.html +func IsMemorySQLite(dsn string) bool { return sqliteMemoryRegexp.MatchString(dsn) } + +// NewSQLiteTestDatabase creates a new, unique SQLite database +// which is shared amongst all callers and identified by an individual file name. +// The database file is created in the system's temporary directory, and not actively +// removed to allow debugging in case of test failures. +func NewSQLiteTestDatabase(t testing.TB) string { + fn, err := os.MkdirTemp("", "sqlite-test-db-*") + require.NoError(t, err) + return NewSQLiteDatabase(fn) +} + +// NewSQLiteDatabase creates a new on-disk, unique SQLite database +// which is shared amongst all callers and identified by an individual file name. +// This is sometimes necessary over a in-memory database, for example when multiple tests/goroutines run in parallel +// and access the same table. +// This would result in a locking error from SQLite when running in-memory. +// Additionally, shared cache mode is deprecated and discouraged, and the problem is better solved with the WAL, +// according to official docs. +// +// In tests, use [NewSQLiteTestDatabase] instead. +func NewSQLiteDatabase(name string) string { + return fmt.Sprintf("sqlite://%s/db.sqlite?_fk=true&_busy_timeout=100000&_pragma=journal_mode(WAL)&_time_format=sqlite", name) +} diff --git a/oryx/dbal/testhelpers.go b/oryx/dbal/testhelpers.go new file mode 100644 index 0000000..91df8af --- /dev/null +++ b/oryx/dbal/testhelpers.go @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "bytes" + "encoding/hex" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + "github.com/ory/x/fsx" + "github.com/ory/x/sqlcon/dockertest" +) + +var hashDumpRegex = regexp.MustCompile(`-- migrations hash: ([^\n]+)\n`) + +func RestoreFromSchemaDump(t testing.TB, c *pop.Connection, migrations fs.FS, writeTo string) func(testing.TB) { + newHash, err := fsx.DirHash(migrations) + require.NoError(t, err) + + dumpFilename := filepath.Join(writeTo, c.Dialect.Name()+"_dump.sql") + + updateDump := func(t testing.TB) { + dump := dockertest.DumpSchema(t, c) + f, err := os.OpenFile(dumpFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + require.NoError(t, err) + defer f.Close() + + _, _ = fmt.Fprintf(f, "-- migrations hash: %x\n\n%s", newHash, dump) + t.Fatal("database schema restored from migrations and dump updated, please re-run the test") + } + + dump, err := os.ReadFile(dumpFilename) + if errors.Is(err, fs.ErrNotExist) { + return updateDump + } + require.NoError(t, err) + + matches := hashDumpRegex.FindSubmatch(dump) + if len(matches) != 2 { + return updateDump + } + + currentHash, err := hex.DecodeString(string(matches[1])) + require.NoError(t, err) + + if !bytes.Equal(newHash, currentHash) { + return updateDump + } + + require.NoError(t, c.RawQuery(string(dump)).Exec()) + return nil +} diff --git a/oryx/decoderx/http.go b/oryx/decoderx/http.go new file mode 100644 index 0000000..384dc9b --- /dev/null +++ b/oryx/decoderx/http.go @@ -0,0 +1,561 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package decoderx + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/herodot" + + "github.com/ory/x/httpx" + "github.com/ory/x/jsonschemax" +) + +type ( + httpDecoderOptions struct { + keepRequestBody bool + allowedContentTypes []string + allowedHTTPMethods []string + jsonSchemaRef string + jsonSchemaCompiler *jsonschema.Compiler + jsonSchemaValidate bool + maxCircularReferenceDepth uint8 + handleParseErrors parseErrorStrategy + expectJSONFlattened bool + queryAndBody bool + } + + // HTTPDecoderOption configures the HTTP decoder. + HTTPDecoderOption func(*httpDecoderOptions) + + parseErrorStrategy uint8 +) + +const ( + httpContentTypeMultipartForm = "multipart/form-data" + httpContentTypeURLEncodedForm = "application/x-www-form-urlencoded" + httpContentTypeJSON = "application/json" +) + +const ( + // ParseErrorIgnoreConversionErrors will ignore any errors caused by strconv.Parse* and use the + // raw form field value, which is a string, when such a parse error occurs. + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field + // `ratio` will be handled as a string. If the destination struct is a `json.RawMessage`, then + // the output will be `{"ratio": "foobar"}`. + ParseErrorIgnoreConversionErrors parseErrorStrategy = iota + 1 + + // ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the + // default value of the type to be casted, e.g. float64(0), string(""). + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field + // `ratio` will receive the default value for the primitive type (here `0.0` for `number`). + // If the destination struct is a `json.RawMessage`, then the output will be `{"ratio": 0.0}`. + ParseErrorUseEmptyValueOnConversionErrors + + // ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns + // an error. + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` the parser aborts + // and returns an error, here: `strconv.ParseFloat: parsing "foobar"`. + ParseErrorReturnOnConversionErrors +) + +var errKeyNotFound = errors.New("key not found") + +// HTTPFormDecoder configures the HTTP decoder to only accept form-data +// (application/x-www-form-urlencoded, multipart/form-data) +func HTTPFormDecoder() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedContentTypes = []string{httpContentTypeMultipartForm, httpContentTypeURLEncodedForm} + } +} + +// HTTPJSONDecoder configures the HTTP decoder to only accept JSON data +// (application/json). +func HTTPJSONDecoder() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedContentTypes = []string{httpContentTypeJSON} + } +} + +// HTTPKeepRequestBody configures the HTTP decoder to allow other +// HTTP request body readers to read the body as well by keeping +// the data in memory. +func HTTPKeepRequestBody(keep bool) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.keepRequestBody = keep + } +} + +// HTTPDecoderSetValidatePayloads sets if payloads should be validated or not. +func HTTPDecoderSetValidatePayloads(validate bool) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.jsonSchemaValidate = validate + o.keepRequestBody = true + } +} + +// HTTPDecoderJSONFollowsFormFormat if set tells the decoder that JSON follows the same conventions +// as the form decoder, meaning `{"foo.bar": "..."}` is translated to `{"foo": {"bar": "..."}}`. +func HTTPDecoderJSONFollowsFormFormat() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.expectJSONFlattened = true + o.keepRequestBody = true + } +} + +// HTTPDecoderAllowedMethods sets the allowed HTTP methods. Defaults are POST, PUT, PATCH. +func HTTPDecoderAllowedMethods(method ...string) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedHTTPMethods = method + } +} + +// HTTPDecoderUseQueryAndBody will check both the HTTP body and the HTTP query params when decoding. +// Only relevant for non-GET operations. +func HTTPDecoderUseQueryAndBody() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.queryAndBody = true + } +} + +// HTTPDecoderSetIgnoreParseErrorsStrategy sets a strategy for dealing with strconv.Parse* errors: +// +// - decoderx.ParseErrorIgnoreConversionErrors will ignore any parse errors caused by strconv.Parse* and use the +// raw form field value, which is a string, when such a parse error occurs. (default) +// - decoderx.ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the +// default value of the type to be casted, e.g. float64(0), string(""). +// - decoderx.ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns +// an error. +func HTTPDecoderSetIgnoreParseErrorsStrategy(strategy parseErrorStrategy) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.handleParseErrors = strategy + } +} + +// HTTPDecoderSetMaxCircularReferenceDepth sets the maximum recursive reference resolution depth. +func HTTPDecoderSetMaxCircularReferenceDepth(depth uint8) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.maxCircularReferenceDepth = depth + } +} + +// HTTPJSONSchemaCompiler sets a JSON schema to be used for validation and type assertion of +// incoming requests. +func HTTPJSONSchemaCompiler(ref string, compiler *jsonschema.Compiler) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + if compiler == nil { + compiler = jsonschema.NewCompiler() + } + compiler.ExtractAnnotations = true + o.jsonSchemaCompiler = compiler + o.jsonSchemaRef = ref + o.jsonSchemaValidate = true + } +} + +// HTTPRawJSONSchemaCompiler uses a JSON Schema Compiler with the provided JSON Schema in raw byte form. +func HTTPRawJSONSchemaCompiler(raw []byte) (HTTPDecoderOption, error) { + compiler := jsonschema.NewCompiler() + id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) + if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { + return nil, err + } + compiler.ExtractAnnotations = true + + return func(o *httpDecoderOptions) { + o.jsonSchemaCompiler = compiler + o.jsonSchemaRef = id + o.jsonSchemaValidate = true + }, nil +} + +// MustHTTPRawJSONSchemaCompiler uses HTTPRawJSONSchemaCompiler and panics on error. +func MustHTTPRawJSONSchemaCompiler(raw []byte) HTTPDecoderOption { + f, err := HTTPRawJSONSchemaCompiler(raw) + if err != nil { + panic(err) + } + return f +} + +func newHTTPDecoderOptions(fs []HTTPDecoderOption) *httpDecoderOptions { + o := &httpDecoderOptions{ + allowedContentTypes: []string{ + httpContentTypeMultipartForm, httpContentTypeURLEncodedForm, httpContentTypeJSON, + }, + allowedHTTPMethods: []string{"POST", "PUT", "PATCH"}, + maxCircularReferenceDepth: 5, + handleParseErrors: ParseErrorIgnoreConversionErrors, + } + + for _, f := range fs { + f(o) + } + + return o +} + +func validateRequest(r *http.Request, c *httpDecoderOptions) error { + method := strings.ToUpper(r.Method) + + if !slices.Contains(c.allowedHTTPMethods, method) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to decode body because HTTP Request Method was "%s" but only %v are supported.`, method, c.allowedHTTPMethods)) + } + + if method != "GET" { + if r.ContentLength == 0 { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to decode HTTP Request Body because its HTTP Header "Content-Length" is zero.`)) + } + + if !httpx.HasContentType(r, c.allowedContentTypes...) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`HTTP %s Request used unknown HTTP Header "Content-Type: %s", only %v are supported.`, method, r.Header.Get("Content-Type"), c.allowedContentTypes)) + } + } + + return nil +} + +func validatePayload(ctx context.Context, raw json.RawMessage, c *httpDecoderOptions) error { + if !c.jsonSchemaValidate { + return nil + } + + if c.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("JSON Schema Validation is required but no compiler was provided.")) + } + + schema, err := c.jsonSchemaCompiler.Compile(ctx, c.jsonSchemaRef) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to load JSON Schema from location: %s", c.jsonSchemaRef).WithDebug(err.Error())) + } + + if err := schema.Validate(bytes.NewBuffer(raw)); err != nil { + if errors.As(err, new(*jsonschema.ValidationError)) { + return errors.WithStack(err) + } + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to process JSON Schema and input: %s", err).WithDebug(err.Error())) + } + + return nil +} + +// Decode takes an HTTP Request Body and decodes it into destination. +func Decode(r *http.Request, destination any, opts ...HTTPDecoderOption) error { + c := newHTTPDecoderOptions(opts) + if err := validateRequest(r, c); err != nil { + return err + } + + if r.Method == "GET" { + return decodeForm(r, destination, c) + } else if httpx.HasContentType(r, httpContentTypeJSON) { + if c.expectJSONFlattened { + return decodeJSONForm(r, destination, c) + } + return decodeJSON(r, destination, c) + } else if httpx.HasContentType(r, httpContentTypeMultipartForm, httpContentTypeURLEncodedForm) { + return decodeForm(r, destination, c) + } + + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to determine decoder for content type: %s", r.Header.Get("Content-Type"))) +} + +func requestBody(r *http.Request, o *httpDecoderOptions) (reader io.ReadCloser, err error) { + if strings.ToUpper(r.Method) == "GET" { + return io.NopCloser(bytes.NewBufferString(r.URL.Query().Encode())), nil + } + + if !o.keepRequestBody { + return r.Body, nil + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + return nil, errors.Wrapf(err, "unable to read body") + } + + _ = r.Body.Close() // must close + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil +} + +func decodeJSONForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + if o.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) + } + + paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) + } + + reader, err := requestBody(r, o) + if err != nil { + return err + } + + var interim json.RawMessage + if err := json.NewDecoder(reader).Decode(&interim); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithReason("Unable to decode form as JSON.")) + } + + parsed := gjson.ParseBytes(interim) + if !parsed.IsObject() { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected JSON sent in request body to be an object but got: %s", parsed.Type.String())) + } + + values := url.Values{} + parsed.ForEach(func(k, v gjson.Result) bool { + values.Set(k.String(), v.String()) + return true + }) + + if o.queryAndBody { + _ = r.ParseForm() + for k := range r.Form { + values.Set(k, r.Form.Get(k)) + } + } + + raw, err := decodeURLValues(values, paths, o) + if err != nil { + return err + } + + if err := json.Unmarshal(raw, destination); err != nil { + return errors.WithStack(err) + } + + return validatePayload(r.Context(), raw, o) +} + +func decodeForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + if o.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) + } + + reader, err := requestBody(r, o) + if err != nil { + return err + } + + defer func() { + r.Body = reader + }() + + if err := r.ParseForm(); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode HTTP %s form body: %s", strings.ToUpper(r.Method), err).WithDebug(err.Error())) + } + + paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) + } + + values := r.PostForm + if r.Method == "GET" || o.queryAndBody { + values = r.Form + } + + raw, err := decodeURLValues(values, paths, o) + if err != nil && !errors.Is(err, errKeyNotFound) { + return err + } + + if err := json.NewDecoder(bytes.NewReader(raw)).Decode(destination); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode JSON payload: %s", err)) + } + + return validatePayload(r.Context(), raw, o) +} + +func decodeURLValues(values url.Values, paths []jsonschemax.Path, o *httpDecoderOptions) (json.RawMessage, error) { + raw := json.RawMessage(`{}`) + for key := range values { + for _, path := range paths { + if key == path.Name { + var err error + switch path.Type.(type) { + case []string: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + case []float64: + for k, v := range values[key] { + var f float64 + if f, err = strconv.ParseFloat(v, 64); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a number."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetailf("index", "%d", k). + WithDetail("value", v)) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) + } + } + case []bool: + for k, v := range values[key] { + var b bool + if b, err = strconv.ParseBool(v); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), b) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a boolean."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetailf("index", "%d", k). + WithDetail("value", v)) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), b) + } + } + case []interface{}: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + case bool: + v := values[key][len(values[key])-1] + if len(v) == 0 { + if !path.Required { + continue + } + v = "false" + } + + var b bool + if b, err = strconv.ParseBool(v); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, b) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a boolean."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetail("value", values.Get(key))) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name, b) + } + case float64: + v := values.Get(key) + if len(v) == 0 { + if !path.Required { + continue + } + v = "0.0" + } + + var f float64 + if f, err = strconv.ParseFloat(v, 64); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, f) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a number."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetail("value", values.Get(key))) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name, f) + } + case string: + v := values.Get(key) + if len(v) == 0 { + continue + } + + raw, err = sjson.SetBytes(raw, path.Name, v) + case map[string]interface{}: + v := values.Get(key) + if len(v) == 0 && !path.Required { + continue + } + + raw, err = sjson.SetRawBytes(raw, path.Name, []byte(v)) + case []map[string]interface{}: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + } + + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to type assert values from HTTP Post Body: %s", err)) + } + break + } + } + } + + for _, path := range paths { + if path.TypeHint != jsonschemax.JSON { + continue + } + + if !gjson.GetBytes(raw, path.Name).Exists() { + var err error + raw, err = sjson.SetRawBytes(raw, path.Name, []byte(`{}`)) + if err != nil { + return nil, errors.WithStack(err) + } + } + } + + return raw, nil +} + +func decodeJSON(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + reader, err := requestBody(r, o) + if err != nil { + return err + } + + raw, err := io.ReadAll(reader) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to read HTTP POST body: %s", err)) + } + + dc := json.NewDecoder(bytes.NewReader(raw)) + if err := dc.Decode(destination); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode JSON payload: %s", err).WithDebugf("Received request body: %s", string(raw))) + } + + if err := validatePayload(r.Context(), raw, o); err != nil { + if o.expectJSONFlattened && strings.Contains(err.Error(), "json: unknown field") { + return decodeJSONForm(r, destination, o) + } + return err + } + + return nil +} diff --git a/oryx/decoderx/stub/consent.json b/oryx/decoderx/stub/consent.json new file mode 100644 index 0000000..6539260 --- /dev/null +++ b/oryx/decoderx/stub/consent.json @@ -0,0 +1,53 @@ +{ + "$id": "https://example.com/ory.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "traits": { + "additionalProperties": false, + "properties": { + "consent": { + "additionalProperties": false, + "properties": { + "tos": { + "description": "yyyymmdd of when this was accepted", + "title": "I accept the Terms of Service https://www.ory.sh/ptos", + "const": true, + "maxLength": 30 + }, + "inner": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "required": ["foo"] + } + }, + "required": ["tos"], + "title": "Consent", + "type": "object" + }, + "notrequired": { + "additionalProperties": false, + "properties": { + "tos": { + "description": "yyyymmdd of when this was accepted", + "title": "I accept the Terms of Service https://www.ory.sh/ptos", + "const": true, + "maxLength": 30 + } + }, + "required": ["tos"], + "title": "Consent", + "type": "object" + } + }, + "required": ["consent"], + "type": "object" + } + }, + "title": "Person", + "type": "object" +} diff --git a/oryx/decoderx/stub/dynamic-object.json b/oryx/decoderx/stub/dynamic-object.json new file mode 100644 index 0000000..beed127 --- /dev/null +++ b/oryx/decoderx/stub/dynamic-object.json @@ -0,0 +1,22 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + } + }, + "dynamic_object": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/oryx/decoderx/stub/nested.json b/oryx/decoderx/stub/nested.json new file mode 100644 index 0000000..4efa3af --- /dev/null +++ b/oryx/decoderx/stub/nested.json @@ -0,0 +1,36 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "required": ["node"], + "properties": { + "node": { + "type": "object", + "required": ["node"], + "properties": { + "node": { + "type": "object", + "properties": { + "node": { + "type": "object", + "properties": { + "leaf": { + "type": "string" + } + }, + "required": ["leaf"] + }, + "leaf": { + "type": "string" + } + }, + "required": ["leaf"] + }, + "leaf": { + "type": "string" + } + } + } + } +} diff --git a/oryx/decoderx/stub/person.json b/oryx/decoderx/stub/person.json new file mode 100644 index 0000000..7779aac --- /dev/null +++ b/oryx/decoderx/stub/person.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": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + } + }, + "age": { + "type": "integer" + }, + "ratio": { + "type": "number" + }, + "consent": { + "type": "boolean" + }, + "newsletter": { + "type": "boolean" + } + } +} diff --git a/oryx/decoderx/stub/required-defaults.json b/oryx/decoderx/stub/required-defaults.json new file mode 100644 index 0000000..62edd80 --- /dev/null +++ b/oryx/decoderx/stub/required-defaults.json @@ -0,0 +1,57 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + }, + "required": ["first"] + }, + "name2": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + }, + "required": ["first"] + }, + "age": { + "type": "integer" + }, + "age2": { + "type": "integer" + }, + "ratio": { + "type": "number" + }, + "ratio2": { + "type": "number" + }, + "consent": { + "type": "boolean" + }, + "consent2": { + "type": "boolean" + }, + "newsletter": { + "type": "boolean" + }, + "newsletter2": { + "type": "boolean" + } + }, + "required": ["age2", "ratio2", "consent2", "newsletter2", "name2"] +} diff --git a/oryx/decoderx/stub/schema.json b/oryx/decoderx/stub/schema.json new file mode 100644 index 0000000..c748fbd --- /dev/null +++ b/oryx/decoderx/stub/schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string" + } + } +} diff --git a/oryx/docs/alpha_num.png b/oryx/docs/alpha_num.png new file mode 100644 index 0000000..2bbc656 Binary files /dev/null and b/oryx/docs/alpha_num.png differ diff --git a/oryx/docs/num.png b/oryx/docs/num.png new file mode 100644 index 0000000..ecaadf6 Binary files /dev/null and b/oryx/docs/num.png differ diff --git a/oryx/docs/result_num.png b/oryx/docs/result_num.png new file mode 100644 index 0000000..98bb043 Binary files /dev/null and b/oryx/docs/result_num.png differ diff --git a/oryx/errorsx/errors.go b/oryx/errorsx/errors.go new file mode 100644 index 0000000..801141c --- /dev/null +++ b/oryx/errorsx/errors.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorsx + +import ( + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +// Deprecated: you should probably use errors.As instead. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok || cause.Cause() == nil { + break + } + err = cause.Cause() + } + return err +} + +// WithStack mirror pkg/errors.WithStack but does not wrap existing stack +// traces. +// Deprecated: you should probably use errors.WithStack instead and only annotate stacks when it makes sense. +func WithStack(err error) error { + if e, ok := err.(StackTracer); ok && len(e.StackTrace()) > 0 { + return err + } + + return errors.WithStack(err) +} + +// StatusCodeCarrier can be implemented by an error to support setting status codes in the error itself. +type StatusCodeCarrier interface { + // StatusCode returns the status code of this error. + StatusCode() int +} + +// RequestIDCarrier can be implemented by an error to support error contexts. +type RequestIDCarrier interface { + // RequestID returns the ID of the request that caused the error, if applicable. + RequestID() string +} + +// ReasonCarrier can be implemented by an error to support error contexts. +type ReasonCarrier interface { + // Reason returns the reason for the error, if applicable. + Reason() string +} + +// DebugCarrier can be implemented by an error to support error contexts. +type DebugCarrier interface { + // Debug returns debugging information for the error, if applicable. + Debug() string +} + +// StatusCarrier can be implemented by an error to support error contexts. +type StatusCarrier interface { + // ID returns the error id, if applicable. + Status() string +} + +// DetailsCarrier can be implemented by an error to support error contexts. +type DetailsCarrier interface { + // Details returns details on the error, if applicable. + Details() map[string]interface{} +} + +// IDCarrier can be implemented by an error to support error contexts. +type IDCarrier interface { + // ID returns application error ID on the error, if applicable. + ID() string +} + +type StackTracer interface { + StackTrace() errors.StackTrace +} + +func GetCodeFromHerodotError(err error) (code int, ok bool) { + herodotErr := &herodot.DefaultError{} + isHerodot := errors.As(err, &herodotErr) + + return herodotErr.CodeField, isHerodot +} diff --git a/oryx/fetcher/fetcher.go b/oryx/fetcher/fetcher.go new file mode 100644 index 0000000..78195ef --- /dev/null +++ b/oryx/fetcher/fetcher.go @@ -0,0 +1,187 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fetcher + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + stderrors "errors" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + + "github.com/ory/x/httpx" +) + +// Fetcher is able to load file contents from http, https, file, and base64 locations. +type Fetcher struct { + hc *retryablehttp.Client + limit int64 + cache *ristretto.Cache[[]byte, []byte] + ttl time.Duration + schemes []string +} + +type opts struct { + hc *retryablehttp.Client + limit int64 + cache *ristretto.Cache[[]byte, []byte] + ttl time.Duration + schemes []string +} + +var ErrUnknownScheme = stderrors.New("unknown scheme") + +// WithClient sets the http.Client the fetcher uses. +func WithClient(hc *retryablehttp.Client) Modifier { + return func(o *opts) { + o.hc = hc + } +} + +// WithMaxHTTPMaxBytes reads at most limit bytes from the HTTP response body, +// returning bytes.ErrToLarge if the limit would be exceeded. +func WithMaxHTTPMaxBytes(limit int64) Modifier { + return func(o *opts) { + o.limit = limit + } +} + +func WithCache(cache *ristretto.Cache[[]byte, []byte], ttl time.Duration) Modifier { + return func(o *opts) { + if ttl < 0 { + return + } + o.cache = cache + o.ttl = ttl + } +} + +func WithAllowedSchemes(schemes ...string) Modifier { + return func(o *opts) { + o.schemes = slices.Clone(schemes) + } +} + +func newOpts() *opts { + return &opts{ + hc: httpx.NewResilientClient(), + schemes: []string{"http", "https", "file", "base64"}, + } +} + +type Modifier func(*opts) + +// redactedSource returns a safe representation of the source for use in error +// messages. base64:// sources embed the full payload (which may contain +// secrets such as private keys) and are therefore redacted; all other sources +// are returned unchanged. +func redactedSource(source string) string { + if strings.HasPrefix(source, "base64://") { + return "base64://[redacted]" + } + return source +} + +// NewFetcher creates a new fetcher instance. +func NewFetcher(opts ...Modifier) *Fetcher { + o := newOpts() + for _, f := range opts { + f(o) + } + return &Fetcher{hc: o.hc, limit: o.limit, cache: o.cache, ttl: o.ttl, schemes: o.schemes} +} + +// FetchContext fetches the file contents from the source and allows to pass a +// context that is used for HTTP requests. +func (f *Fetcher) FetchContext(ctx context.Context, source string) (*bytes.Buffer, error) { + b, err := f.FetchBytes(ctx, source) + if err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil +} + +// FetchBytes fetches the file contents from the source and allows to pass a +// context that is used for HTTP requests. +func (f *Fetcher) FetchBytes(ctx context.Context, source string) ([]byte, error) { + if !slices.ContainsFunc(f.schemes, func(scheme string) bool { + return strings.HasPrefix(source, scheme+"://") + }) { + return nil, errors.WithStack(fmt.Errorf("%w: in source %q: allowed schemes: %s", ErrUnknownScheme, redactedSource(source), strings.Join(f.schemes, ", "))) + } + switch { + case strings.HasPrefix(source, "http://"), strings.HasPrefix(source, "https://"): + return f.fetchRemote(ctx, source) + case strings.HasPrefix(source, "file://"): + b, err := os.ReadFile(strings.TrimPrefix(source, "file://")) + if err != nil { + return nil, errors.Wrapf(err, "read file: %s", redactedSource(source)) + } + return b, nil + case strings.HasPrefix(source, "base64://"): + src, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(source, "base64://")) + if err != nil { + return nil, errors.Wrapf(err, "base64decode: %s", redactedSource(source)) + } + return src, nil + default: + return nil, errors.Wrap(ErrUnknownScheme, "unknown scheme in source: "+redactedSource(source)) + } +} + +func (f *Fetcher) fetchRemote(ctx context.Context, source string) (b []byte, err error) { + if f.cache != nil { + cacheKey := sha256.Sum256([]byte(source)) + if v, ok := f.cache.Get(cacheKey[:]); ok { + b = make([]byte, len(v)) + copy(b, v) + return b, nil + } + defer func() { + if err == nil && len(b) > 0 { + toCache := make([]byte, len(b)) + copy(toCache, b) + f.cache.SetWithTTL(cacheKey[:], toCache, int64(len(toCache)), f.ttl) + } + }() + } + + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, source, nil) + if err != nil { + return nil, errors.Wrapf(err, "new request: %s", redactedSource(source)) + } + res, err := f.hc.Do(req) + if err != nil { + return nil, errors.Wrap(err, redactedSource(source)) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("expected http response status code 200 but got %d when fetching: %s", res.StatusCode, redactedSource(source)) + } + + if f.limit > 0 { + var buf bytes.Buffer + n, err := io.Copy(&buf, io.LimitReader(res.Body, f.limit+1)) + if n > f.limit { + return nil, bytes.ErrTooLarge + } + if err != nil { + return nil, err + } + return buf.Bytes(), nil + } + return io.ReadAll(res.Body) +} diff --git a/oryx/flagx/flagx.go b/oryx/flagx/flagx.go new file mode 100644 index 0000000..67fb859 --- /dev/null +++ b/oryx/flagx/flagx.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flagx + +import ( + "time" + + "github.com/spf13/pflag" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewFlagSet(name string) *pflag.FlagSet { + return pflag.NewFlagSet(name, pflag.ContinueOnError) +} + +// MustGetBool returns a bool flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetBool(cmd *cobra.Command, name string) bool { + ok, err := cmd.Flags().GetBool(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ok +} + +// MustGetString returns a string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetString(cmd *cobra.Command, name string) string { + s, err := cmd.Flags().GetString(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return s +} + +// MustGetDuration returns a time.Duration flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetDuration(cmd *cobra.Command, name string) time.Duration { + d, err := cmd.Flags().GetDuration(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return d +} + +// MustGetStringSlice returns a []string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringSlice(cmd *cobra.Command, name string) []string { + ss, err := cmd.Flags().GetStringSlice(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetStringArray returns a []string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringArray(cmd *cobra.Command, name string) []string { + ss, err := cmd.Flags().GetStringArray(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetStringToStringMap returns a map[string]string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringToStringMap(cmd *cobra.Command, name string) map[string]string { + ss, err := cmd.Flags().GetStringToString(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetInt returns a int flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetInt(cmd *cobra.Command, name string) int { + ss, err := cmd.Flags().GetInt(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetUint8 returns a uint8 flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetUint8(cmd *cobra.Command, name string) uint8 { + v, err := cmd.Flags().GetUint8(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return v +} + +// MustGetUint32 returns a uint32 flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetUint32(cmd *cobra.Command, name string) uint32 { + v, err := cmd.Flags().GetUint32(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return v +} diff --git a/oryx/fsx/dirhash.go b/oryx/fsx/dirhash.go new file mode 100644 index 0000000..682f346 --- /dev/null +++ b/oryx/fsx/dirhash.go @@ -0,0 +1,34 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fsx + +import ( + "crypto/sha512" + "io" + "io/fs" +) + +// DirHash computes a directory hash from all files contained in any subdirectories. +func DirHash(dir fs.FS) ([]byte, error) { + hash := sha512.New() + err := fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + _, _ = io.WriteString(hash, path) // hash write never errors + f, err := dir.Open(path) + if err != nil { + return err + } + _, _ = io.Copy(hash, f) // hash write never errors + if err = f.Close(); err != nil { + return err + } + return nil + }) + return hash.Sum(nil), err +} diff --git a/oryx/fsx/merge.go b/oryx/fsx/merge.go new file mode 100644 index 0000000..5b758e8 --- /dev/null +++ b/oryx/fsx/merge.go @@ -0,0 +1,229 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fsx + +import ( + "io" + "io/fs" + "sort" + "time" + + "github.com/pkg/errors" +) + +type ( + mergedFS []fs.FS + mergedFile struct { + files []fs.File + unprocessedDirEntries dirEntries + } + mergedFileInfo []fs.FileInfo + dirEntries []fs.DirEntry +) + +var ( + _ fs.StatFS = (mergedFS)(nil) + _ fs.ReadDirFS = (mergedFS)(nil) + _ fs.ReadDirFile = (*mergedFile)(nil) + _ fs.FileInfo = (mergedFileInfo)(nil) + _ sort.Interface = (dirEntries)(nil) +) + +// Merge multiple filesystems. Later file systems are shadowed by previous ones. +func Merge(fss ...fs.FS) fs.FS { + return mergedFS(fss) +} + +func (m mergedFS) Open(name string) (fs.File, error) { + var file mergedFile + for _, fsys := range m { + f, err := fsys.Open(name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, errors.WithStack(err) + } + + file.files = append(file.files, f) + } + if len(file.files) == 0 { + return nil, errors.WithStack(fs.ErrNotExist) + } + + return &file, nil +} + +func (m mergedFS) Stat(name string) (fs.FileInfo, error) { + for i, fsys := range m { + info, err := fs.Stat(fsys, name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + + switch { + case err != nil: + return nil, errors.WithStack(err) + case info.IsDir(): + dirs := mergedFileInfo{info} + for j := i + 1; j < len(m); j++ { + info, err := fs.Stat(m[j], name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + dirs = append(dirs, info) + } + return dirs, nil + default: + return info, nil + } + } + return nil, errors.WithStack(fs.ErrNotExist) +} + +func (m mergedFS) ReadDir(name string) ([]fs.DirEntry, error) { + var entries dirEntries + + for _, fsys := range m { + e, err := fs.ReadDir(fsys, name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + entries = append(entries, e...) + } + if len(entries) == 0 { + return nil, errors.WithStack(fs.ErrNotExist) + } + + entries.clean() + return entries, nil +} + +func (m mergedFileInfo) Name() string { + return m[0].Name() +} + +func (m mergedFileInfo) Size() int64 { + return m[0].Size() +} + +func (m mergedFileInfo) Mode() fs.FileMode { + return m[0].Mode() +} + +func (m mergedFileInfo) ModTime() time.Time { + return m[0].ModTime() +} + +func (m mergedFileInfo) IsDir() bool { + return m[0].IsDir() +} + +func (m mergedFileInfo) Sys() interface{} { + return m +} + +func (d dirEntries) Len() int { + return len(d) +} + +func (d dirEntries) Less(i, j int) bool { + return d[i].Name() < d[j].Name() +} + +func (d dirEntries) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +func (d *dirEntries) clean() { + sort.Sort(d) + + for i := 1; i < len(*d); i++ { + if (*d)[i-1].Name() == (*d)[i].Name() { + if len(*d)-i == 1 { + // remove the last entry; we're done + *d = (*d)[:i] + return + } + // remove the duplicate entry at index i + *d = append((*d)[:i], (*d)[i+1:]...) + + // need to check the same index again + i-- + } + } +} + +func (m *mergedFile) Stat() (fs.FileInfo, error) { + return m.files[0].Stat() +} + +func (m *mergedFile) Read(bytes []byte) (int, error) { + return m.files[0].Read(bytes) +} + +func (m *mergedFile) Close() error { + var firstErr error + for _, f := range m.files { + if err := f.Close(); err != nil { + if firstErr == nil { + firstErr = errors.WithStack(err) + } + } + } + return firstErr +} + +func (m *mergedFile) ReadDir(n int) ([]fs.DirEntry, error) { + if m.unprocessedDirEntries != nil { + if n <= 0 { + entries := m.unprocessedDirEntries + m.unprocessedDirEntries = nil + return entries, nil + } + if n >= len(m.unprocessedDirEntries) { + entries := m.unprocessedDirEntries + m.unprocessedDirEntries = nil + return entries, io.EOF + } + + var entries dirEntries + entries, m.unprocessedDirEntries = m.unprocessedDirEntries[:n], m.unprocessedDirEntries[n:] + return entries, nil + } + + var entries dirEntries + for _, f := range m.files { + if f, ok := f.(fs.ReadDirFile); ok { + e, err := f.ReadDir(-1) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + entries = append(entries, e...) + } + } + if entries == nil { + if n > 0 { + return nil, io.EOF + } + return nil, nil + } + + entries.clean() + if n <= 0 { + return entries, nil + } + if n >= len(entries) { + return entries, io.EOF + } + + entries, m.unprocessedDirEntries = entries[:n], entries[n:] + return entries, nil +} diff --git a/oryx/go.mod b/oryx/go.mod new file mode 100644 index 0000000..5559107 --- /dev/null +++ b/oryx/go.mod @@ -0,0 +1,216 @@ +module github.com/ory/x + +go 1.26 + +require ( + code.dny.dev/ssrf v0.2.0 + connectrpc.com/connect v1.20.0 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/avast/retry-go/v4 v4.6.1 + github.com/bmatcuk/doublestar/v2 v2.0.4 + github.com/bradleyjkemp/cupaloy/v2 v2.8.0 + github.com/cockroachdb/cockroach-go/v2 v2.4.3 + github.com/dgraph-io/ristretto/v2 v2.4.0 + github.com/evanphx/json-patch/v5 v5.9.11 + github.com/fsnotify/fsnotify v1.9.0 + github.com/ghodss/yaml v1.0.0 + github.com/go-jose/go-jose/v3 v3.0.5 + github.com/go-openapi/jsonpointer v0.21.2 + github.com/go-openapi/runtime v0.28.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/gobuffalo/httptest v1.5.2 + github.com/gobwas/glob v0.2.3 + github.com/goccy/go-yaml v1.18.0 + github.com/gofrs/flock v0.12.1 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/go-jsonnet v0.21.0 + github.com/gorilla/securecookie v1.1.2 + github.com/gorilla/websocket v1.5.3 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf + github.com/jackc/pgx/v5 v5.10.0 + github.com/jackc/puddle/v2 v2.2.2 + github.com/jmoiron/sqlx v1.4.0 + github.com/knadh/koanf/maps v0.1.2 + github.com/knadh/koanf/parsers/json v0.1.0 + github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/posflag v0.1.0 + github.com/knadh/koanf/providers/rawbytes v0.1.0 + github.com/knadh/koanf/v2 v2.2.2 + github.com/laher/mergefs v0.1.1 + github.com/landlock-lsm/go-landlock v0.8.1 + github.com/lestrrat-go/jwx v1.2.31 + github.com/mailhog/data v1.0.1 + github.com/moby/moby/client v0.4.1 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/ory/analytics-go/v5 v5.0.1 + github.com/ory/dockertest/v4 v4.0.0-beta.4 + github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 + github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e + github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 + github.com/pelletier/go-toml v1.9.5 + github.com/peterhellberg/link v1.2.0 + github.com/pkg/errors v0.9.1 + github.com/pkg/profile v1.7.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.68.1 + github.com/rakutentech/jwk-go v1.2.0 + github.com/rs/cors v1.11.1 + github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cast v1.9.2 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.19.0 + github.com/tidwall/sjson v1.2.5 + github.com/urfave/negroni v1.0.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 + go.opentelemetry.io/contrib/propagators/b3 v1.44.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 + go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 + go.opentelemetry.io/otel/exporters/zipkin v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + go.opentelemetry.io/proto/otlp v1.10.0 + go.uber.org/goleak v1.3.0 + go.uber.org/mock v0.5.2 + golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 + modernc.org/sqlite v1.52.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/XSAM/otelsql v0.41.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/fizz v1.14.4 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.10 // indirect + github.com/gobuffalo/nulls v0.4.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.22 // indirect + github.com/gobuffalo/plush/v5 v5.0.7 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jaegertracing/jaeger-idl v0.9.0 // indirect + github.com/jandelgado/gcov2lcov v1.1.1 // 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/kylelemons/godebug v1.1.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.12.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nyaruka/phonenumbers v1.6.5 // 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.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/segmentio/backo-go v1.1.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect + modernc.org/libc v1.72.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +tool ( + github.com/jandelgado/gcov2lcov + go.uber.org/mock/mockgen + golang.org/x/tools/cmd/goimports +) diff --git a/oryx/go.sum b/oryx/go.sum new file mode 100644 index 0000000..30bce92 --- /dev/null +++ b/oryx/go.sum @@ -0,0 +1,636 @@ +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= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +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/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8= +github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM= +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/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE= +github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +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/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= +github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +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/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= +github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +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/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +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/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +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/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/helpers v0.6.10 h1:puKDCOrJ0EIq5ScnTRgKyvEZ05xQa+gwRGCpgoh6Ek8= +github.com/gobuffalo/helpers v0.6.10/go.mod h1:r52L6VSnByLJFOmURp1irvzgSakk7RodChi1YbGwk8I= +github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= +github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= +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/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.22 h1:bPQr5PsiTg54UGMsfvnIAvFmUfxzD/ri+wbpu7PlmTM= +github.com/gobuffalo/plush/v4 v4.1.22/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/plush/v5 v5.0.7 h1:nI8sIt5tZAN2tCZHeaXkH7HAvxvvk3sJHG2TtrKeSHM= +github.com/gobuffalo/plush/v5 v5.0.7/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU= +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/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.2.0+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/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +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/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= +github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +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/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +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/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +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 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.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +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/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +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/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaegertracing/jaeger-idl v0.9.0 h1:dI4olA7ArW3cjXwVbic/aYKDbdlfe7V+9wPQqAdzu8Y= +github.com/jaegertracing/jaeger-idl v0.9.0/go.mod h1:W+9vbcr2cVZyS6z/cbr540EOzSkKYml3hmaWEavxkB0= +github.com/jandelgado/gcov2lcov v1.1.1 h1:CHUNoAglvb34DqmMoZchnzDbA3yjpzT8EoUvVqcAY+s= +github.com/jandelgado/gcov2lcov v1.1.1/go.mod h1:tMVUlMVtS1po2SB8UkADWhOT5Y5Q13XOce2AYU69JuI= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +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/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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= +github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= +github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/laher/mergefs v0.1.1 h1:nV2bTS57vrmbMxeR6uvJpI8LyGl3QHj4bLBZO3aUV58= +github.com/laher/mergefs v0.1.1/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q= +github.com/landlock-lsm/go-landlock v0.8.1 h1:Krs1co16IzN7bQcFYIdtNF+BKwZem3geRBkVsZtlCKU= +github.com/landlock-lsm/go-landlock v0.8.1/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +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.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +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/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.31 h1:/OM9oNl/fzyldpv5HKZ9m7bTywa7COUfg8gujd9nJ54= +github.com/lestrrat-go/jwx v1.2.31/go.mod h1:eQJKoRwWcLg4PfD5CFA5gIZGxhPgoPYq9pZISdxLf0c= +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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mailhog/data v1.0.1 h1:7I+opBvVdi4EMJaihXavM98jp/ovt4o6mz47446RAW8= +github.com/mailhog/data v1.0.1/go.mod h1:tjR/iXRhbSUKHzAAMd99RygVaDB5rIDC/bmWc363MzU= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +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/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.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nyaruka/phonenumbers v1.6.5 h1:aBCaUhfpRA7hU6fsXk+p7KF1aNx4nQlq9hGeo2qdFg8= +github.com/nyaruka/phonenumbers v1.6.5/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +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/v4 v4.0.0-beta.4 h1:QcrNrobOP+5IjSDmS4//EuBtwiFuznQhi5xTe8oFSoM= +github.com/ory/dockertest/v4 v4.0.0-beta.4/go.mod h1:p9kfE14tzK8+WU4F9YbIZlzhCzQ2pH7H1KIfBKrF3DM= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 h1:xSbAB0yH2CUPqf9tiei3EFo7xpqY5nsAGdcrwqH9o6o= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22/go.mod h1:by6EjQDF4bDC2OuWOBd9EBRrTjtl0+kxSzyvuizM0Iw= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e h1:4tUrC7x4YWRVMFp+c64KACNSGchW1zXo4l6Pa9/1hA8= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e/go.mod h1:XWLxVK4un/iuIcrw+6lCeanbF3NZwO5k6RdLeu/loQk= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 h1:LNreY5V2pXsUgiY/rVaU5uhIsOUHzZ+PP/NUgfqubr0= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6/go.mod h1:vd8H2inBRK+ZF+r5jDdx/fYPwqWrbVoAD4t7vAfXeA4= +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/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= +github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rakutentech/jwk-go v1.2.0 h1:vNJwedPkRR+32V5WGNj0JP4COes93BGERvzQLBjLy4c= +github.com/rakutentech/jwk-go v1.2.0/go.mod h1:pI0bYVntqaJ27RCpaC75MTUacheW0Rk4+8XzWWe1OWM= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.6.1/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= +github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= +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.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/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/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +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.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/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 h1:pV2nZ1iE87X9ym+crkD9k15zTLbN08+IC4C7sk9sQYM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1/go.mod h1:nvgrM8LaG2+5G7WxbtjEPiSkg87+d0/ltZZK70p7FVo= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +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.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0 h1:zv7PRYGLrQHkdeZj0c5SNAZOJcw55XgaTezUkNpwA+w= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0/go.mod h1:3+VZyCi6hFW+UuxFF+wSOvwsOwncfBpQfP7Qdb3JXKg= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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-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= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +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/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= +modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= +modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= +modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/oryx/hasherx/hash_comparator.go b/oryx/hasherx/hash_comparator.go new file mode 100644 index 0000000..be2745f --- /dev/null +++ b/oryx/hasherx/hash_comparator.go @@ -0,0 +1,227 @@ +package hasherx + +import ( + "context" + "crypto/subtle" + "encoding/base64" + "fmt" + "math" + "regexp" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/pbkdf2" +) + +var ErrUnknownHashAlgorithm = errors.New("unknown hash algorithm") + +// Compare the given password with the given hash. +func Compare(ctx context.Context, password []byte, hash []byte) error { + switch { + case IsBcryptHash(hash): + return CompareBcrypt(ctx, password, hash) + case IsArgon2idHash(hash): + return CompareArgon2id(ctx, password, hash) + case IsArgon2iHash(hash): + return CompareArgon2i(ctx, password, hash) + case IsPbkdf2Hash(hash): + return ComparePbkdf2(ctx, password, hash) + default: + return errors.WithStack(ErrUnknownHashAlgorithm) + } +} + +func CompareBcrypt(_ context.Context, password []byte, hash []byte) error { + if err := validateBcryptPasswordLength(password); err != nil { + return err + } + + err := bcrypt.CompareHashAndPassword(hash, password) + if err != nil { + return err + } + + return nil +} + +func CompareArgon2id(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + mem := uint64(p.Memory) + if mem > math.MaxUint32 { + return errors.WithStack(ErrInvalidHash) + } + + // Derive the key from the other password using the same parameters. + otherHash := argon2.IDKey(password, salt, p.Iterations, uint32(mem), p.Parallelism, p.KeyLength) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +func CompareArgon2i(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + mem := uint64(p.Memory) + if mem > math.MaxUint32 { + return errors.WithStack(ErrInvalidHash) + } + + // Derive the key from the other password using the same parameters. + otherHash := argon2.Key(password, salt, p.Iterations, uint32(mem), p.Parallelism, p.KeyLength) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +func ComparePbkdf2(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodePbkdf2Hash(string(hash)) + if err != nil { + return err + } + + // Derive the key from the other password using the same parameters. + otherHash := pbkdf2.Key(password, salt, int(p.Iterations), int(p.KeyLength), getPseudorandomFunctionForPbkdf2(p.Algorithm)) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +var ( + isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`) + isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`) + isArgon2iHash = regexp.MustCompile(`^\$argon2i\$`) + isPbkdf2Hash = regexp.MustCompile(`^\$pbkdf2-sha[0-9]{1,3}\$`) +) + +func IsBcryptHash(hash []byte) bool { + return isBcryptHash.Match(hash) +} + +func IsArgon2idHash(hash []byte) bool { + return isArgon2idHash.Match(hash) +} + +func IsArgon2iHash(hash []byte) bool { + return isArgon2iHash.Match(hash) +} + +func IsPbkdf2Hash(hash []byte) bool { + return isPbkdf2Hash.Match(hash) +} + +func decodeArgon2idHash(encodedHash string) (p *Argon2Config, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + var version int + _, err = fmt.Sscanf(parts[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + p = new(Argon2Config) + _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + saltLength := uint(len(salt)) + if saltLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.SaltLength = uint32(saltLength) + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, nil, err + } + keyLength := uint(len(hash)) + if keyLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.KeyLength = uint32(keyLength) + + return p, salt, hash, nil +} + +// decodePbkdf2Hash decodes PBKDF2 encoded password hash. +// format: $pbkdf2-$i=,l=$$ +func decodePbkdf2Hash(encodedHash string) (p *PBKDF2Config, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 5 { + return nil, nil, nil, ErrInvalidHash + } + + p = new(PBKDF2Config) + digestParts := strings.SplitN(parts[1], "-", 2) + if len(digestParts) != 2 { + return nil, nil, nil, ErrInvalidHash + } + p.Algorithm = digestParts[1] + + _, err = fmt.Sscanf(parts[2], "i=%d,l=%d", &p.Iterations, &p.KeyLength) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + saltLength := uint(len(salt)) + if saltLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.SaltLength = uint32(saltLength) + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + keyLength := uint(len(hash)) + if keyLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.KeyLength = uint32(keyLength) + + return p, salt, hash, nil +} diff --git a/oryx/hasherx/hasher.go b/oryx/hasherx/hasher.go new file mode 100644 index 0000000..c254724 --- /dev/null +++ b/oryx/hasherx/hasher.go @@ -0,0 +1,20 @@ +package hasherx + +import ( + "context" +) + +// Hasher provides methods for generating and comparing password hashes. +type Hasher interface { + // Generate returns a hash derived from the password or an error if the hash method failed. + Generate(ctx context.Context, password []byte) ([]byte, error) + + // Understands returns whether the given hash can be understood by this hasher. + Understands(hash []byte) bool +} + +type HashProvider interface { + Hasher() Hasher +} + +const tracingComponent = "github.com/ory/kratos/hash" diff --git a/oryx/hasherx/hasher_argon2.go b/oryx/hasherx/hasher_argon2.go new file mode 100644 index 0000000..238ab39 --- /dev/null +++ b/oryx/hasherx/hasher_argon2.go @@ -0,0 +1,118 @@ +package hasherx + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "math" + "time" + + "github.com/ory/x/otelx" + + "github.com/inhies/go-bytesize" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" +) + +var ( + ErrInvalidHash = errors.New("the encoded hash is not in the correct format") + ErrIncompatibleVersion = errors.New("incompatible version of argon2") + ErrMismatchedHashAndPassword = errors.New("passwords do not match") +) + +type ( + // Argon2Config is the configuration for a Argon2 hasher. + Argon2Config struct { + // Memory is the amount of memory to use. + Memory bytesize.ByteSize `json:"memory"` + + // Iterations is the number of iterations to use. + Iterations uint32 `json:"iterations"` + + // Parallelism is the number of threads to use. + Parallelism uint8 `json:"parallelism"` + + // SaltLength is the length of the salt to use. + SaltLength uint32 `json:"salt_length"` + + // KeyLength is the length of the key to use. + KeyLength uint32 `json:"key_length"` + + // ExpectedDuration is the expected duration of the hash. + ExpectedDuration time.Duration `json:"expected_duration"` + + // ExpectedDeviation is the expected deviation of the hash. + ExpectedDeviation time.Duration `json:"expected_deviation"` + + // DedicatedMemory is the amount of dedicated memory to use. + DedicatedMemory bytesize.ByteSize `json:"dedicated_memory"` + } + // Argon2 is a hasher that uses the Argon2 algorithm. + Argon2 struct { + c Argon2Configurator + } + // Argon2Configurator is a function that returns the Argon2 configuration. + Argon2Configurator interface { + HasherArgon2Config(ctx context.Context) *Argon2Config + } +) + +func NewHasherArgon2(c Argon2Configurator) *Argon2 { + return &Argon2{c: c} +} + +func toKB(mem bytesize.ByteSize) (uint32, error) { + kb := uint64(mem / bytesize.KB) + if kb > math.MaxUint32 { + return 0, errors.Errorf("memory %v is too large", mem) + } + return uint32(kb), nil +} + +// Generate generates a hash for the given password. +func (h *Argon2) Generate(ctx context.Context, password []byte) (_ []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Argon2.Generate") + defer otelx.End(span, &err) + p := h.c.HasherArgon2Config(ctx) + span.SetAttributes(attribute.String("argon2.config", fmt.Sprintf("#%v", p))) + + salt := make([]byte, p.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + mem, err := toKB(p.Memory) + if err != nil { + return nil, err + } + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash := argon2.IDKey(password, salt, p.Iterations, mem, p.Parallelism, p.KeyLength) + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, mem, p.Iterations, p.Parallelism, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +// Understands checks if the given hash is in the correct format. +func (h *Argon2) Understands(hash []byte) bool { + return IsArgon2idHash(hash) +} diff --git a/oryx/hasherx/hasher_bcrypt.go b/oryx/hasherx/hasher_bcrypt.go new file mode 100644 index 0000000..64be1f4 --- /dev/null +++ b/oryx/hasherx/hasher_bcrypt.go @@ -0,0 +1,68 @@ +package hasherx + +import ( + "context" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "golang.org/x/crypto/bcrypt" + + "github.com/ory/x/otelx" +) + +// ErrBcryptPasswordLengthReached is returned when the password is longer than 72 bytes. +var ErrBcryptPasswordLengthReached = errors.Errorf("passwords are limited to a maximum length of 72 characters") + +type ( + // Bcrypt is a hasher that uses the bcrypt algorithm. + Bcrypt struct { + c BCryptConfigurator + } + // BCryptConfig is the configuration for the bcrypt hasher. + BCryptConfig struct { + Cost uint32 `json:"cost"` + } + // BCryptConfigurator is the interface that must be implemented by a configuration provider for the bcrypt hasher. + BCryptConfigurator interface { + HasherBcryptConfig(ctx context.Context) *BCryptConfig + } +) + +func NewHasherBcrypt(c BCryptConfigurator) *Bcrypt { + return &Bcrypt{c: c} +} + +// Generate generates a hash for the given password. +func (h *Bcrypt) Generate(ctx context.Context, password []byte) (hash []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Bcrypt.Generate") + defer otelx.End(span, &err) + + if err := validateBcryptPasswordLength(password); err != nil { + return nil, err + } + + cost := int(h.c.HasherBcryptConfig(ctx).Cost) + span.SetAttributes(attribute.Int("bcrypt.cost", cost)) + hash, err = bcrypt.GenerateFromPassword(password, cost) + if err != nil { + return nil, err + } + + return hash, nil +} + +func validateBcryptPasswordLength(password []byte) error { + // Bcrypt truncates the password to the first 72 bytes, following the OpenBSD implementation, + // so if password is longer than 72 bytes, function returns an error + // See https://en.wikipedia.org/wiki/Bcrypt#User_input + if len(password) > 72 { + return ErrBcryptPasswordLengthReached + } + return nil +} + +// Understands checks if the given hash is in the correct format. +func (h *Bcrypt) Understands(hash []byte) bool { + return IsBcryptHash(hash) +} diff --git a/oryx/hasherx/hasher_pbkdf2.go b/oryx/hasherx/hasher_pbkdf2.go new file mode 100644 index 0000000..de65955 --- /dev/null +++ b/oryx/hasherx/hasher_pbkdf2.go @@ -0,0 +1,103 @@ +package hasherx + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" // #nosec G505 - compatibility for imported passwords + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "fmt" + "hash" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/sha3" + + "github.com/ory/x/otelx" +) + +type ( + // PBKDF2 is a PBKDF2 hasher. + PBKDF2 struct { + c PBKDF2Configurator + } + + // PBKDF2Config is the configuration for a PBKDF2 hasher. + PBKDF2Config struct { + // Algorithm can be one of sha1, sha224, sha256, sha384, sha512 + Algorithm string + // Iterations is the number of iterations to use. + Iterations uint32 + // KeyLength is the length of the salt. + SaltLength uint32 + // KeyLength is the length of the key. + KeyLength uint32 + } + + // PBKDF2Configurator is a configurator for a PBKDF2 hasher. + PBKDF2Configurator interface { + HasherPBKDF2Config(ctx context.Context) *PBKDF2Config + } +) + +// NewHasherPBKDF2 creates a new PBKDF2 hasher. +func NewHasherPBKDF2(c PBKDF2Configurator) *PBKDF2 { + return &PBKDF2{c: c} +} + +// Generate generates a hash for the given password. +func (h *PBKDF2) Generate(ctx context.Context, password []byte) (hash []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "hash.PBKDF2.Generate") + defer otelx.End(span, &err) + + conf := h.c.HasherPBKDF2Config(ctx) + salt := make([]byte, conf.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + key := pbkdf2.Key(password, salt, int(conf.Iterations), int(conf.KeyLength), getPseudorandomFunctionForPbkdf2(conf.Algorithm)) + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$pbkdf2-%s$i=%d,l=%d$%s$%s", + conf.Algorithm, + conf.Iterations, + conf.KeyLength, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(key), + ); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +// Understands checks if the given hash is in the correct format. +func (h *PBKDF2) Understands(hash []byte) bool { + return IsPbkdf2Hash(hash) +} + +func getPseudorandomFunctionForPbkdf2(alg string) func() hash.Hash { + switch alg { + case "sha1": + return sha1.New + case "sha224": + return sha3.New224 + case "sha256": + return sha256.New + case "sha384": + return sha3.New384 + case "sha512": + return sha512.New + default: + return sha256.New + } +} diff --git a/oryx/healthx/doc.go b/oryx/healthx/doc.go new file mode 100644 index 0000000..200b47d --- /dev/null +++ b/oryx/healthx/doc.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package healthx providers helpers for returning health status information via HTTP. +package healthx + +import "strings" + +// The health status of the service. +// +// swagger:model healthStatus +type swaggerHealthStatus struct { + // Status always contains "ok". + Status string `json:"status"` +} + +// The not ready status of the service. +// +// swagger:model healthNotReadyStatus +type swaggerNotReadyStatus struct { + // Errors contains a list of errors that caused the not ready status. + Errors map[string]string `json:"errors"` +} + +func (s swaggerNotReadyStatus) Error() string { + var errs []string + for _, err := range s.Errors { + errs = append(errs, err) + } + return strings.Join(errs, "; ") +} + +// swagger:model version +type swaggerVersion struct { + // Version is the service's version. + Version string `json:"version"` +} diff --git a/oryx/healthx/handler.go b/oryx/healthx/handler.go new file mode 100644 index 0000000..092464a --- /dev/null +++ b/oryx/healthx/handler.go @@ -0,0 +1,222 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package healthx + +import ( + "net/http" + + "github.com/ory/herodot" + "github.com/ory/x/httprouterx" +) + +const ( + // AliveCheckPath is the path where information about the life state of the instance is provided. + AliveCheckPath = "/health/alive" + // ReadyCheckPath is the path where information about the ready state of the instance is provided. + ReadyCheckPath = "/health/ready" + // VersionPath is the path where information about the software version of the instance is provided. + VersionPath = "/version" +) + +// RoutesToObserve returns a string of all the available routes of this module. +func RoutesToObserve() []string { + return []string{ + AliveCheckPath, + ReadyCheckPath, + VersionPath, + } +} + +// ReadyChecker should return an error if the component is not ready yet. +type ReadyChecker func(r *http.Request) error + +// ReadyCheckers is a map of ReadyCheckers. +type ReadyCheckers map[string]ReadyChecker + +// NoopReadyChecker is always ready. +func NoopReadyChecker() error { + return nil +} + +// Handler handles HTTP requests to health and version endpoints. +type Handler struct { + H herodot.Writer + VersionString string + ReadyChecks ReadyCheckers +} + +type options struct { + middleware func(http.Handler) http.Handler +} + +type Options func(*options) + +// NewHandler instantiates a handler. +func NewHandler( + h herodot.Writer, + version string, + readyChecks ReadyCheckers, +) *Handler { + return &Handler{ + H: h, + VersionString: version, + ReadyChecks: readyChecks, + } +} + +// SetHealthRoutes registers this handler's routes for health checking. +func (h *Handler) SetHealthRoutes(r httprouterx.Router, shareErrors bool, opts ...Options) { + o := &options{} + aliveHandler := h.Alive() + readyHandler := h.Ready(shareErrors) + + for _, opt := range opts { + opt(o) + } + + if o.middleware != nil { + aliveHandler = o.middleware(aliveHandler) + readyHandler = o.middleware(readyHandler) + } + + r.GET(AliveCheckPath, aliveHandler.ServeHTTP) + r.GET(ReadyCheckPath, readyHandler.ServeHTTP) +} + +// SetVersionRoutes registers this handler's routes for health checking. +func (h *Handler) SetVersionRoutes(r httprouterx.Router, opts ...Options) { + o := &options{} + versionHandler := h.Version() + + for _, opt := range opts { + opt(o) + } + + if o.middleware != nil { + versionHandler = o.middleware(versionHandler) + } + + r.GET(VersionPath, versionHandler.ServeHTTP) +} + +// Alive returns an ok status if the instance is ready to handle HTTP requests. +// +// swagger:route GET /health/alive health isInstanceAlive +// +// # Check alive status +// +// This endpoint returns a 200 status code when the HTTP server is up running. +// This status does currently not include checks whether the database connection is working. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// 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. +// +// Produces: +// - application/json +// - text/plain +// +// Responses: +// 200: healthStatus +// default: unexpectedError +func (h *Handler) Alive() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + h.H.Write(rw, r, &swaggerHealthStatus{ + Status: "ok", + }) + }) +} + +// swagger:model unexpectedError +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type unexpectedError string + +// Ready returns an ok status if the instance is ready to handle HTTP requests and all ReadyCheckers are ok. +// +// swagger:route GET /health/ready health isInstanceReady +// +// # Check readiness status +// +// This endpoint returns a 200 status code when the HTTP server is up running and the environment dependencies (e.g. +// the database) are responsive as well. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// 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. +// +// Produces: +// - application/json +// - text/plain +// +// Responses: +// 200: healthStatus +// 503: healthNotReadyStatus +// default: unexpectedError +func (h *Handler) Ready(shareErrors bool) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var notReady = swaggerNotReadyStatus{ + Errors: map[string]string{}, + } + + for n, c := range h.ReadyChecks { + if err := c(r); err != nil { + if shareErrors { + notReady.Errors[n] = err.Error() + } else { + notReady.Errors[n] = "error may contain sensitive information and was obfuscated" + } + } + } + + if len(notReady.Errors) > 0 { + h.H.WriteErrorCode(rw, r, http.StatusServiceUnavailable, ¬Ready) + return + } + + h.H.Write(rw, r, &swaggerHealthStatus{ + Status: "ok", + }) + }) +} + +// Version returns this service's versions. +// +// swagger:route GET /version version getVersion +// +// # Get service version +// +// This endpoint returns the service version typically notated using semantic versioning. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// 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. +// +// Produces: +// - application/json +// +// Responses: +// 200: version +func (h *Handler) Version() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + h.H.Write(rw, r, &swaggerVersion{ + Version: h.VersionString, + }) + }) +} + +// WithMiddleware accepts a http.Handler to be run on the +// route handlers +func WithMiddleware(h func(http.Handler) http.Handler) Options { + return func(o *options) { + o.middleware = h + } +} diff --git a/oryx/healthx/openapi/patch.yaml b/oryx/healthx/openapi/patch.yaml new file mode 100644 index 0000000..2d4baef --- /dev/null +++ b/oryx/healthx/openapi/patch.yaml @@ -0,0 +1,112 @@ +- op: replace + path: /paths/~1health~1alive + value: + get: + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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. + operationId: isAlive + responses: + '200': + content: + application/json: + schema: + required: + - status + type: object + properties: + status: + description: Always "ok". + type: string + description: '{{.ProjectHumanName}} is ready to accept connections.' + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1health~1ready + value: + get: + operationId: isReady + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of {{.ProjectHumanName}}, the health status will never + refer to the cluster state, only to a single instance. + responses: + '200': + content: + application/json: + schema: + required: + - status + type: object + properties: + status: + description: Always "ok". + type: string + description: '{{.ProjectHumanName}} is ready to accept requests.' + '503': + content: + application/json: + schema: + required: + - errors + properties: + errors: + additionalProperties: + type: string + description: Errors contains a list of errors that caused the not ready status. + type: object + type: object + description: Ory Kratos is not yet ready to accept requests. + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server and Database Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1version + value: + get: + description: |- + This endpoint returns the version of {{.ProjectHumanName}}. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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. + operationId: getVersion + responses: + '200': + content: + application/json: + schema: + type: object + required: + - version + properties: + version: + description: The version of {{.ProjectHumanName}}. + type: string + description: Returns the {{.ProjectHumanName}} version. + summary: Return Running Software Version. + tags: {{ .HealthPathTags | toJson }} diff --git a/oryx/httprouterx/router.go b/oryx/httprouterx/router.go new file mode 100644 index 0000000..03c982a --- /dev/null +++ b/oryx/httprouterx/router.go @@ -0,0 +1,164 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httprouterx + +import ( + "context" + "net/http" + "path" + "strings" +) + +const ( + AdminPrefix = "/admin" + + afterMatchHooks contextKey = 1 +) + +type ( + contextKey int + AfterMatchHookFunc func(*http.Request) + + router struct { + mux *http.ServeMux + prefix string + } + RouterAdmin struct{ router } + RouterPublic struct{ router } + + Router interface { + http.Handler + + Handle(pattern string, handler http.Handler) + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + + GET(route string, handle http.HandlerFunc) + OPTIONS(route string, handle http.HandlerFunc) + HEAD(route string, handle http.HandlerFunc) + POST(route string, handle http.HandlerFunc) + PUT(route string, handle http.HandlerFunc) + PATCH(route string, handle http.HandlerFunc) + DELETE(route string, handle http.HandlerFunc) + } +) + +func newRouter() *router { return &router{mux: http.NewServeMux()} } + +// NewRouter creates a new general purpose router. It should only be used when neither the admin nor the public router is applicable. +func NewRouter() Router { return newRouter() } + +// NewRouterAdmin creates a new admin router. +func NewRouterAdmin() *RouterAdmin { + return &RouterAdmin{router: *newRouter()} +} + +func (r *RouterAdmin) ToPublic() *RouterPublic { + return &RouterPublic{router: router{ + mux: r.mux, + prefix: "", // do not copy the admin prefix + }} +} + +// NewRouterPublic returns a public router. +func NewRouterPublic() *RouterPublic { + return &RouterPublic{router: *newRouter()} +} + +// NewRouterAdminWithPrefix creates a new router with the admin prefix. +func NewRouterAdminWithPrefix() *RouterAdmin { + r := NewRouterAdmin() + r.prefix = AdminPrefix + return r +} + +func (r *router) GET(route string, handle http.HandlerFunc) { + r.handle(http.MethodGet, route, handle) +} + +func (r *router) OPTIONS(route string, handle http.HandlerFunc) { + r.handle(http.MethodOptions, route, handle) +} + +func (r *router) HEAD(route string, handle http.HandlerFunc) { + r.handle(http.MethodHead, route, handle) +} + +func (r *router) POST(route string, handle http.HandlerFunc) { + r.handle(http.MethodPost, route, handle) +} + +func (r *router) PUT(route string, handle http.HandlerFunc) { + r.handle(http.MethodPut, route, handle) +} + +func (r *router) PATCH(route string, handle http.HandlerFunc) { + r.handle(http.MethodPatch, route, handle) +} + +func (r *router) DELETE(route string, handle http.HandlerFunc) { + r.handle(http.MethodDelete, route, handle) +} + +func (r *router) handle(method string, route string, handler http.Handler) { + route = path.Join(r.prefix, route) + r.mux.Handle(method+" "+route, afterMatchHookExecutor(handler)) +} + +func afterMatchHookExecutor(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if hooks, ok := r.Context().Value(afterMatchHooks).([]AfterMatchHookFunc); ok { + for _, hook := range hooks { + hook(r) + } + } + h.ServeHTTP(w, r) + }) +} + +// Handle registers the handler for the given pattern. It does not prepend the admin prefix, so the caller is responsible for ensuring that the pattern is correct. +func (r *router) Handle(pattern string, handler http.Handler) { + r.mux.Handle(pattern, afterMatchHookExecutor(handler)) +} + +// HandleFunc registers the handler for the given pattern. It does not prepend the admin prefix, so the caller is responsible for ensuring that the pattern is correct. +func (r *router) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { + r.Handle(pattern, http.HandlerFunc(handler)) +} + +func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.mux.ServeHTTP(w, req) } + +func TrimTrailingSlashNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") + + next(rw, r) +} + +func NoCacheNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Method == "GET" { + rw.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate") + } + + next(rw, r) +} + +func AddAdminPrefixIfNotPresentNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if !strings.HasPrefix(r.URL.Path, AdminPrefix) { + r.URL.Path = path.Join(AdminPrefix, r.URL.Path) + if r.URL.RawPath != "" { + r.URL.RawPath = path.Join(AdminPrefix, r.URL.RawPath) + } + } + + next(rw, r) +} + +// WithAfterMatchHook adds the passed AfterMatchHookFunc to the request's context. The hook is called after the +// router matched the request, but before it is passed to the handler. This allows middlewares to access the matched +// pattern and other information that is only available after the router has matched the request. +// Note that this hook is called multiple times per request if routers are nested, the hook should anticipate that. +func WithAfterMatchHook(req *http.Request, newHooks ...AfterMatchHookFunc) *http.Request { + hooks, _ := req.Context().Value(afterMatchHooks).([]AfterMatchHookFunc) + ctx := context.WithValue(req.Context(), afterMatchHooks, append(hooks, newHooks...)) + return req.WithContext(ctx) +} diff --git a/oryx/httpx/assert.go b/oryx/httpx/assert.go new file mode 100644 index 0000000..c913267 --- /dev/null +++ b/oryx/httpx/assert.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "net/http" +) + +func GetResponseMeta(w http.ResponseWriter) (status, size int) { + switch t := w.(type) { + case interface{ Status() int }: + status = t.Status() + } + + switch t := w.(type) { + case interface{ Size() int }: + size = t.Size() + case interface{ Written() int64 }: + size = int(t.Written()) + } + + return +} diff --git a/oryx/httpx/chan_handler.go b/oryx/httpx/chan_handler.go new file mode 100644 index 0000000..42b9a20 --- /dev/null +++ b/oryx/httpx/chan_handler.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import "net/http" + +type chanHandler <-chan http.HandlerFunc + +var _ http.Handler = chanHandler(nil) + +func (c chanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + (<-c)(w, r) +} + +// NewChanHandler returns a new handler and corresponding channel for sending handler funcs. +// Useful for testing. The argument buf specifies the channel capacity, so pass 0 for a sync handler. +func NewChanHandler(buf int) (http.Handler, chan<- http.HandlerFunc) { + c := make(chan http.HandlerFunc, buf) + return chanHandler(c), c +} diff --git a/oryx/httpx/client_info.go b/oryx/httpx/client_info.go new file mode 100644 index 0000000..1f966e4 --- /dev/null +++ b/oryx/httpx/client_info.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "net" + "net/http" + "strconv" + "strings" +) + +type GeoLocation struct { + City string + Region string + Country string + Latitude *float64 + Longitude *float64 +} + +func GetClientIPAddressesWithoutInternalIPs(ipAddresses []string) (string, error) { + var res string + + for i := len(ipAddresses) - 1; i >= 0; i-- { + ip := strings.TrimSpace(ipAddresses[i]) + + if !net.ParseIP(ip).IsPrivate() { + res = ip + break + } + } + + return res, nil +} + +func ClientIP(r *http.Request) string { + if trueClientIP := r.Header.Get("True-Client-IP"); trueClientIP != "" { + return trueClientIP + } else if cfConnectingIP := r.Header.Get("Cf-Connecting-IP"); cfConnectingIP != "" { + return cfConnectingIP + } else if realClientIP := r.Header.Get("X-Real-IP"); realClientIP != "" { + return realClientIP + } else if forwardedIP := r.Header.Get("X-Forwarded-For"); forwardedIP != "" { + ip, _ := GetClientIPAddressesWithoutInternalIPs(strings.Split(forwardedIP, ",")) + return ip + } else { + return r.RemoteAddr + } +} + +func parseFloatHeaderValue(headerValue string) *float64 { + if headerValue == "" { + return nil + } + + val, err := strconv.ParseFloat(headerValue, 64) + if err != nil { + return nil + } + + return &val +} + +func ClientGeoLocation(r *http.Request) *GeoLocation { + return &GeoLocation{ + City: r.Header.Get("Cf-Ipcity"), + Region: r.Header.Get("Cf-Region-Code"), + Country: r.Header.Get("Cf-Ipcountry"), + Longitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplongitude")), + Latitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplatitude")), + } +} diff --git a/oryx/httpx/content_type.go b/oryx/httpx/content_type.go new file mode 100644 index 0000000..6c01c9f --- /dev/null +++ b/oryx/httpx/content_type.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "mime" + "net/http" + "slices" + "strings" +) + +// HasContentType determines whether the request `content-type` includes a +// server-acceptable mime-type +// +// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`) +func HasContentType(r *http.Request, mimetypes ...string) bool { + contentType := r.Header.Get("Content-Type") + if contentType == "" { + return slices.Contains(mimetypes, "application/octet-stream") + } + + mediaType, _, err := mime.ParseMediaType(strings.TrimSpace(contentType)) + if err != nil { + return false + } + return slices.Contains(mimetypes, mediaType) +} diff --git a/oryx/httpx/gzip_server.go b/oryx/httpx/gzip_server.go new file mode 100644 index 0000000..1ee6603 --- /dev/null +++ b/oryx/httpx/gzip_server.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "compress/gzip" + "fmt" + "io" + "net/http" + "strings" +) + +type CompressionRequestReader struct { + ErrHandler func(w http.ResponseWriter, r *http.Request, err error) +} + +func defaultCompressionErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) +} + +func NewCompressionRequestReader(eh func(w http.ResponseWriter, r *http.Request, err error)) *CompressionRequestReader { + if eh == nil { + eh = defaultCompressionErrorHandler + } + + return &CompressionRequestReader{ + ErrHandler: eh, + } +} + +func (c *CompressionRequestReader) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + for _, enc := range strings.Split(r.Header.Get("Content-Encoding"), ",") { + switch enc = strings.TrimSpace(enc); enc { + case "gzip": + reader, err := gzip.NewReader(r.Body) + if err != nil { + c.ErrHandler(w, r, err) + return + } + r.Body = io.NopCloser(reader) + case "identity", "": + // nothing to do + default: + c.ErrHandler(w, r, fmt.Errorf("%s content encoding not supported", enc)) + } + } + + next(w, r) +} diff --git a/oryx/httpx/request.go b/oryx/httpx/request.go new file mode 100644 index 0000000..b18d1a2 --- /dev/null +++ b/oryx/httpx/request.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// NewRequestJSON returns a new JSON *http.Request. +func NewRequestJSON(method, url string, data interface{}) (*http.Request, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(data); err != nil { + return nil, errors.WithStack(err) + } + req, err := http.NewRequest(method, url, &b) + if err != nil { + return nil, errors.WithStack(err) + } + req.Header.Set("Content-Type", "application/json") + return req, nil +} + +// NewRequestForm returns a new POST Form *http.Request. +func NewRequestForm(method, url string, data url.Values) (*http.Request, error) { + req, err := http.NewRequest(method, url, strings.NewReader(data.Encode())) + if err != nil { + return nil, errors.WithStack(err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req, nil +} + +// MustNewRequest returns a new *http.Request or fatals. +func MustNewRequest(method, url string, body io.Reader, contentType string) *http.Request { + req, err := http.NewRequest(method, url, body) + if err != nil { + panic(err) + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + return req +} diff --git a/oryx/httpx/resilient_client.go b/oryx/httpx/resilient_client.go new file mode 100644 index 0000000..7d87323 --- /dev/null +++ b/oryx/httpx/resilient_client.go @@ -0,0 +1,140 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-retryablehttp" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/logrusx" +) + +type resilientOptions struct { + c *http.Client + l interface{} + retryWaitMin time.Duration + retryWaitMax time.Duration + retryMax int + noInternalIPs bool + internalIPExceptions []string +} + +func newResilientOptions() *resilientOptions { + return &resilientOptions{ + c: &http.Client{Timeout: time.Minute}, + retryWaitMin: 1 * time.Second, + retryWaitMax: 30 * time.Second, + retryMax: 4, + l: log.New(io.Discard, "", log.LstdFlags), + } +} + +// ResilientOptions is a set of options for the ResilientClient. +type ResilientOptions func(o *resilientOptions) + +// ResilientClientWithMaxRetry sets the maximum number of retries. +func ResilientClientWithMaxRetry(retryMax int) ResilientOptions { + return func(o *resilientOptions) { o.retryMax = retryMax } +} + +// ResilientClientWithMinRetryWait sets the minimum wait time between retries. +func ResilientClientWithMinRetryWait(retryWaitMin time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.retryWaitMin = retryWaitMin } +} + +// ResilientClientWithMaxRetryWait sets the maximum wait time for a retry. +func ResilientClientWithMaxRetryWait(retryWaitMax time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.retryWaitMax = retryWaitMax } +} + +// ResilientClientWithConnectionTimeout sets the connection timeout for the client. +func ResilientClientWithConnectionTimeout(connTimeout time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.c.Timeout = connTimeout } +} + +// ResilientClientWithLogger sets the logger to be used by the client. +func ResilientClientWithLogger(l *logrusx.Logger) ResilientOptions { + return func(o *resilientOptions) { o.l = l } +} + +// ResilientClientDisallowInternalIPs disallows internal IPs from being used. +func ResilientClientDisallowInternalIPs() ResilientOptions { + return func(o *resilientOptions) { o.noInternalIPs = true } +} + +// ResilientClientAllowInternalIPRequestsTo allows requests to the glob-matching URLs even +// if they are internal IPs. +func ResilientClientAllowInternalIPRequestsTo(urlGlobs ...string) ResilientOptions { + return func(o *resilientOptions) { o.internalIPExceptions = urlGlobs } +} + +// ResilientClientNoFollowRedirects configures the client to not follow redirects. +func ResilientClientNoFollowRedirects() ResilientOptions { + return func(o *resilientOptions) { + o.c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // don't follow redirects + } + } +} + +// NewResilientClient creates a new ResilientClient. +func NewResilientClient(opts ...ResilientOptions) *retryablehttp.Client { + o := newResilientOptions() + for _, f := range opts { + f(o) + } + + if o.noInternalIPs { + o.c.Transport = &noInternalIPRoundTripper{ + onWhitelist: allowInternalAllowIPv6, + notOnWhitelist: prohibitInternalAllowIPv6, + internalIPExceptions: o.internalIPExceptions, + } + } else { + o.c.Transport = allowInternalAllowIPv6 + } + + cl := retryablehttp.NewClient() + cl.HTTPClient = o.c + cl.Logger = o.l + cl.RetryWaitMin = o.retryWaitMin + cl.RetryWaitMax = o.retryWaitMax + cl.RetryMax = o.retryMax + cl.CheckRetry = retryablehttp.DefaultRetryPolicy + cl.Backoff = retryablehttp.DefaultBackoff + return cl +} + +// SetOAuth2 modifies the given client to enable OAuth2 authentication. Requests +// with the client should always use the returned context. +// +// client := http.NewResilientClient(opts...) +// ctx, client = httpx.SetOAuth2(ctx, client, oauth2Config, oauth2Token) +// req, err := retryablehttp.NewRequestWithContext(ctx, ...) +// if err != nil { /* ... */ } +// res, err := client.Do(req) +func SetOAuth2(ctx context.Context, cl *retryablehttp.Client, c OAuth2Config, t *oauth2.Token) (context.Context, *retryablehttp.Client) { + ctx = context.WithValue(ctx, oauth2.HTTPClient, cl.HTTPClient) + cl.HTTPClient = c.Client(ctx, t) + return ctx, cl +} + +type ( + OAuth2Config interface { + Client(context.Context, *oauth2.Token) *http.Client + } + ClientProvider interface { + HTTPClient(ctx context.Context, opts ...ResilientOptions) *retryablehttp.Client + } + WriterProvider interface { + Writer() herodot.Writer + } +) diff --git a/oryx/httpx/ssrf.go b/oryx/httpx/ssrf.go new file mode 100644 index 0000000..cfa52b9 --- /dev/null +++ b/oryx/httpx/ssrf.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "net" + "net/http" + "net/http/httptrace" + "time" + + "github.com/gobwas/glob" + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/ory/x/ipx" +) + +var _ http.RoundTripper = (*noInternalIPRoundTripper)(nil) + +type noInternalIPRoundTripper struct { + onWhitelist, notOnWhitelist http.RoundTripper + internalIPExceptions []string +} + +// RoundTrip implements http.RoundTripper. +func (n noInternalIPRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + incoming := IncomingRequestURL(request) + incoming.RawQuery = "" + incoming.RawFragment = "" + for _, exception := range n.internalIPExceptions { + compiled, err := glob.Compile(exception, '.', '/') + if err != nil { + return nil, err + } + if compiled.Match(incoming.String()) { + return n.onWhitelist.RoundTrip(request) + } + } + + return n.notOnWhitelist.RoundTrip(request) +} + +var ( + prohibitInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.ProhibitInternalDialFunc)) + allowInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.AllowInternalDialFunc)) +) + +func ssrfTransport(dial func(ctx context.Context, network, address string) (net.Conn, error)) *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dial, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} + +// OTELTraceTransport wraps the given http.Transport with OpenTelemetry instrumentation. +func OTELTraceTransport(t *http.Transport) http.RoundTripper { + return otelhttp.NewTransport(t, otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace { + return otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutHeaders(), otelhttptrace.WithoutSubSpans()) + })) +} diff --git a/oryx/httpx/transports.go b/oryx/httpx/transports.go new file mode 100644 index 0000000..bb28134 --- /dev/null +++ b/oryx/httpx/transports.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import "net/http" + +// WrapTransportWithHeader wraps a http.Transport to always use the values from the given header. +func WrapTransportWithHeader(parent http.RoundTripper, h http.Header) *TransportWithHeader { + return &TransportWithHeader{ + RoundTripper: parent, + h: h, + } +} + +// NewTransportWithHeader returns a new http.Transport that always uses the values from the given header. +func NewTransportWithHeader(h http.Header) *TransportWithHeader { + return &TransportWithHeader{ + RoundTripper: http.DefaultTransport, + h: h, + } +} + +// TransportWithHeader is an http.RoundTripper that always uses the values from the given header. +type TransportWithHeader struct { + http.RoundTripper + h http.Header +} + +// RoundTrip implements http.RoundTripper. +func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) { + for k := range ct.h { + req.Header.Set(k, ct.h.Get(k)) + } + return ct.RoundTripper.RoundTrip(req) +} + +// NewTransportWithHost returns a new http.Transport that always uses the given host. +func NewTransportWithHost(host string) *TransportWithHost { + return &TransportWithHost{ + RoundTripper: http.DefaultTransport, + host: host, + } +} + +// WrapRoundTripperWithHost wraps a http.RoundTripper that always uses the given host. +func WrapRoundTripperWithHost(parent http.RoundTripper, host string) *TransportWithHost { + return &TransportWithHost{ + RoundTripper: parent, + host: host, + } +} + +// TransportWithHost is an http.RoundTripper that always uses the given host. +type TransportWithHost struct { + http.RoundTripper + host string +} + +// RoundTrip implements http.RoundTripper. +func (ct *TransportWithHost) RoundTrip(req *http.Request) (*http.Response, error) { + req.Host = ct.host + return ct.RoundTripper.RoundTrip(req) +} + +type TransportFunc func(*http.Request) (*http.Response, error) + +func (f TransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } diff --git a/oryx/httpx/url.go b/oryx/httpx/url.go new file mode 100644 index 0000000..ff206e9 --- /dev/null +++ b/oryx/httpx/url.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "cmp" + "net/http" + "net/url" +) + +// IncomingRequestURL returns the URL of the incoming HTTP request by looking at the host, TLS, and X-Forwarded-* headers. +func IncomingRequestURL(r *http.Request) *url.URL { + source := *r.URL + source.Host = cmp.Or(source.Host, r.Header.Get("X-Forwarded-Host"), r.Host) + + if proto := r.Header.Get("X-Forwarded-Proto"); len(proto) > 0 { + source.Scheme = proto + } + + if source.Scheme == "" { + source.Scheme = "https" + if r.TLS == nil { + source.Scheme = "http" + } + } + + return &source +} diff --git a/oryx/httpx/wait_for.go b/oryx/httpx/wait_for.go new file mode 100644 index 0000000..c85f5d2 --- /dev/null +++ b/oryx/httpx/wait_for.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "io" + "net/http" + "time" + + "github.com/avast/retry-go/v4" + "github.com/pkg/errors" + "github.com/tidwall/gjson" +) + +// WaitForEndpoint waits for the endpoint to be available. +func WaitForEndpoint(ctx context.Context, endpoint string, opts ...retry.Option) error { + return WaitForEndpointWithClient(ctx, http.DefaultClient, endpoint, opts...) +} + +// WaitForEndpointWithClient waits for the endpoint to be available while using the given http.Client. +func WaitForEndpointWithClient(ctx context.Context, client *http.Client, endpoint string, opts ...retry.Option) error { + return retry.Do(func() error { + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + return err + } + + res, err := client.Do(req) + if err != nil { + return err + } + defer func() { + _ = res.Body.Close() + }() + + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if gjson.GetBytes(body, "status").String() != "ok" { + return errors.Errorf("status is not yet ok: %s", body) + } + + return nil + }, + append([]retry.Option{ + retry.DelayType(retry.BackOffDelay), + retry.Delay(time.Second), + retry.MaxDelay(time.Second * 2), + retry.Attempts(20), + }, opts...)...) +} diff --git a/oryx/ioutilx/pkger.go b/oryx/ioutilx/pkger.go new file mode 100644 index 0000000..ebd2012 --- /dev/null +++ b/oryx/ioutilx/pkger.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ioutilx + +import ( + "io" +) + +// MustReadAll reads a reader or panics. +func MustReadAll(r io.Reader) []byte { + all, err := io.ReadAll(r) + if err != nil { + panic(err) + } + return all +} diff --git a/oryx/ipx/cidr.go b/oryx/ipx/cidr.go new file mode 100644 index 0000000..341e792 --- /dev/null +++ b/oryx/ipx/cidr.go @@ -0,0 +1,23 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "iter" + "net/netip" +) + +func Hosts(prefix netip.Prefix) iter.Seq[netip.Addr] { + prefix = prefix.Masked() + return func(yield func(netip.Addr) bool) { + if !prefix.IsValid() { + return + } + for addr := prefix.Addr().Next(); prefix.Contains(addr); addr = addr.Next() { + if !yield(addr) { + return + } + } + } +} diff --git a/oryx/ipx/ip_validator.go b/oryx/ipx/ip_validator.go new file mode 100644 index 0000000..d7d88b7 --- /dev/null +++ b/oryx/ipx/ip_validator.go @@ -0,0 +1,128 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "context" + stderrors "errors" + "net" + "net/netip" + "net/url" + "time" + + "code.dny.dev/ssrf" + "golang.org/x/sync/errgroup" + + "github.com/pkg/errors" +) + +// IsAssociatedIPAllowedWhenSet is a wrapper for IsAssociatedIPAllowed which returns valid +// when ipOrHostnameOrURL is empty. +func IsAssociatedIPAllowedWhenSet(ctx context.Context, ipOrHostnameOrURL string) error { + if ipOrHostnameOrURL == "" { + return nil + } + return IsAssociatedIPAllowed(ctx, ipOrHostnameOrURL) +} + +// AreAllAssociatedIPsAllowed fails if one of the pairs is failing. +func AreAllAssociatedIPsAllowed(ctx context.Context, pairs map[string]string) error { + g, ctx := errgroup.WithContext(ctx) + for key, ipOrHostnameOrURL := range pairs { + g.Go(func() error { + return errors.Wrapf(IsAssociatedIPAllowed(ctx, ipOrHostnameOrURL), "key %s validation is failing", key) + }) + } + return g.Wait() +} + +// IsAssociatedIPAllowed returns nil for a domain (with NS lookup), IP, or IPv6 address if it +// does not resolve to a private IP subnet. This is a first level of defense against +// SSRF attacks by disallowing any domain or IP to resolve to a private network range. +// +// Please keep in mind that validations for domains is valid only when looking up. +// A malicious actor could easily update the DSN record post validation to point +// to an internal IP +func IsAssociatedIPAllowed(ctx context.Context, ipOrHostnameOrURL string) error { + ipOrHostname := ipOrHostnameOrURL + if parsed, err := url.ParseRequestURI(ipOrHostnameOrURL); err == nil { + ipOrHostname = parsed.Hostname() + } + + if ip, err := netip.ParseAddr(ipOrHostname); err == nil { + if !allowed(ip) { + return errors.Errorf("ip %s is not a permitted destination", ip) + } + return nil + } + + if addr, err := netip.ParseAddrPort(ipOrHostnameOrURL); err == nil { + if !allowed(addr.Addr()) { + return errors.Errorf("ip %s is not a permitted destination", addr.Addr()) + } + return nil + } + + ctx, cancel := context.WithTimeoutCause(ctx, 2*time.Second, errors.New("DNS lookup timed out")) + defer cancel() + ips, err := resolver.LookupNetIP(ctx, "ip", ipOrHostname) + if err != nil { + if dnsErr, ok := stderrors.AsType[*net.DNSError](err); ok { + // Copy the `*net.DNSError` before masking `Server` to avoid a data + // race: the DNS resolver uses `singleflight` to deduplicate + // concurrent lookups, so multiple goroutines may receive the same + // `*net.DNSError` pointer. Mutating it in place races with concurrent + // readers (e.g. the `otelhttp` `dnsDone` trace hook). + maskedDNS := *dnsErr + maskedDNS.Server = "" // Mask our DNS server's IP address. + return errors.Wrapf(&maskedDNS, "failed to resolve %s", ipOrHostnameOrURL) + } + } + + for _, ip := range ips { + if !allowed(ip) { + return errors.Wrapf(&net.DNSError{ + Err: "no such host", + Name: ipOrHostname, + Server: "", + IsTimeout: false, + IsTemporary: false, + IsNotFound: true, + }, "failed to resolve %s", ipOrHostnameOrURL) + } + } + + return nil +} + +var resolver = &net.Resolver{PreferGo: true} + +func allowed(ip netip.Addr) bool { + if !ip.IsGlobalUnicast() { + return false + } + + if ip.Is4() { + for _, net := range ssrf.IPv4DeniedPrefixes { + if net.Contains(ip) { + return false + } + } + } else { // IPv6 + // ip.IsGlobalUnicast returns true for IPv6 addresses which fall outside + // of the current IANA-allocated 2000::/3 global unicast space. Hence, + // we need to check for ourselves if the IPv6 address is within the + // currently allocated block. + if !ssrf.IPv6GlobalUnicast.Contains(ip) { + return false + } + for _, net := range ssrf.IPv6DeniedPrefixes { + if net.Contains(ip) { + return false + } + } + } + + return true +} diff --git a/oryx/ipx/ssrf.go b/oryx/ipx/ssrf.go new file mode 100644 index 0000000..38ea4c8 --- /dev/null +++ b/oryx/ipx/ssrf.go @@ -0,0 +1,100 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "context" + "errors" + "net" + "net/netip" + "time" + + "code.dny.dev/ssrf" +) + +var ( + ProhibitInternalDialFunc = ssrfDialFunc( + ssrf.WithAnyPort(), + ssrf.WithNetworks("tcp4", "tcp6"), + ) + + AllowInternalDialFunc = ssrfDialFunc( + ssrf.WithAnyPort(), + ssrf.WithNetworks("tcp4", "tcp6"), + ssrf.WithAllowedV4Prefixes( + netip.MustParsePrefix("10.0.0.0/8"), // Private-Use (RFC 1918) + netip.MustParsePrefix("127.0.0.0/8"), // Loopback (RFC 1122, Section 3.2.1.3)) + netip.MustParsePrefix("169.254.0.0/16"), // Link Local (RFC 3927) + netip.MustParsePrefix("172.16.0.0/12"), // Private-Use (RFC 1918) + netip.MustParsePrefix("192.168.0.0/16"), // Private-Use (RFC 1918) + ), + ssrf.WithAllowedV6Prefixes( + netip.MustParsePrefix("::1/128"), // Loopback (RFC 4193) + netip.MustParsePrefix("fc00::/7"), // Unique Local (RFC 4193) + ), + ) +) + +func ssrfDialFunc(opt ...ssrf.Option) func(ctx context.Context, network, address string) (net.Conn, error) { + dialer := net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + dialer.Control = ssrf.New(opt...).Safe + return func(ctx context.Context, network, address string) (net.Conn, error) { + c, err := dialer.DialContext(ctx, network, address) + if err == nil { + return c, nil + } + + if dnsErr, ok := errors.AsType[*net.DNSError](err); ok { + // Copy the `*net.DNSError` before masking `Server` to avoid a data + // race: the DNS resolver uses `singleflight` to deduplicate + // concurrent lookups, so multiple goroutines may receive the same + // `*net.DNSError` pointer. Mutating it in place races with concurrent + // readers (e.g. the `otelhttp` `dnsDone` trace hook). + maskedDNS := *dnsErr + maskedDNS.Server = "" // Mask our DNS server's IP address. + // Also copy the outer `*net.OpError` (if present) to preserve the + // full error chain that callers depend on, replacing its `Err` field + // with the masked copy. + // Surprisingly Go does not have a good way of manipulating error chains. + if opErr, ok := errors.AsType[*net.OpError](err); ok { + maskedOp := *opErr + maskedOp.Err = &maskedDNS + return nil, &maskedOp + } + return nil, &maskedDNS + } + + if !errors.Is(err, ssrf.ErrProhibitedIP) { + return nil, err + } + + host, _, _ := net.SplitHostPort(address) + _, addrErr := netip.ParseAddrPort(address) + if addrErr != nil { + // We were given a DNS name: the error we return must look like a DNS error. + return nil, &net.OpError{ + Op: "dial", + Net: network, + Addr: nil, + Err: &net.DNSError{ + Err: "no such host", + Name: host, + Server: "", + IsTimeout: false, + IsTemporary: false, + IsNotFound: true, + }, + } + } + return nil, &net.OpError{ + Op: "dial", + Net: network, + Addr: nil, + Err: errors.New("no route to host"), + } + } +} diff --git a/oryx/josex/encoding.go b/oryx/josex/encoding.go new file mode 100644 index 0000000..57ff0e4 --- /dev/null +++ b/oryx/josex/encoding.go @@ -0,0 +1,55 @@ +/*- + * Copyright 2019 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josex + +import "io" + +// Base64Reader wraps an input stream consisting of either standard or url-safe +// base64 data, and maps it to a raw (unpadded) standard encoding. This can be used +// to read any base64-encoded data as input, whether padded, unpadded, standard or +// url-safe. +type Base64Reader struct { + In io.Reader +} + +func (r Base64Reader) Read(p []byte) (n int, err error) { + n, err = r.In.Read(p) + if err != nil { + return + } + + for i := range n { + switch p[i] { + // Map - to + + case 0x2D: + p[i] = 0x2B + // Map _ to / + case 0x5F: + p[i] = 0x2F + // Strip = + case 0x3D: + n = i + default: + } + } + + if n == 0 { + err = io.EOF + } + + return +} diff --git a/oryx/josex/generate.go b/oryx/josex/generate.go new file mode 100644 index 0000000..055a4c5 --- /dev/null +++ b/oryx/josex/generate.go @@ -0,0 +1,121 @@ +/*- + * Copyright 2019 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josex + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + + "github.com/go-jose/go-jose/v3" +) + +// NewSigningKey generates a keypair for corresponding SignatureAlgorithm. +func NewSigningKey(alg jose.SignatureAlgorithm, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch alg { + case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA: + keylen := map[jose.SignatureAlgorithm]int{ + jose.ES256: 256, + jose.ES384: 384, + jose.ES512: 521, // sic! + jose.EdDSA: 256, + } + if bits != 0 && bits != keylen[alg] { + return nil, nil, errors.New("invalid elliptic curve key size, this algorithm does not support arbitrary size") + } + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, nil, errors.New("invalid key size for RSA key, 2048 or more is required") + } + } + switch alg { + case jose.ES256: + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ES384: + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ES512: + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.EdDSA: + pub, key, err := ed25519.GenerateKey(rand.Reader) + return pub, key, err + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + default: + return nil, nil, fmt.Errorf("unknown algorithm %s for signing key", alg) + } +} + +// NewEncryptionKey generates a keypair for corresponding KeyAlgorithm. +func NewEncryptionKey(alg jose.KeyAlgorithm, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch alg { + case jose.RSA1_5, jose.RSA_OAEP, jose.RSA_OAEP_256: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, nil, errors.New("invalid key size for RSA key, 2048 or more is required") + } + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ECDH_ES, jose.ECDH_ES_A128KW, jose.ECDH_ES_A192KW, jose.ECDH_ES_A256KW: + var crv elliptic.Curve + switch bits { + case 0, 256: + crv = elliptic.P256() + case 384: + crv = elliptic.P384() + case 521: + crv = elliptic.P521() + default: + return nil, nil, errors.New("invalid elliptic curve key size, use one of 256, 384, or 521") + } + key, err := ecdsa.GenerateKey(crv, rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + default: + return nil, nil, fmt.Errorf("unknown algorithm %s for encryption key", alg) + } +} diff --git a/oryx/josex/public.go b/oryx/josex/public.go new file mode 100644 index 0000000..667a2cb --- /dev/null +++ b/oryx/josex/public.go @@ -0,0 +1,29 @@ +package josex + +import ( + "crypto" + + "github.com/go-jose/go-jose/v3" +) + +// ToPublicKey returns the public key of the given private key. +func ToPublicKey(k *jose.JSONWebKey) jose.JSONWebKey { + if key := k.Public(); key.Key != nil { + return key + } + + // HSM workaround - jose does not understand crypto.Signer / HSM so we need to manually + // extract the public key. + switch key := k.Key.(type) { + case crypto.Signer: + newKey := *k + newKey.Key = key.Public() + return newKey + case jose.OpaqueSigner: + newKey := *k + newKey.Key = key.Public().Key + return newKey + } + + return jose.JSONWebKey{} +} diff --git a/oryx/josex/utils.go b/oryx/josex/utils.go new file mode 100644 index 0000000..036f36a --- /dev/null +++ b/oryx/josex/utils.go @@ -0,0 +1,103 @@ +/*- + * Copyright 2019 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josex + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + + "github.com/go-jose/go-jose/v3" +) + +// LoadJSONWebKey returns a *jose.JSONWebKey for a given JSON string. +func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) { + var jwk jose.JSONWebKey + err := jwk.UnmarshalJSON(json) + if err != nil { + return nil, err + } + if !jwk.Valid() { + return nil, errors.New("invalid JWK key") + } + if jwk.IsPublic() != pub { + return nil, errors.New("priv/pub JWK key mismatch") + } + return &jwk, nil +} + +// LoadPublicKey loads a public key from PEM/DER/JWK-encoded data. +func LoadPublicKey(data []byte) (interface{}, error) { + input := data + + block, _ := pem.Decode(data) + if block != nil { + input = block.Bytes + } + + // Try to load SubjectPublicKeyInfo + pub, err0 := x509.ParsePKIXPublicKey(input) + if err0 == nil { + return pub, nil + } + + cert, err1 := x509.ParseCertificate(input) + if err1 == nil { + return cert.PublicKey, nil + } + + jwk, err2 := LoadJSONWebKey(data, true) + if err2 == nil { + return jwk, nil + } + + return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s' and '%s'", err0, err1, err2) +} + +// LoadPrivateKey loads a private key from PEM/DER/JWK-encoded data. +func LoadPrivateKey(data []byte) (interface{}, error) { + input := data + + block, _ := pem.Decode(data) + if block != nil { + input = block.Bytes + } + + var priv interface{} + priv, err0 := x509.ParsePKCS1PrivateKey(input) + if err0 == nil { + return priv, nil + } + + priv, err1 := x509.ParsePKCS8PrivateKey(input) + if err1 == nil { + return priv, nil + } + + priv, err2 := x509.ParseECPrivateKey(input) + if err2 == nil { + return priv, nil + } + + jwk, err3 := LoadJSONWebKey(input, false) + if err3 == nil { + return jwk, nil + } + + return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s', '%s' and '%s'", err0, err1, err2, err3) +} diff --git a/oryx/jsonnetsecure/cmd.go b/oryx/jsonnetsecure/cmd.go new file mode 100644 index 0000000..6b9cb63 --- /dev/null +++ b/oryx/jsonnetsecure/cmd.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "bufio" + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/ory/x/landlockx" +) + +const ( + GiB uint64 = 1024 * 1024 * 1024 + // Generous limit on virtual memory including the peak memory allocated by the Go runtime, the Jsonnet VM, + // and the Jsonnet script. + // This number was acquired by running: + // Found by trial and error with: + // `ulimit -Sv 1048576 && echo '{"Snippet": "{user_id: std.repeat(\'a\', 1000)}"}' | kratos jsonnet -0` + // NOTE: Ideally we'd like to limit RSS but that is not possible on Linux with `ulimit/setrlimit(2)` - only with cgroups. + virtualMemoryLimitBytes = 2 * GiB +) + +func NewJsonnetCmd() *cobra.Command { + var null bool + cmd := &cobra.Command{ + Use: "jsonnet", + Short: "Run Jsonnet as a CLI command", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Lock the worker down to its already-open stdio: the + // only I/O it does is read snippets/parameters from stdin + // and write JSON to stdout/stderr. Denying all path-based + // filesystem access prevents a malicious snippet from + // touching the operator's file system even if the + // jsonnet VM ever exposed a path-import primitive. + if err := landlockx.ApplyEmpty(nil); err != nil { + return errors.Wrap(err, "failed to apply empty landlock sandbox") + } + + // This could fail because current limits are lower than what we tried to set, + // so we still continue in this case. + SetVirtualMemoryLimit(virtualMemoryLimitBytes) + + if null { + return scan(cmd.OutOrStdout(), cmd.InOrStdin()) + } + + input, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return errors.Wrap(err, "failed to read from stdin") + } + + json, err := eval(input) + if err != nil { + return errors.Wrap(err, "failed to evaluate jsonnet") + } + + if _, err := io.WriteString(cmd.OutOrStdout(), json); err != nil { + return errors.Wrap(err, "failed to write json output") + } + return nil + }, + } + cmd.Flags().BoolVarP(&null, "null", "0", false, + `Read multiple snippets and parameters from stdin separated by null bytes. +Output will be in the same order as inputs, separated by null bytes. +Evaluation errors will also be reported to stdout, separated by null bytes. +Non-recoverable errors are written to stderr and the program will terminate with a non-zero exit code.`) + + return cmd +} + +func scan(w io.Writer, r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Split(splitNull) + for scanner.Scan() { + json, err := eval(scanner.Bytes()) + if err != nil { + json = fmt.Sprintf("ERROR: %s", err) + } + if _, err := fmt.Fprintf(w, "%s%c", json, 0); err != nil { + return errors.Wrap(err, "failed to write json output") + } + } + return errors.Wrap(scanner.Err(), "failed to read from stdin") +} + +func eval(input []byte) (json string, err error) { + var params processParameters + if err := params.Decode(input); err != nil { + return "", err + } + + vm := MakeInProcessVM() + + for _, it := range params.ExtCodes { + vm.ExtCode(it.Key, it.Value) + } + for _, it := range params.ExtVars { + vm.ExtVar(it.Key, it.Value) + } + for _, it := range params.TLACodes { + vm.TLACode(it.Key, it.Value) + } + for _, it := range params.TLAVars { + vm.TLAVar(it.Key, it.Value) + } + + return vm.EvaluateAnonymousSnippet(params.Filename, params.Snippet) +} diff --git a/oryx/jsonnetsecure/cmd/root.go b/oryx/jsonnetsecure/cmd/root.go new file mode 100644 index 0000000..d6e017d --- /dev/null +++ b/oryx/jsonnetsecure/cmd/root.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/ory/x/jsonnetsecure" +) + +func main() { + if err := jsonnetsecure.NewJsonnetCmd().ExecuteContext(context.Background()); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/oryx/jsonnetsecure/jsonnet.go b/oryx/jsonnetsecure/jsonnet.go new file mode 100644 index 0000000..8666cfd --- /dev/null +++ b/oryx/jsonnetsecure/jsonnet.go @@ -0,0 +1,153 @@ +package jsonnetsecure + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path" + "runtime" + "testing" + + "github.com/google/go-jsonnet" + "github.com/pkg/errors" +) + +type ( + VM interface { + EvaluateAnonymousSnippet(filename string, snippet string) (json string, formattedErr error) + ExtCode(key string, val string) + ExtVar(key string, val string) + TLACode(key string, val string) + TLAVar(key string, val string) + } + + kv struct { + Key, Value string + } + processParameters struct { + Filename, Snippet string + TLACodes, TLAVars, ExtCodes, ExtVars []kv + } + + vmOptions struct { + jsonnetBinaryPath string + args []string + ctx context.Context + pool *pool + } + + Option func(o *vmOptions) +) + +func (pp *processParameters) EncodeTo(w io.Writer) error { + return json.NewEncoder(w).Encode(pp) +} + +func (pp *processParameters) Decode(d []byte) error { + return json.Unmarshal(d, pp) +} + +func newVMOptions() *vmOptions { + jsonnetBinaryPath, _ := os.Executable() + return &vmOptions{ + jsonnetBinaryPath: jsonnetBinaryPath, + ctx: context.Background(), + } +} + +func WithContext(ctx context.Context) Option { + return func(o *vmOptions) { + o.ctx = ctx + } +} + +func WithJsonnetBinary(jsonnetBinaryPath string) Option { + return func(o *vmOptions) { + o.jsonnetBinaryPath = jsonnetBinaryPath + } +} + +func WithProcessArgs(args ...string) Option { + return func(o *vmOptions) { + o.args = args + } +} + +// ErrNoProcessPool is returned by MakeSecureVM when called without a process +// pool. It is a distinct error because the alternative — quietly returning an +// in-process VM — would strip the isolation callers of this package rely on. +var ErrNoProcessPool = errors.New("jsonnetsecure: a process pool is required; use MakeInProcessVM to evaluate in this process without isolation") + +// MakeSecureVM returns a VM that evaluates snippets in a worker process taken +// from p, so that a snippet which exhausts memory, spins on the CPU, or crashes +// takes down only that worker. +// +// p is a required argument rather than an option because a VM without a pool +// offers no isolation at all. Passing a nil pool returns ErrNoProcessPool. +func MakeSecureVM(p Pool, opts ...Option) (VM, error) { + // A nil *pool inside a non-nil Pool interface is not reachable from outside + // this package (Pool has an unexported method), but check the concrete + // value anyway so a future in-package mistake cannot slip through. + concrete, _ := p.(*pool) + if p == nil || concrete == nil { + return nil, errors.WithStack(ErrNoProcessPool) + } + + options := newVMOptions() + for _, o := range opts { + o(options) + } + options.pool = concrete + + return NewProcessPoolVM(options), nil +} + +// MakeInProcessVM returns a Jsonnet VM that evaluates in the calling process +// with imports disabled. It provides no isolation: a malicious or buggy snippet +// can exhaust this process's memory and CPU, so it is only safe for trusted +// input. The two legitimate uses are the jsonnet subcommand, which is itself +// the isolation boundary, and offline CLI linting. Everything that evaluates +// tenant-supplied Jsonnet must use MakeSecureVM. +func MakeInProcessVM() *jsonnet.VM { + vm := jsonnet.MakeVM() + vm.Importer(new(ErrorImporter)) + return vm +} + +// ErrorImporter errors when calling "import". +type ErrorImporter struct{} + +// Import fetches data from a map entry. +// All paths are treated as absolute keys. +func (importer *ErrorImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) { + return jsonnet.Contents{}, "", fmt.Errorf("import not available %v", importedPath) +} + +func JsonnetTestBinary(t testing.TB) string { + t.Helper() + + // We can force the usage of a given jsonnet executable. + // Useful to test different versions, or run the tests under wine. + if s := os.Getenv("ORY_JSONNET_PATH"); s != "" { + return s + } + + var stderr bytes.Buffer + // Using `t.TempDir()` results in permissions errors on Windows, sometimes. + outPath := path.Join(os.TempDir(), "jsonnet") + if runtime.GOOS == "windows" { + outPath = outPath + ".exe" + } + cmd := exec.Command("go", "build", "-o", outPath, "github.com/ory/x/jsonnetsecure/cmd") + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil || stderr.Len() != 0 { + t.Fatalf("building the Go binary returned error: %v\n%s", err, stderr.String()) + } + + return outPath +} diff --git a/oryx/jsonnetsecure/jsonnet_pool.go b/oryx/jsonnetsecure/jsonnet_pool.go new file mode 100644 index 0000000..2732d1e --- /dev/null +++ b/oryx/jsonnetsecure/jsonnet_pool.go @@ -0,0 +1,313 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +// Known limitations/edge cases: +// - The child process exiting early (e.g. crashing) or getting killed (e.g. reaching some OS limit) +// is not detected and no error will be returned in this case from `eval()`. +// - Misbehaving jsonnet scripts in the middle of a batch being passed to the child process for evaluation may result in +// no error (as mentioned above), and other valid scripts in this batch may result +// in an error (because the output from the child process is truncated). +// +// Possible remediations: +// - Do not pass a batch of scripts to a worker, only pass one script at a time (to isolate misbehaving scripts) +// - Validate that the output is valid JSON (to detect truncated output) +// - Detect the child process exiting (to return an error) + +import ( + "bufio" + "context" + "encoding/json" + "io" + "math" + "os/exec" + "strings" + "time" + + "github.com/jackc/puddle/v2" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" +) + +const ( + KiB = 1024 + jsonnetOutputLimit = 256 * KiB + jsonnetErrLimit = 1 * KiB +) + +type ( + processPoolVM struct { + path string + args []string + ctx context.Context + params processParameters + pool *pool + } + Pool interface { + Close() + // Stat returns a snapshot of the worker pool's statistics. + Stat() *puddle.Stat + private() + } + pool struct { + puddle *puddle.Pool[worker] + } + worker struct { + cmd *exec.Cmd + stdin chan<- []byte + stdout <-chan string + stderr <-chan string + } + contextKeyType string +) + +var ( + ErrProcessPoolClosed = errors.New("jsonnetsecure: process pool closed") + + _ VM = (*processPoolVM)(nil) + _ Pool = (*pool)(nil) + + contextValuePath contextKeyType = "argc" + contextValueArgs contextKeyType = "argv" +) + +func NewProcessPool(size int) Pool { + size = max(5, min(size, math.MaxInt32)) + pud, err := puddle.NewPool(&puddle.Config[worker]{ + MaxSize: int32(size), //nolint:gosec // disable G115 // because of the previous min/max, 5 <= size <= math.MaxInt32 + Constructor: newWorker, + Destructor: worker.destroy, + }) + if err != nil { + panic(err) // this should never happen, see implementation of puddle.NewPool + } + for range size { + // warm pool + go pud.CreateResource(context.Background()) + } + go func() { + for { + time.Sleep(10 * time.Second) + for _, proc := range pud.AcquireAllIdle() { + if proc.Value().cmd.ProcessState != nil { + proc.Destroy() + } else { + proc.Release() + } + } + } + }() + return &pool{pud} +} + +func (*pool) private() {} + +func (p *pool) Close() { + p.puddle.Close() +} + +func (p *pool) Stat() *puddle.Stat { + return p.puddle.Stat() +} + +func newWorker(ctx context.Context) (_ worker, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jsonnetsecure.newWorker") + defer otelx.End(span, &err) + + path, _ := ctx.Value(contextValuePath).(string) + if path == "" { + return worker{}, errors.New("newWorker: missing binary path in context") + } + args, _ := ctx.Value(contextValueArgs).([]string) + cmd := exec.Command(path, append(args, "-0")...) + cmd.Env = []string{"GOMAXPROCS=1"} + cmd.WaitDelay = 100 * time.Millisecond + + span.SetAttributes(semconv.ProcessCommand(cmd.Path), semconv.ProcessCommandArgs(cmd.Args...)) + + stdin, err := cmd.StdinPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stdin pipe") + } + + in := make(chan []byte, 1) + go func(c <-chan []byte) { + for input := range c { + if _, err := stdin.Write(append(input, 0)); err != nil { + stdin.Close() + return + } + } + }(in) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stdout pipe") + } + stderr, err := cmd.StderrPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stderr pipe") + } + + if err := cmd.Start(); err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to start process") + } + + span.SetAttributes(semconv.ProcessPID(cmd.Process.Pid)) + + scan := func(c chan<- string, r io.Reader, maxTokenSize int) { + defer close(c) + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 0, 64*KiB), maxTokenSize) + + scanner.Split(splitNull) + for scanner.Scan() { + c <- scanner.Text() + } + if err := scanner.Err(); err != nil { + c <- "ERROR: scan: " + err.Error() + } + } + out := make(chan string, 1) + go scan(out, stdout, jsonnetOutputLimit) + errs := make(chan string, 1) + go scan(errs, stderr, jsonnetErrLimit) + + w := worker{ + cmd: cmd, + stdin: in, + stdout: out, + stderr: errs, + } + _, err = w.eval(ctx, []byte("{}")) // warm up + if err != nil { + w.destroy() + return worker{}, errors.Wrap(err, "newWorker: warm up failed") + } + + return w, nil +} + +func (w worker) destroy() { + close(w.stdin) + w.cmd.Process.Kill() + w.cmd.Wait() +} + +func (w worker) eval(ctx context.Context, processParams []byte) (output string, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jsonnetsecure.worker.eval", trace.WithAttributes( + semconv.ProcessPID(w.cmd.Process.Pid))) + defer otelx.End(span, &err) + + // The worker is exclusively acquired, so the schedstat delta over this + // evaluation is attributable to it. cpu_time_us is time actually spent on + // a CPU; runqueue_wait_us is time runnable but waiting for one. Span wall + // time far exceeding their sum means the process was not even runnable, + // e.g. dequeued by cgroup CPU throttling. + // + // A kernel without CONFIG_SCHED_INFO reports "0 0 0" instead of failing + // the read. The worker has already spent CPU time on its warm-up + // evaluation, so a zero cpuTime means the fields are dead — skip them + // rather than record misleading zeros. + if before, ok := readSchedstat(w.cmd.Process.Pid); ok && before.cpuTime > 0 { + defer func() { + after, ok := readSchedstat(w.cmd.Process.Pid) + if !ok { + return + } + span.SetAttributes( + attribute.Int64("jsonnet.worker.cpu_time_us", (after.cpuTime-before.cpuTime).Microseconds()), + attribute.Int64("jsonnet.worker.runqueue_wait_us", (after.runqueueWait-before.runqueueWait).Microseconds()), + ) + }() + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case w.stdin <- processParams: + break + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case output := <-w.stdout: + return output, nil + case err := <-w.stderr: + return "", errors.New(err) + } +} + +func (vm *processPoolVM) EvaluateAnonymousSnippet(filename string, snippet string) (_ string, err error) { + tracer := trace.SpanFromContext(vm.ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(vm.ctx, "jsonnetsecure.processPoolVM.EvaluateAnonymousSnippet", trace.WithAttributes(attribute.String("filename", filename))) + defer otelx.End(span, &err) + + params := vm.params + params.Filename = filename + params.Snippet = snippet + pp, err := json.Marshal(params) + if err != nil { + return "", errors.Wrap(err, "jsonnetsecure: marshal") + } + + ctx = context.WithValue(ctx, contextValuePath, vm.path) + ctx = context.WithValue(ctx, contextValueArgs, vm.args) + worker, err := vm.pool.puddle.Acquire(ctx) + if err != nil { + return "", errors.Wrap(err, "jsonnetsecure: acquire") + } + + ctx, cancel := context.WithTimeoutCause(ctx, 1*time.Second, errors.Errorf("failed to run jsonnet within 1s: filename=%s", filename)) + defer cancel() + result, err := worker.Value().eval(ctx, pp) + if err != nil { + worker.Destroy() + return "", errors.Wrap(err, "jsonnetsecure: eval") + } else { + worker.Release() + } + + if strings.HasPrefix(result, "ERROR: ") { + return "", errors.New("jsonnetsecure: " + result) + } + + return result, nil +} + +func NewProcessPoolVM(opts *vmOptions) VM { + ctx := opts.ctx + if ctx == nil { + ctx = context.Background() + } + return &processPoolVM{ + path: opts.jsonnetBinaryPath, + args: opts.args, + ctx: ctx, + pool: opts.pool, + } +} + +func (vm *processPoolVM) ExtCode(key string, val string) { + vm.params.ExtCodes = append(vm.params.ExtCodes, kv{key, val}) +} + +func (vm *processPoolVM) ExtVar(key string, val string) { + vm.params.ExtVars = append(vm.params.ExtVars, kv{key, val}) +} + +func (vm *processPoolVM) TLACode(key string, val string) { + vm.params.TLACodes = append(vm.params.TLACodes, kv{key, val}) +} + +func (vm *processPoolVM) TLAVar(key string, val string) { + vm.params.TLAVars = append(vm.params.TLAVars, kv{key, val}) +} diff --git a/oryx/jsonnetsecure/limit_unix.go b/oryx/jsonnetsecure/limit_unix.go new file mode 100644 index 0000000..b02566d --- /dev/null +++ b/oryx/jsonnetsecure/limit_unix.go @@ -0,0 +1,29 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !windows + +package jsonnetsecure + +import ( + "fmt" + "runtime/debug" + "syscall" + + "github.com/pkg/errors" +) + +func SetVirtualMemoryLimit(limitBytes uint64) error { + // Tell the Go runtime about the limit. + debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant. + + lim := syscall.Rlimit{ + Cur: limitBytes, + Max: limitBytes, + } + err := syscall.Setrlimit(syscall.RLIMIT_AS, &lim) + if err != nil { + return errors.WithStack(fmt.Errorf("failed to set virtual memory limit: %v", err)) + } + return nil +} diff --git a/oryx/jsonnetsecure/limit_windows.go b/oryx/jsonnetsecure/limit_windows.go new file mode 100644 index 0000000..3557c7a --- /dev/null +++ b/oryx/jsonnetsecure/limit_windows.go @@ -0,0 +1,16 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows + +package jsonnetsecure + +import "runtime/debug" + +func SetVirtualMemoryLimit(limitBytes uint64) error { + // Tell the Go runtime about the limit. + debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant. + + // TODO No OS limit for now. Apparently there is a Windows-specific equivalent (Job control)? + return nil +} diff --git a/oryx/jsonnetsecure/null.go b/oryx/jsonnetsecure/null.go new file mode 100644 index 0000000..42d6e92 --- /dev/null +++ b/oryx/jsonnetsecure/null.go @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import "bytes" + +func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) { + // Look for a null byte; if found, return the position after it, + // the data before it, and no error. + if i := bytes.IndexByte(data, 0); i >= 0 { + return i + 1, data[0:i], nil + } + + // If we're at EOF, we have a final, non-terminated word. Return it. + if atEOF && len(data) != 0 { + return len(data), data, nil + } + + // Request more data. + return 0, nil, nil +} diff --git a/oryx/jsonnetsecure/provider.go b/oryx/jsonnetsecure/provider.go new file mode 100644 index 0000000..2407109 --- /dev/null +++ b/oryx/jsonnetsecure/provider.go @@ -0,0 +1,60 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "context" + "os" + "runtime" + "testing" +) + +type ( + VMProvider interface { + // JsonnetVM creates a new secure process-isolated Jsonnet VM whose + // execution is bound to the provided context, i.e., + // cancelling the context will terminate the VM process. + JsonnetVM(context.Context) (VM, error) + } + + // TestProvider provides a secure VM by running go build on github.com/ory/x/jsonnetsecure/cmd + TestProvider struct { + jsonnetBinary string + pool Pool + } + + // DefaultProvider provides a secure VM by calling the currently + // running the current binary with the provided subcommand. + DefaultProvider struct { + Subcommand string + Pool Pool + } +) + +func NewTestProvider(t testing.TB) *TestProvider { + pool := NewProcessPool(runtime.GOMAXPROCS(0)) + t.Cleanup(pool.Close) + return &TestProvider{JsonnetTestBinary(t), pool} +} + +func (p *TestProvider) JsonnetVM(ctx context.Context) (VM, error) { + return MakeSecureVM( + p.pool, + WithContext(ctx), + WithJsonnetBinary(p.jsonnetBinary), + ) +} + +func (p *DefaultProvider) JsonnetVM(ctx context.Context) (VM, error) { + self, err := os.Executable() + if err != nil { + return nil, err + } + return MakeSecureVM( + p.Pool, + WithContext(ctx), + WithJsonnetBinary(self), + WithProcessArgs(p.Subcommand), + ) +} diff --git a/oryx/jsonnetsecure/schedstat.go b/oryx/jsonnetsecure/schedstat.go new file mode 100644 index 0000000..9829b7c --- /dev/null +++ b/oryx/jsonnetsecure/schedstat.go @@ -0,0 +1,53 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "bytes" + "os" + "strconv" + "time" +) + +// schedstat holds the scheduler statistics of a process as reported by the +// Linux kernel in /proc//schedstat: the CPU time the process actually +// spent running, and the time it spent runnable but waiting for a CPU (due to +// contention or cgroup CPU throttling). +type schedstat struct { + cpuTime time.Duration + runqueueWait time.Duration +} + +// readSchedstat returns the cumulative scheduler statistics for pid. It +// reports ok=false on platforms without /proc//schedstat (e.g. macOS, +// Windows) and on Linux kernels built without CONFIG_SCHED_INFO. +func readSchedstat(pid int) (schedstat, bool) { + raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/schedstat") + if err != nil { + return schedstat{}, false + } + return parseSchedstat(raw) +} + +// parseSchedstat parses the /proc//schedstat format: three +// space-separated integers (run time in nanoseconds, runqueue wait time in +// nanoseconds, number of timeslices). +func parseSchedstat(raw []byte) (schedstat, bool) { + fields := bytes.Fields(raw) + if len(fields) < 2 { + return schedstat{}, false + } + cpuTime, err := strconv.ParseInt(string(fields[0]), 10, 64) + if err != nil || cpuTime < 0 { + return schedstat{}, false + } + runqueueWait, err := strconv.ParseInt(string(fields[1]), 10, 64) + if err != nil || runqueueWait < 0 { + return schedstat{}, false + } + return schedstat{ + cpuTime: time.Duration(cpuTime), + runqueueWait: time.Duration(runqueueWait), + }, true +} diff --git a/oryx/jsonnetsecure/stub/import.jsonnet b/oryx/jsonnetsecure/stub/import.jsonnet new file mode 100644 index 0000000..02b09a7 --- /dev/null +++ b/oryx/jsonnetsecure/stub/import.jsonnet @@ -0,0 +1 @@ +{ foo: 'bar' } diff --git a/oryx/jsonnetx/format.go b/oryx/jsonnetx/format.go new file mode 100644 index 0000000..767886b --- /dev/null +++ b/oryx/jsonnetx/format.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "fmt" + "os" + + "github.com/bmatcuk/doublestar/v2" + "github.com/google/go-jsonnet/formatter" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewFormatCommand() *cobra.Command { + var verbose, write bool + cmd := &cobra.Command{ + Use: "format path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]", + Long: `Formats JSONNet files using the official JSONNet formatter. + +Use -w or --write to write output back to files instead of stdout. + +` + GlobHelp, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + for _, pattern := range args { + files, err := doublestar.Glob(pattern) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Glob pattern %q is not valid: %s\n", pattern, err) + return cmdx.FailSilently(cmd) + } + + for _, file := range files { + if fi, err := os.Stat(file); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Matching file %q could not be opened: %s\n", file, err) + return cmdx.FailSilently(cmd) + } else if fi.IsDir() { + continue + } + + if verbose { + fmt.Printf("Processing file: %s\n", file) + } + + //#nosec G304 -- false positive + content, err := os.ReadFile(file) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to read file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + + output, err := formatter.Format(file, string(content), formatter.DefaultOptions()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "File %q could not be formatted: %s", file, err) + } + + if write { + err := os.WriteFile(file, []byte(output), 0o644) // #nosec + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to write file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + } else { + fmt.Println(output) + } + } + } + return nil + }, + } + cmd.Flags().BoolVarP(&write, "write", "w", false, "Write formatted output back to file.") + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output.") + return cmd +} diff --git a/oryx/jsonnetx/lint.go b/oryx/jsonnetx/lint.go new file mode 100644 index 0000000..bbba28a --- /dev/null +++ b/oryx/jsonnetx/lint.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "fmt" + "os" + + "github.com/bmatcuk/doublestar/v2" + "github.com/google/go-jsonnet" + "github.com/google/go-jsonnet/linter" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewLintCommand() *cobra.Command { + var verbose bool + cmd := &cobra.Command{ + Use: "lint path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]", + Long: `Lints JSONNet files using the official JSONNet linter and exits with a status code of 1 when issues are detected. + +` + GlobHelp, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + for _, pattern := range args { + files, err := doublestar.Glob(pattern) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Glob pattern %q is not valid: %s\n", pattern, err) + return cmdx.FailSilently(cmd) + } + + for _, file := range files { + if fi, err := os.Stat(file); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Matching file %q could not be opened: %s\n", file, err) + return cmdx.FailSilently(cmd) + } else if fi.IsDir() { + continue + } + + if verbose { + fmt.Printf("Processing file: %s\n", file) + } + + //#nosec G304 -- false positive + content, err := os.ReadFile(file) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to read file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + + if linter.LintSnippet(jsonnet.MakeVM(), cmd.ErrOrStderr(), []linter.Snippet{{FileName: file, Code: string(content)}}) { + return cmdx.FailSilently(cmd) + } + } + } + return nil + }, + } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output.") + return cmd +} diff --git a/oryx/jsonnetx/root.go b/oryx/jsonnetx/root.go new file mode 100644 index 0000000..01644d5 --- /dev/null +++ b/oryx/jsonnetx/root.go @@ -0,0 +1,56 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "github.com/spf13/cobra" +) + +const GlobHelp = `Glob patterns supports the following special terms in the patterns: + + Special Terms | Meaning + ------------- | ------- + '*' | matches any sequence of non-path-separators + '**' | matches any sequence of characters, including path separators + '?' | matches any single non-path-separator character + '[class]' | matches any single non-path-separator character against a class of characters ([see below](#character-classes)) + '{alt1,...}' | matches a sequence of characters if one of the comma-separated alternatives matches + + Any character with a special meaning can be escaped with a backslash ('\'). + + #### Character Classes + + Character classes support the following: + + Class | Meaning + ---------- | ------- + '[abc]' | matches any single character within the set + '[a-z]' | matches any single character in the range + '[^class]' | matches any single character which does *not* match the class +` + +// RootCommand represents the jsonnet command +// Deprecated: use NewRootCommand instead. +var RootCommand = &cobra.Command{ + Use: "jsonnet", + Short: "Helpers for linting and formatting JSONNet code", +} + +// RegisterCommandRecursive adds all jsonnet helpers to the RootCommand +// Deprecated: use NewRootCommand instead. +func RegisterCommandRecursive(parent *cobra.Command) { + parent.AddCommand(RootCommand) + + RootCommand.AddCommand(NewFormatCommand()) + RootCommand.AddCommand(NewLintCommand()) +} + +func NewRootCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "jsonnet", + Short: "Helpers for linting and formatting JSONNet code", + } + cmd.AddCommand(NewFormatCommand(), NewLintCommand()) + return cmd +} diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json new file mode 100644 index 0000000..f9ee1c4 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json @@ -0,0 +1,3534 @@ +[ + { + "Title": "Access Rules", + "Description": "Configure access rules. All sub-keys support configuration reloading without restarting.", + "Examples": null, + "Name": "access_rules", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Repositories", + "Description": "Locations (list of URLs) where access rules should be fetched from on boot. It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules:\n\n- If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be fetched from the local file system.\n- If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{\"id\":\"foo-rule\",\"authenticators\":[....]}]`)).\n- If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be fetched from the provided HTTP(s) location.", + "Examples": [ + "[\"file://path/to/rules.json\",\"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d\",\"https://path-to-my-rules/rules.json\"]" + ], + "Name": "access_rules.repositories", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "access_rules.repositories.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Authenticators", + "Description": "For more information on authenticators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authn", + "Examples": null, + "Name": "authenticators", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous", + "Description": "The [`anonymous` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#anonymous).", + "Examples": null, + "Name": "authenticators.anonymous", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.anonymous.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous Subject", + "Description": "Sets the anonymous username.", + "Examples": [ + "guest", + "anon", + "anonymous", + "unknown" + ], + "Name": "authenticators.anonymous.config.subject", + "Default": "anonymous", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.anonymous.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Session", + "Description": "The [`cookie_session` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#cookie_session).", + "Examples": null, + "Name": "authenticators.cookie_session", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Session Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.cookie_session.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Session Check URL", + "Description": "The origin to proxy requests to. If the response is a 200 with body `{ \"subject\": \"...\", \"extra\": {} }`. The request will pass the subject through successfully, otherwise it will be marked as unauthorized.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://session-store-host" + ], + "Name": "authenticators.cookie_session.config.check_session_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Only Cookies", + "Description": "A list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if none of the passed cookies are set on the request.", + "Examples": null, + "Name": "authenticators.cookie_session.config.only", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.cookie_session.config.only.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.cookie_session.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Token (jwt)", + "Description": "The [`jwt` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#jwt).", + "Examples": null, + "Name": "authenticators.jwt", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JWT Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.jwt.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.allowed_algorithms", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.allowed_algorithms.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Key URLs", + "Description": "URLs where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web Token. Usually something like \"https://my-keys.com/.well-known/jwks.json\". The response of that endpoint must return a JSON Web Key Set (JWKS).\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ], + "Name": "authenticators.jwt.config.jwks_urls", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.jwks_urls.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Required Token Scope", + "Description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.jwt.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Scope Strategy", + "Description": "Sets the strategy validation algorithm.", + "Examples": null, + "Name": "authenticators.jwt.config.scope_strategy", + "Default": "none", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "hierarchic", + "exact", + "wildcard", + "none" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Intended Audience", + "Description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "Examples": null, + "Name": "authenticators.jwt.config.target_audience", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.target_audience.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.token_from", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header", + "Description": "The header (case insensitive) that must contain a token for request authentication. It can't be set along with query_parameter.", + "Examples": null, + "Name": "authenticators.jwt.config.token_from.header", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Query Parameter", + "Description": "The query parameter (case sensitive) that must contain a token for request authentication. It can't be set along with header.", + "Examples": null, + "Name": "authenticators.jwt.config.token_from.query_parameter", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.trusted_issuers", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.trusted_issuers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.jwt.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "No Operation (noop)", + "Description": "The [`noop` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#noop).", + "Examples": null, + "Name": "authenticators.noop", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.noop.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Credentials", + "Description": "The [`oauth2_client_credentials` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_client_credentials).", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Credentials Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Request Permissions (Token Scope)", + "Description": "Scopes is an array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this rule.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "The OAuth 2.0 Token Endpoint that will be used to validate the client credentials.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/oauth2/token" + ], + "Name": "authenticators.oauth2_client_credentials.config.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.oauth2_client_credentials.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Token Introspection", + "Description": "The [`oauth2_introspection` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_introspection).", + "Examples": null, + "Name": "authenticators.oauth2_introspection", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Introspection Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Introspection URL", + "Description": "The OAuth 2.0 Token Introspection endpoint URL.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/oauth2/introspection" + ], + "Name": "authenticators.oauth2_introspection.config.introspection_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Pre-Authorization", + "Description": "Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client ID", + "Description": "The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.client_id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Secret", + "Description": "The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.client_secret", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.enabled", + "Default": null, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": [ + true + ], + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Scope", + "Description": "The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant.", + "Examples": [ + [ + "[\"foo\", \"bar\"]" + ] + ], + "Name": "authenticators.oauth2_introspection.config.pre_authorization.scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Token URL", + "Description": "The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Required Scope", + "Description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Scope Strategy", + "Description": "Sets the strategy validation algorithm.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.scope_strategy", + "Default": "none", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "hierarchic", + "exact", + "wildcard", + "none" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Target Audience", + "Description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.target_audience", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.target_audience.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Token From", + "Description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header", + "Description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from.header", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Query Parameter", + "Description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from.query_parameter", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Trusted Issuers", + "Description": "The token must have been issued by one of the issuers listed in this array.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.trusted_issuers", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.trusted_issuers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.oauth2_introspection.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Unauthorized", + "Description": "The [`unauthorized` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#unauthorized).", + "Examples": null, + "Name": "authenticators.unauthorized", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.unauthorized.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Authorizers", + "Description": "For more information on authorizers head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authz", + "Examples": null, + "Name": "authorizers", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow", + "Description": "The [`allow` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "Examples": null, + "Name": "authorizers.allow", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.allow.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Deny", + "Description": "The [`deny` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "Examples": null, + "Name": "authorizers.deny", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.deny.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ORY Keto Access Control Policies Engine", + "Description": "The [`keto_engine_acp_ory` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#keto_engine_acp_ory).", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ORY Keto Access Control Policy Authorizer Configuration", + "Description": "This section is optional when the authorizer is disabled.", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base URL", + "Description": "The base URL of ORY Keto.\n\n\u003eIf this authorizer is enabled, this value is required.", + "Examples": [ + "http://my-keto/" + ], + "Name": "authorizers.keto_engine_acp_ory.config.base_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.flavor", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.required_action", + "Default": "unset", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.required_resource", + "Default": "unset", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.subject", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.keto_engine_acp_ory.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Log", + "Description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "Examples": null, + "Name": "log", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Format", + "Description": "The log format can either be text or JSON.", + "Examples": null, + "Name": "log.format", + "Default": "text", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "text", + "json" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Level", + "Description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL.", + "Examples": null, + "Name": "log.level", + "Default": "info", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "panic", + "fatal", + "error", + "warn", + "info", + "debug" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Mutators", + "Description": "For more information on mutators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/mutator", + "Examples": null, + "Name": "mutators", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Cookie", + "Description": "The [`cookie` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#cookie).", + "Examples": null, + "Name": "mutators.cookie", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.cookie.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.cookie.config.cookies", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.cookie.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Header", + "Description": "The [`header` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#header).", + "Examples": null, + "Name": "mutators.header", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.header.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.header.config.headers", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.header.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Hydrator", + "Description": "The [`hydrator` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#hydrator).", + "Examples": null, + "Name": "mutators.hydrator", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Hydrator Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.hydrator.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic.password", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic.username", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry.delay_in_milliseconds", + "Default": 3, + "Type": 0, + "TypeHint": 3, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": "0", + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry.number_of_retries", + "Default": 100, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": "0", + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.hydrator.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ID Token (JSON Web Token)", + "Description": "The [`id_token` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#id_token).", + "Examples": null, + "Name": "mutators.id_token", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ID Token Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.id_token.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.id_token.config.claims", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Issuer URL", + "Description": "Sets the \"iss\" value of the ID Token.\n\n\u003eIf this mutator is enabled, this value is required.", + "Examples": null, + "Name": "mutators.id_token.config.issuer_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Key URL", + "Description": "Sets the URL where keys should be fetched from. Supports remote locations (http, https) as well as local filesystem paths.\n\n\u003eIf this mutator is enabled, this value is required.", + "Examples": [ + "https://fetch-keys/from/this/location.json", + "file:///from/this/absolute/location.json", + "file://../from/this/relative/location.json" + ], + "Name": "mutators.id_token.config.jwks_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Expire After", + "Description": "Sets the time-to-live of the JSON Web Token.", + "Examples": [ + "1h", + "1m", + "30s" + ], + "Name": "mutators.id_token.config.ttl", + "Default": "1m", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.id_token.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "No Operation (noop)", + "Description": "The [`noop` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#noop).", + "Examples": null, + "Name": "mutators.noop", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.noop.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Profiling", + "Description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "Examples": null, + "Name": "profiling", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "cpu", + "mem" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP(s)", + "Description": "", + "Examples": null, + "Name": "serve", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP REST API", + "Description": "", + "Examples": null, + "Name": "serve.api", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cross Origin Resource Sharing (CORS)", + "Description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "Examples": null, + "Name": "serve.api.cors", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow HTTP Credentials", + "Description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "Examples": null, + "Name": "serve.api.cors.allow_credentials", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Request HTTP Headers", + "Description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.api.cors.allowed_headers", + "Default": [ + "Authorization", + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed HTTP Methods", + "Description": "A list of methods the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.api.cors.allowed_methods", + "Default": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_methods.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Origins", + "Description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "Examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ], + "Name": "serve.api.cors.allowed_origins", + "Default": [ + "*" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_origins.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable Debugging", + "Description": "Set to true to debug server side CORS issues.", + "Examples": null, + "Name": "serve.api.cors.debug", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable CORS", + "Description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered.", + "Examples": null, + "Name": "serve.api.cors.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Response HTTP Headers", + "Description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "Examples": null, + "Name": "serve.api.cors.exposed_headers", + "Default": [ + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.exposed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Maximum Age", + "Description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age.", + "Examples": null, + "Name": "serve.api.cors.max_age", + "Default": 0, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Host", + "Description": "The network interface to listen on.", + "Examples": [ + "localhost", + "127.0.0.1" + ], + "Name": "serve.api.host", + "Default": "", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Port", + "Description": "The port to listen on.", + "Examples": null, + "Name": "serve.api.port", + "Default": 4456, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTPS", + "Description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "Examples": null, + "Name": "serve.api.tls", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.tls.cert", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.api.tls.cert.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.api.tls.cert.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.tls.key", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.api.tls.key.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.api.tls.key.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Reverse Proxy", + "Description": "", + "Examples": null, + "Name": "serve.proxy", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cross Origin Resource Sharing (CORS)", + "Description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "Examples": null, + "Name": "serve.proxy.cors", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow HTTP Credentials", + "Description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "Examples": null, + "Name": "serve.proxy.cors.allow_credentials", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Request HTTP Headers", + "Description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.proxy.cors.allowed_headers", + "Default": [ + "Authorization", + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed HTTP Methods", + "Description": "A list of methods the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.proxy.cors.allowed_methods", + "Default": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_methods.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Origins", + "Description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "Examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ], + "Name": "serve.proxy.cors.allowed_origins", + "Default": [ + "*" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_origins.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable Debugging", + "Description": "Set to true to debug server side CORS issues.", + "Examples": null, + "Name": "serve.proxy.cors.debug", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable CORS", + "Description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered.", + "Examples": null, + "Name": "serve.proxy.cors.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Response HTTP Headers", + "Description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "Examples": null, + "Name": "serve.proxy.cors.exposed_headers", + "Default": [ + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.exposed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Maximum Age", + "Description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age.", + "Examples": null, + "Name": "serve.proxy.cors.max_age", + "Default": 0, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Host", + "Description": "The network interface to listen on. Leave empty to listen on all interfaces.", + "Examples": [ + "localhost", + "127.0.0.1" + ], + "Name": "serve.proxy.host", + "Default": "", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Port", + "Description": "The port to listen on.", + "Examples": null, + "Name": "serve.proxy.port", + "Default": 4455, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Timeouts", + "Description": "Control the reverse proxy's HTTP timeouts.", + "Examples": null, + "Name": "serve.proxy.timeout", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Idle Timeout", + "Description": " The maximum amount of time to wait for any action of a request session, reading data or writing the response.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.idle", + "Default": "120s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Read Timeout", + "Description": "The maximum duration for reading the entire request, including the body.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.read", + "Default": "5s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Write Timeout", + "Description": "The maximum duration before timing out writes of the response. Increase this parameter to prevent unexpected closing a client connection if an upstream request is responding slowly.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.write", + "Default": "120s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTPS", + "Description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "Examples": null, + "Name": "serve.proxy.tls", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.tls.cert", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.proxy.tls.cert.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.proxy.tls.cert.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.tls.key", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.proxy.tls.key.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.proxy.tls.key.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json new file mode 100644 index 0000000..f1b5721 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json new file mode 100644 index 0000000..cf25af4 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json @@ -0,0 +1,23 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "dsn", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json new file mode 100644 index 0000000..88d7567 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json @@ -0,0 +1,305 @@ +[ + { + "Title": "OpenID Connect and OAuth2 Providers", + "Description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://accounts.google.com/o/oauth2/v2/auth" + ], + "Name": "providers.#.auth_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.client_id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.client_secret", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "google" + ], + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://accounts.google.com" + ], + "Name": "providers.#.issuer_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Jsonnet Mapper URL", + "Description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "Examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ], + "Name": "providers.#.mapper_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Provider", + "Description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "Examples": [ + "google" + ], + "Name": "providers.#.provider", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "offline_access", + "profile" + ], + "Name": "providers.#.scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Azure AD Tenant", + "Description": "The Azure AD Tenant to use for authentication.", + "Examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ], + "Name": "providers.#.tenant", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://www.googleapis.com/oauth2/v4/token" + ], + "Name": "providers.#.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json new file mode 100644 index 0000000..6513d10 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json @@ -0,0 +1,86 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": "asdf", + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": true, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json new file mode 100644 index 0000000..6513d10 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json @@ -0,0 +1,86 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": "asdf", + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": true, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json new file mode 100644 index 0000000..52c81f2 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json @@ -0,0 +1,46 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": { + "foobar": "bar" + } + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": null, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json new file mode 100644 index 0000000..7deafa7 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json @@ -0,0 +1,44 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json new file mode 100644 index 0000000..4dbf7ff --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json new file mode 100644 index 0000000..78536e1 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#.foo", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json b/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json new file mode 100644 index 0000000..3e460cb --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json @@ -0,0 +1,233 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bars", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "email", + "Pattern": ".*", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bars", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "email", + "Pattern": ".*", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/README.md b/oryx/jsonschemax/README.md new file mode 100644 index 0000000..5306c69 --- /dev/null +++ b/oryx/jsonschemax/README.md @@ -0,0 +1,120 @@ +# JSON Schema Helpers + +This package contains utilities for working with JSON Schemas. + +## Listing all Possible JSON Schema Paths + +Using `jsonschemax.ListPaths()` you can get a list of all possible JSON paths in +a JSON Schema. + +```go +package main + +import ( + "bytes" + "fmt" + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +var schema = "..." + +func main() { + c := jsonschema.NewCompiler() + _ = c.AddResource("test.json", bytes.NewBufferString(schema)) + paths, _ := jsonschemax.ListPaths("test.json", c) + fmt.Printf("%+v", paths) +} +``` + +All keys are delimited using `.`. Please note that arrays are denoted with `#` +when `ListPathsWithArraysIncluded` is used. For example, the JSON Schema + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } +} +``` + +Results in paths: + +```json +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] +``` diff --git a/oryx/jsonschemax/error.go b/oryx/jsonschemax/error.go new file mode 100644 index 0000000..f8b0482 --- /dev/null +++ b/oryx/jsonschemax/error.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "github.com/ory/jsonschema/v3" +) + +// ErrorType is the schema error type. +type ErrorType int + +const ( + // ErrorTypeMissing represents a validation that failed because a value is missing. + ErrorTypeMissing ErrorType = iota + 1 +) + +// Error represents a schema error. +type Error struct { + // Type is the error type. + Type ErrorType + + // DocumentPointer is the JSON Pointer in the document. + DocumentPointer string + + // SchemaPointer is the JSON Pointer in the schema. + SchemaPointer string + + // DocumentFieldName is a pointer to the document in dot-notation: fo.bar.baz + DocumentFieldName string +} + +// NewFromSanthoshError converts github.com/santhosh-tekuri/jsonschema.ValidationError to Error. +func NewFromSanthoshError(validationError jsonschema.ValidationError) *Error { + return &Error{ + // DocumentPointer: JSONPointerToDotNotation(validationError.InstancePtr), + // SchemaPointer: JSONPointerToDotNotation(validationError.SchemaPtr), + // DocumentFieldName: JSONPointerToDotNotation(validationError.InstancePtr), + } +} diff --git a/oryx/jsonschemax/keys.go b/oryx/jsonschemax/keys.go new file mode 100644 index 0000000..ab9638c --- /dev/null +++ b/oryx/jsonschemax/keys.go @@ -0,0 +1,447 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "math/big" + "regexp" + "slices" + "sort" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" +) + +type ( + byName []Path + PathEnhancer interface { + EnhancePath(Path) map[string]interface{} + } + TypeHint int +) + +func (s byName) Len() int { return len(s) } +func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name } + +const ( + String TypeHint = iota + 1 + Float + Int + Bool + JSON + Nil + + BoolSlice + StringSlice + IntSlice + FloatSlice +) + +// Path represents a JSON Schema Path. +type Path struct { + // Title of the path. + Title string + + // Description of the path. + Description string + + // Examples of the path. + Examples []interface{} + + // Name is the JSON path name. + Name string + + // Default is the default value of that path. + Default interface{} + + // Type is a prototype (e.g. float64(0)) of the path type. + Type interface{} + + TypeHint + + // Format is the format of the path if defined + Format string + + // Pattern is the pattern of the path if defined + Pattern *regexp.Regexp + + // Enum are the allowed enum values + Enum []interface{} + + // first element in slice is constant value. note: slice is used to capture nil constant. + Constant []interface{} + + // ReadOnly is whether the value is readonly + ReadOnly bool + + // -1 if not specified + MinLength int + MaxLength int + + // Required if set indicates this field is required. + Required bool + + Minimum *big.Float + Maximum *big.Float + + MultipleOf *big.Float + + CustomProperties map[string]interface{} +} + +// ListPathsBytes works like ListPathsWithRecursion but prepares the JSON Schema itself. +func ListPathsBytes(ctx context.Context, raw json.RawMessage, maxRecursion int16) ([]Path, error) { + compiler := jsonschema.NewCompiler() + compiler.ExtractAnnotations = true + id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) + if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { + return nil, err + } + compiler.ExtractAnnotations = true + return runPathsFromCompiler(ctx, id, compiler, maxRecursion, false) +} + +// ListPathsWithRecursion will follow circular references until maxRecursion is reached, without +// returning an error. +func ListPathsWithRecursion(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion uint8) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, int16(maxRecursion), false) +} + +// ListPaths lists all paths of a JSON Schema. Will return an error +// if circular references are found. +func ListPaths(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, -1, false) +} + +// ListPathsWithArraysIncluded lists all paths of a JSON Schema. Will return an error +// if circular references are found. +// Includes arrays with `#`. +func ListPathsWithArraysIncluded(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, -1, true) +} + +// ListPathsWithInitializedSchema loads the paths from the schema without compiling it. +// +// You MUST ensure that the compiler was using `ExtractAnnotations = true`. +func ListPathsWithInitializedSchema(schema *jsonschema.Schema) ([]Path, error) { + return runPaths(schema, -1, false) +} + +// ListPathsWithInitializedSchemaAndArraysIncluded loads the paths from the schema without compiling it. +// +// You MUST ensure that the compiler was using `ExtractAnnotations = true`. +// Includes arrays with `#`. +func ListPathsWithInitializedSchemaAndArraysIncluded(schema *jsonschema.Schema) ([]Path, error) { + return runPaths(schema, -1, true) +} + +func runPathsFromCompiler(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion int16, includeArrays bool) ([]Path, error) { + if compiler == nil { + compiler = jsonschema.NewCompiler() + } + + compiler.ExtractAnnotations = true + + schema, err := compiler.Compile(ctx, ref) + if err != nil { + return nil, errors.WithStack(err) + } + + return runPaths(schema, maxRecursion, includeArrays) +} + +func runPaths(schema *jsonschema.Schema, maxRecursion int16, includeArrays bool) ([]Path, error) { + pointers := map[string]bool{} + paths, err := listPaths(schema, nil, nil, pointers, 0, maxRecursion, includeArrays) + if err != nil { + return nil, errors.WithStack(err) + } + + sort.Stable(paths) + return makeUnique(paths) +} + +func makeUnique(in byName) (byName, error) { + cache := make(map[string]Path) + for _, p := range in { + vc, ok := cache[p.Name] + if !ok { + cache[p.Name] = p + continue + } + + if fmt.Sprintf("%T", p.Type) != fmt.Sprintf("%T", p.Type) { + return nil, errors.Errorf("multiple types %+v are not supported for path: %s", []interface{}{p.Type, vc.Type}, p.Name) + } + + if vc.Default == nil { + cache[p.Name] = p + } + } + + k := 0 + out := make([]Path, len(cache)) + for _, v := range cache { + out[k] = v + k++ + } + + paths := byName(out) + sort.Sort(paths) + return paths, nil +} + +func appendPointer(in map[string]bool, pointer *jsonschema.Schema) map[string]bool { + out := make(map[string]bool) + for k, v := range in { + out[k] = v + } + out[fmt.Sprintf("%p", pointer)] = true + return out +} + +func listPaths(schema *jsonschema.Schema, parent *jsonschema.Schema, parents []string, pointers map[string]bool, currentRecursion int16, maxRecursion int16, includeArrays bool) (byName, error) { + var pathType interface{} + var pathTypeHint TypeHint + var paths []Path + _, isCircular := pointers[fmt.Sprintf("%p", schema)] + + if len(schema.Constant) > 0 { + switch schema.Constant[0].(type) { + case float64, json.Number: + pathType = float64(0) + pathTypeHint = Float + case int8, int16, int, int64: + pathType = int64(0) + pathTypeHint = Int + case string: + pathType = "" + pathTypeHint = String + case bool: + pathType = false + pathTypeHint = Bool + default: + pathType = schema.Constant[0] + pathTypeHint = JSON + } + } else if len(schema.Types) == 1 { + switch schema.Types[0] { + case "null": + pathType = nil + pathTypeHint = Nil + case "boolean": + pathType = false + pathTypeHint = Bool + case "number": + pathType = float64(0) + pathTypeHint = Float + case "integer": + pathType = float64(0) + pathTypeHint = Int + case "string": + pathType = "" + pathTypeHint = String + case "array": + pathType = []interface{}{} + if schema.Items != nil { + var itemSchemas []*jsonschema.Schema + switch t := schema.Items.(type) { + case []*jsonschema.Schema: + itemSchemas = t + case *jsonschema.Schema: + itemSchemas = []*jsonschema.Schema{t} + } + var types []string + for _, is := range itemSchemas { + types = append(types, is.Types...) + if is.Ref != nil { + types = append(types, is.Ref.Types...) + } + } + types = slices.Compact(types) + if len(types) == 1 { + switch types[0] { + case "boolean": + pathType = []bool{} + pathTypeHint = BoolSlice + case "number": + pathType = []float64{} + pathTypeHint = FloatSlice + case "integer": + pathType = []float64{} + pathTypeHint = IntSlice + case "string": + pathType = []string{} + pathTypeHint = StringSlice + default: + pathType = []interface{}{} + pathTypeHint = JSON + } + } + } + case "object": + pathType = map[string]interface{}{} + pathTypeHint = JSON + } + } else if len(schema.Types) > 2 { + pathType = nil + pathTypeHint = JSON + } + + var def interface{} = schema.Default + if v, ok := def.(json.Number); ok { + def, _ = v.Float64() + } + + if (pathType != nil || schema.Default != nil) && len(parents) > 0 { + name := parents[len(parents)-1] + var required bool + if parent != nil { + for _, r := range parent.Required { + if r == name { + required = true + break + } + } + } + + path := Path{ + Name: strings.Join(parents, "."), + Default: def, + Type: pathType, + TypeHint: pathTypeHint, + Format: schema.Format, + Pattern: schema.Pattern, + Enum: schema.Enum, + Constant: schema.Constant, + MinLength: schema.MinLength, + MaxLength: schema.MaxLength, + Minimum: schema.Minimum, + Maximum: schema.Maximum, + MultipleOf: schema.MultipleOf, + ReadOnly: schema.ReadOnly, + Title: schema.Title, + Description: schema.Description, + Examples: schema.Examples, + Required: required, + } + + for _, e := range schema.Extensions { + if enhancer, ok := e.(PathEnhancer); ok { + path.CustomProperties = enhancer.EnhancePath(path) + } + } + paths = append(paths, path) + } + + if isCircular { + if maxRecursion == -1 { + return nil, errors.Errorf("detected circular dependency in schema path: %s", strings.Join(parents, ".")) + } else if currentRecursion > maxRecursion { + return paths, nil + } + currentRecursion++ + } + + if schema.Ref != nil { + path, err := listPaths(schema.Ref, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Not != nil { + path, err := listPaths(schema.Not, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.If != nil { + path, err := listPaths(schema.If, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Then != nil { + path, err := listPaths(schema.Then, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Else != nil { + path, err := listPaths(schema.Else, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.AllOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.AnyOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.OneOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for name, sub := range schema.Properties { + path, err := listPaths(sub, schema, append(parents, name), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Items != nil && includeArrays { + switch t := schema.Items.(type) { + case []*jsonschema.Schema: + for _, sub := range t { + path, err := listPaths(sub, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + case *jsonschema.Schema: + path, err := listPaths(t, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + } + + return paths, nil +} diff --git a/oryx/jsonschemax/pointer.go b/oryx/jsonschemax/pointer.go new file mode 100644 index 0000000..f0c279f --- /dev/null +++ b/oryx/jsonschemax/pointer.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// JSONPointerToDotNotation converts JSON Pointer "#/foo/bar" to dot-notation "foo.bar". +func JSONPointerToDotNotation(pointer string) (string, error) { + if !strings.HasPrefix(pointer, "#/") { + return pointer, errors.Errorf("remote JSON pointers are not supported: %s", pointer) + } + + var path []string + for _, item := range strings.Split(strings.TrimPrefix(pointer, "#/"), "/") { + item = strings.Replace(item, "~1", "/", -1) + item = strings.Replace(item, "~0", "~", -1) + item, err := url.PathUnescape(item) + if err != nil { + return "", err + } + path = append(path, strings.ReplaceAll(item, ".", "\\.")) + } + + return strings.Join(path, "."), nil +} diff --git a/oryx/jsonschemax/print.go b/oryx/jsonschemax/print.go new file mode 100644 index 0000000..45c1b91 --- /dev/null +++ b/oryx/jsonschemax/print.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" +) + +func FormatValidationErrorForCLI(w io.Writer, conf []byte, err error) { + if err == nil { + return + } + + if e := new(jsonschema.ValidationError); errors.As(err, &e) { + _, _ = fmt.Fprintln(w, "The configuration contains values or keys which are invalid:") + pointer, validation := FormatError(e) + + if pointer == "#" { + if len(e.Causes) == 0 { + _, _ = fmt.Fprintln(w, "(root)") + _, _ = fmt.Fprintln(w, "^-- "+validation) + _, _ = fmt.Fprintln(w, "") + } + } else { + spaces := make([]string, len(pointer)+3) + _, _ = fmt.Fprintf(w, "%s: %+v", pointer, gjson.GetBytes(conf, pointer).Value()) + _, _ = fmt.Fprintln(w, "") + _, _ = fmt.Fprintf(w, "%s^-- %s", strings.Join(spaces, " "), validation) + _, _ = fmt.Fprintln(w, "") + _, _ = fmt.Fprintln(w, "") + } + + for _, cause := range e.Causes { + FormatValidationErrorForCLI(w, conf, cause) + } + return + } +} + +func FormatError(e *jsonschema.ValidationError) (string, string) { + var ( + err error + pointer string + message string + ) + + pointer = e.InstancePtr + message = e.Message + switch ctx := e.Context.(type) { + case *jsonschema.ValidationErrorContextRequired: + if len(ctx.Missing) > 0 { + message = "one or more required properties are missing" + pointer = ctx.Missing[0] + } + } + + // We can ignore the error as it will simply echo the pointer. + pointer, err = JSONPointerToDotNotation(pointer) + if err != nil { + pointer = e.InstancePtr + } + + return pointer, message +} diff --git a/oryx/jsonschemax/stub/.config.yaml b/oryx/jsonschemax/stub/.config.yaml new file mode 100644 index 0000000..2367e5d --- /dev/null +++ b/oryx/jsonschemax/stub/.config.yaml @@ -0,0 +1,3 @@ +dsn: memory +items: + - id: 1 diff --git a/oryx/jsonschemax/stub/.oathkeeper.schema.json b/oryx/jsonschemax/stub/.oathkeeper.schema.json new file mode 100644 index 0000000..7126e15 --- /dev/null +++ b/oryx/jsonschemax/stub/.oathkeeper.schema.json @@ -0,0 +1,1073 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/oathkeeper/v0.32.1-beta.1/.schemas/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Oathkeeper Configuration", + "type": "object", + "definitions": { + "tlsxSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + }, + "tlsx": { + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + } + } + }, + "cors": { + "title": "Cross Origin Resource Sharing (CORS)", + "description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "title": "Enable CORS", + "description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered." + }, + "allowed_origins": { + "title": "Allowed Origins", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "default": ["*"], + "uniqueItems": true, + "examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + }, + "allowed_methods": { + "type": "array", + "title": "Allowed HTTP Methods", + "description": "A list of methods the client is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ] + }, + "uniqueItems": true, + "default": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "allowed_headers": { + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "title": "Allowed Request HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Authorization", "Content-Type"] + }, + "exposed_headers": { + "description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "title": "Allowed Response HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Content-Type"] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow HTTP Credentials", + "default": false, + "description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates." + }, + "max_age": { + "type": "number", + "default": 0, + "title": "Maximum Age", + "description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age." + }, + "debug": { + "type": "boolean", + "default": false, + "title": "Enable Debugging", + "description": "Set to true to debug server side CORS issues." + } + }, + "additionalProperties": false + }, + "handlerSwitch": { + "title": "Enabled", + "type": "boolean", + "default": false, + "examples": [true], + "description": "En-/disables this component." + }, + "scopeStrategy": { + "title": "Scope Strategy", + "type": "string", + "enum": ["hierarchic", "exact", "wildcard", "none"], + "default": "none", + "description": "Sets the strategy validation algorithm." + }, + "configAuthenticatorsAnonymous": { + "type": "object", + "title": "Anonymous Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "subject": { + "type": "string", + "title": "Anonymous Subject", + "examples": ["guest", "anon", "anonymous", "unknown"], + "default": "anonymous", + "description": "Sets the anonymous username." + } + }, + "additionalProperties": false + }, + "configAuthenticatorsCookieSession": { + "type": "object", + "title": "Cookie Session Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "check_session_url": { + "title": "Session Check URL", + "type": "string", + "format": "uri", + "description": "The origin to proxy requests to. If the response is a 200 with body `{ \"subject\": \"...\", \"extra\": {} }`. The request will pass the subject through successfully, otherwise it will be marked as unauthorized.\n\n>If this authenticator is enabled, this value is required.", + "examples": ["https://session-store-host"] + }, + "only": { + "type": "array", + "items": { + "type": "string", + "additionalItems": false + }, + "title": "Only Cookies", + "description": "A list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if none of the passed cookies are set on the request." + } + }, + "required": ["check_session_url"], + "additionalProperties": false + }, + "configAuthenticatorsJwt": { + "type": "object", + "title": "JWT Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "required": ["jwks_urls"], + "properties": { + "required_scope": { + "type": "array", + "title": "Required Token Scope", + "description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "items": { + "type": "string" + } + }, + "target_audience": { + "title": "Intended Audience", + "type": "array", + "description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "items": { + "type": "string" + } + }, + "trusted_issuers": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowed_algorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "jwks_urls": { + "title": "JSON Web Key URLs", + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "URLs where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web Token. Usually something like \"https://my-keys.com/.well-known/jwks.json\". The response of that endpoint must return a JSON Web Key Set (JWKS).\n\n>If this authenticator is enabled, this value is required.", + "examples": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ] + }, + "scope_strategy": { + "$ref": "#/definitions/scopeStrategy" + }, + "token_from": { + "title": "Token From", + "description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "oneOf": [ + { + "type": "object", + "required": ["header"], + "properties": { + "header": { + "title": "Header", + "type": "string", + "description": "The header (case insensitive) that must contain a token for request authentication. It can't be set along with query_parameter." + } + } + }, + { + "type": "object", + "required": ["query_parameter"], + "properties": { + "query_parameter": { + "title": "Query Parameter", + "type": "string", + "description": "The query parameter (case sensitive) that must contain a token for request authentication. It can't be set along with header." + } + } + } + ] + } + }, + "additionalProperties": false + }, + "configAuthenticatorsOauth2ClientCredentials": { + "type": "object", + "title": "OAuth 2.0 Client Credentials Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "token_url": { + "type": "string", + "description": "The OAuth 2.0 Token Endpoint that will be used to validate the client credentials.\n\n>If this authenticator is enabled, this value is required.", + "format": "uri", + "examples": ["https://my-website.com/oauth2/token"] + }, + "required_scope": { + "type": "array", + "title": "Request Permissions (Token Scope)", + "description": "Scopes is an array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this rule.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "items": { + "type": "string" + } + } + }, + "required": ["token_url"], + "additionalProperties": false + }, + "configAuthenticatorsOauth2Introspection": { + "type": "object", + "title": "OAuth 2.0 Introspection Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "introspection_url": { + "type": "string", + "format": "uri", + "examples": ["https://my-website.com/oauth2/introspection"], + "title": "OAuth 2.0 Introspection URL", + "description": "The OAuth 2.0 Token Introspection endpoint URL.\n\n>If this authenticator is enabled, this value is required." + }, + "scope_strategy": { + "$ref": "#/definitions/scopeStrategy" + }, + "pre_authorization": { + "title": "Pre-Authorization", + "description": "Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.", + "type": "object", + "additionalProperties": false, + "required": ["client_id", "client_secret", "token_url"], + "properties": { + "enabled": { + "const": true + }, + "client_id": { + "type": "string", + "title": "OAuth 2.0 Client ID", + "description": "The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant.\n\n>If pre-authorization is enabled, this value is required." + }, + "client_secret": { + "type": "string", + "title": "OAuth 2.0 Client Secret", + "description": "The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant.\n\n>If pre-authorization is enabled, this value is required." + }, + "token_url": { + "type": "string", + "format": "uri", + "title": "OAuth 2.0 Token URL", + "description": "The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed.\n\n>If pre-authorization is enabled, this value is required." + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "title": "OAuth 2.0 Scope", + "description": "The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant.", + "examples": [["[\"foo\", \"bar\"]"]] + } + } + }, + "required_scope": { + "title": "Required Scope", + "description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "type": "array", + "items": { + "type": "string" + } + }, + "target_audience": { + "title": "Target Audience", + "description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "type": "array", + "items": { + "type": "string" + } + }, + "trusted_issuers": { + "title": "Trusted Issuers", + "description": "The token must have been issued by one of the issuers listed in this array.", + "type": "array", + "items": { + "type": "string" + } + }, + "token_from": { + "title": "Token From", + "description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "type": "object", + "oneOf": [ + { + "required": ["header"], + "properties": { + "header": { + "title": "Header", + "type": "string", + "description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter." + } + } + }, + { + "required": ["query_parameter"], + "properties": { + "query_parameter": { + "title": "Query Parameter", + "type": "string", + "description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header." + } + } + } + ] + } + }, + "required": ["introspection_url"], + "additionalProperties": false + }, + "configAuthorizersKetoEngineAcpOry": { + "type": "object", + "title": "ORY Keto Access Control Policy Authorizer Configuration", + "description": "This section is optional when the authorizer is disabled.", + "properties": { + "base_url": { + "title": "Base URL", + "type": "string", + "format": "uri", + "description": "The base URL of ORY Keto.\n\n>If this authorizer is enabled, this value is required.", + "examples": ["http://my-keto/"] + }, + "required_action": { + "type": "string", + "default": "unset" + }, + "required_resource": { + "type": "string", + "default": "unset" + }, + "subject": { + "type": "string" + }, + "flavor": { + "type": "string" + } + }, + "required": ["base_url", "required_action", "required_resource"], + "additionalProperties": false + }, + "configMutatorsCookie": { + "type": "object", + "title": "Cookie Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["cookies"], + "properties": { + "cookies": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "configMutatorsHeader": { + "type": "object", + "title": "Header Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["headers"], + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "configMutatorsHydrator": { + "type": "object", + "title": "Hydrator Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "properties": { + "api": { + "additionalProperties": false, + "required": ["url"], + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "auth": { + "type": "object", + "additionalProperties": false, + "properties": { + "basic": { + "required": ["username", "password"], + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + }, + "retry": { + "type": "object", + "additionalProperties": false, + "properties": { + "number_of_retries": { + "type": "number", + "minimum": 0, + "default": 100 + }, + "delay_in_milliseconds": { + "type": "integer", + "minimum": 0, + "default": 3 + } + } + } + } + } + }, + "required": ["api"], + "additionalProperties": false + }, + "configMutatorsIdToken": { + "type": "object", + "title": "ID Token Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["jwks_url", "issuer_url"], + "properties": { + "claims": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "title": "Issuer URL", + "description": "Sets the \"iss\" value of the ID Token.\n\n>If this mutator is enabled, this value is required." + }, + "jwks_url": { + "type": "string", + "format": "uri", + "title": "JSON Web Key URL", + "description": "Sets the URL where keys should be fetched from. Supports remote locations (http, https) as well as local filesystem paths.\n\n>If this mutator is enabled, this value is required.", + "examples": [ + "https://fetch-keys/from/this/location.json", + "file:///from/this/absolute/location.json", + "file://../from/this/relative/location.json" + ] + }, + "ttl": { + "type": "string", + "title": "Expire After", + "description": "Sets the time-to-live of the JSON Web Token.", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1m", + "examples": ["1h", "1m", "30s"] + } + }, + "additionalProperties": false + } + }, + "properties": { + "serve": { + "title": "HTTP(s)", + "additionalProperties": false, + "type": "object", + "properties": { + "api": { + "type": "object", + "title": "HTTP REST API", + "additionalProperties": false, + "properties": { + "port": { + "type": "number", + "default": 4456, + "title": "Port", + "description": "The port to listen on." + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "proxy": { + "type": "object", + "title": "HTTP Reverse Proxy", + "additionalProperties": false, + "properties": { + "port": { + "type": "number", + "default": 4455, + "title": "Port", + "description": "The port to listen on." + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on. Leave empty to listen on all interfaces." + }, + "timeout": { + "title": "HTTP Timeouts", + "description": "Control the reverse proxy's HTTP timeouts.", + "type": "object", + "additionalProperties": false, + "properties": { + "read": { + "title": "HTTP Read Timeout", + "type": "string", + "default": "5s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": "The maximum duration for reading the entire request, including the body.", + "examples": ["5s", "5m", "5h"] + }, + "write": { + "title": "HTTP Write Timeout", + "type": "string", + "default": "120s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": "The maximum duration before timing out writes of the response. Increase this parameter to prevent unexpected closing a client connection if an upstream request is responding slowly.", + "examples": ["5s", "5m", "5h"] + }, + "idle": { + "title": "HTTP Idle Timeout", + "type": "string", + "default": "120s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": " The maximum amount of time to wait for any action of a request session, reading data or writing the response.", + "examples": ["5s", "5m", "5h"] + } + } + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + } + } + }, + "access_rules": { + "title": "Access Rules", + "description": "Configure access rules. All sub-keys support configuration reloading without restarting.", + "type": "object", + "additionalProperties": false, + "properties": { + "repositories": { + "title": "Repositories", + "description": "Locations (list of URLs) where access rules should be fetched from on boot. It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules:\n\n- If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be fetched from the local file system.\n- If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{\"id\":\"foo-rule\",\"authenticators\":[....]}]`)).\n- If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be fetched from the provided HTTP(s) location.", + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "examples": [ + "[\"file://path/to/rules.json\",\"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d\",\"https://path-to-my-rules/rules.json\"]" + ] + } + } + }, + "authenticators": { + "title": "Authenticators", + "type": "object", + "description": "For more information on authenticators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authn", + "additionalProperties": false, + "properties": { + "anonymous": { + "title": "Anonymous", + "description": "The [`anonymous` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#anonymous).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsAnonymous" + } + } + }, + "noop": { + "title": "No Operation (noop)", + "description": "The [`noop` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#noop).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "unauthorized": { + "title": "Unauthorized", + "description": "The [`unauthorized` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#unauthorized).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "cookie_session": { + "title": "Cookie Session", + "description": "The [`cookie_session` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#cookie_session).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsCookieSession" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "jwt": { + "title": "JSON Web Token (jwt)", + "description": "The [`jwt` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#jwt).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsJwt" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "oauth2_client_credentials": { + "title": "OAuth 2.0 Client Credentials", + "description": "The [`oauth2_client_credentials` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_client_credentials).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsOauth2ClientCredentials" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "oauth2_introspection": { + "title": "OAuth 2.0 Token Introspection", + "description": "The [`oauth2_introspection` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_introspection).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsOauth2Introspection" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "authorizers": { + "title": "Authorizers", + "type": "object", + "description": "For more information on authorizers head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authz", + "additionalProperties": false, + "properties": { + "allow": { + "title": "Allow", + "description": "The [`allow` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "deny": { + "title": "Deny", + "description": "The [`deny` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "keto_engine_acp_ory": { + "title": "ORY Keto Access Control Policies Engine", + "description": "The [`keto_engine_acp_ory` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#keto_engine_acp_ory).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthorizersKetoEngineAcpOry" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "mutators": { + "title": "Mutators", + "type": "object", + "description": "For more information on mutators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/mutator", + "additionalProperties": false, + "properties": { + "noop": { + "title": "No Operation (noop)", + "description": "The [`noop` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#noop).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "cookie": { + "title": "HTTP Cookie", + "description": "The [`cookie` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#cookie).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsCookie" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "header": { + "title": "HTTP Header", + "description": "The [`header` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#header).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsHeader" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "hydrator": { + "title": "Hydrator", + "description": "The [`hydrator` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#hydrator).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsHydrator" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "id_token": { + "title": "ID Token (JSON Web Token)", + "description": "The [`id_token` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#id_token).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsIdToken" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "log": { + "title": "Log", + "description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "type": "string", + "default": "info", + "enum": ["panic", "fatal", "error", "warn", "info", "debug"], + "title": "Level", + "description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL." + }, + "format": { + "type": "string", + "default": "text", + "enum": ["text", "json"], + "title": "Format", + "description": "The log format can either be text or JSON." + } + }, + "additionalProperties": false + }, + "profiling": { + "title": "Profiling", + "description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "type": "string", + "enum": ["cpu", "mem"] + } + }, + "required": [], + "additionalProperties": false +} diff --git a/oryx/jsonschemax/stub/config.schema.json b/oryx/jsonschemax/stub/config.schema.json new file mode 100644 index 0000000..537e6ac --- /dev/null +++ b/oryx/jsonschemax/stub/config.schema.json @@ -0,0 +1,12 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "config", + "type": "object", + "properties": { + "dsn": { + "type": "string" + } + }, + "required": ["dsn"] +} diff --git a/oryx/jsonschemax/stub/json/.project-stub-name.json b/oryx/jsonschemax/stub/json/.project-stub-name.json new file mode 100644 index 0000000..798f6eb --- /dev/null +++ b/oryx/jsonschemax/stub/json/.project-stub-name.json @@ -0,0 +1,7 @@ +{ + "serve": { + "admin": { + "port": 1 + } + } +} diff --git a/oryx/jsonschemax/stub/nested-array.schema.json b/oryx/jsonschemax/stub/nested-array.schema.json new file mode 100644 index 0000000..b70c935 --- /dev/null +++ b/oryx/jsonschemax/stub/nested-array.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + } + }, + "additionalProperties": false, + "required": [], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + } + } + } +} diff --git a/oryx/jsonschemax/stub/nested-simple-array.schema.json b/oryx/jsonschemax/stub/nested-simple-array.schema.json new file mode 100644 index 0000000..7bfe7a8 --- /dev/null +++ b/oryx/jsonschemax/stub/nested-simple-array.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } +} diff --git a/oryx/jsonschemax/stub/toml/.project-stub-name.toml b/oryx/jsonschemax/stub/toml/.project-stub-name.toml new file mode 100644 index 0000000..5db36ed --- /dev/null +++ b/oryx/jsonschemax/stub/toml/.project-stub-name.toml @@ -0,0 +1,4 @@ +[serve] + + [serve.admin] + port = "2" \ No newline at end of file diff --git a/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml b/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml new file mode 100644 index 0000000..f41a4a0 --- /dev/null +++ b/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml @@ -0,0 +1,4 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + port: 3 diff --git a/oryx/jsonschemax/stub/yml/.project-stub-name.yml b/oryx/jsonschemax/stub/yml/.project-stub-name.yml new file mode 100644 index 0000000..ccd7c3b --- /dev/null +++ b/oryx/jsonschemax/stub/yml/.project-stub-name.yml @@ -0,0 +1,3 @@ +serve: + admin: + port: 4 diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json @@ -0,0 +1 @@ +"foo" diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json new file mode 100644 index 0000000..fab1a3b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json @@ -0,0 +1,3 @@ +{ + "some": "key" +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json new file mode 100644 index 0000000..7306c23 --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json @@ -0,0 +1,3 @@ +{ + "some_key": 1234 +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json new file mode 100644 index 0000000..2d0d20a --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "source": "base64://aGVsbG8gd29ybGQ=" + }, + "array": [ + { + "nested": { + "source": "base64://aGVsbG8gd29ybGQ=" + } + }, + "base64://aGVsbG8gd29ybGQ=" + ] + } +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json new file mode 100644 index 0000000..bfa283b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json @@ -0,0 +1 @@ +"https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json new file mode 100644 index 0000000..a167c0b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "ignore_this_key": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "base64://aGVsbG8gd29ybGQ=" + } + }, + "base64://aGVsbG8gd29ybGQ=" + ] + } +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json b/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json new file mode 100644 index 0000000..f056437 --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json @@ -0,0 +1,4 @@ +{ + "key": "https://foobar.com", + "bar": "base64://YXNkZg==" +} diff --git a/oryx/jsonx/debug.go b/oryx/jsonx/debug.go new file mode 100644 index 0000000..9b738f1 --- /dev/null +++ b/oryx/jsonx/debug.go @@ -0,0 +1,76 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "fmt" + "slices" +) + +// Anonymize takes a JSON byte array and anonymizes its content by +// recursively replacing all values with a string indicating their type. +// +// It recurses into nested objects and arrays, but ignores the "schemas" and "id". +func Anonymize(data []byte, except ...string) []byte { + obj := make(map[string]any) + if err := json.Unmarshal(data, &obj); err != nil { + return []byte(fmt.Sprintf(`{"error": "invalid JSON", "message": %q}`, err.Error())) + } + + anonymize(obj, except...) + + out, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return []byte(fmt.Sprintf(`{"error": "could not marshal JSON shape", "message": %q}`, err.Error())) + } + + return out +} + +func anonymize(obj map[string]any, except ...string) { + for k, v := range obj { + if slices.Contains(except, k) { + continue + } + + switch v := v.(type) { + case []any: + for elIdx, el := range v { + switch el := el.(type) { + case map[string]any: + anonymize(el) + v[elIdx] = el + default: + v[elIdx] = jsonType(el) + } + } + + case map[string]any: + anonymize(v) + obj[k] = v + default: + obj[k] = jsonType(v) + } + } +} + +func jsonType(v any) string { + switch v := v.(type) { + case string: + return "string" + case float64: + return "number" + case bool: + return "boolean" + case nil: + return "null" + case []any: + return "array" + case map[string]any: + return "object" + default: + return fmt.Sprintf("%T", v) + } +} diff --git a/oryx/jsonx/decoder.go b/oryx/jsonx/decoder.go new file mode 100644 index 0000000..d7a00a1 --- /dev/null +++ b/oryx/jsonx/decoder.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "io" +) + +// NewStrictDecoder is a shorthand for json.Decoder.DisallowUnknownFields +func NewStrictDecoder(b io.Reader) *json.Decoder { + d := json.NewDecoder(b) + d.DisallowUnknownFields() + return d +} diff --git a/oryx/jsonx/embed.go b/oryx/jsonx/embed.go new file mode 100644 index 0000000..ae4f94c --- /dev/null +++ b/oryx/jsonx/embed.go @@ -0,0 +1,113 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/base64" + "encoding/json" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/x/osx" +) + +type options struct { + ignoreKeys []string + onlySchemes []string +} + +type OptionsModifier func(*options) + +func newOptions(o []OptionsModifier) *options { + opt := &options{} + for _, f := range o { + f(opt) + } + return opt +} + +func WithIgnoreKeys(keys ...string) OptionsModifier { + return func(o *options) { + o.ignoreKeys = keys + } +} + +func WithOnlySchemes(scheme ...string) OptionsModifier { + return func(o *options) { + o.onlySchemes = scheme + } +} + +func EmbedSources(in json.RawMessage, opts ...OptionsModifier) (out json.RawMessage, err error) { + out = make([]byte, len(in)) + copy(out, in) + if err := embed(gjson.ParseBytes(in), nil, &out, newOptions(opts)); err != nil { + return nil, err + } + return out, nil +} + +func embed(parsed gjson.Result, parents []string, result *json.RawMessage, o *options) (err error) { + if parsed.IsObject() { + parsed.ForEach(func(k, v gjson.Result) bool { + err = embed(v, append(parents, strings.ReplaceAll(k.String(), ".", "\\.")), result, o) + return err == nil + }) + if err != nil { + return err + } + } else if parsed.IsArray() { + for kk, vv := range parsed.Array() { + if err = embed(vv, append(parents, strconv.Itoa(kk)), result, o); err != nil { + return err + } + } + } else if parsed.Type != gjson.String { + return nil + } + + if len(parents) > 0 && slices.Contains(o.ignoreKeys, parents[len(parents)-1]) { + return nil + } + + loc, err := url.ParseRequestURI(parsed.String()) + if err != nil { + // Not a URL, return + return nil + } + + if len(o.onlySchemes) == 0 { + if loc.Scheme != "file" && loc.Scheme != "http" && loc.Scheme != "https" && loc.Scheme != "base64" { + // Not a known pattern, ignore + return nil + } + } else if !slices.Contains(o.onlySchemes, loc.Scheme) { + // Not a known pattern, ignore + return nil + } + + contents, err := osx.ReadFileFromAllSources(loc.String()) + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(contents) + key := strings.Join(parents, ".") + if key == "" { + key = "@" + } + + interim, err := sjson.SetBytes(*result, key, "base64://"+encoded) + if err != nil { + return err + } + + *result = interim + return +} diff --git a/oryx/jsonx/fixture/embed/1.json b/oryx/jsonx/fixture/embed/1.json new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/oryx/jsonx/fixture/embed/1.json @@ -0,0 +1 @@ +"foo" diff --git a/oryx/jsonx/fixture/embed/2.json b/oryx/jsonx/fixture/embed/2.json new file mode 100644 index 0000000..fab1a3b --- /dev/null +++ b/oryx/jsonx/fixture/embed/2.json @@ -0,0 +1,3 @@ +{ + "some": "key" +} diff --git a/oryx/jsonx/fixture/embed/3.json b/oryx/jsonx/fixture/embed/3.json new file mode 100644 index 0000000..7306c23 --- /dev/null +++ b/oryx/jsonx/fixture/embed/3.json @@ -0,0 +1,3 @@ +{ + "some_key": 1234 +} diff --git a/oryx/jsonx/fixture/embed/4.json b/oryx/jsonx/fixture/embed/4.json new file mode 100644 index 0000000..279ceab --- /dev/null +++ b/oryx/jsonx/fixture/embed/4.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + } + }, + "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + ] + } +} diff --git a/oryx/jsonx/fixture/embed/5.json b/oryx/jsonx/fixture/embed/5.json new file mode 100644 index 0000000..bfa283b --- /dev/null +++ b/oryx/jsonx/fixture/embed/5.json @@ -0,0 +1 @@ +"https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" diff --git a/oryx/jsonx/fixture/embed/6.json b/oryx/jsonx/fixture/embed/6.json new file mode 100644 index 0000000..1fde753 --- /dev/null +++ b/oryx/jsonx/fixture/embed/6.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "ignore_this_key": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + } + }, + "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + ] + } +} diff --git a/oryx/jsonx/flatten.go b/oryx/jsonx/flatten.go new file mode 100644 index 0000000..e4e04ba --- /dev/null +++ b/oryx/jsonx/flatten.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/tidwall/gjson" +) + +// Flatten flattens a JSON object using dot notation. +func Flatten(raw json.RawMessage) map[string]interface{} { + parsed := gjson.ParseBytes(raw) + if !parsed.IsObject() { + return nil + } + + flattened := make(map[string]interface{}) + flatten(parsed, nil, flattened) + return flattened +} + +func flatten(parsed gjson.Result, parents []string, flattened map[string]interface{}) { + if parsed.IsObject() { + parsed.ForEach(func(k, v gjson.Result) bool { + flatten(v, append(parents, strings.ReplaceAll(k.String(), ".", "\\.")), flattened) + return true + }) + } else if parsed.IsArray() { + for kk, vv := range parsed.Array() { + flatten(vv, append(parents, strconv.Itoa(kk)), flattened) + } + } else { + flattened[strings.Join(parents, ".")] = parsed.Value() + } +} diff --git a/oryx/jsonx/get.go b/oryx/jsonx/get.go new file mode 100644 index 0000000..24129d9 --- /dev/null +++ b/oryx/jsonx/get.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "reflect" + "strings" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func jsonKey(f reflect.StructField) *string { + if jsonTag := f.Tag.Get("json"); jsonTag != "" { + if jsonTag == "-" { + return nil + } + return &strings.Split(jsonTag, ",")[0] + } else if f.Anonymous { + return nil + } else if f.IsExported() { + return &f.Name + } + return nil +} + +// AllValidJSONKeys returns all JSON keys from the struct or *struct type. +// It does not return keys from nested slices, but embedded/nested structs. +func AllValidJSONKeys(s interface{}) (keys []string) { + t := reflect.TypeOf(s) + v := reflect.ValueOf(s) + if t.Kind() == reflect.Pointer { + t = t.Elem() + v = v.Elem() + } + for i := range t.NumField() { + f := t.Field(i) + jKey := jsonKey(f) + if k := f.Type.Kind(); k == reflect.Struct || k == reflect.Pointer { + subKeys := AllValidJSONKeys(v.Field(i).Interface()) + for _, subKey := range subKeys { + if jKey != nil { + keys = append(keys, *jKey+"."+subKey) + } else { + keys = append(keys, subKey) + } + } + } else if jKey != nil { + keys = append(keys, *jKey) + } + } + return keys +} + +// ParseEnsureKeys returns a result that has the GetRequireValidKey function. +func ParseEnsureKeys(original interface{}, raw []byte) *Result { + return &Result{ + keys: AllValidJSONKeys(original), + result: gjson.ParseBytes(raw), + } +} + +type Result struct { + result gjson.Result + keys []string +} + +// GetRequireValidKey ensures that the key is valid before returning the result. +func (r *Result) GetRequireValidKey(t require.TestingT, key string) gjson.Result { + require.Contains(t, r.keys, key) + return r.result.Get(key) +} + +func GetRequireValidKey(t require.TestingT, original interface{}, raw []byte, key string) gjson.Result { + return ParseEnsureKeys(original, raw).GetRequireValidKey(t, key) +} diff --git a/oryx/jsonx/helpers.go b/oryx/jsonx/helpers.go new file mode 100644 index 0000000..26ff3c7 --- /dev/null +++ b/oryx/jsonx/helpers.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshalJSONString(t *testing.T, i interface{}) string { + out, err := json.Marshal(i) + require.NoError(t, err) + return string(out) +} diff --git a/oryx/jsonx/patch.go b/oryx/jsonx/patch.go new file mode 100644 index 0000000..6fd185c --- /dev/null +++ b/oryx/jsonx/patch.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/gobwas/glob" + "github.com/pkg/errors" +) + +var opAllowList = map[string]struct{}{ + "add": {}, + "remove": {}, + "replace": {}, +} + +func isUnsupported(op jsonpatch.Operation) bool { + _, ok := opAllowList[op.Kind()] + + return !ok +} + +func isElementAccess(path string) bool { + if path == "" { + return false + } + elements := strings.Split(path, "/") + lastElement := elements[len(elements)-1:][0] + if lastElement == "-" { + return true + } + if _, err := strconv.Atoi(lastElement); err == nil { + return true + } + + return false +} + +// ApplyJSONPatch applies a JSON patch to an object and returns the modified +// object. The original object is not modified. It returns an error if the patch +// is invalid or if the patch includes paths that are denied. denyPaths is a +// list of path globs (interpreted with [glob.Compile] that are not allowed to +// be patched. +func ApplyJSONPatch[T any](p json.RawMessage, object T, denyPaths ...string) (result T, err error) { + patch, err := jsonpatch.DecodePatch(p) + if err != nil { + return result, errors.WithStack(err) + } + + denyPattern := fmt.Sprintf("{%s}", strings.ToLower(strings.Join(denyPaths, ","))) + matcher, err := glob.Compile(denyPattern, '/') + if err != nil { + return result, errors.WithStack(err) + } + + for _, op := range patch { + // Some operations are buggy, see https://github.com/evanphx/json-patch/pull/158 + if isUnsupported(op) { + return result, errors.Errorf("unsupported operation: %s", op.Kind()) + } + path, err := op.Path() + if err != nil { + return result, errors.Errorf("error parsing patch operations: %v", err) + } + if matcher.Match(strings.ToLower(path)) { + return result, errors.Errorf("patch includes denied path: %s", path) + } + + // JSON patch officially rejects replacing paths that don't exist, but we want to be more tolerant. + // Therefore, we will ensure that all paths that we want to replace exist in the original document. + if op.Kind() == "replace" && !isElementAccess(path) { + op["op"] = new(json.RawMessage(`"add"`)) + } + } + + original, err := json.Marshal(object) + if err != nil { + return result, errors.WithStack(err) + } + + options := jsonpatch.NewApplyOptions() + options.EnsurePathExistsOnAdd = true + + modified, err := patch.ApplyWithOptions(original, options) + if err != nil { + return result, errors.WithStack(err) + } + + err = json.Unmarshal(modified, &result) + return result, errors.WithStack(err) +} diff --git a/oryx/jsonx/stub/random.json b/oryx/jsonx/stub/random.json new file mode 100644 index 0000000..101ee9d --- /dev/null +++ b/oryx/jsonx/stub/random.json @@ -0,0 +1,64 @@ +{ + "floating": [ + -1273085434, + 953442581, + { + "ready": "silent", + "worker": "situation", + "joy": "difference", + "probably": -413625494, + "gray": { + "parent": "pull", + "shore": -738396277, + "usually": 1050049449, + "hold": [ + [ + 181518765, + [ + { + "steam": { + "box": false, + "cry": 1463961818, + "appropriate": 1249911539, + "through": 695239749, + "ago": true, + "entirely": -851427469 + }, + "leather": "across", + "flies": -1571371799, + "over": 512666854, + "thank": true, + "shaking": true + }, + "hit", + -648178744.4899056, + 1225027271, + -1481507228, + true + ], + -2114582277, + 1390060204.9360588, + 1615602630.9049141, + "darkness" + ], + 63427197.713988304, + -580344963.961421, + "stems", + 1016960217.612642, + 1240918909 + ], + "buy": true, + "wonder": false + }, + "little": "cloud" + }, + "grade", + false, + "thou" + ], + "wagon": -1722583702, + "shop": 1294397217, + "spend": "greatest", + "product": "whale", + "fall": "to" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json new file mode 100644 index 0000000..ecb9a86 --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "8d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/fetcher.go b/oryx/jwksx/fetcher.go new file mode 100644 index 0000000..4b476a3 --- /dev/null +++ b/oryx/jwksx/fetcher.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "encoding/json" + "net/http" + "sync" + + "github.com/go-jose/go-jose/v3" + "github.com/pkg/errors" +) + +// Fetcher is a small helper for fetching JSON Web Keys from remote endpoints. +// +// DEPRECATED: Use FetcherNext instead. +type Fetcher struct { + sync.RWMutex + remote string + c *http.Client + keys map[string]jose.JSONWebKey +} + +// NewFetcher returns a new fetcher that can download JSON Web Keys from remote endpoints. +// +// DEPRECATED: Use FetcherNext instead. +func NewFetcher(remote string) *Fetcher { + return &Fetcher{ + remote: remote, + c: http.DefaultClient, + keys: make(map[string]jose.JSONWebKey), + } +} + +// GetKey retrieves a JSON Web Key from the cache, fetches it from a remote if it is not yet cached or returns an error. +// +// DEPRECATED: Use FetcherNext instead. +func (f *Fetcher) GetKey(kid string) (*jose.JSONWebKey, error) { + f.RLock() + if k, ok := f.keys[kid]; ok { + f.RUnlock() + return &k, nil + } + f.RUnlock() + + res, err := f.c.Get(f.remote) + if err != nil { + return nil, errors.WithStack(err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("expected status code 200 but got %d when requesting %s", res.StatusCode, f.remote) + } + + var set jose.JSONWebKeySet + if err := json.NewDecoder(res.Body).Decode(&set); err != nil { + return nil, errors.WithStack(err) + } + + for _, k := range set.Keys { + f.Lock() + f.keys[k.KeyID] = k + f.Unlock() + } + + f.RLock() + defer f.RUnlock() + if k, ok := f.keys[kid]; ok { + return &k, nil + } + + return nil, errors.Errorf("unable to find JSON Web Key with ID: %s", kid) +} diff --git a/oryx/jwksx/fetcher_v2.go b/oryx/jwksx/fetcher_v2.go new file mode 100644 index 0000000..1e00622 --- /dev/null +++ b/oryx/jwksx/fetcher_v2.go @@ -0,0 +1,179 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "context" + "crypto/sha256" + "time" + + "github.com/ory/herodot" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/fetcher" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" + + "github.com/dgraph-io/ristretto/v2" + "github.com/lestrrat-go/jwx/jwk" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +var ErrUnableToFindKeyID = errors.New("specified JWK kid can not be found in the JWK sets") + +type ( + fetcherNextOptions struct { + forceKID string + cacheTTL time.Duration + useCache bool + httpClient *retryablehttp.Client + schemes []string + } + // FetcherNext is a JWK fetcher that can be used to fetch JWKs from multiple locations. + FetcherNext struct { + cache *ristretto.Cache[[]byte, jwk.Set] + } + // FetcherNextOption is a functional option for the FetcherNext. + FetcherNextOption func(*fetcherNextOptions) +) + +// NewFetcherNext returns a new FetcherNext instance. +func NewFetcherNext(cache *ristretto.Cache[[]byte, jwk.Set]) *FetcherNext { + return &FetcherNext{ + cache: cache, + } +} + +// WithForceKID forces the key ID to be used. Required when multiple JWK sets are configured. +func WithForceKID(kid string) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.forceKID = kid + } +} + +// WithCacheTTL sets the cache TTL. If not set, the TTL is unlimited. +func WithCacheTTL(ttl time.Duration) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.cacheTTL = ttl + } +} + +// WithCacheEnabled enables the cache. +func WithCacheEnabled() FetcherNextOption { + return func(o *fetcherNextOptions) { + o.useCache = true + } +} + +func WithAllowedSchemes(schemes ...string) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.schemes = schemes + } +} + +// WithHTTPClient will use the given HTTP client to fetch the JSON Web Keys. +func WithHTTPClient(c *retryablehttp.Client) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.httpClient = c + } +} + +func (f *FetcherNext) ResolveKey(ctx context.Context, locations string, modifiers ...FetcherNextOption) (jwk.Key, error) { + return f.ResolveKeyFromLocations(ctx, []string{locations}, modifiers...) +} + +func (f *FetcherNext) ResolveKeyFromLocations(ctx context.Context, locations []string, modifiers ...FetcherNextOption) (jwk.Key, error) { + opts := new(fetcherNextOptions) + for _, m := range modifiers { + m(opts) + } + + if len(locations) > 1 && opts.forceKID == "" { + return nil, errors.Errorf("a key ID must be specified when multiple JWK sets are configured") + } + + set := jwk.NewSet() + eg := new(errgroup.Group) + for k := range locations { + location := locations[k] + eg.Go(func() error { + remoteSet, err := f.fetch(ctx, location, opts) + if err != nil { + return err + } + + iterator := remoteSet.Iterate(ctx) + for iterator.Next(ctx) { + // Pair().Value is always of type jwk.Key when generated by Iterate. + set.Add(iterator.Pair().Value.(jwk.Key)) + } + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + if opts.forceKID != "" { + key, found := set.LookupKeyID(opts.forceKID) + if !found { + return nil, errors.WithStack(ErrUnableToFindKeyID) + } + + return key, nil + } + + // No KID was forced? Use the first key we can find. + key, found := set.Get(0) + if !found { + return nil, errors.WithStack(ErrUnableToFindKeyID) + } + + return key, nil +} + +// fetch fetches the JWK set from the given location and if enabled, may use the cache to look up the JWK set. +func (f *FetcherNext) fetch(ctx context.Context, location string, opts *fetcherNextOptions) (_ jwk.Set, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jwksx.FetcherNext.fetch", trace.WithAttributes(attribute.String("location", location))) + defer otelx.End(span, &err) + + cacheKey := sha256.Sum256([]byte(location)) + if opts.useCache { + if result, found := f.cache.Get(cacheKey[:]); found { + return result, nil + } + } + + var fopts []fetcher.Modifier + if opts.httpClient != nil { + fopts = append(fopts, fetcher.WithClient(opts.httpClient)) + } + if len(opts.schemes) > 0 { + fopts = append(fopts, fetcher.WithAllowedSchemes(opts.schemes...)) + } + + result, err := fetcher.NewFetcher(fopts...).FetchContext(ctx, location) + if err != nil { + return nil, err + } + + set, err := jwk.ParseReader(result) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("failed to parse JWK set").WithWrap(err)) + } + + if opts.useCache { + f.cache.SetWithTTL(cacheKey[:], set, 1, opts.cacheTTL) + } + + return set, nil +} diff --git a/oryx/jwksx/generator.go b/oryx/jwksx/generator.go new file mode 100644 index 0000000..7fabb11 --- /dev/null +++ b/oryx/jwksx/generator.go @@ -0,0 +1,129 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "io" + + "github.com/go-jose/go-jose/v3" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "golang.org/x/crypto/ed25519" +) + +// GenerateSigningKeys generates a JSON Web Key Set for signing. +func GenerateSigningKeys(id, alg string, bits int) (*jose.JSONWebKeySet, error) { + if id == "" { + id = uuid.Must(uuid.NewV4()).String() + } + + key, err := generate(jose.SignatureAlgorithm(alg), bits) + if err != nil { + return nil, err + } + + return &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Algorithm: alg, + Use: "sig", + Key: key, + KeyID: id, + Certificates: []*x509.Certificate{}, + }, + }, + }, nil +} + +// GenerateSigningKeysAvailableAlgorithms lists available algorithms that are supported by GenerateSigningKeys. +func GenerateSigningKeysAvailableAlgorithms() []string { + return []string{ + string(jose.HS256), string(jose.HS384), string(jose.HS512), + string(jose.ES256), string(jose.ES384), string(jose.ES512), string(jose.EdDSA), + string(jose.RS256), string(jose.RS384), string(jose.RS512), string(jose.PS256), string(jose.PS384), string(jose.PS512), + } +} + +// generate generates keypair for corresponding SignatureAlgorithm. +func generate(alg jose.SignatureAlgorithm, bits int) (crypto.PrivateKey, error) { + switch alg { + case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA: + keylen := map[jose.SignatureAlgorithm]int{ + jose.ES256: 256, + jose.ES384: 384, + jose.ES512: 521, // sic! + jose.EdDSA: 256, + } + if bits != 0 && bits != keylen[alg] { + return nil, errors.Errorf(`jwksx: "%s" does not support arbitrary key length`, alg) + } + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, errors.Errorf(`jwksx: key size must be at least 2048 bit for algorithm "%s"`, alg) + } + case jose.HS256: + if bits == 0 { + bits = 256 + } + if bits < 256 { + return nil, errors.Errorf(`jwksx: key size must be at least 256 bit for algorithm "%s"`, alg) + } + case jose.HS384: + if bits == 0 { + bits = 384 + } + if bits < 384 { + return nil, errors.Errorf(`jwksx: key size must be at least 2038448 bit for algorithm "%s"`, alg) + } + case jose.HS512: + if bits == 0 { + bits = 1024 + } + if bits < 512 { + return nil, errors.Errorf(`jwksx: key size must be at least 512 bit for algorithm "%s"`, alg) + } + } + + switch alg { + case jose.ES256: + // The cryptographic operations are implemented using constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.ES384: + // NB: The cryptographic operations do not use constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.ES512: + // NB: The cryptographic operations do not use constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.EdDSA: + _, key, err := ed25519.GenerateKey(rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + key, err := rsa.GenerateKey(rand.Reader, bits) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.HS256, jose.HS384, jose.HS512: + if bits%8 != 0 { + return nil, errors.Errorf(`jwksx: key size must be a multiple of 8 for algorithm "%s" but got: %d`, alg, bits) + } + + key := make([]byte, bits/8) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return nil, errors.Wrapf(err, "jwks: unable to generate key") + } + return key, nil + default: + return nil, errors.Errorf(`jwksx: available algorithms are "%+v" but unknown algorithm was requested: "%s"`, GenerateSigningKeysAvailableAlgorithms(), alg) + } +} diff --git a/oryx/jwtmiddleware/middleware.go b/oryx/jwtmiddleware/middleware.go new file mode 100644 index 0000000..4fd80d0 --- /dev/null +++ b/oryx/jwtmiddleware/middleware.go @@ -0,0 +1,155 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwtmiddleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/urfave/negroni" + + "github.com/ory/x/jwksx" +) + +type Middleware struct { + o *middlewareOptions + wku string + jm *jwtmiddleware.JWTMiddleware +} + +type middlewareOptions struct { + Debug bool + ExcludePaths []string + SigningMethod jwt.SigningMethod + ErrorWriter herodot.Writer +} + +type MiddlewareOption func(*middlewareOptions) + +func SessionFromContext(ctx context.Context) (json.RawMessage, error) { + raw := ctx.Value(jwtmiddleware.ContextKey{}) + if raw == nil { + return nil, errors.WithStack(herodot.ErrUnauthorized().WithReasonf("Could not find credentials in the request.")) + } + + token, ok := raw.(*jwt.Token) + if !ok { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithDebugf(`Expected context key "%T" to transport value of type *jwt.MapClaims but got type: %T`, jwtmiddleware.ContextKey{}, raw)) + } + + session, err := json.Marshal(token.Claims) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithDebugf("Unable to encode session data: %s", err)) + } + + return session, nil +} + +func MiddlewareDebugEnabled() MiddlewareOption { + return func(o *middlewareOptions) { + o.Debug = true + } +} + +func MiddlewareExcludePaths(paths ...string) MiddlewareOption { + return func(o *middlewareOptions) { + o.ExcludePaths = append(o.ExcludePaths, paths...) + } +} + +func MiddlewareAllowSigningMethod(method jwt.SigningMethod) MiddlewareOption { + return func(o *middlewareOptions) { + o.SigningMethod = method + } +} + +func MiddlewareErrorWriter(w herodot.Writer) MiddlewareOption { + return func(o *middlewareOptions) { + o.ErrorWriter = w + } +} + +func NewMiddleware( + wellKnownURL string, + opts ...MiddlewareOption, +) *Middleware { + c := &middlewareOptions{ + SigningMethod: jwt.SigningMethodES256, + ErrorWriter: herodot.NewJSONWriter(nil), + } + + for _, o := range opts { + o(c) + } + jc := jwksx.NewFetcher(wellKnownURL) + return &Middleware{ + o: c, + wku: wellKnownURL, + jm: jwtmiddleware.New( + func(ctx context.Context, rawToken string) (any, error) { + return jwt.NewParser( + jwt.WithValidMethods([]string{c.SigningMethod.Alg()}), + ).Parse(rawToken, func(token *jwt.Token) (interface{}, error) { + if raw, ok := token.Header["kid"]; !ok { + return nil, errors.New(`jwt from authorization HTTP header is missing value for "kid" in token header`) + } else if kid, ok := raw.(string); !ok { + return nil, fmt.Errorf(`jwt from authorization HTTP header is expecting string value for "kid" in tokenWithoutKid header but got: %T`, raw) + } else if k, err := jc.GetKey(kid); err != nil { + return nil, err + } else { + return k.Key, nil + } + }) + }, + jwtmiddleware.WithCredentialsOptional(false), + jwtmiddleware.WithTokenExtractor(func(r *http.Request) (string, error) { + // wrapping the extractor to get a herodot.ErrorContainer + token, err := jwtmiddleware.AuthHeaderTokenExtractor(r) + if err != nil { + return "", herodot.ErrUnauthorized().WithReason(err.Error()) + } + return token, nil + }), + jwtmiddleware.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, jwtmiddleware.ErrJWTInvalid): + reason := "The token is invalid or expired." + if err := errors.Unwrap(err); err != nil { + reason = err.Error() + } + c.ErrorWriter.WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithReason(reason))) + case errors.Is(err, jwtmiddleware.ErrJWTMissing): + c.ErrorWriter.WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithReason("The token is missing."))) + default: + c.ErrorWriter.WriteError(w, r, err) + } + }), + ), + } +} + +// Deprecated: use Middleware as a negroni.Handler directly instead. +func (h *Middleware) NegroniHandler() negroni.Handler { + return negroni.HandlerFunc(h.ServeHTTP) +} + +func (h *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + for _, excluded := range h.o.ExcludePaths { + if strings.HasPrefix(r.URL.Path, excluded) { + next(w, r) + return + } + } + + h.jm.CheckJWT(next).ServeHTTP(w, r) +} diff --git a/oryx/jwtmiddleware/stub/jwks.json b/oryx/jwtmiddleware/stub/jwks.json new file mode 100644 index 0000000..57d130c --- /dev/null +++ b/oryx/jwtmiddleware/stub/jwks.json @@ -0,0 +1,10 @@ +{ + "use": "sig", + "kty": "EC", + "kid": "b71ff5bd-a016-4ac0-9f3f-a172552578ea", + "crv": "P-256", + "alg": "ES256", + "x": "7fVj_SeCx3TnkHANRWrpEho9BcYkU953LHUvKsSF5Wo", + "y": "2A9D_AAFPiJQLSJQ_h600Fy9jUrg9Q88gNPPZwHDb7o", + "d": "sRl-e-tGEVsNBF8FgEado9NAEipxhAFXGMryWDgbUMo" +} diff --git a/oryx/jwtx/claims.go b/oryx/jwtx/claims.go new file mode 100644 index 0000000..7bbbca9 --- /dev/null +++ b/oryx/jwtx/claims.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwtx + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/mapx" +) + +// Claims represents a JSON Web Token's standard claims. +type Claims struct { + // Audience identifies the recipients that the JWT is intended for. + Audience []string `json:"aud"` + + // Issuer identifies the principal that issued the JWT. + Issuer string `json:"iss"` + + // Subject identifies the principal that is the subject of the JWT. + Subject string `json:"sub"` + + // ExpiresAt identifies the expiration time on or after which the JWT most not be accepted for processing. + ExpiresAt time.Time `json:"exp"` + + // IssuedAt identifies the time at which the JWT was issued. + IssuedAt time.Time `json:"iat"` + + // NotBefore identifies the time before which the JWT must not be accepted for processing. + NotBefore time.Time `json:"nbf"` + + // JTI provides a unique identifier for the JWT. + JTI string `json:"jti"` +} + +// ParseMapStringInterfaceClaims converts map[string]interface{} to *Claims. +func ParseMapStringInterfaceClaims(claims map[string]interface{}) *Claims { + c := make(map[interface{}]interface{}) + for k, v := range claims { + c[k] = v + } + return ParseMapInterfaceInterfaceClaims(c) +} + +// ParseMapInterfaceInterfaceClaims converts map[interface{}]interface{} to *Claims. +func ParseMapInterfaceInterfaceClaims(claims map[interface{}]interface{}) *Claims { + result := &Claims{ + Issuer: mapx.GetStringDefault(claims, "iss", ""), + Subject: mapx.GetStringDefault(claims, "sub", ""), + JTI: mapx.GetStringDefault(claims, "jti", ""), + } + + if aud, err := mapx.GetString(claims, "aud"); err == nil { + result.Audience = []string{aud} + } else if errors.Is(err, mapx.ErrKeyCanNotBeTypeAsserted) { + if aud, err := mapx.GetStringSlice(claims, "aud"); err == nil { + result.Audience = aud + } else { + result.Audience = []string{} + } + } else { + result.Audience = []string{} + } + + if exp, err := mapx.GetTime(claims, "exp"); err == nil { + result.ExpiresAt = exp + } + + if iat, err := mapx.GetTime(claims, "iat"); err == nil { + result.IssuedAt = iat + } + + if nbf, err := mapx.GetTime(claims, "nbf"); err == nil { + result.NotBefore = nbf + } + + return result +} diff --git a/oryx/landlockx/landlockx.go b/oryx/landlockx/landlockx.go new file mode 100644 index 0000000..99f2e84 --- /dev/null +++ b/oryx/landlockx/landlockx.go @@ -0,0 +1,149 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package landlockx + +import ( + "os" + "path/filepath" + + ll "github.com/landlock-lsm/go-landlock/landlock" + llsyscall "github.com/landlock-lsm/go-landlock/landlock/syscall" + + "github.com/ory/x/logrusx" +) + +// IsSupported returns true if the running kernel supports Landlock LSM. +// On non-Linux platforms or kernels older than 5.13, this returns false. +func IsSupported() bool { + v, err := llsyscall.LandlockGetABIVersion() + return err == nil && v > 0 +} + +// runningExe returns the kernel-canonical path of the running binary. +// jsonnetsecure re-execs the running binary as a sandboxed worker, so +// the rule we attach must use the inode the kernel resolves at execve +// time. os.Executable on Linux returns the readlink of /proc/self/exe; +// EvalSymlinks resolves any further symlinks so the rule attaches to +// the actual file inode. +func runningExe() string { + exe, err := os.Executable() + if err != nil { + return "" + } + if real, err := filepath.EvalSymlinks(exe); err == nil { + return real + } + return exe +} + +// Apply activates the Landlock LSM filesystem sandbox for the calling process. +// +// roPaths is a list of paths that the process may read. Each path is stat-ed +// to determine whether to apply directory or file access rights; non-existent +// paths are silently skipped. +// +// rwDirs is a list of directories that the process may read and write +// recursively. Missing paths are silently ignored. +// +// Apply is a no-op on non-Linux platforms and on Linux kernels older than +// 5.13 (BestEffort degrades gracefully). +func Apply(l *logrusx.Logger, roPaths, rwDirs []string) error { + rules := defaultRules() + + for _, p := range roPaths { + info, err := os.Stat(p) + if err != nil { + continue + } + if info.IsDir() { + rules = append(rules, ll.RODirs(p)) + } else { + rules = append(rules, ll.ROFiles(p)) + } + } + + for _, d := range rwDirs { + rules = append(rules, ll.RWDirs(d).IgnoreIfMissing()) + } + + if !IsSupported() { + l.Warn("Landlock LSM is not available on this kernel; filesystem sandbox not applied.") + return nil + } + + if err := ll.V3.BestEffort().RestrictPaths(rules...); err != nil { + return err + } + + l.Info("Landlock filesystem sandbox is active.") + return nil +} + +// ApplyEmpty activates a Landlock filesystem sandbox with no allowed +// paths, denying every filesystem access the V3 ABI handles. Already-open +// file descriptors (stdin, stdout, stderr inherited from the parent) +// keep working, but no path-based open/read/write/exec is permitted. +// +// Use it inside helper subprocesses that only need stdio — for example, +// the hidden `jsonnet` worker that jsonnetsecure re-execs to evaluate +// mappers in isolation. +// +// Landlock layers stack: this call adds a strictly-more-restrictive +// layer on top of whatever the parent already enforced. +// +// ApplyEmpty is a no-op on non-Linux platforms and on Linux kernels +// older than 5.13. +func ApplyEmpty(l *logrusx.Logger) error { + if !IsSupported() { + if l != nil { + l.Warn("Landlock LSM is not available on this kernel; empty sandbox not applied.") + } + return nil + } + if err := ll.V3.BestEffort().RestrictPaths(); err != nil { + return err + } + if l != nil { + l.Info("Landlock empty filesystem sandbox is active.") + } + return nil +} + +// defaultRules returns the rules every kratos serve process needs regardless +// of operator config: +// - /dev/null is a kernel-managed sink used by subprocess plumbing +// (exec.Command stdin redirection, log discarding, etc.); it cannot +// leak data. +// - /etc/resolv.conf carries the DNS nameserver list and /etc/hosts +// the static host table; Go's pure-Go resolver reads both for every +// hostname lookup. /etc/nsswitch.conf is intentionally not granted: +// the binary is built with CGO_ENABLED=0, so when nsswitch.conf is +// unreadable the resolver falls back to "files then DNS", which is +// what we want anyway. +// - the running binary is re-exec'd by jsonnetsecure for sandboxed mapper +// evaluation, so it needs read+execute. The binary must be statically +// linked (CGO_ENABLED=0) — execve of a dynamically-linked binary also +// requires read+execute on the system dynamic linker (PT_INTERP) and +// its NEEDED libraries, which we deliberately do not grant. +// +// /etc/ssl (system trust roots for outbound HTTPS) is intentionally not +// included. Cloud binaries blank-import +// golang.org/x/crypto/x509roots/fallback (see cloudlib/x509_roots.go) +// and set `godebug x509usefallbackroots=1` in their go.mod, so +// crypto/x509 ships its own embedded Mozilla bundle and never reads +// /etc/ssl at runtime. Operators who need to trust an additional CA +// continue to set SSL_CERT_FILE / SSL_CERT_DIR and list the file under +// security.landlock.allowed_paths. +func defaultRules() []ll.Rule { + rules := []ll.Rule{ + ll.RWFiles("/dev/null"), + ll.ROFiles("/etc/resolv.conf").IgnoreIfMissing(), + ll.ROFiles("/etc/hosts").IgnoreIfMissing(), + } + if exe := runningExe(); exe != "" { + access := ll.AccessFSSet(llsyscall.AccessFSReadFile | llsyscall.AccessFSExecute) + rules = append(rules, ll.PathAccess(access, exe).IgnoreIfMissing()) + } + return rules +} diff --git a/oryx/landlockx/paths.go b/oryx/landlockx/paths.go new file mode 100644 index 0000000..1ccfd4c --- /dev/null +++ b/oryx/landlockx/paths.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package landlockx + +import ( + "path/filepath" + "strings" + + "github.com/ory/x/sqlxx" +) + +// SQLiteDirFromDSN returns the directory containing the SQLite database +// referenced by the DSN. Granting the directory once is simpler and more +// robust than enumerating every sibling: SQLite manages -journal in +// rollback mode and -wal/-shm in WAL mode, plus transient -mj-XXXXX +// rollback masters during multi-database checkpoints — the latter use +// random suffixes that a per-file rule cannot cover. +// +// Returns an empty string for non-SQLite DSNs and for the in-memory +// SQLite DSN. +func SQLiteDirFromDSN(dsn string) string { + scheme, path, err := sqlxx.ExtractSchemeFromDSN(dsn) + if err != nil || (scheme != "sqlite" && scheme != "sqlite3") { + return "" + } + path, _, _ = strings.Cut(path, "?") + if path == "" || strings.Contains(path, ":memory:") { + return "" + } + return filepath.Dir(path) +} diff --git a/oryx/logrusx/config.schema.json b/oryx/logrusx/config.schema.json new file mode 100644 index 0000000..568ea90 --- /dev/null +++ b/oryx/logrusx/config.schema.json @@ -0,0 +1,43 @@ +{ + "$id": "ory://logging-config", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Log", + "description": "Configure logging using the following options. Logs will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "title": "Level", + "description": "The level of log entries to show. Debug enables stack traces on errors.", + "type": "string", + "default": "info", + "enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"] + }, + "format": { + "title": "Log Format", + "description": "The output format of log messages.", + "type": "string", + "default": "text", + "enum": ["json", "json_pretty", "gelf", "text"] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs.", + "default": false + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "additional_redacted_headers": { + "type": "array", + "title": "Additional redacted headers", + "description": "List of HTTP headers which will be redacted.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/oryx/logrusx/helper.go b/oryx/logrusx/helper.go new file mode 100644 index 0000000..551346a --- /dev/null +++ b/oryx/logrusx/helper.go @@ -0,0 +1,293 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logrusx + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "reflect" + "strings" + "sync" + + "github.com/sirupsen/logrus" + + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/errorsx" +) + +// loggerConfig holds the mutable state that can change after construction via +// UseConfig. It is accessed via a pointer so all shallow copies of Logger +// (WithField, WithFields, WithContext, NewEntry) share and see the same config. +type loggerConfig struct { + mu sync.RWMutex + leakSensitive bool + redactionText string + additionalRedactedHeaders map[string]struct{} +} + +type ( + Logger struct { + *logrus.Entry + cfg *loggerConfig + opts []Option + } + Provider interface { + Logger() *Logger + } +) + +var opts = otelhttptrace.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + +func (l *Logger) Logrus() *logrus.Logger { + return l.Entry.Logger +} + +func (l *Logger) NewEntry() *Logger { + ll := *l + ll.Entry = logrus.NewEntry(l.Logger) + return &ll +} + +func (l *Logger) WithContext(ctx context.Context) *Logger { + ll := *l + ll.Entry = l.Logger.WithContext(ctx) + return &ll +} + +func (l *Logger) HTTPHeadersRedacted(h http.Header) map[string]any { + // Snapshot the mutable config fields once under the read lock so we neither + // hold the lock during iteration nor risk a concurrent map write via UseConfig. + l.cfg.mu.RLock() + leakSensitive := l.cfg.leakSensitive + redactionText := l.cfg.redactionText + additionalHeaders := l.cfg.additionalRedactedHeaders + l.cfg.mu.RUnlock() + + headers := map[string]any{} + + for key, value := range h { + switch keyLower := strings.ToLower(key); keyLower { + case "authorization", "cookie", "set-cookie", "x-session-token": + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + case "location": + locationURL, err := url.Parse(h.Get("Location")) + if err != nil { + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + continue + } + if leakSensitive { + headers[keyLower] = locationURL.String() + } else { + locationURL.RawQuery = "" + locationURL.Fragment = "" + headers[keyLower] = locationURL.Redacted() + } + default: + if _, ok := additionalHeaders[keyLower]; ok { + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + continue + } + headers[keyLower] = h.Get(key) + } + } + + return headers +} + +func (l *Logger) WithRequest(r *http.Request) *Logger { + headers := l.HTTPHeadersRedacted(r.Header) + if ua := r.UserAgent(); len(ua) > 0 { + headers["user-agent"] = ua + } + + scheme := "https" + if r.TLS == nil { + scheme = "http" + } + + ll := l.WithField("http_request", map[string]any{ + "remote": r.RemoteAddr, + "method": r.Method, + "path": r.URL.EscapedPath(), + "query": l.maybeRedact(r.URL.RawQuery), + "scheme": scheme, + "host": r.Host, + "headers": headers, + }) + + spanCtx := trace.SpanContextFromContext(r.Context()) + if !spanCtx.IsValid() { + _, _, spanCtx = otelhttptrace.Extract(r.Context(), r, opts) + } + if spanCtx.IsValid() { + traces := make(map[string]string, 2) + if spanCtx.HasTraceID() { + traces["trace_id"] = spanCtx.TraceID().String() + } + if spanCtx.HasSpanID() { + traces["span_id"] = spanCtx.SpanID().String() + } + ll = ll.WithField("otel", traces) + } + return ll +} + +func (l *Logger) WithSpanFromContext(ctx context.Context) *Logger { + spanCtx := trace.SpanContextFromContext(ctx) + if !spanCtx.IsValid() { + return l + } + + traces := make(map[string]string, 2) + if spanCtx.HasTraceID() { + traces["trace_id"] = spanCtx.TraceID().String() + } + if spanCtx.HasSpanID() { + traces["span_id"] = spanCtx.SpanID().String() + } + return l.WithField("otel", traces) +} + +func (l *Logger) Logf(level logrus.Level, format string, args ...any) { + l.cfg.mu.RLock() + leakSensitive := l.cfg.leakSensitive + l.cfg.mu.RUnlock() + + if !leakSensitive { + for i, arg := range args { + switch urlArg := arg.(type) { + case url.URL: + urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path} + args[i] = urlCopy + case *url.URL: + urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path} + args[i] = &urlCopy + default: + continue + } + } + } + l.Entry.Logf(level, format, args...) +} + +func (l *Logger) Tracef(format string, args ...any) { + l.Logf(logrus.TraceLevel, format, args...) +} + +func (l *Logger) Debugf(format string, args ...any) { + l.Logf(logrus.DebugLevel, format, args...) +} + +func (l *Logger) Infof(format string, args ...any) { + l.Logf(logrus.InfoLevel, format, args...) +} + +func (l *Logger) Warnf(format string, args ...any) { + l.Logf(logrus.WarnLevel, format, args...) +} + +func (l *Logger) Errorf(format string, args ...any) { + l.Logf(logrus.ErrorLevel, format, args...) +} + +func (l *Logger) Fatalf(format string, args ...any) { + l.Logf(logrus.FatalLevel, format, args...) + l.Entry.Logger.Exit(1) +} + +func (l *Logger) Panicf(format string, args ...any) { + l.Logf(logrus.PanicLevel, format, args...) +} + +func (l *Logger) WithFields(f logrus.Fields) *Logger { + ll := *l + ll.Entry = l.Entry.WithFields(f) + return &ll +} + +func (l *Logger) WithField(key string, value any) *Logger { + ll := *l + ll.Entry = l.Entry.WithField(key, value) + return &ll +} + +// maybeRedactWith is the lock-free redaction helper; callers must supply +// pre-snapshotted values of leakSensitive and redactionText. +func maybeRedactWith(value any, leakSensitive bool, redactionText string) any { + if fmt.Sprintf("%v", value) == "" || value == nil { + return nil + } + if !leakSensitive { + return redactionText + } + return value +} + +func (l *Logger) maybeRedact(value any) any { + l.cfg.mu.RLock() + leak := l.cfg.leakSensitive + text := l.cfg.redactionText + l.cfg.mu.RUnlock() + return maybeRedactWith(value, leak, text) +} + +func (l *Logger) WithSensitiveField(key string, value any) *Logger { + return l.WithField(key, l.maybeRedact(value)) +} + +func (l *Logger) WithError(err error) *Logger { + if err == nil { + return l + } + + ctx := map[string]any{"message": err.Error()} + if l.Entry.Logger.IsLevelEnabled(logrus.DebugLevel) { + if e, ok := err.(errorsx.StackTracer); ok { + ctx["stack_trace"] = fmt.Sprintf("%+v", e.StackTrace()) + } else { + ctx["stack_trace"] = fmt.Sprintf("stack trace could not be recovered from error type %s", reflect.TypeOf(err)) + } + } + if c := errorsx.ReasonCarrier(nil); errors.As(err, &c) { + ctx["reason"] = c.Reason() + } + if c := errorsx.RequestIDCarrier(nil); errors.As(err, &c) && c.RequestID() != "" { + ctx["request_id"] = c.RequestID() + } + if c := errorsx.DetailsCarrier(nil); errors.As(err, &c) && c.Details() != nil { + ctx["details"] = c.Details() + } + if c := errorsx.StatusCarrier(nil); errors.As(err, &c) && c.Status() != "" { + ctx["status"] = c.Status() + } + if c := errorsx.StatusCodeCarrier(nil); errors.As(err, &c) && c.StatusCode() != 0 { + ctx["status_code"] = c.StatusCode() + } + if c := errorsx.DebugCarrier(nil); errors.As(err, &c) { + ctx["debug"] = c.Debug() + } + + return l.WithField("error", ctx) +} + +func (l *Logger) StdLogger(lvl logrus.Level) *log.Logger { + return log.New(writer{l.Logger, lvl}, "", 0) +} + +type writer struct { + l *logrus.Logger + lvl logrus.Level +} + +func (w writer) Write(p []byte) (n int, err error) { + w.l.Log(w.lvl, strings.TrimSuffix(string(p), "\n")) + return len(p), nil +} diff --git a/oryx/logrusx/logrus.go b/oryx/logrusx/logrus.go new file mode 100644 index 0000000..eff9a29 --- /dev/null +++ b/oryx/logrusx/logrus.go @@ -0,0 +1,278 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logrusx + +import ( + "bytes" + "cmp" + _ "embed" + "io" + "maps" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + + gelf "github.com/seatgeek/logrus-gelf-formatter" + + "github.com/ory/x/stringsx" +) + +type ( + options struct { + l *logrus.Logger + level *logrus.Level + formatter logrus.Formatter + format string + reportCaller bool + exitFunc func(int) + leakSensitive bool + redactionText string + additionalRedactedHeaders []string + hooks []logrus.Hook + c configurator + } + Option func(*options) + nullConfigurator struct{} + configurator interface { + Bool(key string) bool + String(key string) string + Strings(path string) []string + } +) + +//go:embed config.schema.json +var ConfigSchema string + +const ConfigSchemaID = "ory://logging-config" + +// AddConfigSchema adds the logging schema to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddConfigSchema(c interface { + AddResource(url string, r io.Reader) error +}, +) error { + return c.AddResource(ConfigSchemaID, bytes.NewBufferString(ConfigSchema)) +} + +func newLogger(parent *logrus.Logger, o *options) *logrus.Logger { + l := parent + if l == nil { + l = logrus.New() + } + + if o.exitFunc != nil { + l.ExitFunc = o.exitFunc + } + + for _, hook := range o.hooks { + l.AddHook(hook) + } + + setLevel(l, o) + setFormatter(l, o) + + l.ReportCaller = o.reportCaller || l.IsLevelEnabled(logrus.TraceLevel) + return l +} + +func setLevel(l *logrus.Logger, o *options) { + if o.level != nil { + l.SetLevel(*o.level) + } else { + level, err := logrus.ParseLevel(cmp.Or( + o.c.String("log.level"), + os.Getenv("LOG_LEVEL"))) + if err != nil { + level = logrus.InfoLevel + } + l.SetLevel(level) + } +} + +func setFormatter(l *logrus.Logger, o *options) { + if o.formatter != nil { + l.SetFormatter(o.formatter) + } else { + var unknownFormat bool // we first have to set the formatter before we can complain about the unknown format + var formatter logrus.Formatter + + format := stringsx.SwitchExact(cmp.Or(o.format, o.c.String("log.format"), os.Getenv("LOG_FORMAT"))) + switch { + case format.AddCase("json"): + formatter = &logrus.JSONFormatter{PrettyPrint: false, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true} + case format.AddCase("json_pretty"): + formatter = &logrus.JSONFormatter{PrettyPrint: true, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true} + case format.AddCase("gelf"): + formatter = new(gelf.GelfFormatter) + default: + unknownFormat = true + fallthrough + case format.AddCase("text", ""): + formatter = &logrus.TextFormatter{ + DisableQuote: true, + DisableTimestamp: false, + FullTimestamp: true, + } + } + + l.SetFormatter(formatter) + + if unknownFormat { + l.WithError(format.ToUnknownCaseErr()).Warn("got unknown \"log.format\", falling back to \"text\"") + } + } +} + +func ForceLevel(level logrus.Level) Option { + return func(o *options) { + o.level = &level + } +} + +func ForceFormatter(formatter logrus.Formatter) Option { + return func(o *options) { + o.formatter = formatter + } +} + +func WithConfigurator(c configurator) Option { + return func(o *options) { + o.c = c + } +} + +func ForceFormat(format string) Option { + return func(o *options) { + o.format = format + } +} + +func WithHook(hook logrus.Hook) Option { + return func(o *options) { + o.hooks = append(o.hooks, hook) + } +} + +func WithExitFunc(exitFunc func(int)) Option { + return func(o *options) { + o.exitFunc = exitFunc + } +} + +func ReportCaller(reportCaller bool) Option { + return func(o *options) { + o.reportCaller = reportCaller + } +} + +func UseLogger(l *logrus.Logger) Option { + return func(o *options) { + o.l = l + } +} + +func LeakSensitive() Option { + return func(o *options) { + o.leakSensitive = true + } +} + +func RedactionText(text string) Option { + return func(o *options) { + o.redactionText = text + } +} + +func WithAdditionalRedactedHeaders(headers []string) Option { + return func(o *options) { + o.additionalRedactedHeaders = headers + } +} + +func toHeaderMap(headers []string) map[string]struct{} { + m := make(map[string]struct{}, len(headers)) + for _, h := range headers { + m[strings.ToLower(h)] = struct{}{} + } + return m +} + +func (c *nullConfigurator) Bool(_ string) bool { return false } +func (c *nullConfigurator) String(_ string) string { return "" } +func (c *nullConfigurator) Strings(_ string) []string { return []string{} } + +func newOptions(opts []Option) *options { + o := new(options) + o.c = new(nullConfigurator) + for _, f := range opts { + f(o) + } + return o +} + +// New creates a new logger with all the important fields set. +func New(name string, version string, opts ...Option) *Logger { + o := newOptions(opts) + return &Logger{ + opts: opts, + cfg: &loggerConfig{ + leakSensitive: o.leakSensitive || o.c.Bool("log.leak_sensitive_values"), + redactionText: cmp.Or(o.redactionText, `Value is sensitive and has been redacted. To see the value set config key "log.leak_sensitive_values = true" or environment variable "LOG_LEAK_SENSITIVE_VALUES=true".`), + additionalRedactedHeaders: toHeaderMap(func() []string { + if len(o.additionalRedactedHeaders) > 0 { + return o.additionalRedactedHeaders + } + return o.c.Strings("log.additional_redacted_headers") + }()), + }, + Entry: newLogger(o.l, o).WithFields(logrus.Fields{ + "audience": "application", "service_name": name, "service_version": version, + }), + } +} + +func NewT(t testing.TB, opts ...Option) *Logger { + opts = append(opts, LeakSensitive(), WithExitFunc(func(code int) { + t.Fatalf("Logger exited with code %d", code) + })) + l := New(t.Name(), "test", opts...) + l.Logger.SetOutput(t.Output()) + return l +} + +func (l *Logger) UseConfig(c configurator) { + // Update logrus level and formatter first, using their mutex-safe methods, + // before acquiring our own lock to avoid any lock ordering issues. + o := newOptions(append(l.opts, WithConfigurator(c))) + setLevel(l.Entry.Logger, o) + setFormatter(l.Entry.Logger, o) + + l.cfg.mu.Lock() + defer l.cfg.mu.Unlock() + l.cfg.leakSensitive = l.cfg.leakSensitive || c.Bool("log.leak_sensitive_values") + l.cfg.redactionText = cmp.Or(c.String("log.redaction_text"), l.cfg.redactionText) + // Replace the map rather than mutating it in-place so that readers holding + // a snapshot of the old pointer can iterate it safely without a lock. + newHeaders := maps.Clone(l.cfg.additionalRedactedHeaders) + for k := range toHeaderMap(c.Strings("log.additional_redacted_headers")) { + newHeaders[k] = struct{}{} + } + l.cfg.additionalRedactedHeaders = newHeaders +} + +func (l *Logger) ReportError(r *http.Request, code int, err error, args ...interface{}) { + logger := l.WithError(err).WithRequest(r).WithField("http_response", map[string]interface{}{ + "status_code": code, + }) + switch { + case code < 500: + logger.Info(args...) + default: + logger.Error(args...) + } +} diff --git a/oryx/mailhog/memory.go b/oryx/mailhog/memory.go new file mode 100644 index 0000000..5ee5ce7 --- /dev/null +++ b/oryx/mailhog/memory.go @@ -0,0 +1,212 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package mailhog is a copy of github.com/mailhog/storage with the missing +// read mutex operations added to List, Count, Search, and Load. +package mailhog + +import ( + "errors" + "strings" + "sync" + + "github.com/mailhog/data" +) + +// InMemory is an in memory storage backend +type InMemory struct { + MessageIDIndex map[string]int + Messages []*data.Message + mu sync.RWMutex +} + +// NewInMemory creates a new in memory storage backend +func NewInMemory() *InMemory { + return &InMemory{ + MessageIDIndex: make(map[string]int), + Messages: make([]*data.Message, 0), + } +} + +// Store stores a message and returns its storage ID +func (memory *InMemory) Store(m *data.Message) (string, error) { + memory.mu.Lock() + defer memory.mu.Unlock() + memory.Messages = append(memory.Messages, m) + memory.MessageIDIndex[string(m.ID)] = len(memory.Messages) - 1 + return string(m.ID), nil +} + +// Count returns the number of stored messages +func (memory *InMemory) Count() int { + memory.mu.RLock() + defer memory.mu.RUnlock() + return len(memory.Messages) +} + +// Search finds messages matching the query +func (memory *InMemory) Search(kind, query string, start, limit int) (*data.Messages, int, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + // FIXME needs optimising, or replacing with a proper db! + query = strings.ToLower(query) + var filteredMessages = make([]*data.Message, 0) + for _, m := range memory.Messages { + doAppend := false + + switch kind { + case "to": + for _, to := range m.To { + if strings.Contains(strings.ToLower(to.Mailbox+"@"+to.Domain), query) { + doAppend = true + break + } + } + if !doAppend { + if hdr, ok := m.Content.Headers["To"]; ok { + for _, to := range hdr { + if strings.Contains(strings.ToLower(to), query) { + doAppend = true + break + } + } + } + } + case "from": + if strings.Contains(strings.ToLower(m.From.Mailbox+"@"+m.From.Domain), query) { + doAppend = true + } + if !doAppend { + if hdr, ok := m.Content.Headers["From"]; ok { + for _, from := range hdr { + if strings.Contains(strings.ToLower(from), query) { + doAppend = true + break + } + } + } + } + case "containing": + if strings.Contains(strings.ToLower(m.Content.Body), query) { + doAppend = true + } + if !doAppend { + for _, hdr := range m.Content.Headers { + for _, v := range hdr { + if strings.Contains(strings.ToLower(v), query) { + doAppend = true + } + } + } + } + } + + if doAppend { + filteredMessages = append(filteredMessages, m) + } + } + + var messages = make([]data.Message, 0) + + if len(filteredMessages) == 0 || start > len(filteredMessages) { + msgs := data.Messages(messages) + return &msgs, 0, nil + } + + if start+limit > len(filteredMessages) { + limit = len(filteredMessages) - start + } + + start = len(filteredMessages) - start - 1 + end := start - limit + + if start < 0 { + start = 0 + } + if end < -1 { + end = -1 + } + + for i := start; i > end; i-- { + //for _, m := range memory.MessageIndex[start:end] { + messages = append(messages, *filteredMessages[i]) + } + + msgs := data.Messages(messages) + return &msgs, len(filteredMessages), nil +} + +// List lists stored messages by index +func (memory *InMemory) List(start int, limit int) (*data.Messages, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + var messages = make([]data.Message, 0) + + if len(memory.Messages) == 0 || start > len(memory.Messages) { + msgs := data.Messages(messages) + return &msgs, nil + } + + if start+limit > len(memory.Messages) { + limit = len(memory.Messages) - start + } + + start = len(memory.Messages) - start - 1 + end := start - limit + + if start < 0 { + start = 0 + } + if end < -1 { + end = -1 + } + + for i := start; i > end; i-- { + //for _, m := range memory.MessageIndex[start:end] { + messages = append(messages, *memory.Messages[i]) + } + + msgs := data.Messages(messages) + return &msgs, nil +} + +// DeleteOne deletes an individual message by storage ID +func (memory *InMemory) DeleteOne(id string) error { + memory.mu.Lock() + defer memory.mu.Unlock() + + var index int + var ok bool + + if index, ok = memory.MessageIDIndex[id]; !ok && true { + return errors.New("message not found") + } + + delete(memory.MessageIDIndex, id) + for k, v := range memory.MessageIDIndex { + if v > index { + memory.MessageIDIndex[k] = v - 1 + } + } + memory.Messages = append(memory.Messages[:index], memory.Messages[index+1:]...) + return nil +} + +// DeleteAll deletes all in memory messages +func (memory *InMemory) DeleteAll() error { + memory.mu.Lock() + defer memory.mu.Unlock() + memory.Messages = make([]*data.Message, 0) + memory.MessageIDIndex = make(map[string]int) + return nil +} + +// Load returns an individual message by storage ID +func (memory *InMemory) Load(id string) (*data.Message, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + if idx, ok := memory.MessageIDIndex[id]; ok { + return memory.Messages[idx], nil + } + return nil, nil +} diff --git a/oryx/mapx/type_assert.go b/oryx/mapx/type_assert.go new file mode 100644 index 0000000..39a5b6b --- /dev/null +++ b/oryx/mapx/type_assert.go @@ -0,0 +1,180 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package mapx + +import ( + "encoding/json" + "errors" + "math" + "time" +) + +// ErrKeyDoesNotExist is returned when the key does not exist in the map. +var ErrKeyDoesNotExist = errors.New("key is not present in map") + +// ErrKeyCanNotBeTypeAsserted is returned when the key can not be type asserted. +var ErrKeyCanNotBeTypeAsserted = errors.New("key could not be type asserted") + +// GetString returns a string for a given key in values. +func GetString[K comparable](values map[K]any, key K) (string, error) { + if v, ok := values[key]; !ok { + return "", ErrKeyDoesNotExist + } else if sv, ok := v.(string); !ok { + return "", ErrKeyCanNotBeTypeAsserted + } else { + return sv, nil + } +} + +// GetStringSlice returns a string slice for a given key in values. +func GetStringSlice[K comparable](values map[K]any, key K) ([]string, error) { + v, ok := values[key] + if !ok { + return nil, ErrKeyDoesNotExist + } + switch v := v.(type) { + case []string: + return v, nil + case []any: + vs := make([]string, len(v)) + for k, v := range v { + var ok bool + vs[k], ok = v.(string) + if !ok { + return nil, ErrKeyCanNotBeTypeAsserted + } + } + return vs, nil + } + return nil, ErrKeyCanNotBeTypeAsserted +} + +// GetTime returns a string slice for a given key in values. +func GetTime[K comparable](values map[K]any, key K) (time.Time, error) { + v, ok := values[key] + if !ok { + return time.Time{}, ErrKeyDoesNotExist + } + + switch v := v.(type) { + case time.Time: + return v, nil + case int64: + return time.Unix(v, 0), nil + case int32: + return time.Unix(int64(v), 0), nil + case int: + return time.Unix(int64(v), 0), nil + case float64: + if v < math.MinInt64 || v > math.MaxInt64 { + return time.Time{}, errors.New("value is out of range") + } + return time.Unix(int64(v), 0), nil + case float32: + if v < math.MinInt64 || v > math.MaxInt64 { + return time.Time{}, errors.New("value is out of range") + } + return time.Unix(int64(v), 0), nil + } + + return time.Time{}, ErrKeyCanNotBeTypeAsserted +} + +// GetInt64 returns an int64 for a given key in values. +func GetInt64[K comparable](values map[K]any, key K) (int64, error) { + v, ok := values[key] + if !ok { + return 0, ErrKeyDoesNotExist + } + switch v := v.(type) { + case json.Number: + return v.Int64() + case int64: + return v, nil + case int: + return int64(v), nil + case int32: + return int64(v), nil + case uint: + vv := uint64(v) + if vv > math.MaxInt64 { + return 0, errors.New("value is out of range") + } + return int64(vv), nil + case uint32: + return int64(v), nil + case uint64: + if v > math.MaxInt64 { + return 0, errors.New("value is out of range") + } + return int64(v), nil + } + return 0, ErrKeyCanNotBeTypeAsserted +} + +// GetInt32 returns an int32 for a given key in values. +func GetInt32[K comparable](values map[K]any, key K) (int32, error) { + v, err := GetInt64(values, key) + if err != nil { + return 0, err + } + if v > math.MaxInt32 || v < math.MinInt32 { + return 0, errors.New("value is out of range") + } + return int32(v), nil +} + +// GetInt returns an int for a given key in values. +func GetInt[K comparable](values map[K]any, key K) (int, error) { + v, err := GetInt64(values, key) + if err != nil { + return 0, err + } + if v > math.MaxInt || v < math.MinInt { + return 0, errors.New("value is out of range") + } + return int(v), nil +} + +// GetFloat64Default returns a float64 or the default value for a given key in values. +func GetFloat64Default[K comparable](values map[K]any, key K, defaultValue float64) float64 { + f, err := GetFloat64(values, key) + if err != nil { + return defaultValue + } + return f +} + +// GetFloat64 returns a float64 for a given key in values. +func GetFloat64[K comparable](values map[K]any, key K) (float64, error) { + v, ok := values[key] + if !ok { + return 0, ErrKeyDoesNotExist + } + switch v := v.(type) { + case json.Number: + return v.Float64() + case float32: + return float64(v), nil + case float64: + return v, nil + } + return 0, ErrKeyCanNotBeTypeAsserted +} + +// GetStringDefault returns a string or the default value for a given key in values. +func GetStringDefault[K comparable](values map[K]any, key K, defaultValue string) string { + if s, err := GetString(values, key); err == nil { + return s + } + return defaultValue +} + +// GetStringSliceDefault returns a string slice or the default value for a given key in values. +func GetStringSliceDefault[K comparable](values map[K]any, key K, defaultValue []string) []string { + if s, err := GetStringSlice(values, key); err == nil { + return s + } + return defaultValue +} diff --git a/oryx/metricsx/metrics.go b/oryx/metricsx/metrics.go new file mode 100644 index 0000000..25aab63 --- /dev/null +++ b/oryx/metricsx/metrics.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package metricsx + +import ( + "runtime" + "sync" +) + +// MemoryStatistics is a JSON-able version of runtime.MemStats +type MemoryStatistics struct { + sync.Mutex + // Alloc is bytes of allocated heap objects. + Alloc uint64 `json:"alloc"` + // TotalAlloc is cumulative bytes allocated for heap objects. + TotalAlloc uint64 `json:"totalAlloc"` + // Sys is the total bytes of memory obtained from the OS. + Sys uint64 `json:"sys"` + // Lookups is the number of pointer lookups performed by the + // runtime. + Lookups uint64 `json:"lookups"` + // Mallocs is the cumulative count of heap objects allocated. + // The number of live objects is Mallocs - Frees. + Mallocs uint64 `json:"mallocs"` + // Frees is the cumulative count of heap objects freed. + Frees uint64 `json:"frees"` + // HeapAlloc is bytes of allocated heap objects. + HeapAlloc uint64 `json:"heapAlloc"` + // HeapSys is bytes of heap memory obtained from the OS. + HeapSys uint64 `json:"heapSys"` + // HeapIdle is bytes in idle (unused) spans. + HeapIdle uint64 `json:"heapIdle"` + // HeapInuse is bytes in in-use spans. + HeapInuse uint64 `json:"heapInuse"` + // HeapReleased is bytes of physical memory returned to the OS. + HeapReleased uint64 `json:"heapReleased"` + // HeapObjects is the number of allocated heap objects. + HeapObjects uint64 `json:"heapObjects"` + // NumGC is the number of completed GC cycles. + NumGC uint32 `json:"numGC"` +} + +// ToMap converts to a map[string]interface{}. +func (ms *MemoryStatistics) ToMap() map[string]interface{} { + return map[string]interface{}{ + "alloc": ms.Alloc, + "totalAlloc": ms.TotalAlloc, + "sys": ms.Sys, + "lookups": ms.Lookups, + "mallocs": ms.Mallocs, + "frees": ms.Frees, + "heapAlloc": ms.HeapAlloc, + "heapSys": ms.HeapSys, + "heapIdle": ms.HeapIdle, + "heapInuse": ms.HeapInuse, + "heapReleased": ms.HeapReleased, + "heapObjects": ms.HeapObjects, + "numGC": ms.NumGC, + "nonInteraction": 1, + } +} + +// Update takes the most recent stats from runtime. +func (ms *MemoryStatistics) Update() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + ms.Lock() + defer ms.Unlock() + ms.Alloc = m.Alloc + ms.TotalAlloc = m.TotalAlloc + ms.Sys = m.Sys + ms.Lookups = m.Lookups + ms.Mallocs = m.Mallocs + ms.Frees = m.Frees + ms.HeapAlloc = m.HeapAlloc + ms.HeapSys = m.HeapSys + ms.HeapIdle = m.HeapIdle + ms.HeapInuse = m.HeapInuse + ms.HeapReleased = m.HeapReleased + ms.HeapObjects = m.HeapObjects + ms.NumGC = m.NumGC +} diff --git a/oryx/metricsx/middleware.go b/oryx/metricsx/middleware.go new file mode 100644 index 0000000..0205382 --- /dev/null +++ b/oryx/metricsx/middleware.go @@ -0,0 +1,427 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package metricsx + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "math" + "net/http" + "net/url" + "os" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "connectrpc.com/connect" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/gofrs/uuid" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/resilience" + "github.com/ory/x/urlx" + + "github.com/ory/analytics-go/v5" +) + +const ( + XForwardedHostHeader = "X-Forwarded-Host" + AuthorityHeader = ":authority" +) + +var ( + instance *Service + lock sync.Mutex + knownHeaders = []string{AuthorityHeader, XForwardedHostHeader} +) + +// Service helps with providing context on metrics. +type Service struct { + optOut bool + instanceId string + + o *Options + + c analytics.Client + l *logrusx.Logger + + mem *MemoryStatistics +} + +// Hash returns a hashed string of the value. +func Hash(value string) string { + sha := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sha[:]) +} + +// Options configures the metrics service. +type Options struct { + // Service represents the service name, for example "ory-hydra". + Service string + + // DeploymentId represents the cluster id, typically a hash of some unique configuration properties. + DeploymentId string + + // DBDialect specifies the database dialect in use (e.g., "postgres", "mysql", "sqlite"). + DBDialect string + + // When this instance was started + StartTime time.Time + + // IsDevelopment should be true if we assume that we're in a development environment. + IsDevelopment bool + + // WriteKey is the segment API key. + WriteKey string + + // WhitelistedPaths represents a list of paths that can be transmitted in clear text to segment. + WhitelistedPaths []string + + // BuildVersion represents the build version. + BuildVersion string + + // BuildHash represents the build git hash. + BuildHash string + + // BuildTime represents the build time. + BuildTime string + + // Hostname is a public URL configured for the service, used to derive hosted name for telemetry. + Hostname string + + // Config overrides the analytics.Config. If nil, sensible defaults will be used. + Config *analytics.Config + + // MemoryInterval sets how often memory statistics should be transmitted. Defaults to every 12 hours. + MemoryInterval time.Duration +} + +type void struct{} + +func (v *void) Logf(format string, args ...interface{}) { +} + +func (v *void) Errorf(format string, args ...interface{}) { +} + +// New returns a new metrics service. If one has been instantiated already, no new instance will be created. +func New( + cmd *cobra.Command, + l *logrusx.Logger, + c *configx.Provider, + o *Options, +) *Service { + lock.Lock() + defer lock.Unlock() + + if instance != nil { + return instance + } + + o.StartTime = time.Now() + + if o.BuildTime == "" { + o.BuildTime = "unknown" + } + + if o.BuildVersion == "" { + o.BuildVersion = "unknown" + } + + if o.BuildHash == "" { + o.BuildHash = "unknown" + } + + if o.Config == nil { + o.Config = &analytics.Config{ + Interval: time.Hour * 6, + } + } + + o.Config.Logger = new(void) + + if o.MemoryInterval < time.Minute { + o.MemoryInterval = time.Hour * 12 + } + + segment, err := analytics.NewWithConfig(o.WriteKey, *o.Config) + if err != nil { + l.WithError(err).Fatalf("Unable to initialise software quality assurance features.") + return nil + } + + optOut, err := cmd.Flags().GetBool("sqa-opt-out") + if err != nil { + cmdx.Must(err, `Unable to get command line flag "sqa-opt-out": %s`, err) + } + + if !optOut { + optOut = c.Bool("sqa.opt_out") + } + + if !optOut { + optOut = c.Bool("sqa_opt_out") + } + + if !optOut { + optOut, _ = strconv.ParseBool(os.Getenv("SQA_OPT_OUT")) + } + + if !optOut { + optOut, _ = strconv.ParseBool(os.Getenv("SQA-OPT-OUT")) + } + + if !optOut { + l.Info("Software quality assurance features are enabled. Learn more at: https://www.ory.com/docs/ecosystem/sqa") + } + + m := &Service{ + optOut: optOut, + instanceId: uuid.Must(uuid.NewV4()).String(), + o: o, + c: segment, + l: l, + mem: new(MemoryStatistics), + } + + instance = m + + go m.Identify() + go m.Track() + + return m +} + +// Identify enables reporting to segment. +func (sw *Service) Identify() { + IdentifySend(sw, true) + + // User has not opt-out then make identify to be sent every 6 hours + if !sw.optOut { + for range time.Tick(time.Hour * 6) { + IdentifySend(sw, false) + } + } +} + +func IdentifySend(sw *Service, startup bool) { + if err := resilience.Retry(sw.l, time.Minute*5, time.Hour*6, func() error { + return sw.c.Enqueue(analytics.Identify{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + + DatabaseDialect: sw.o.DBDialect, + ProductVersion: sw.o.BuildVersion, + ProductBuild: sw.o.BuildHash, + UptimeDeployment: 0, + UptimeInstance: math.Round(time.Since(sw.o.StartTime).Seconds()), + IsDevelopment: sw.o.IsDevelopment, + IsOptOut: sw.optOut, + Startup: startup, + }) + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized environment information") + } +} + +// Track commits memory statistics to segment. +func (sw *Service) Track() { + if sw.optOut { + return + } + + for { + sw.mem.Update() + if err := sw.c.Enqueue(analytics.Track{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + + CPU: runtime.NumCPU(), + OsName: runtime.GOOS, + OsArchitecture: runtime.GOARCH, + Alloc: sw.mem.Alloc, + TotalAlloc: sw.mem.TotalAlloc, + Frees: sw.mem.Frees, + Mallocs: sw.mem.Mallocs, + Lookups: sw.mem.Lookups, + Sys: sw.mem.Sys, + NumGC: sw.mem.NumGC, + HeapAlloc: sw.mem.HeapAlloc, + HeapInuse: sw.mem.HeapInuse, + HeapIdle: sw.mem.HeapIdle, + HeapObjects: sw.mem.HeapObjects, + HeapReleased: sw.mem.HeapReleased, + HeapSys: sw.mem.HeapSys, + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + } + time.Sleep(sw.o.MemoryInterval) + } +} + +// ServeHTTP is a middleware for sending meta information to segment. +func (sw *Service) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + var start time.Time + if !sw.optOut { + start = time.Now() + } + + next(rw, r) + + if sw.optOut { + return + } + + latency := time.Since(start).Milliseconds() + path := sw.anonymizePath(r.URL.Path) + host := urlx.ExtractPublicAddress(sw.o.Hostname, r.Header.Get(XForwardedHostHeader), r.Host) + + // Collecting request info + stat, _ := httpx.GetResponseMeta(rw) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: path, + RequestCode: stat, + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } +} + +func (sw *Service) UnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + var start time.Time + if !sw.optOut { + start = time.Now() + } + + resp, err := handler(ctx, req) + + if sw.optOut { + return resp, err + } + + latency := time.Since(start).Milliseconds() + + hosts := []string{sw.o.Hostname} + if md, ok := metadata.FromIncomingContext(ctx); ok { + for _, h := range knownHeaders { + if v := md.Get(h); len(v) > 0 { + hosts = append(hosts, v[0]) + } + } + } + host := urlx.ExtractPublicAddress(hosts...) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: info.FullMethod, + RequestCode: int(status.Code(err)), + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } + + return resp, err +} + +func (sw *Service) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if sw.optOut { + return next(ctx, req) + } + + start := time.Now() + resp, err := next(ctx, req) + latency := time.Since(start).Milliseconds() + + hosts := []string{sw.o.Hostname} + for _, h := range append([]string{"Host"}, knownHeaders...) { + if v := req.Header().Get(h); len(v) > 0 { + hosts = append(hosts, v) + } + } + host := urlx.ExtractPublicAddress(hosts...) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: req.Spec().Procedure, + RequestCode: int(status.Code(err)), + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } + + return resp, err + } +} + +func (sw *Service) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return next +} +func (sw *Service) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return next +} + +func (sw *Service) StreamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + // this needs a bit of thought, but we don't have streaming RPCs currently anyway + sw.l.Info("The telemetry stream interceptor is not yet implemented!") + return handler(srv, stream) +} + +func (sw *Service) Close() error { + return sw.c.Close() +} + +func (sw *Service) anonymizePath(path string) string { + paths := sw.o.WhitelistedPaths + path = strings.ToLower(path) + + for _, p := range paths { + p = strings.ToLower(p) + if path == p { + return p + } else if len(path) > len(p) && path[:len(p)+1] == p+"/" { + return p + } + } + + return "/" +} + +func (sw *Service) anonymizeQuery(query url.Values, salt string) string { + for _, q := range query { + for i, s := range q { + if s != "" { + s = Hash(s + "|" + salt) + q[i] = s + } + } + } + return query.Encode() +} diff --git a/oryx/migratest/refresh.go b/oryx/migratest/refresh.go new file mode 100644 index 0000000..68583c5 --- /dev/null +++ b/oryx/migratest/refresh.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build refresh +// +build refresh + +package migratest + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func WriteFixtureOnError(t *testing.T, err error, actual interface{}, location string) { + content, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err) + require.NoError(t, os.MkdirAll(filepath.Dir(location), 0777)) + require.NoError(t, os.WriteFile(location, content, 0666)) +} diff --git a/oryx/migratest/run.go b/oryx/migratest/run.go new file mode 100644 index 0000000..0b8d4d8 --- /dev/null +++ b/oryx/migratest/run.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migratest + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func ContainsExpectedIds(t *testing.T, path string, ids []string) { + files, err := os.ReadDir(path) + require.NoError(t, err) + + for _, f := range files { + if filepath.Ext(f.Name()) == ".json" { + expected := strings.TrimSuffix(filepath.Base(f.Name()), ".json") + assert.Contains(t, ids, expected) + } + } +} + +func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id string) { + location := filepath.Join("fixtures", prefix, id+".json") + //#nosec G304 -- false positive + expected, err := os.ReadFile(location) + WriteFixtureOnError(t, err, actual, location) + + actualJSON, err := json.Marshal(actual) + require.NoError(t, err) + + if !assert.JSONEq(t, string(expected), string(actualJSON)) { + WriteFixtureOnError(t, nil, actual, location) + } +} diff --git a/oryx/migratest/strict.go b/oryx/migratest/strict.go new file mode 100644 index 0000000..3ff9d50 --- /dev/null +++ b/oryx/migratest/strict.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !refresh +// +build !refresh + +package migratest + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func WriteFixtureOnError(t *testing.T, err error, actual interface{}, location string) { + require.NoError(t, err) +} diff --git a/oryx/networkx/listener.go b/oryx/networkx/listener.go new file mode 100644 index 0000000..eadfd78 --- /dev/null +++ b/oryx/networkx/listener.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "net" + "strings" + + "github.com/ory/x/configx" +) + +func AddressIsUnixSocket(address string) bool { + return strings.HasPrefix(address, "unix:") +} + +func MakeListener(address string, socketPermission *configx.UnixPermission) (net.Listener, error) { + if AddressIsUnixSocket(address) { + addr := strings.TrimPrefix(address, "unix:") + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + err = socketPermission.SetPermission(addr) + if err != nil { + return nil, err + } + return l, nil + } + return net.Listen("tcp", address) +} diff --git a/oryx/networkx/manager.go b/oryx/networkx/manager.go new file mode 100644 index 0000000..707c5f7 --- /dev/null +++ b/oryx/networkx/manager.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "embed" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/sqlcon" +) + +// Migrations of the network manager. Apply by merging with your local migrations using +// fsx.Merge() and then passing all to the migration box. +// +//go:embed migrations/sql/*.sql +var Migrations embed.FS + +func Determine(c *pop.Connection) (*Network, error) { + var p Network + if err := sqlcon.HandleError(c.Q().Order("created_at ASC").First(&p)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + np := NewNetwork() + if err := c.Create(np); err != nil { + return nil, err + } + return np, nil + } + return nil, err + } + return &p, nil +} diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql new file mode 100644 index 0000000..beb6b14 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `networks`; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql new file mode 100644 index 0000000..0ba5bfc --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE `networks` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql new file mode 100644 index 0000000..f808e33 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "networks" ( +"id" TEXT PRIMARY KEY, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz b/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz new file mode 100644 index 0000000..e6e32ac --- /dev/null +++ b/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz @@ -0,0 +1 @@ +drop_table("networks") diff --git a/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz b/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz new file mode 100644 index 0000000..52cd069 --- /dev/null +++ b/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz @@ -0,0 +1,3 @@ +create_table("networks") { + t.Column("id", "uuid", {primary: true}) +} diff --git a/oryx/networkx/network.go b/oryx/networkx/network.go new file mode 100644 index 0000000..e9be927 --- /dev/null +++ b/oryx/networkx/network.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "time" + + "github.com/gofrs/uuid" +) + +type Network struct { + ID uuid.UUID `json:"id" db:"id"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` +} + +func (p Network) TableName() string { + return "networks" +} + +func NewNetwork() *Network { + return &Network{ + ID: uuid.Must(uuid.NewV4()), + } +} diff --git a/oryx/openapix/doc.go b/oryx/openapix/doc.go new file mode 100644 index 0000000..654b27f --- /dev/null +++ b/oryx/openapix/doc.go @@ -0,0 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package openapi contains definitions commonly used in Ory's APIs +// such as pagination, JSON patches, and more. +package openapix diff --git a/oryx/openapix/jsonpatch.go b/oryx/openapix/jsonpatch.go new file mode 100644 index 0000000..3876906 --- /dev/null +++ b/oryx/openapix/jsonpatch.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openapix + +// A JSONPatchDocument request +// +// swagger:model jsonPatchDocument +type JSONPatchDocument []JSONPatch + +// A JSONPatch document as defined by RFC 6902 +// +// swagger:model jsonPatch +type JSONPatch struct { + // The operation to be performed. One of "add", "remove", "replace", "move", "copy", or "test". + // + // required: true + // example: replace + Op string `json:"op"` + + // The path to the target path. Uses JSON pointer notation. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // required: true + // example: /name + Path string `json:"path"` + + // The value to be used within the operations. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // example: foobar + Value interface{} `json:"value"` + + // This field is used together with operation "move" and uses JSON Pointer notation. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // example: /name + From string `json:"from"` +} diff --git a/oryx/openapix/pagination.go b/oryx/openapix/pagination.go new file mode 100644 index 0000000..d906362 --- /dev/null +++ b/oryx/openapix/pagination.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openapix + +// swagger:model tokenPaginationHeaders +type TokenPaginationHeaders struct { + // The link header contains pagination links. + // + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // in: header + Link string `json:"link"` + + // The total number of clients. + // + // in: header + XTotalCount string `json:"x-total-count"` +} + +// swagger:model tokenPagination +type TokenPaginationParams struct { + // Items per page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} diff --git a/oryx/osx/env.go b/oryx/osx/env.go new file mode 100644 index 0000000..e5462ac --- /dev/null +++ b/oryx/osx/env.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package osx + +import ( + "cmp" + "os" +) + +// GetenvDefault returns an environment variable or the default value if it is empty. +func GetenvDefault(key string, def string) string { + return cmp.Or(os.Getenv(key), def) +} diff --git a/oryx/osx/file.go b/oryx/osx/file.go new file mode 100644 index 0000000..bdb2d30 --- /dev/null +++ b/oryx/osx/file.go @@ -0,0 +1,221 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package osx + +import ( + "encoding/base64" + "io" + "net/url" + "os" + "strings" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/pkg/errors" + + "github.com/ory/x/httpx" +) + +type options struct { + disableFileLoader bool + disableHTTPLoader bool + disableBase64Loader bool + base64enc *base64.Encoding + disableResilientBase64Loader bool + hc *retryablehttp.Client +} + +type Option func(o *options) + +func (o *options) apply(opts []Option) *options { + for _, f := range opts { + f(o) + } + return o +} + +func newOptions() *options { + return &options{ + disableFileLoader: false, + disableHTTPLoader: false, + disableBase64Loader: false, + base64enc: base64.RawURLEncoding, + hc: httpx.NewResilientClient(), + } +} + +// WithDisabledFileLoader disables the file loader. +func WithDisabledFileLoader() Option { + return func(o *options) { + o.disableFileLoader = true + } +} + +// WithEnabledFileLoader enables the file loader. +func WithEnabledFileLoader() Option { + return func(o *options) { + o.disableFileLoader = false + } +} + +// WithDisabledHTTPLoader disables the HTTP loader. +func WithDisabledHTTPLoader() Option { + return func(o *options) { + o.disableHTTPLoader = true + } +} + +// WithEnabledHTTPLoader enables the HTTP loader. +func WithEnabledHTTPLoader() Option { + return func(o *options) { + o.disableHTTPLoader = false + } +} + +// WithDisabledBase64Loader disables the base64 loader. +func WithDisabledBase64Loader() Option { + return func(o *options) { + o.disableBase64Loader = true + } +} + +// WithEnabledBase64Loader disables the base64 loader. +func WithEnabledBase64Loader() Option { + return func(o *options) { + o.disableBase64Loader = false + } +} + +// WithBase64Encoding sets the base64 encoding. +func WithBase64Encoding(enc *base64.Encoding) Option { + return func(o *options) { + o.base64enc = enc + } +} + +// WithoutResilientBase64Encoding sets the base64 encoding. +func WithoutResilientBase64Encoding() Option { + return func(o *options) { + o.disableResilientBase64Loader = true + } +} + +// WithHTTPClient sets the HTTP client. +func WithHTTPClient(hc *retryablehttp.Client) Option { + return func(o *options) { + o.hc = hc + } +} + +// RestrictedReadFile works similar to ReadFileFromAllSources but has all +// sources disabled per default. You need to enable the loaders you wish to use +// explicitly. +func RestrictedReadFile(source string, opts ...Option) (bytes []byte, err error) { + o := newOptions() + o.disableFileLoader = true + o.disableBase64Loader = true + o.disableHTTPLoader = true + return readFile(source, o.apply(opts)) +} + +// ReadFileFromAllSources reads a file from base64, http, https, and file sources. +// +// Using options, you can disable individual loaders. For example, the following will +// return an error: +// +// ReadFileFromAllSources("https://foo.bar/baz.txt", WithDisabledHTTPLoader()) +// +// Possible formats are: +// +// - /path/to/file +// - file:///path/to/file +// - https://host.com/path/to/file +// - http://host.com/path/to/file +// - base64:// +// +// For more options, check: +// +// - WithDisabledFileLoader +// - WithDisabledHTTPLoader +// - WithDisabledBase64Loader +// - WithBase64Encoding +// - WithHTTPClient +func ReadFileFromAllSources(source string, opts ...Option) (bytes []byte, err error) { + return readFile(source, newOptions().apply(opts)) +} + +func readFile(source string, o *options) (bytes []byte, err error) { + parsed, err := url.Parse(source) + if err != nil { + return nil, errors.Wrap(err, "failed to parse URL") + } + + switch parsed.Scheme { + case "": + if o.disableFileLoader { + return nil, errors.New("file loader disabled") + } + + //#nosec G304 -- false positive + bytes, err = os.ReadFile(source) + if err != nil { + return nil, errors.Wrap(err, "unable to read the file") + } + case "file": + if o.disableFileLoader { + return nil, errors.New("file loader disabled") + } + + //#nosec G304 -- false positive + bytes, err = os.ReadFile(parsed.Host + parsed.Path) + if err != nil { + return nil, errors.Wrap(err, "unable to read the file") + } + case "http", "https": + if o.disableHTTPLoader { + return nil, errors.New("http(s) loader disabled") + } + resp, err := o.hc.Get(parsed.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to load remote file") + } + defer resp.Body.Close() + + bytes, err = io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "unable to read the HTTP response body") + } + case "base64": + if o.disableBase64Loader { + return nil, errors.New("base64 loader disabled") + } + + if o.disableResilientBase64Loader { + bytes, err = o.base64enc.DecodeString(strings.TrimPrefix(source, "base64://")) + if err != nil { + return nil, errors.Wrap(err, "unable to base64 decode the location") + } + return bytes, nil + } + + for _, enc := range []*base64.Encoding{ + base64.StdEncoding, + base64.URLEncoding, + base64.RawURLEncoding, + base64.RawStdEncoding, + } { + bytes, err = enc.DecodeString(strings.TrimPrefix(source, "base64://")) + if err == nil { + return bytes, nil + } + } + + return nil, errors.Wrap(err, "unable to base64 decode the location") + default: + return nil, errors.Errorf("unsupported source `%s`", parsed.Scheme) + } + + return bytes, nil + +} diff --git a/oryx/osx/stub/text.txt b/oryx/osx/stub/text.txt new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/oryx/osx/stub/text.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/oryx/otelx/attribute.go b/oryx/otelx/attribute.go new file mode 100644 index 0000000..c84489b --- /dev/null +++ b/oryx/otelx/attribute.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "database/sql" + "fmt" + + "go.opentelemetry.io/otel/attribute" +) + +const nullString = "" + +func StringAttrs(attrs map[string]string) []attribute.KeyValue { + s := make([]attribute.KeyValue, 0, len(attrs)) + for k, v := range attrs { + s = append(s, attribute.String(k, v)) + } + return s +} + +func AutoInt[I int | int32 | int64](k string, v I) attribute.KeyValue { + // Internally, the OpenTelemetry SDK uses int64 for all integer values anyway. + return attribute.Int64(k, int64(v)) +} + +func Nullable[V any, VN *V | sql.Null[V], A func(string, V) attribute.KeyValue](a A, k string, v VN) attribute.KeyValue { + switch v := any(v).(type) { + case *V: + if v == nil { + return attribute.String(k, nullString) + } + return a(k, *v) + case sql.Null[V]: + if !v.Valid { + return attribute.String(k, nullString) + } + return a(k, v.V) + } + // This should never happen, as the type switch above is exhaustive to the generic type VN. + return attribute.String(k, fmt.Sprintf("", v)) +} + +func NullString[V *string | sql.Null[string]](k string, v V) attribute.KeyValue { + return Nullable(attribute.String, k, v) +} + +func NullStringer(k string, v fmt.Stringer) attribute.KeyValue { + if v == nil { + return attribute.String(k, nullString) + } + return attribute.String(k, v.String()) +} diff --git a/oryx/otelx/config.go b/oryx/otelx/config.go new file mode 100644 index 0000000..843f4fe --- /dev/null +++ b/oryx/otelx/config.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "bytes" + _ "embed" + "io" +) + +type JaegerConfig struct { + LocalAgentAddress string `json:"local_agent_address"` + Sampling JaegerSampling `json:"sampling"` +} + +type ZipkinConfig struct { + ServerURL string `json:"server_url"` + Sampling ZipkinSampling `json:"sampling"` +} + +type OTLPConfig struct { + ServerURL string `json:"server_url"` + Insecure bool `json:"insecure"` + Sampling OTLPSampling `json:"sampling"` + AuthorizationHeader string `json:"authorization_header"` +} + +type JaegerSampling struct { + ServerURL string `json:"server_url"` + TraceIDRatio float64 `json:"trace_id_ratio"` +} + +type ZipkinSampling struct { + SamplingRatio float64 `json:"sampling_ratio"` +} + +type OTLPSampling struct { + SamplingRatio float64 `json:"sampling_ratio"` +} + +type ProvidersConfig struct { + Jaeger JaegerConfig `json:"jaeger"` + Zipkin ZipkinConfig `json:"zipkin"` + OTLP OTLPConfig `json:"otlp"` +} + +type Config struct { + ServiceName string `json:"service_name"` + DeploymentEnvironment string `json:"deployment_environment"` + Provider string `json:"provider"` + Providers ProvidersConfig `json:"providers"` +} + +//go:embed config.schema.json +var ConfigSchema []byte + +const ConfigSchemaID = "ory://tracing-config" + +// AddConfigSchema adds the tracing schema to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddConfigSchema(c interface { + AddResource(url string, r io.Reader) error +}, +) error { + return c.AddResource(ConfigSchemaID, bytes.NewReader(ConfigSchema)) +} diff --git a/oryx/otelx/config.schema.json b/oryx/otelx/config.schema.json new file mode 100644 index 0000000..1a668f3 --- /dev/null +++ b/oryx/otelx/config.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ory://tracing-config", + "type": "object", + "additionalProperties": false, + "description": "Configure distributed tracing using OpenTelemetry", + "properties": { + "provider": { + "type": "string", + "description": "Set this to the tracing backend you wish to use. Supports Jaeger, Zipkin, and OTEL.", + "enum": ["jaeger", "otel", "zipkin"], + "examples": ["jaeger"] + }, + "service_name": { + "type": "string", + "description": "Specifies the service name to use on the tracer.", + "examples": ["Ory Hydra", "Ory Kratos", "Ory Keto", "Ory Oathkeeper"] + }, + "deployment_environment": { + "type": "string", + "description": "Specifies the deployment environment to use on the tracer.", + "examples": ["development", "staging", "production"] + }, + "providers": { + "type": "object", + "additionalProperties": false, + "properties": { + "jaeger": { + "type": "object", + "additionalProperties": false, + "description": "Configures the jaeger tracing backend.", + "properties": { + "local_agent_address": { + "type": "string", + "description": "The address of the jaeger-agent where spans should be sent to.", + "anyOf": [ + { + "title": "IPv6 Address and Port", + "pattern": "^\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]:([0-9]*)$" + }, + { + "title": "IPv4 Address and Port", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}:([0-9]*)$" + }, + { + "title": "Hostname and Port", + "pattern": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]):([0-9]*)$" + } + ], + "examples": ["127.0.0.1:6831"] + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["server_url", "trace_id_ratio"] + }, + "additionalProperties": false, + "properties": { + "server_url": { + "type": "string", + "description": "The address of jaeger-agent's HTTP sampling server", + "format": "uri", + "examples": ["http://localhost:5778/sampling"] + }, + "trace_id_ratio": { + "type": "number", + "description": "Trace Id ratio sample", + "examples": [0.5] + } + } + } + } + }, + "zipkin": { + "type": "object", + "additionalProperties": false, + "description": "Configures the zipkin tracing backend.", + "properties": { + "server_url": { + "type": "string", + "description": "The address of the Zipkin server where spans should be sent to.", + "format": "uri", + "examples": ["http://localhost:9411/api/v2/spans"] + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["sampling_ratio"] + }, + "additionalProperties": false, + "properties": { + "sampling_ratio": { + "type": "number", + "description": "Sampling ratio for spans.", + "examples": [0.4] + } + } + } + } + }, + "otlp": { + "type": "object", + "additionalProperties": false, + "description": "Configures the OTLP tracing backend.", + "properties": { + "server_url": { + "type": "string", + "description": "The endpoint of the OTLP exporter (HTTP) where spans should be sent to.", + "anyOf": [ + { + "title": "IPv6 Address and Port", + "pattern": "^\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]:([0-9]*)$" + }, + { + "title": "IPv4 Address and Port", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}:([0-9]*)$" + }, + { + "title": "Hostname and Port", + "pattern": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]):([0-9]*)$" + } + ], + "examples": ["localhost:4318"] + }, + "insecure": { + "type": "boolean", + "description": "Will use HTTP if set to true; defaults to HTTPS." + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["sampling_ratio"] + }, + "additionalProperties": false, + "properties": { + "sampling_ratio": { + "type": "number", + "description": "Sampling ratio for spans.", + "examples": [0.4] + } + } + }, + "authorization_header": { + "type": "string", + "examples": ["Bearer 2389s8fs9d8fus9f"] + } + } + } + } + } + } +} diff --git a/oryx/otelx/jaeger.go b/oryx/otelx/jaeger.go new file mode 100644 index 0000000..5da5f77 --- /dev/null +++ b/oryx/otelx/jaeger.go @@ -0,0 +1,88 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "net" + + "go.opentelemetry.io/contrib/propagators/b3" + jaegerPropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/contrib/samplers/jaegerremote" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +// SetupJaeger configures and returns a Jaeger tracer. +// +// The returned tracer will by default attempt to send spans to a local Jaeger agent. +// Optionally, [otelx.JaegerConfig.LocalAgentAddress] can be set to specify a different target. +// +// By default, unless a parent sampler has taken a sampling decision, every span is sampled. +// [otelx.JaegerSampling.TraceIDRatio] may be used to customize the sampling probability, +// optionally alongside [otelx.JaegerSampling.ServerURL] to consult a remote server +// for the sampling strategy to be used. +func SetupJaeger(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + host, port, err := net.SplitHostPort(c.Providers.Jaeger.LocalAgentAddress) + if err != nil { + return nil, err + } + + exp, err := jaeger.New( + jaeger.WithAgentEndpoint( + jaeger.WithAgentHost(host), jaeger.WithAgentPort(port), + ), + ) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment))), + } + + samplingServerURL := c.Providers.Jaeger.Sampling.ServerURL + traceIdRatio := c.Providers.Jaeger.Sampling.TraceIDRatio + + sampler := sdktrace.TraceIDRatioBased(traceIdRatio) + + if samplingServerURL != "" { + sampler = jaegerremote.New( + "jaegerremote", + jaegerremote.WithSamplingServerURL(samplingServerURL), + jaegerremote.WithInitialSampler(sampler), + ) + } + + // Respect any sampling decision taken by the client. + sampler = sdktrace.ParentBased(sampler) + tpOpts = append(tpOpts, sdktrace.WithSampler(sampler)) + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + // At the moment, software across our cloud stack only support Zipkin (B3) + // and Jaeger propagation formats. Proposals for standardized formats for + // context propagation are in the works (ref: https://www.w3.org/TR/trace-context/ + // and https://www.w3.org/TR/baggage/). + // + // Simply add propagation.TraceContext{} and propagation.Baggage{} + // here to enable those as well. + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + jaegerPropagator.Jaeger{}, + b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader|b3.B3SingleHeader)), + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prop) + return tp.Tracer(tracerName), nil +} diff --git a/oryx/otelx/middleware.go b/oryx/otelx/middleware.go new file mode 100644 index 0000000..2b7c6f4 --- /dev/null +++ b/oryx/otelx/middleware.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "cmp" + "net/http" + "strings" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/ory/x/httprouterx" +) + +var withDefaultFilters = otelhttp.WithFilter(func(r *http.Request) bool { + return !(strings.HasPrefix(r.URL.Path, "/health") || + strings.HasPrefix(r.URL.Path, "/admin/health") || + strings.HasPrefix(r.URL.Path, "/metrics") || + strings.HasPrefix(r.URL.Path, "/admin/metrics")) +}) + +func NewMiddleware(next http.Handler, operation string, opts ...otelhttp.Option) http.Handler { + myOpts := []otelhttp.Option{ + withDefaultFilters, + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return cmp.Or(r.Pattern, operation, r.Method+" "+r.URL.Path) + }), + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callback := func(r2 *http.Request) { + r.Pattern = cmp.Or(r.Pattern, r2.Pattern) + } + r2 := httprouterx.WithAfterMatchHook(r, callback) + next.ServeHTTP(w, r2) + r.Pattern = cmp.Or(r2.Pattern, r.Pattern) // best-effort in case callback never is called + }) + return otelhttp.NewHandler(handler, operation, append(myOpts, opts...)...) +} diff --git a/oryx/otelx/otel.go b/oryx/otelx/otel.go new file mode 100644 index 0000000..298e828 --- /dev/null +++ b/oryx/otelx/otel.go @@ -0,0 +1,122 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" + + "github.com/ory/x/logrusx" + "github.com/ory/x/stringsx" +) + +type ( + Tracer struct { + tracer trace.Tracer + } + Provider interface { + Tracer(ctx context.Context) *Tracer + } +) + +// New creates a new tracer. If name is empty, a default tracer name is used +// instead. See: https://godocs.io/go.opentelemetry.io/otel/sdk/trace#TracerProvider.Tracer +func New(name string, l *logrusx.Logger, c *Config) (*Tracer, error) { + t := &Tracer{} + + if err := t.setup(name, l, c); err != nil { + return nil, err + } + + return t, nil +} + +// NewNoop creates a new no-op tracer. +func NewNoop() *Tracer { + tp := noop.NewTracerProvider() + t := &Tracer{tracer: tp.Tracer("")} + return t +} + +type simpleProvider struct{ t *Tracer } + +func (p simpleProvider) Tracer(_ context.Context) *Tracer { return p.t } + +func NewNoopProvider() Provider { return &simpleProvider{t: NewNoop()} } + +// setup constructs the tracer based on the given configuration. +func (t *Tracer) setup(name string, l *logrusx.Logger, c *Config) error { + switch f := stringsx.SwitchExact(c.Provider); { + case f.AddCase("jaeger"): + tracer, err := SetupJaeger(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("Jaeger tracer configured! Sending spans to %s", c.Providers.Jaeger.LocalAgentAddress) + case f.AddCase("zipkin"): + tracer, err := SetupZipkin(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("Zipkin tracer configured! Sending spans to %s", c.Providers.Zipkin.ServerURL) + case f.AddCase("otel"): + tracer, err := SetupOTLP(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("OTLP tracer configured! Sending spans to %s", c.Providers.OTLP.ServerURL) + case f.AddCase(""): + l.Infof("No tracer configured - skipping tracing setup") + t.tracer = noop.NewTracerProvider().Tracer(name) + default: + return f.ToUnknownCaseErr() + } + + return nil +} + +// IsLoaded returns true if the tracer has been loaded. +func (t *Tracer) IsLoaded() bool { + if t == nil || t.tracer == nil { + return false + } + return true +} + +// Tracer returns the underlying OpenTelemetry tracer. +func (t *Tracer) Tracer() trace.Tracer { + return t.tracer +} + +// WithOTLP returns a new tracer with the underlying OpenTelemetry Tracer +// replaced. +func (t *Tracer) WithOTLP(other trace.Tracer) *Tracer { + return &Tracer{other} +} + +// Provider returns a TracerProvider which in turn yields this tracer unmodified. +func (t *Tracer) Provider() trace.TracerProvider { + return tracerProvider{t: t.Tracer()} +} + +type tracerProvider struct { + embedded.TracerProvider + t trace.Tracer +} + +var _ trace.TracerProvider = tracerProvider{} + +// Tracer implements trace.TracerProvider. +func (tp tracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { + return tp.t +} diff --git a/oryx/otelx/otlp.go b/oryx/otelx/otlp.go new file mode 100644 index 0000000..f5c3d7d --- /dev/null +++ b/oryx/otelx/otlp.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + + "go.opentelemetry.io/contrib/propagators/b3" + jaegerPropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +func SetupOTLP(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + ctx := context.Background() + + clientOpts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(c.Providers.OTLP.ServerURL), + } + + if c.Providers.OTLP.Insecure { + clientOpts = append(clientOpts, otlptracehttp.WithInsecure()) + } + + if c.Providers.OTLP.AuthorizationHeader != "" { + clientOpts = append(clientOpts, + otlptracehttp.WithHeaders(map[string]string{"Authorization": c.Providers.OTLP.AuthorizationHeader}), + ) + } + + exp, err := otlptrace.New( + ctx, otlptracehttp.NewClient(clientOpts...), + ) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment), + )), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased( + c.Providers.OTLP.Sampling.SamplingRatio, + ))), + } + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + jaegerPropagator.Jaeger{}, + b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader|b3.B3SingleHeader)), + propagation.Baggage{}, + )) + + return tp.Tracer(tracerName), nil +} diff --git a/oryx/otelx/semconv/context.go b/oryx/otelx/semconv/context.go new file mode 100644 index 0000000..a67bfd4 --- /dev/null +++ b/oryx/otelx/semconv/context.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/httpx" +) + +type contextKey int + +const contextKeyAttributes contextKey = iota + +func ContextWithAttributes(ctx context.Context, attrs ...attribute.KeyValue) context.Context { + existing, _ := ctx.Value(contextKeyAttributes).([]attribute.KeyValue) + return context.WithValue(ctx, contextKeyAttributes, append(existing, attrs...)) +} + +func AttributesFromContext(ctx context.Context) []attribute.KeyValue { + fromCtx, _ := ctx.Value(contextKeyAttributes).([]attribute.KeyValue) + uniq := make(map[attribute.Key]struct{}) + attrs := make([]attribute.KeyValue, 0) + for i := len(fromCtx) - 1; i >= 0; i-- { + if _, ok := uniq[fromCtx[i].Key]; !ok { + uniq[fromCtx[i].Key] = struct{}{} + attrs = append(attrs, fromCtx[i]) + } + } + reverse(attrs) + return attrs +} + +func Middleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ctx := ContextWithAttributes(r.Context(), + append( + AttrGeoLocation(*httpx.ClientGeoLocation(r)), + AttrClientIP(httpx.ClientIP(r)), + )..., + ) + + next(rw, r.WithContext(ctx)) +} + +func reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} diff --git a/oryx/otelx/semconv/deprecated.go b/oryx/otelx/semconv/deprecated.go new file mode 100644 index 0000000..4a2615a --- /dev/null +++ b/oryx/otelx/semconv/deprecated.go @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + + otelattr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// NewDeprecatedFeatureUsedEvent creates a new event indicating that a deprecated feature was used. +// It returns the event name and a trace.EventOption that can be used to +// add the event to a span. +// +// span.AddEvent(NewDeprecatedFeatureUsedEvent(ctx, "deprecated-feature-id", otelattr.String("key", "value"))) +func NewDeprecatedFeatureUsedEvent(ctx context.Context, deprecatedCodeFeatureID string, attrs ...otelattr.KeyValue) (string, trace.EventOption) { + return DeprecatedFeatureUsed.String(), + trace.WithAttributes( + append( + append( + attrs, + AttributesFromContext(ctx)..., + ), + AttrDeprecatedFeatureID(deprecatedCodeFeatureID), + )..., + ) +} + +const ( + AttributeKeyDeprecatedCodePathIDAttributeKey AttributeKey = "DeprecatedFeatureID" + DeprecatedFeatureUsed Event = "DeprecatedFeatureUsed" +) + +func AttrDeprecatedFeatureID(id string) otelattr.KeyValue { + return otelattr.String(AttributeKeyDeprecatedCodePathIDAttributeKey.String(), id) +} diff --git a/oryx/otelx/semconv/events.go b/oryx/otelx/semconv/events.go new file mode 100644 index 0000000..7e9759a --- /dev/null +++ b/oryx/otelx/semconv/events.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package semconv contains OpenTelemetry semantic convention constants. +package semconv + +import ( + "github.com/gofrs/uuid" + otelattr "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/httpx" +) + +type Event string + +func (e Event) String() string { + return string(e) +} + +type AttributeKey string + +func (a AttributeKey) String() string { + return string(a) +} + +const ( + AttributeKeyIdentityID AttributeKey = "IdentityID" + AttributeKeyNID AttributeKey = "ProjectID" + AttributeKeyClientIP AttributeKey = "ClientIP" + AttributeKeyGeoLocationCity AttributeKey = "GeoLocationCity" + AttributeKeyGeoLocationRegion AttributeKey = "GeoLocationRegion" + AttributeKeyGeoLocationCountry AttributeKey = "GeoLocationCountry" + AttributeKeyGeoLocationLatitude AttributeKey = "GeoLocationLatitude" + AttributeKeyGeoLocationLongitude AttributeKey = "GeoLocationLongitude" + AttributeKeyWorkspace AttributeKey = "WorkspaceID" + AttributeKeySubscriptionID AttributeKey = "SubscriptionID" + AttributeKeyProjectEnvironment AttributeKey = "ProjectEnvironment" + AttributeKeyWorkspaceAPIKeyID AttributeKey = "WorkspaceAPIKeyID" + AttributeKeyProjectAPIKeyID AttributeKey = "ProjectAPIKeyID" +) + +func AttrIdentityID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyIdentityID.String(), uuidOrString(val)) +} + +func AttrNID(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyNID.String(), val.String()) +} + +func AttrWorkspace(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyWorkspace.String(), val.String()) +} + +func AttrSubscription(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeySubscriptionID.String(), val.String()) +} + +func AttrProjectEnvironment(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyProjectEnvironment.String(), val) +} + +func AttrClientIP(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyClientIP.String(), val) +} + +func AttrGeoLocation(val httpx.GeoLocation) []otelattr.KeyValue { + geoLocationAttributes := make([]otelattr.KeyValue, 0, 3) + + if val.City != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationCity.String(), val.City)) + } + if val.Country != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationCountry.String(), val.Country)) + } + if val.Region != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationRegion.String(), val.Region)) + } + if val.Latitude != nil { + geoLocationAttributes = append(geoLocationAttributes, otelattr.Float64(AttributeKeyGeoLocationLatitude.String(), *val.Latitude)) + } + if val.Longitude != nil { + geoLocationAttributes = append(geoLocationAttributes, otelattr.Float64(AttributeKeyGeoLocationLongitude.String(), *val.Longitude)) + } + + return geoLocationAttributes +} + +func AttrWorkspaceAPIKeyID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyWorkspaceAPIKeyID.String(), uuidOrString(val)) +} + +func AttrProjectAPIKeyID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyProjectAPIKeyID.String(), uuidOrString(val)) +} + +func uuidOrString[V string | uuid.UUID](val V) string { + switch val := any(val).(type) { + case string: + return val + case uuid.UUID: + return val.String() + } + panic("unreachable") +} diff --git a/oryx/otelx/semconv/warning.go b/oryx/otelx/semconv/warning.go new file mode 100644 index 0000000..79f9ae4 --- /dev/null +++ b/oryx/otelx/semconv/warning.go @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + + otelattr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// NewWarning creates a new warning event with the given ID and attributes. +// It returns the event name and a trace.EventOption that can be used to +// add the event to a span. +// +// span.AddEvent(NewWarning(ctx, "warning-id", otelattr.String("key", "value"))) +func NewWarning(ctx context.Context, id string, attrs ...otelattr.KeyValue) (string, trace.EventOption) { + return Warning.String(), + trace.WithAttributes( + append( + append( + attrs, + AttributesFromContext(ctx)..., + ), + otelattr.String(AttributeWarningID.String(), id), + )..., + ) +} + +const ( + Warning Event = "Warning" + AttributeWarningID AttributeKey = "WarningID" +) + +func AttrWarningID(id string) otelattr.KeyValue { + return otelattr.String(AttributeWarningID.String(), id) +} diff --git a/oryx/otelx/withspan.go b/oryx/otelx/withspan.go new file mode 100644 index 0000000..5fda63d --- /dev/null +++ b/oryx/otelx/withspan.go @@ -0,0 +1,148 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + "errors" + "fmt" + "reflect" + "runtime" + "strings" + + pkgerrors "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +// WithSpan wraps execution of f in a span identified by name. +// +// If f returns an error or panics, the span status will be set to the error +// state. The error (or panic) will be propagated unmodified. +// +// f will be wrapped in a child span by default. To make a new root span +// instead, pass the trace.WithNewRoot() option. +func WithSpan(ctx context.Context, name string, f func(context.Context) error, opts ...trace.SpanStartOption) (err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, name, opts...) + defer func() { + defer span.End() + if r := recover(); r != nil { + setErrorStatusPanic(span, r) + panic(r) + } else if err != nil { + span.SetStatus(codes.Error, err.Error()) + setErrorTags(span, err) + } + }() + return f(ctx) +} + +// End finishes span, and automatically sets the error state if *err is not nil +// or during panicking. +// +// Usage: +// +// func Divide(ctx context.Context, numerator, denominator int) (ratio int, err error) { +// ctx, span := tracer.Start(ctx, "Divide") +// defer otelx.End(span, &err) +// if denominator == 0 { +// return 0, errors.New("cannot divide by zero") +// } +// return numerator / denominator, nil +// } +// +// During a panic, we don't fully conform to OpenTelemetry's semantic +// conventions because that would require us to emit a span event to attach the +// stacktrace and error type, and we don't want to do that. Instead, we set the +// tags on the span directly. +// https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/ +// +// For improved compatibility with Datadog, we also set some additional tags as +// documented here: +// https://docs.datadoghq.com/standard-attributes/?product=apm&search=error +func End(span trace.Span, err *error) { + defer span.End() + if r := recover(); r != nil { + setErrorStatusPanic(span, r) + panic(r) + } + if err == nil || *err == nil { + return + } + span.SetStatus(codes.Error, (*err).Error()) + setErrorTags(span, *err) +} + +func setErrorStatusPanic(span trace.Span, recovered any) { + span.SetAttributes( + // OpenTelemetry says to add these attributes to an event, not the span + // itself. We don't want to do that, so we're adding them to the span + // directly. + semconv.ExceptionEscaped(true), + // OpenTelemetry describes "exception.stacktrace" We don't love that, + // though, so we're using "error.stack" instead, like DataDog). + attribute.String("error.stack", stacktrace()), + ) + if t := reflect.TypeOf(recovered); t != nil { + span.SetAttributes(semconv.ExceptionType(t.String())) + } + switch e := recovered.(type) { + case error: + span.SetStatus(codes.Error, "panic: "+e.Error()) + setErrorTags(span, e) + case string, fmt.Stringer: + span.SetStatus(codes.Error, fmt.Sprintf("panic: %v", e)) + default: + span.SetStatus(codes.Error, "panic") + case nil: + // nothing + } +} + +func setErrorTags(span trace.Span, err error) { + span.SetAttributes( + attribute.String("error", err.Error()), + attribute.String("error.message", err.Error()), // DataDog compat + attribute.String("error.type", fmt.Sprintf("%T", errors.Unwrap(err))), // the innermost error type is the most useful here + ) + if e := interface{ StackTrace() pkgerrors.StackTrace }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.stack", fmt.Sprintf("%+v", e.StackTrace()))) + } + if e := interface{ Reason() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.reason", e.Reason())) + } + if e := interface{ Debug() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.debug", e.Debug())) + } + if e := interface{ ID() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.id", e.ID())) + } + if e := interface{ Details() map[string]interface{} }(nil); errors.As(err, &e) { + for k, v := range e.Details() { + span.SetAttributes(attribute.String("error.details."+k, fmt.Sprintf("%v", v))) + } + } +} + +func stacktrace() string { + pc := make([]uintptr, 5) + n := runtime.Callers(4, pc) + if n == 0 { + return "" + } + pc = pc[:n] + frames := runtime.CallersFrames(pc) + + var builder strings.Builder + for { + frame, more := frames.Next() + fmt.Fprintf(&builder, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line) + if !more { + break + } + } + return builder.String() +} diff --git a/oryx/otelx/zipkin.go b/oryx/otelx/zipkin.go new file mode 100644 index 0000000..59922c4 --- /dev/null +++ b/oryx/otelx/zipkin.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/zipkin" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +func SetupZipkin(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + exp, err := zipkin.New(c.Providers.Zipkin.ServerURL) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment), + )), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased( + c.Providers.Zipkin.Sampling.SamplingRatio, + ))), + } + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + return tp.Tracer(tracerName), nil +} diff --git a/oryx/package-lock.json b/oryx/package-lock.json new file mode 100644 index 0000000..9ad1dbd --- /dev/null +++ b/oryx/package-lock.json @@ -0,0 +1,657 @@ +{ + "name": "x", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "license-checker": "^25.0.1", + "prettier": "3.8.1" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "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", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/oryx/package.go b/oryx/package.go new file mode 100644 index 0000000..5b760d6 --- /dev/null +++ b/oryx/package.go @@ -0,0 +1,4 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x diff --git a/oryx/package.json b/oryx/package.json new file mode 100644 index 0000000..6a0de08 --- /dev/null +++ b/oryx/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "prettier": { + "proseWrap": "always", + "semi": false, + "trailingComma": "all" + }, + "devDependencies": { + "license-checker": "^25.0.1", + "prettier": "3.8.1" + } +} diff --git a/oryx/pagination/README.md b/oryx/pagination/README.md new file mode 100644 index 0000000..69ba0e0 --- /dev/null +++ b/oryx/pagination/README.md @@ -0,0 +1,29 @@ +# pagination + +A simple helper for dealing with pagination. + +``` +go get github.com/ory/pagination +``` + +## Example + +```go +package main + +import ( + "github.com/ory/pagination" + "net/http" + "net/url" + "fmt" +) + +func main() { + u, _ := url.Parse("http://localhost/foo?offset=0&limit=10") + limit, offset := pagination.Parse(&http.Request{URL: u}, 5, 5, 10) + + items := []string{"a", "b", "c", "d"} + start, end := pagination.Index(limit, offset, len(items)) + fmt.Printf("Got items: %v", items[start:end]) +} +``` diff --git a/oryx/pagination/header.go b/oryx/pagination/header.go new file mode 100644 index 0000000..17533e5 --- /dev/null +++ b/oryx/pagination/header.go @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +import ( + "fmt" + "math" + "net/http" + "net/url" + "strconv" + "strings" +) + +func header(u *url.URL, rel string, limit, offset int64) string { + q := u.Query() + q.Set("limit", fmt.Sprintf("%d", limit)) + q.Set("offset", fmt.Sprintf("%d", offset)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +type formatter func(location *url.URL, rel string, itemsPerPage int64, offset int64) string + +// HeaderWithFormatter adds an HTTP header for pagination which uses a custom formatter for generating the URL links. +func HeaderWithFormatter(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int, f formatter) { + if itemsPerPage <= 0 { + itemsPerPage = 1 + } + + itemsPerPage64 := int64(itemsPerPage) + offset := int64(page) * itemsPerPage64 + + // lastOffset will either equal the offset required to contain the remainder, + // or the limit. + var lastOffset int64 + if total%itemsPerPage64 == 0 { + lastOffset = total - itemsPerPage64 + } else { + lastOffset = (total / itemsPerPage64) * itemsPerPage64 + } + + w.Header().Set("X-Total-Count", strconv.FormatInt(total, 10)) + + // Check for last page + if offset >= lastOffset { + if total == 0 { + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64), + f(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64), + }, ",")) + return + } + + if total <= itemsPerPage64 { + w.Header().Set("link", f(u, "first", total, 0)) + return + } + + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "prev", itemsPerPage64, lastOffset-itemsPerPage64), + }, ",")) + return + } + + if offset < itemsPerPage64 { + w.Header().Set("Link", strings.Join([]string{ + f(u, "next", itemsPerPage64, itemsPerPage64), + f(u, "last", itemsPerPage64, lastOffset), + }, ",")) + return + } + + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64), + f(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64), + f(u, "last", itemsPerPage64, lastOffset), + }, ",")) +} + +// Header adds an http header for pagination using a responsewriter where backwards compatibility is required. +// The header will contain links any combination of the first, last, next, or previous (prev) pages in a paginated list (given a limit and an offset, and optionally a total). +// If total is not set, then no "last" page will be calculated. +// If no limit is provided, then it will default to 1. +func Header(w http.ResponseWriter, u *url.URL, total int, limit, offset int) { + var page int + if limit == 0 { + limit = 1 + } + + page = int(math.Floor(float64(offset) / float64(limit))) + HeaderWithFormatter(w, u, int64(total), page, limit, header) +} diff --git a/oryx/pagination/items.go b/oryx/pagination/items.go new file mode 100644 index 0000000..6094f2a --- /dev/null +++ b/oryx/pagination/items.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +// MaxItemsPerPage is used to prevent DoS attacks against large lists by limiting the items per page to 500. +func MaxItemsPerPage(max, is int) int { + if is > max { + return max + } + return is +} diff --git a/oryx/pagination/keysetclient/parse_header.go b/oryx/pagination/keysetclient/parse_header.go new file mode 100644 index 0000000..4ab09fd --- /dev/null +++ b/oryx/pagination/keysetclient/parse_header.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package keysetclient parses keyset pagination metadata from HTTP responses. +// It is the client-side counterpart to keysetpagination and deliberately +// carries no SQL dependencies, so API clients such as the Ory CLI do not link +// the database layer. +package keysetclient + +import ( + "net/http" + "net/url" + + "github.com/peterhellberg/link" +) + +// Result represents a parsed result of the link HTTP header. +type Result struct { + // NextToken is the next page token. If it's empty, there is no next page. + NextToken string + + // FirstToken is the first page token. + FirstToken string +} + +// ParseHeader parses the response header's Link. +func ParseHeader(r *http.Response) *Result { + links := link.ParseResponse(r) + return &Result{ + NextToken: findRel(links, "next"), + FirstToken: findRel(links, "first"), + } +} + +func findRel(links link.Group, rel string) string { + for idx, l := range links { + if idx == rel { + parsed, err := url.Parse(l.URI) + if err != nil { + continue + } + + return parsed.Query().Get("page_token") + } + } + + return "" +} diff --git a/oryx/pagination/keysetpagination/header.go b/oryx/pagination/keysetpagination/header.go new file mode 100644 index 0000000..ec16640 --- /dev/null +++ b/oryx/pagination/keysetpagination/header.go @@ -0,0 +1,112 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="next" + // + Link string `json:"link"` +} + +func header(u *url.URL, rel, token string, size int) string { + q := u.Query() + q.Set("page_token", token) + q.Set("page_size", strconv.Itoa(size)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +// Header adds the Link header for the page encoded by the paginator. +// It contains links to the first and next page, if one exists. +func Header(w http.ResponseWriter, u *url.URL, p *Paginator) { + size := p.Size() + link := []string{header(u, "first", p.defaultToken.Encode(), size)} + if !p.isLast { + link = append(link, header(u, "next", p.Token().Encode(), size)) + } + w.Header().Set("Link", strings.Join(link, ",")) +} + +// Parse returns the pagination options from the URL query. +func Parse(q url.Values, p PageTokenConstructor) ([]Option, error) { + var opts []Option + if pt := cmp.Or(q["page_token"]...); pt != "" { + pageToken, err := url.QueryUnescape(pt) + if err != nil { + return nil, errors.WithStack(err) + } + parsed, err := p(pageToken) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithToken(parsed)) + } + if ps := cmp.Or(q["page_size"]...); ps != "" { + size, err := strconv.Atoi(ps) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithSize(size)) + } + return opts, nil +} diff --git a/oryx/pagination/keysetpagination/page_token.go b/oryx/pagination/keysetpagination/page_token.go new file mode 100644 index 0000000..c881678 --- /dev/null +++ b/oryx/pagination/keysetpagination/page_token.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "encoding/base64" + "fmt" + "strings" +) + +type PageToken = interface { + Parse(string) map[string]string + Encode() string +} + +var _ PageToken = new(StringPageToken) +var _ PageToken = new(MapPageToken) + +type StringPageToken string + +func (s StringPageToken) Parse(idField string) map[string]string { + return map[string]string{idField: string(s)} +} + +func (s StringPageToken) Encode() string { + return string(s) +} + +func NewStringPageToken(s string) (PageToken, error) { + return StringPageToken(s), nil +} + +type MapPageToken map[string]string + +func (m MapPageToken) Parse(_ string) map[string]string { + return map[string]string(m) +} + +const pageTokenColumnDelim = "/" + +func (m MapPageToken) Encode() string { + elems := make([]string, 0, len(m)) + for k, v := range m { + elems = append(elems, fmt.Sprintf("%s=%s", k, v)) + } + + // For now: use Base64 instead of URL escaping, as the Timestamp format we need to use can contain a `+` sign, + // which represents a space in URLs, so it's not properly encoded by the Go library. + return base64.RawStdEncoding.EncodeToString([]byte(strings.Join(elems, pageTokenColumnDelim))) +} + +func NewMapPageToken(s string) (PageToken, error) { + b, err := base64.RawStdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + tokens := strings.Split(string(b), pageTokenColumnDelim) + + r := map[string]string{} + + for _, p := range tokens { + if columnName, value, found := strings.Cut(p, "="); found { + r[columnName] = value + } + } + + return MapPageToken(r), nil +} + +var _ PageTokenConstructor = NewMapPageToken +var _ PageTokenConstructor = NewStringPageToken + +type PageTokenConstructor = func(string) (PageToken, error) diff --git a/oryx/pagination/keysetpagination/paginator.go b/oryx/pagination/keysetpagination/paginator.go new file mode 100644 index 0000000..8f47562 --- /dev/null +++ b/oryx/pagination/keysetpagination/paginator.go @@ -0,0 +1,258 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "errors" + "fmt" + + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/columns" +) + +type ( + Item = interface{ PageToken() PageToken } + + Order string + + columnOrdering struct { + name string + order Order + } + Paginator struct { + token, defaultToken PageToken + size, defaultSize, maxSize int + isLast bool + additionalColumn columnOrdering + } + Option func(*Paginator) *Paginator +) + +var ErrUnknownOrder = errors.New("unknown order") + +const ( + OrderDescending Order = "DESC" + OrderAscending Order = "ASC" + + DefaultSize = 100 + DefaultMaxSize = 500 +) + +func (o Order) extract() (string, string, error) { + switch o { + case OrderAscending: + return ">", string(o), nil + case OrderDescending: + return "<", string(o), nil + default: + return "", "", ErrUnknownOrder + } +} + +func (p *Paginator) Token() PageToken { + if p.token == nil { + return p.defaultToken + } + return p.token +} + +func (p *Paginator) Size() int { + size := p.size + if size <= 0 { + size = p.defaultSize + if size == 0 { + size = 100 + } + } + if size > p.maxSize { + size = p.maxSize + } + return size +} + +func (p *Paginator) IsLast() bool { + return p.isLast +} + +func (p *Paginator) ToOptions() []Option { + opts := make([]Option, 0, 7) + if p.token != nil { + opts = append(opts, WithToken(p.token)) + } + if p.defaultToken != nil { + opts = append(opts, WithDefaultToken(p.defaultToken)) + } + if p.size > 0 { + opts = append(opts, WithSize(p.size)) + } + if p.defaultSize != DefaultSize { + opts = append(opts, WithDefaultSize(p.defaultSize)) + } + if p.maxSize != DefaultMaxSize { + opts = append(opts, WithMaxSize(p.maxSize)) + } + if p.additionalColumn.name != "" { + opts = append(opts, WithColumn(p.additionalColumn.name, p.additionalColumn.order)) + } + if p.isLast { + opts = append(opts, withIsLast(p.isLast)) + } + return opts +} + +func (p *Paginator) multipleOrderFieldsQuery(q *pop.Query, idField string, cols map[string]*columns.Column, quoteAndContextualize func(string) string) { + tokenParts := p.Token().Parse(idField) + idValue := tokenParts[idField] + + column, ok := cols[p.additionalColumn.name] + if !ok { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + quoteName := quoteAndContextualize(column.Name) + + value, ok := tokenParts[column.Name] + + if !ok { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + sign, keyword, err := p.additionalColumn.order.extract() + if err != nil { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + q. + Where(fmt.Sprintf("(%s %s ? OR (%s = ? AND %s > ?))", quoteName, sign, quoteName, quoteAndContextualize(idField)), value, value, idValue). + Order(fmt.Sprintf("%s %s", quoteName, keyword)) + +} + +// Paginate returns a function that paginates a pop.Query. +// Usage: +// +// q := c.Where("foo = ?", foo).Scope(keysetpagination.Paginate[MyItemType](paginator)) +// +// This function works regardless of whether your type implements the Item +// interface with pointer or value receivers. To understand the type parameters, +// see this document: +// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example +func Paginate[I any, PI interface { + Item + *I +}](p *Paginator) pop.ScopeFunc { + model := pop.Model{Value: new(I)} + id := model.IDField() + tableName := model.Alias() + return func(q *pop.Query) *pop.Query { + quote := q.Connection.Dialect.Quote + eid := quote(tableName) + "." + quote(id) + + quoteAndContextualize := func(name string) string { + return quote(tableName) + "." + quote(name) + } + p.multipleOrderFieldsQuery(q, id, model.Columns().Cols, quoteAndContextualize) + + return q. + Limit(p.Size() + 1). + // we always need to order by the id field last + Order(fmt.Sprintf(`%s ASC`, eid)) + } +} + +// Result removes the last item (if applicable) and returns the paginator for the next page. +// +// This function works regardless of whether your type implements the Item +// interface with pointer or value receivers. To understand the type parameters, +// see this document: +// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example +func Result[I any, PI interface { + Item + *I +}](items []I, p *Paginator) ([]I, *Paginator) { + if len(items) > p.Size() { + items = items[:p.Size()] + return items, &Paginator{ + token: PI(&items[len(items)-1]).PageToken(), + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } + } + return items, &Paginator{ + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + isLast: true, + } +} + +func WithDefaultToken(t PageToken) Option { + return func(opts *Paginator) *Paginator { + opts.defaultToken = t + return opts + } +} + +func WithDefaultSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.defaultSize = size + return opts + } +} + +func WithMaxSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.maxSize = size + return opts + } +} + +func WithToken(t PageToken) Option { + return func(opts *Paginator) *Paginator { + opts.token = t + return opts + } +} + +func WithSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.size = size + return opts + } +} + +func WithColumn(name string, order Order) Option { + return func(opts *Paginator) *Paginator { + opts.additionalColumn = columnOrdering{ + name: name, + order: order, + } + return opts + } +} + +func withIsLast(isLast bool) Option { + return func(opts *Paginator) *Paginator { + opts.isLast = isLast + return opts + } +} + +func GetPaginator(modifiers ...Option) *Paginator { + opts := &Paginator{ + // these can still be overridden by the modifiers, but they should never be unset + maxSize: DefaultMaxSize, + defaultSize: DefaultSize, + } + for _, f := range modifiers { + opts = f(opts) + } + return opts +} diff --git a/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json b/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json new file mode 100644 index 0000000..8fd784f --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json @@ -0,0 +1,67 @@ +{ + "e": "2023-01-01T01:00:00Z", + "c": [ + { + "n": "id", + "v": "token" + }, + { + "n": "string", + "o": 1, + "v": "str" + }, + { + "n": "string-zero", + "o": 1, + "v": "" + }, + { + "n": "time", + "vt": "2023-01-01T00:00:00Z" + }, + { + "n": "uuid", + "vu": "b652bbbd-1a84-4fb7-9437-6b78ba2686db" + }, + { + "n": "nulluuid", + "vu": "b652bbbd-1a84-4fb7-9437-6b78ba2686da" + }, + { + "n": "int64", + "vi": 64 + }, + { + "n": "int64-zero", + "vi": 0 + }, + { + "n": "empty-nullint64", + "vi": 0 + }, + { + "n": "nullint64", + "vi": 64 + }, + { + "n": "nullstring", + "v": "str" + }, + { + "n": "empty-nullstring", + "v": "" + }, + { + "n": "null-nulluuid", + "vn": true + }, + { + "n": "null-nullstring", + "vn": true + }, + { + "n": "null-nullint64", + "vn": true + } + ] +} diff --git a/oryx/pagination/keysetpagination_v2/page_token.go b/oryx/pagination/keysetpagination_v2/page_token.go new file mode 100644 index 0000000..f468002 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/page_token.go @@ -0,0 +1,245 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "crypto/rand" + "database/sql" + "encoding/base64" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/x/aead" +) + +// pageTokenContext is passed as additional authenticated data to the AEAD. +// It binds the ciphertext to its purpose: even if the encryption key is +// reused in another context (for example when it is derived from a shared +// system secret), ciphertexts from that context are rejected as page tokens +// and vice versa. +const pageTokenContext = "ory/keysetpagination_v2/page_token" + +// fallbackEncryptionKey seals page tokens when no pagination secrets are +// configured. It is well known, so those tokens are only obfuscated, not +// authenticated. Configure pagination secrets to authenticate tokens. +var fallbackEncryptionKey [32]byte + +type ( + PageToken struct { + testNow func() time.Time + cols []Column + } + jsonPageToken = struct { + ExpiresAt time.Time `json:"e"` + Cols []jsonColumn `json:"c"` + } + + jsonColumn = struct { + Name string `json:"n"` + Order Order `json:"o,omitempty"` + Nullable bool `json:"nl,omitempty"` + HasConstraint bool `json:"hc,omitempty"` + + ValueAny any `json:"v,omitempty"` + ValueTime *time.Time `json:"vt,omitempty"` + ValueUUID *uuid.UUID `json:"vu,omitempty"` + ValueInt64 *int64 `json:"vi,omitempty"` + ValueNull bool `json:"vn,omitempty"` + } + Column struct { + Name string + Order Order + Value any + Nullable bool + // HasConstraint marks if the column is already constrained by WHERE condition. + HasConstraint bool + } +) + +func (t PageToken) Columns() []Column { return t.cols } + +// Encrypt encrypts the page token using the first key in the provided keyset. +// It uses a fallback key if no keys are provided. +func (t PageToken) Encrypt(keys [][32]byte) string { + key := fallbackEncryptionKey + if len(keys) > 0 { + key = keys[0] + } + enc, err := t.encrypt(key) + if err != nil { + // This should basically never happen, only if reading from the random source or marshaling the token fails. + // In both cases, we have a bigger problem than just not being able to generate the page token. + // Therefore, if we do get an error, there is no point in returning it to the client, + // as we already have a working result set. With this string, the next page will return an error, + // but that is better than breaking the current page. + return "internal error: failed to generate page token" + } + return enc +} + +func (t PageToken) MarshalJSON() ([]byte, error) { + now := time.Now + if t.testNow != nil { + now = t.testNow + } + toEncode := jsonPageToken{ + ExpiresAt: now().Add(time.Hour).UTC(), + Cols: make([]jsonColumn, len(t.cols)), + } + for i, col := range t.cols { + toEncode.Cols[i] = jsonColumn{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + } + switch v := col.Value.(type) { + case time.Time: + toEncode.Cols[i].ValueTime = new(v) + case uuid.UUID: + toEncode.Cols[i].ValueUUID = new(v) + case uuid.NullUUID: + if v.Valid { + toEncode.Cols[i].ValueUUID = new(v.UUID) + } else { + toEncode.Cols[i].ValueNull = true + } + case sql.NullString: + if v.Valid { + toEncode.Cols[i].ValueAny = new(v.String) + } else { + toEncode.Cols[i].ValueNull = true + } + case int64: + toEncode.Cols[i].ValueInt64 = new(v) + case sql.NullInt64: + if v.Valid { + toEncode.Cols[i].ValueInt64 = new(v.Int64) + } else { + toEncode.Cols[i].ValueNull = true + } + default: + toEncode.Cols[i].ValueAny = v + } + } + return json.Marshal(toEncode) +} + +func ErrPageTokenExpired() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("page token expired, do not persist page tokens") +} + +func (t *PageToken) UnmarshalJSON(data []byte) error { + rawToken := jsonPageToken{} + if err := json.Unmarshal(data, &rawToken); err != nil { + return err + } + t.cols = make([]Column, len(rawToken.Cols)) + for i, col := range rawToken.Cols { + t.cols[i] = Column{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + } + switch { + case col.ValueNull: + t.cols[i].Value = nil + case col.ValueAny != nil: + t.cols[i].Value = col.ValueAny + case col.ValueUUID != nil: + t.cols[i].Value = *col.ValueUUID + case col.ValueTime != nil: + t.cols[i].Value = *col.ValueTime + case col.ValueInt64 != nil: + t.cols[i].Value = *col.ValueInt64 + } + } + + now := time.Now + if t.testNow != nil { + now = t.testNow + } + if rawToken.ExpiresAt.Before(now().UTC()) { + return errors.WithStack(ErrPageTokenExpired()) + } + return nil +} + +func NewPageToken(cols ...Column) PageToken { return PageToken{cols: cols} } + +func (t *PageToken) encrypt(key [32]byte) (string, error) { + raw, err := json.Marshal(t) + if err != nil { + return "", errors.Wrap(err, "cannot marshal page token") + } + + a, err := aead.New(key) + if err != nil { + return "", errors.Wrap(err, "cannot create AEAD") + } + + // The nonce is prepended to the ciphertext. AEADs that manage the nonce + // internally report a nonce size of zero, so this also covers them. + nonce := make([]byte, a.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", errors.Wrap(err, "cannot generate nonce") + } + + return base64.URLEncoding.EncodeToString(a.Seal(nonce, nonce, raw, []byte(pageTokenContext))), nil +} + +func (t *PageToken) decrypt(key [32]byte, s string) error { + if s == "" { + return errors.WithStack(ErrInvalidPaginationToken()) + } + + raw, err := base64.URLEncoding.DecodeString(s) + if err != nil { + return errors.WithStack(ErrInvalidPaginationToken()) + } + + dec, err := openAEAD(key, raw) + if err != nil { + // Tokens issued before the switch to a context-bound AEAD are sealed + // with NaCl secretbox. Remove this fallback once all tokens issued by + // the previous version have expired. + var ok bool + dec, ok = aead.OpenLegacySecretbox(key, raw) + if !ok { + return errors.WithStack(ErrInvalidPaginationToken()) + } + } + + if err := json.Unmarshal(dec, t); err != nil { + if errors.As(err, new(*herodot.DefaultError)) { + return err + } + return errors.WithStack(herodot.ErrInternalServerError().WithReason("unable to unmarshal page token").WithDebug(err.Error())) + } + + return nil +} + +func openAEAD(key [32]byte, raw []byte) ([]byte, error) { + a, err := aead.New(key) + if err != nil { + return nil, errors.Wrap(err, "cannot create AEAD") + } + if len(raw) < a.NonceSize() { + return nil, errors.New("ciphertext too short") + } + nonce, ciphertext := raw[:a.NonceSize()], raw[a.NonceSize():] + bs, err := a.Open(nil, nonce, ciphertext, []byte(pageTokenContext)) + if err != nil { + return nil, errors.Wrap(err, "cannot open AEAD") + } + + return bs, nil +} diff --git a/oryx/pagination/keysetpagination_v2/paginator.go b/oryx/pagination/keysetpagination_v2/paginator.go new file mode 100644 index 0000000..0722f1d --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/paginator.go @@ -0,0 +1,192 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "reflect" + + "github.com/jmoiron/sqlx/reflectx" + + "github.com/ory/herodot" +) + +func ErrInvalidPaginationToken() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("invalid pagination token") +} + +type ( + Paginator struct { + token, defaultToken PageToken + size, defaultSize, maxSize int + isLast bool + } + Option func(*Paginator) +) + +const ( + DefaultSize = 100 + DefaultMaxSize = 500 +) + +var dbStructTagMapper = reflectx.NewMapper("db") + +func (p *Paginator) DefaultToken() PageToken { return p.defaultToken } +func (p *Paginator) IsLast() bool { return p.isLast } + +func (p *Paginator) PageToken() PageToken { + if p.token.cols != nil { + return p.token + } + return p.defaultToken +} + +func (p *Paginator) Size() int { + defaultSize := cmp.Or(p.defaultSize, DefaultSize) + maxSize := cmp.Or(p.maxSize, DefaultMaxSize) + + size := p.size + if size <= 0 { + size = defaultSize + } + if size > maxSize { + size = maxSize + } + + return size +} + +func (p *Paginator) ToOptions() []Option { + opts := make([]Option, 0, 6) + if p.token.cols != nil { + opts = append(opts, WithToken(p.token)) + } + if p.defaultToken.cols != nil { + opts = append(opts, WithDefaultToken(p.defaultToken)) + } + if p.size > 0 { + opts = append(opts, WithSize(p.size)) + } + if p.defaultSize != DefaultSize { + opts = append(opts, WithDefaultSize(p.defaultSize)) + } + if p.maxSize != DefaultMaxSize { + opts = append(opts, WithMaxSize(p.maxSize)) + } + if p.isLast { + opts = append(opts, withIsLast(p.isLast)) + } + return opts +} + +// Result removes the last item (if applicable) and returns the paginator for the next page. +func Result[I any](items []I, p *Paginator) ([]I, *Paginator) { + return ResultFunc(items, p, func(last I, colName string) any { + lastItemVal := reflect.ValueOf(last) + return dbStructTagMapper.FieldByName(lastItemVal, colName).Interface() + }) +} + +// ResultFunc removes the last item (if applicable) and returns the paginator for the next page. +// The extractor function is used to extract the column values from the last item. +func ResultFunc[I any](items []I, p *Paginator, extractor func(last I, colName string) any) ([]I, *Paginator) { + if len(items) <= p.Size() { + return items, &Paginator{ + isLast: true, + + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } + } + + items = items[:p.Size()] + lastItem := items[len(items)-1] + + currentCols := p.PageToken().Columns() + newCols := make([]Column, len(currentCols)) + for i, col := range currentCols { + newCols[i] = Column{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + Value: extractor(lastItem, col.Name), + } + } + + return items, &Paginator{ + token: NewPageToken(newCols...), + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } +} + +func WithSize(size int) Option { + return func(p *Paginator) { p.size = size } +} + +func WithDefaultSize(size int) Option { + return func(p *Paginator) { p.defaultSize = size } +} + +func WithMaxSize(size int) Option { + return func(p *Paginator) { p.maxSize = size } +} + +func WithToken(t PageToken) Option { + return func(p *Paginator) { p.token = t } +} + +func WithDefaultToken(t PageToken) Option { + return func(p *Paginator) { p.defaultToken = t } +} + +func withIsLast(isLast bool) Option { + return func(p *Paginator) { p.isLast = isLast } +} + +func NewPaginator(modifiers ...Option) (*Paginator, error) { + p := &Paginator{ + // these can still be overridden by the modifiers, but they should never be unset + maxSize: DefaultMaxSize, + defaultSize: DefaultSize, + } + for _, f := range modifiers { + f(p) + } + if err := p.validatePageToken(); err != nil { + return nil, err + } + return p, nil +} + +// validatePageToken ensures the page token columns strictly match DefaultToken's schema in order. +func (p *Paginator) validatePageToken() error { + tokenCols := p.PageToken().Columns() + if len(tokenCols) == 0 { + return nil + } + + if columnsMatch(tokenCols, p.defaultToken.Columns()) { + return nil + } + return ErrInvalidPaginationToken() +} + +func columnsMatch(tokenCols, templateCols []Column) bool { + if len(templateCols) == 0 || len(tokenCols) != len(templateCols) { + return false + } + for i, col := range tokenCols { + def := templateCols[i] + if def.Name != col.Name || def.Order != col.Order || def.Nullable != col.Nullable { + return false + } + } + return true +} diff --git a/oryx/pagination/keysetpagination_v2/parse_header.go b/oryx/pagination/keysetpagination_v2/parse_header.go new file mode 100644 index 0000000..a9847f1 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/parse_header.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "net/http" + "net/url" + + "github.com/peterhellberg/link" +) + +// ParseHeader parses the response header's Link and returns the first and next page tokens. +func ParseHeader(r *http.Response) (first, next string, isLast bool) { + links := link.ParseResponse(r) + first, _ = findRel(links, "first") + next, hasNext := findRel(links, "next") + return first, next, !hasNext +} + +func findRel(links link.Group, rel string) (string, bool) { + for idx, l := range links { + if idx == rel { + parsed, err := url.Parse(l.URI) + if err != nil { + continue + } + q := parsed.Query() + + return q.Get("page_token"), q.Has("page_token") + } + } + + return "", false +} diff --git a/oryx/pagination/keysetpagination_v2/query_builder.go b/oryx/pagination/keysetpagination_v2/query_builder.go new file mode 100644 index 0000000..801005e --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/query_builder.go @@ -0,0 +1,163 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "database/sql/driver" + "fmt" + "strings" + + "github.com/ory/pop/v6" +) + +type Order int + +const ( + OrderAscending Order = iota + OrderDescending +) + +func (o Order) extract() (string, string) { + switch o { + case OrderAscending: + return ">", "ASC" + case OrderDescending: + return "<", "DESC" + default: + panic(fmt.Sprintf("keyset pagination: unknown order %d", o)) + } +} + +// Paginate returns a function that paginates a pop.Query. +// Usage: +// +// q := c.Where("foo = ?", foo).Scope(keysetpagination.Paginate[MyItemType](paginator)) +func Paginate[I any](p *Paginator) pop.ScopeFunc { + model := pop.Model{Value: *new(I)} + tableName := model.Alias() + return func(q *pop.Query) *pop.Query { + quoteAndContextualize := func(name string) string { + quote := q.Connection.Dialect.Quote + return quote(tableName) + "." + quote(name) + } + dialect := q.Connection.Dialect.Name() + where, args, order := BuildWhereAndOrder(p.PageToken().Columns(), quoteAndContextualize, dialect) + // IMPORTANT: Ensures correct query logic by grouping conditions. + // Without parentheses, `WHERE otherCond AND pageCond1 OR pageCond2` would be + // evaluated as `(otherCond = ? AND pageCond1) OR pageCond2`, potentially returning + // rows that do not match `otherCond`. + // We fix it by forcing the query to be: `WHERE otherCond AND (paginationCond1 OR paginationCond2)`. + where = "(" + where + ")" + + return q. + Where(where, args...). + Order(order). + Limit(p.Size() + 1) + } +} + +func BuildWhereAndOrder(columns []Column, quote func(string) string, dialect string) (string, []any, string) { + var whereBuilder, orderByBuilder, prevEqual strings.Builder + + keysetCols := make([]Column, 0, len(columns)) + + // ORDER BY includes all columns (even constrained ones) + for i, part := range columns { + column := quote(part.Name) + _, keyword := part.Order.extract() + + if i > 0 { + orderByBuilder.WriteString(", ") + } + + orderByBuilder.WriteString(column + " " + keyword) + + // Postgres orders NULLs differently depending on sort direction; + // (ASC → NULLS LAST, DESC → NULLS FIRST), which does not match the + // assumptions of our keyset pagination logic and other supported DBs. + // We therefore make NULL ordering explicit on Postgres (and YugabyteDB, + // which shares Postgres NULL ordering) to keep pagination stable and + // consistent with sqlite/mysql/cockroachdb. + if (dialect == "postgres" || dialect == "yugabyte") && part.Nullable { + if part.Order == OrderAscending { + orderByBuilder.WriteString(" NULLS FIRST") + } else { + orderByBuilder.WriteString(" NULLS LAST") + } + } + + // Build keyset WHERE only from unconstrained columns + if !part.HasConstraint { + keysetCols = append(keysetCols, part) + } + } + + // If everything is constrained, no keyset predicate is needed. + if len(keysetCols) == 0 { + return "", nil, orderByBuilder.String() + } + + args := make([]any, 0, len(keysetCols)*(len(keysetCols)+1)/2) + prevEqualArgs := make([]any, 0, len(keysetCols)) + + whereBuilder.WriteRune('(') + + for i, part := range keysetCols { + column := quote(part.Name) + sign, _ := part.Order.extract() + + if i > 0 { + whereBuilder.WriteString(") OR (") + } + + whereBuilder.WriteString(prevEqual.String()) + if prevEqual.Len() > 0 { + whereBuilder.WriteString(" AND ") + } + + isNull := part.Nullable && isSQLNull(part.Value) + + if !part.Nullable { + whereBuilder.WriteString(column + " " + sign + " ?") + } else if !isNull { + whereBuilder.WriteString(column + " IS NOT NULL AND " + column + " " + sign + " ?") + } else { + whereBuilder.WriteString(column + " IS NOT NULL") + } + + if i > 0 { + prevEqual.WriteString(" AND ") + } + + if !part.Nullable { + prevEqual.WriteString(column + " = ?") + prevEqualArgs = append(prevEqualArgs, part.Value) + } else if !isNull { + prevEqual.WriteString(column + " = ?") + prevEqualArgs = append(prevEqualArgs, part.Value) + } else { + prevEqual.WriteString(column + " IS NULL") + } + + args = append(args, prevEqualArgs...) + } + + whereBuilder.WriteRune(')') + + return whereBuilder.String(), args, orderByBuilder.String() +} + +// isSQLNull reports whether v represents a SQL NULL value. +func isSQLNull(v any) bool { + if v == nil { + return true + } + + if valuer, ok := v.(driver.Valuer); ok { + val, err := valuer.Value() + return err == nil && val == nil + } + + return false +} diff --git a/oryx/pagination/keysetpagination_v2/request_params.go b/oryx/pagination/keysetpagination_v2/request_params.go new file mode 100644 index 0000000..c782349 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/request_params.go @@ -0,0 +1,126 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Pagination Request Parameters +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="next" + // + Link string `json:"link"` +} + +// SetLinkHeader adds the Link header for the page encoded by the paginator. +// It contains links to the first and next page, if one exists. +func SetLinkHeader(w http.ResponseWriter, keys [][32]byte, u *url.URL, p *Paginator) { + size := p.Size() + link := []string{linkPart(u, "first", p.DefaultToken().Encrypt(keys), size)} + if !p.isLast { + link = append(link, linkPart(u, "next", p.PageToken().Encrypt(keys), size)) + } + w.Header().Set("Link", strings.Join(link, ",")) +} + +func linkPart(u *url.URL, rel, token string, size int) string { + q := u.Query() + q.Set("page_token", token) + q.Set("page_size", strconv.Itoa(size)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=%q", u.String(), rel) +} + +// ParseQueryParams extracts the pagination options from the URL query. +func ParseQueryParams(keys [][32]byte, q url.Values) ([]Option, error) { + var opts []Option + if t := cmp.Or(q["page_token"]...); t != "" { + raw, err := url.QueryUnescape(t) + if err != nil { + return nil, errors.WithStack(err) + } + token, err := ParsePageToken(keys, raw) + if err != nil { + return nil, err + } + opts = append(opts, WithToken(token)) + } + if s := cmp.Or(q["page_size"]...); s != "" { + size, err := strconv.Atoi(s) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithSize(size)) + } + return opts, nil +} + +// ParsePageToken parses a page token from the given raw string using the provided keys. +// If decryption fails with all provided keys (including when no keys are given), it +// falls back to using the fallbackEncryptionKey and returns an error if that also fails. +func ParsePageToken(keys [][32]byte, raw string) (t PageToken, err error) { + for i := range keys { + err = errors.WithStack(t.decrypt(keys[i], raw)) + if errors.Is(err, ErrInvalidPaginationToken()) { + continue + } + // either we successfully decrypted the token, or we got an error that is not ErrInvalidPaginationToken, in both cases we should return immediately + return t, err + } + // as a last resort, try the fallback key + err = t.decrypt(fallbackEncryptionKey, raw) + return t, errors.WithStack(err) +} diff --git a/oryx/pagination/limit.go b/oryx/pagination/limit.go new file mode 100644 index 0000000..85d80e2 --- /dev/null +++ b/oryx/pagination/limit.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package pagination provides helpers for dealing with pagination. +package pagination + +// Index uses limit, offset, and a slice's length to compute start and end indices for said slice. +func Index(limit, offset, length int) (start, end int) { + if offset > length { + return length, length + } else if limit+offset > length { + return offset, length + } + + return offset, offset + limit +} diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json new file mode 100644 index 0000000..6edb585 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page=1\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json new file mode 100644 index 0000000..e8b6289 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json @@ -0,0 +1,4 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=5\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=5\u003e", + "rel=\"first\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json new file mode 100644 index 0000000..62b1457 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=4\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"prev\",\u003chttp://example.com?page=5\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyNTAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json new file mode 100644 index 0000000..c8797e2 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json @@ -0,0 +1,6 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=4\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json new file mode 100644 index 0000000..d7f3092 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=1\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u0026per_page=50\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json new file mode 100644 index 0000000..bf1395c --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=1\u003e", + "rel=\"first\",\u003chttp://example.com?page=21\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIyMSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"next\",\u003chttp://example.com?page=19\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIxOSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"prev\",\u003chttp://example.com?page=99\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiI5OSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/header.go b/oryx/pagination/migrationpagination/header.go new file mode 100644 index 0000000..00bb172 --- /dev/null +++ b/oryx/pagination/migrationpagination/header.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrationpagination + +// swagger:model mixedPaginationRequestParameters +type RequestParameters struct { + // Deprecated Items per Page + // + // DEPRECATED: Please use `page_token` instead. This parameter will be removed in the future. + // + // This is the number of items per page. + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PerPage int `json:"per_page"` + + // Deprecated Pagination Page + // + // DEPRECATED: Please use `page_token` instead. This parameter will be removed in the future. + // + // This value is currently an integer, but it is not sequential. The value is not the page number, but a + // reference. The next page can be any number and some numbers might return an empty list. + // + // For example, page 2 might not follow after page 1. And even if page 3 and 5 exist, but page 4 might not exist. + // The first page can be retrieved by omitting this parameter. Following page pointers will be returned in the + // `Link` header. + // + // required: false + // in: query + Page int `json:"page"` + + // Page Size + // + // This is the number of items per page to return. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model mixedPagePaginationResponseHeaders +type ResponseHeaderAnnotation struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. + // + // The header value may look like follows: + // + // ; rel="first",; rel="next",; rel="prev",; rel="last" + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + // + // DEPRECATED: This header will be removed eventually. Please use the `Link` header + // instead to check whether you are on the last page. + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/migrationpagination/pagination.go b/oryx/pagination/migrationpagination/pagination.go new file mode 100644 index 0000000..09073ab --- /dev/null +++ b/oryx/pagination/migrationpagination/pagination.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrationpagination + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/ory/x/pagination" + "github.com/ory/x/pagination/pagepagination" + "github.com/ory/x/pagination/tokenpagination" +) + +type Paginator struct { + p *pagepagination.PagePaginator + t *tokenpagination.TokenPaginator +} + +func NewPaginator(p *pagepagination.PagePaginator, t *tokenpagination.TokenPaginator) *Paginator { + return &Paginator{p: p, t: t} +} + +func NewDefaultPaginator() *Paginator { + return &Paginator{p: new(pagepagination.PagePaginator), t: new(tokenpagination.TokenPaginator)} +} + +func (p *Paginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + if r.URL.Query().Has("page_token") || r.URL.Query().Has("page_size") { + return p.t.ParsePagination(r) + } + return p.p.ParsePagination(r) +} + +func header(u *url.URL, rel string, itemsPerPage, offset int64) string { + q := u.Query() + q.Set("page_size", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page_token", tokenpagination.Encode(offset)) + q.Set("per_page", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page", fmt.Sprintf("%d", offset/itemsPerPage)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pagination/pagepagination/header.go b/oryx/pagination/pagepagination/header.go new file mode 100644 index 0000000..22013bf --- /dev/null +++ b/oryx/pagination/pagepagination/header.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagepagination + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model pagePaginationRequestParameters +type RequestParameters struct { + // Legacy Items per Page + // + // A DEPRECATED alias for `page_size`. Please transition to using `page_size` going forward. + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PerPage int `json:"per_page"` + + // Legacy Pagination Page + // + // A DEPRECATED alias for `page_token`. Please transition to using `page_token` going forward. + // + // required: false + // in: query + // default: 1 + // min: 1 + Page int `json:"page"` + + // Items per Page + // + // This is the number of items per page to return. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// swagger:model pagePaginationResponseHeaders +type ResponseHeaderAnnotation struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. + // + // This header will include the `per_page` and `page` parameters for legacy reasons, but these parameters will eventually be removed. + // + // Example: Link: ; rel="first",; rel="next",; rel="prev",; rel="last" + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + // + // Example: 123 + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/pagepagination/pagination.go b/oryx/pagination/pagepagination/pagination.go new file mode 100644 index 0000000..a7a3703 --- /dev/null +++ b/oryx/pagination/pagepagination/pagination.go @@ -0,0 +1,79 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagepagination + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/ory/x/pagination" +) + +type PagePaginator struct { + MaxItems int + DefaultItems int +} + +func (p *PagePaginator) defaults() { + if p.MaxItems == 0 { + p.MaxItems = 1000 + } + + if p.DefaultItems == 0 { + p.DefaultItems = 250 + } +} + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func (p *PagePaginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + p.defaults() + + if offsetParam := r.URL.Query().Get("page"); offsetParam == "" { + page = 0 + } else { + if offset, err := strconv.ParseInt(offsetParam, 10, 0); err != nil { + page = 0 + } else { + page = int(offset) + } + } + + if limitParam := r.URL.Query().Get("per_page"); limitParam == "" { + itemsPerPage = p.DefaultItems + } else { + if limit, err := strconv.ParseInt(limitParam, 10, 0); err != nil { + itemsPerPage = p.DefaultItems + } else { + itemsPerPage = int(limit) + } + } + + if itemsPerPage > p.MaxItems { + itemsPerPage = p.MaxItems + } + + if itemsPerPage < 1 { + itemsPerPage = 1 + } + + if page < 0 { + page = 0 + } + + return +} + +func header(u *url.URL, rel string, limit, offset int64) string { + q := u.Query() + q.Set("per_page", fmt.Sprintf("%d", limit)) + q.Set("page", fmt.Sprintf("%d", offset/limit)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pagination/paginationplanner/planner.go b/oryx/pagination/paginationplanner/planner.go new file mode 100644 index 0000000..f148bf0 --- /dev/null +++ b/oryx/pagination/paginationplanner/planner.go @@ -0,0 +1,197 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package paginationplanner + +import ( + "github.com/pkg/errors" + + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" +) + +// PaginationPlanner chooses the best [PaginationPlan] for a queriedColumns. +// Plan is eligible when its required constraint set is satisfied (and an optional Condition matches). +// If no plan matches, the FallbackPlan is used. +type PaginationPlanner struct { + Plans []PaginationPlan + FallbackPlan PaginationPlan +} + +func NewPaginationPlanner(fallbackPlan PaginationPlan, plans []PaginationPlan) (*PaginationPlanner, error) { + if len(fallbackPlan.DefaultPageToken.Columns()) == 0 { + return nil, errors.New("plan must define at least one PageTokenColumn") + } + + for i := range plans { + plan := &plans[i] + if len(plan.ApplicableQueries) == 0 { + return nil, errors.New("plan must define at least one ApplicableQueries") + } + if len(plan.DefaultPageToken.Columns()) == 0 { + return nil, errors.New("plan must define a DefaultPageToken") + } + plan.populateInternals() + } + + return &PaginationPlanner{ + Plans: plans, + FallbackPlan: fallbackPlan, + }, nil +} + +// GetPaginator selects the first eligible plan for the given queriedColumns constraints. +// Eligibility requires an exact ColumnSet match and (if set) Condition match. +// If none match, the FallbackPlan is used for building the Paginator. +func (p *PaginationPlanner) GetPaginator(q Query, pageOpts ...keysetpagination.Option) (*keysetpagination.Paginator, error) { + if len(q) == 0 { + return keysetpagination.NewPaginator(append(pageOpts, keysetpagination.WithDefaultToken(p.FallbackPlan.DefaultPageToken))...) + } + + plan := p.pickPlan(q) + + origCols := plan.DefaultPageToken.Columns() + + // Make a fresh copy so we don't mutate the original backing array. + cols := make([]keysetpagination.Column, len(origCols)) + copy(cols, origCols) + + for i, col := range cols { + if val, ok := q.colByName(col.Name); ok && val.IsConstrained() { + cols[i].HasConstraint = true + } + } + + defaultToken := keysetpagination.WithDefaultToken(keysetpagination.NewPageToken(cols...)) + return keysetpagination.NewPaginator(append(pageOpts, defaultToken)...) +} + +func (p *PaginationPlanner) pickPlan(q Query) PaginationPlan { + constrainedCols := q.constrainedCols() + + for _, plan := range p.Plans { + if _, ok := plan.applicableQueries[constrainedCols]; !ok { + continue + } + if plan.Condition != nil && !plan.Condition(q) { + continue + } + return plan + } + + return p.FallbackPlan +} + +type PaginationPlan struct { + Name string + + // DefaultPageToken defines the columns that the pagination will be made of + DefaultPageToken keysetpagination.PageToken + + // ApplicableQueries defines the exact sets of columns that this pagination plan is applicable for. + ApplicableQueries [][]Column + applicableQueries map[uint]struct{} + + // Condition further restricts the Plan eligibility for given queriedColumns. + // This can be used for plan-specific logic, e.g. verifying partial-index predicates match. + Condition func(q Query) bool +} + +func (pp *PaginationPlan) populateInternals() { + pp.applicableQueries = make(map[uint]struct{}, len(pp.ApplicableQueries)) + for _, cols := range pp.ApplicableQueries { + var colSet uint + for _, col := range cols { + colSet |= col.bit + } + pp.applicableQueries[colSet] = struct{}{} + } +} + +type Table struct { + nextBit uint + usedNames map[string]struct{} +} + +func NewTable() *Table { + return &Table{ + nextBit: 1, + usedNames: make(map[string]struct{}), + } +} + +func (t *Table) NewColumn(name string) Column { + defer func() { t.nextBit = t.nextBit << 1 }() + if _, exists := t.usedNames[name]; exists { + panic("column name already used: " + name) + } + t.usedNames[name] = struct{}{} + return Column{ + name: name, + bit: t.nextBit, + } +} + +type Column struct { + bit uint + name string +} + +func (c Column) Name() string { + return c.name +} + +type ColumnConstraint uint8 + +const ( + colUnconstrained ColumnConstraint = iota + colConstraintEq // col = ? + colConstraintIsNull // col IS NULL +) + +func (cs ColumnConstraint) IsConstrained() bool { + return cs == colConstraintEq || cs == colConstraintIsNull +} + +func (cs ColumnConstraint) IsNull() bool { + return cs == colConstraintIsNull +} + +// Query is used to track which column constraints are set. +type Query map[Column]ColumnConstraint + +func NewQuery() Query { return make(Query) } + +func (qc Query) SetEq(cols ...Column) Query { + return qc.set(colConstraintEq, cols...) +} + +func (qc Query) SetIsNull(cols ...Column) Query { + return qc.set(colConstraintIsNull, cols...) +} + +func (qc Query) set(c ColumnConstraint, cols ...Column) Query { + for _, col := range cols { + qc[col] = c + } + return qc +} + +func (qc Query) colByName(name string) (ColumnConstraint, bool) { + for col, constraint := range qc { + if col.name == name { + return constraint, true + } + } + return colUnconstrained, false +} + +func (qc Query) constrainedCols() uint { + var cols uint + for col, state := range qc { + if !state.IsConstrained() { + continue + } + cols |= col.bit + } + return cols +} diff --git a/oryx/pagination/parse.go b/oryx/pagination/parse.go new file mode 100644 index 0000000..54f051c --- /dev/null +++ b/oryx/pagination/parse.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +import ( + "net/http" + "strconv" +) + +// Parse parses limit and offset from *http.Request with given limits and defaults. +func Parse(r *http.Request, defaultLimit, defaultOffset, maxLimit int) (int, int) { + var offset, limit int + + if offsetParam := r.URL.Query().Get("offset"); offsetParam == "" { + offset = defaultOffset + } else { + if offset64, err := strconv.ParseInt(offsetParam, 10, 64); err != nil { + offset = defaultOffset + } else { + offset = int(offset64) + } + } + + if limitParam := r.URL.Query().Get("limit"); limitParam == "" { + limit = defaultLimit + } else { + if limit64, err := strconv.ParseInt(limitParam, 10, 64); err != nil { + limit = defaultLimit + } else { + limit = int(limit64) + } + } + + if limit > maxLimit { + limit = maxLimit + } + + if limit < 0 { + limit = 0 + } + + if offset < 0 { + offset = 0 + } + + return limit, offset +} diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json new file mode 100644 index 0000000..2ef4cf1 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json new file mode 100644 index 0000000..77b6783 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json @@ -0,0 +1,4 @@ +[ + "\u003chttp://example.com?page_size=5\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json new file mode 100644 index 0000000..821898e --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"prev\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyNTAiLCJ2IjoyfQ\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json new file mode 100644 index 0000000..c131e47 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json @@ -0,0 +1,6 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json new file mode 100644 index 0000000..1fb35d5 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json new file mode 100644 index 0000000..e5697f0 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIyMSIsInYiOjJ9\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIxOSIsInYiOjJ9\u003e", + "rel=\"prev\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiI5OSIsInYiOjJ9\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/header.go b/oryx/pagination/tokenpagination/header.go new file mode 100644 index 0000000..180cea1 --- /dev/null +++ b/oryx/pagination/tokenpagination/header.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tokenpagination + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model tokenPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model tokenPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="first",; rel="next",; rel="prev",; rel="last" + // + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/tokenpagination/pagination.go b/oryx/pagination/tokenpagination/pagination.go new file mode 100644 index 0000000..8658f86 --- /dev/null +++ b/oryx/pagination/tokenpagination/pagination.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tokenpagination + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/x/pagination" + + "github.com/ory/herodot" +) + +func Encode(offset int64) string { + return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":"%d","v":2}`, offset))) +} + +func decode(s string) (int, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return 0, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to parse pagination token: %s", err)) + } + + return int(gjson.Get(string(b), "offset").Int()), nil +} + +type TokenPaginator struct { + MaxItems int + DefaultItems int +} + +func (p *TokenPaginator) defaults() { + if p.MaxItems == 0 { + p.MaxItems = 1000 + } + + if p.DefaultItems == 0 { + p.DefaultItems = 250 + } +} + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func (p *TokenPaginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + p.defaults() + + var offset int + if offsetParam := r.URL.Query().Get("page_token"); len(offsetParam) > 0 { + offset, _ = decode(offsetParam) + } + + if gotLimit, err := strconv.ParseInt(r.URL.Query().Get("page_size"), 10, 0); err == nil { + itemsPerPage = int(gotLimit) + } else { + itemsPerPage = p.DefaultItems + } + + if itemsPerPage > p.MaxItems { + itemsPerPage = p.MaxItems + } + + if itemsPerPage < 1 { + itemsPerPage = 1 + } + + if offset > 0 { + page = offset / itemsPerPage + } + + if page < 0 { + page = 0 + } + + return +} + +func header(u *url.URL, rel string, itemsPerPage, offset int64) string { + q := u.Query() + q.Set("page_size", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page_token", Encode(offset)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pointerx/pointerx.go b/oryx/pointerx/pointerx.go new file mode 100644 index 0000000..0f6bec5 --- /dev/null +++ b/oryx/pointerx/pointerx.go @@ -0,0 +1,13 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pointerx + +// Deref returns the input values de-referenced value, or zero value if nil. +func Deref[T any](p *T) T { + if p == nil { + var zero T + return zero + } + return *p +} diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt new file mode 100644 index 0000000..9ca2705 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt new file mode 100644 index 0000000..96811ff --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt @@ -0,0 +1,225 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Rollback +20200831110752000019 identity_verifiable_address_remove_code Rollback +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL + +Do you wish to execute this migration plan? [y/n]: ------------ WARNING ------------ +Migration aborted. + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt new file mode 100644 index 0000000..fb2bb5e --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt @@ -0,0 +1,217 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: +There are apparently no migrations to roll back. +Please provide the --steps argument with a value larger than 0. + + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt new file mode 100644 index 0000000..112d130 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt @@ -0,0 +1,230 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Rollback +20200831110752000021 identity_verifiable_address_remove_code Rollback +20201201161451000000 credential_types_values Rollback +20201201161451000001 credential_types_values Rollback + +The SQL statements to be executed from top to bottom are: + +------------ 20201201161451000001 - credential_types_values ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME + +------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt new file mode 100644 index 0000000..dda2da5 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt @@ -0,0 +1,225 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Rollback +20200831110752000019 identity_verifiable_address_remove_code Rollback +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL + +Do you wish to execute this migration plan? [y/n]: ------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt new file mode 100644 index 0000000..e066817 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt @@ -0,0 +1,237 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') + +------------ 20201201161451000001 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); + +Do you wish to execute this migration plan? [y/n]: ------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt new file mode 100644 index 0000000..6d297e3 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt @@ -0,0 +1,219 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +The SQL statements to be executed from top to bottom are: + +Do you wish to execute this migration plan? [y/n]: ------------ WARNING ------------ +Migration aborted. + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt new file mode 100644 index 0000000..ad1f3de --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt @@ -0,0 +1,1172 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Pending +20191100000001000001 identities Pending +20191100000001000002 identities Pending +20191100000001000003 identities Pending +20191100000001000004 identities Pending +20191100000001000005 identities Pending +20191100000002000000 requests Pending +20191100000002000001 requests Pending +20191100000002000002 requests Pending +20191100000002000003 requests Pending +20191100000002000004 requests Pending +20191100000003000000 sessions Pending +20191100000004000000 errors Pending +20191100000006000000 courier Pending +20191100000007000000 errors Pending +20191100000007000001 errors Pending +20191100000007000002 errors Pending +20191100000007000003 errors Pending +20191100000008000000 selfservice_verification Pending +20191100000008000001 selfservice_verification Pending +20191100000008000002 selfservice_verification Pending +20191100000008000003 selfservice_verification Pending +20191100000008000004 selfservice_verification Pending +20191100000008000005 selfservice_verification Pending +20191100000010000000 errors Pending +20191100000010000001 errors Pending +20191100000010000002 errors Pending +20191100000010000003 errors Pending +20191100000010000004 errors Pending +20191100000011000000 courier_body_type Pending +20191100000011000001 courier_body_type Pending +20191100000011000002 courier_body_type Pending +20191100000011000003 courier_body_type Pending +20191100000012000000 login_request_forced Pending +20191100000012000001 login_request_forced Pending +20191100000012000002 login_request_forced Pending +20191100000012000003 login_request_forced Pending +20200317160354000000 create_profile_request_forms Pending +20200317160354000001 create_profile_request_forms Pending +20200317160354000002 create_profile_request_forms Pending +20200317160354000003 create_profile_request_forms Pending +20200317160354000004 create_profile_request_forms Pending +20200317160354000005 create_profile_request_forms Pending +20200317160354000006 create_profile_request_forms Pending +20200401183443000000 continuity_containers Pending +20200402142539000000 rename_profile_flows Pending +20200402142539000001 rename_profile_flows Pending +20200402142539000002 rename_profile_flows Pending +20200519101057000000 create_recovery_addresses Pending +20200519101057000001 create_recovery_addresses Pending +20200519101057000002 create_recovery_addresses Pending +20200519101057000003 create_recovery_addresses Pending +20200519101057000004 create_recovery_addresses Pending +20200519101057000005 create_recovery_addresses Pending +20200519101057000006 create_recovery_addresses Pending +20200519101057000007 create_recovery_addresses Pending +20200601101000000000 create_messages Pending +20200601101000000001 create_messages Pending +20200601101000000002 create_messages Pending +20200601101000000003 create_messages Pending +20200605111551000000 messages Pending +20200605111551000001 messages Pending +20200605111551000002 messages Pending +20200605111551000003 messages Pending +20200605111551000004 messages Pending +20200605111551000005 messages Pending +20200605111551000006 messages Pending +20200605111551000007 messages Pending +20200605111551000008 messages Pending +20200605111551000009 messages Pending +20200605111551000010 messages Pending +20200605111551000011 messages Pending +20200607165100000000 settings Pending +20200607165100000001 settings Pending +20200607165100000002 settings Pending +20200607165100000003 settings Pending +20200607165100000004 settings Pending +20200705105359000000 rename_identities_schema Pending +20200810141652000000 flow_type Pending +20200810141652000001 flow_type Pending +20200810141652000002 flow_type Pending +20200810141652000003 flow_type Pending +20200810141652000004 flow_type Pending +20200810141652000005 flow_type Pending +20200810141652000006 flow_type Pending +20200810141652000007 flow_type Pending +20200810141652000008 flow_type Pending +20200810141652000009 flow_type Pending +20200810141652000010 flow_type Pending +20200810141652000011 flow_type Pending +20200810141652000012 flow_type Pending +20200810141652000013 flow_type Pending +20200810141652000014 flow_type Pending +20200810141652000015 flow_type Pending +20200810141652000016 flow_type Pending +20200810141652000017 flow_type Pending +20200810141652000018 flow_type Pending +20200810141652000019 flow_type Pending +20200810161022000000 flow_rename Pending +20200810161022000001 flow_rename Pending +20200810161022000002 flow_rename Pending +20200810161022000003 flow_rename Pending +20200810161022000004 flow_rename Pending +20200810161022000005 flow_rename Pending +20200810161022000006 flow_rename Pending +20200810161022000007 flow_rename Pending +20200810161022000008 flow_rename Pending +20200810162450000000 flow_fields_rename Pending +20200810162450000001 flow_fields_rename Pending +20200810162450000002 flow_fields_rename Pending +20200810162450000003 flow_fields_rename Pending +20200812124254000000 add_session_token Pending +20200812124254000001 add_session_token Pending +20200812124254000002 add_session_token Pending +20200812124254000003 add_session_token Pending +20200812124254000004 add_session_token Pending +20200812124254000005 add_session_token Pending +20200812124254000006 add_session_token Pending +20200812124254000007 add_session_token Pending +20200812160551000000 add_session_revoke Pending +20200812160551000001 add_session_revoke Pending +20200812160551000002 add_session_revoke Pending +20200812160551000003 add_session_revoke Pending +20200812160551000004 add_session_revoke Pending +20200812160551000005 add_session_revoke Pending +20200812160551000006 add_session_revoke Pending +20200812160551000007 add_session_revoke Pending +20200830121710000000 update_recovery_token Pending +20200830130642000000 add_verification_methods Pending +20200830130642000001 add_verification_methods Pending +20200830130642000002 add_verification_methods Pending +20200830130642000003 add_verification_methods Pending +20200830130642000004 add_verification_methods Pending +20200830130642000005 add_verification_methods Pending +20200830130642000006 add_verification_methods Pending +20200830130642000007 add_verification_methods Pending +20200830130642000008 add_verification_methods Pending +20200830130642000009 add_verification_methods Pending +20200830130642000010 add_verification_methods Pending +20200830130643000000 add_verification_methods Pending +20200830130644000000 add_verification_methods Pending +20200830130644000001 add_verification_methods Pending +20200830130645000000 add_verification_methods Pending +20200830130646000000 add_verification_methods Pending +20200830130646000001 add_verification_methods Pending +20200830130646000002 add_verification_methods Pending +20200830130646000003 add_verification_methods Pending +20200830130646000004 add_verification_methods Pending +20200830130646000005 add_verification_methods Pending +20200830130646000006 add_verification_methods Pending +20200830130646000007 add_verification_methods Pending +20200830130646000008 add_verification_methods Pending +20200830130646000009 add_verification_methods Pending +20200830130646000010 add_verification_methods Pending +20200830130646000011 add_verification_methods Pending +20200830154602000000 add_verification_token Pending +20200830154602000001 add_verification_token Pending +20200830154602000002 add_verification_token Pending +20200830154602000003 add_verification_token Pending +20200830154602000004 add_verification_token Pending +20200830172221000000 recovery_token_expires Pending +20200830172221000001 recovery_token_expires Pending +20200830172221000002 recovery_token_expires Pending +20200830172221000003 recovery_token_expires Pending +20200830172221000004 recovery_token_expires Pending +20200830172221000005 recovery_token_expires Pending +20200830172221000006 recovery_token_expires Pending +20200830172221000007 recovery_token_expires Pending +20200830172221000008 recovery_token_expires Pending +20200830172221000009 recovery_token_expires Pending +20200830172221000010 recovery_token_expires Pending +20200830172221000011 recovery_token_expires Pending +20200830172221000012 recovery_token_expires Pending +20200830172221000013 recovery_token_expires Pending +20200830172221000014 recovery_token_expires Pending +20200830172221000015 recovery_token_expires Pending +20200830172221000016 recovery_token_expires Pending +20200830172221000017 recovery_token_expires Pending +20200830172221000018 recovery_token_expires Pending +20200830172221000019 recovery_token_expires Pending +20200830172221000020 recovery_token_expires Pending +20200830172221000021 recovery_token_expires Pending +20200830172221000022 recovery_token_expires Pending +20200830172221000023 recovery_token_expires Pending +20200830172221000024 recovery_token_expires Pending +20200831110752000000 identity_verifiable_address_remove_code Pending +20200831110752000001 identity_verifiable_address_remove_code Pending +20200831110752000002 identity_verifiable_address_remove_code Pending +20200831110752000003 identity_verifiable_address_remove_code Pending +20200831110752000004 identity_verifiable_address_remove_code Pending +20200831110752000005 identity_verifiable_address_remove_code Pending +20200831110752000006 identity_verifiable_address_remove_code Pending +20200831110752000007 identity_verifiable_address_remove_code Pending +20200831110752000008 identity_verifiable_address_remove_code Pending +20200831110752000009 identity_verifiable_address_remove_code Pending +20200831110752000010 identity_verifiable_address_remove_code Pending +20200831110752000011 identity_verifiable_address_remove_code Pending +20200831110752000012 identity_verifiable_address_remove_code Pending +20200831110752000013 identity_verifiable_address_remove_code Pending +20200831110752000014 identity_verifiable_address_remove_code Pending +20200831110752000015 identity_verifiable_address_remove_code Pending +20200831110752000016 identity_verifiable_address_remove_code Pending +20200831110752000017 identity_verifiable_address_remove_code Pending +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20191100000001000000 - identities ------------ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000001000001 - identities ------------ +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +) + +------------ 20191100000001000002 - identities ------------ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) + +------------ 20191100000001000003 - identities ------------ +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +) + +------------ 20191100000001000004 - identities ------------ +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +) + +------------ 20191100000001000005 - identities ------------ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); + +------------ 20191100000002000000 - requests ------------ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000002000001 - requests ------------ +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +) + +------------ 20191100000002000002 - requests ------------ +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000002000003 - requests ------------ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +) + +------------ 20191100000002000004 - requests ------------ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20191100000003000000 - sessions ------------ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20191100000004000000 - errors ------------ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000006000000 - courier ------------ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000007000000 - errors ------------ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; + +------------ 20191100000007000001 - errors ------------ + + +------------ 20191100000007000002 - errors ------------ + + +------------ 20191100000007000003 - errors ------------ + + +------------ 20191100000008000000 - selfservice_verification ------------ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20191100000008000001 - selfservice_verification ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) + +------------ 20191100000008000002 - selfservice_verification ------------ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) + +------------ 20191100000008000003 - selfservice_verification ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) + +------------ 20191100000008000004 - selfservice_verification ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) + +------------ 20191100000008000005 - selfservice_verification ------------ +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000010000000 - errors ------------ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) + +------------ 20191100000010000001 - errors ------------ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" + +------------ 20191100000010000002 - errors ------------ +DROP TABLE "selfservice_errors" + +------------ 20191100000010000003 - errors ------------ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; + +------------ 20191100000010000004 - errors ------------ + + +------------ 20191100000011000000 - courier_body_type ------------ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000011000001 - courier_body_type ------------ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages" + +------------ 20191100000011000002 - courier_body_type ------------ +DROP TABLE "courier_messages" + +------------ 20191100000011000003 - courier_body_type ------------ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; + +------------ 20191100000012000000 - login_request_forced ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; + +------------ 20191100000012000001 - login_request_forced ------------ + + +------------ 20191100000012000002 - login_request_forced ------------ + + +------------ 20191100000012000003 - login_request_forced ------------ + + +------------ 20200317160354000000 - create_profile_request_forms ------------ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20200317160354000001 - create_profile_request_forms ------------ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT + +------------ 20200317160354000002 - create_profile_request_forms ------------ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests + +------------ 20200317160354000003 - create_profile_request_forms ------------ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200317160354000004 - create_profile_request_forms ------------ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests" + +------------ 20200317160354000005 - create_profile_request_forms ------------ + +DROP TABLE "selfservice_profile_management_requests" + +------------ 20200317160354000006 - create_profile_request_forms ------------ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; + +------------ 20200401183443000000 - continuity_containers ------------ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20200402142539000000 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" + +------------ 20200402142539000001 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" + +------------ 20200402142539000002 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; + +------------ 20200519101057000000 - create_recovery_addresses ------------ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20200519101057000001 - create_recovery_addresses ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) + +------------ 20200519101057000002 - create_recovery_addresses ------------ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) + +------------ 20200519101057000003 - create_recovery_addresses ------------ +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20200519101057000004 - create_recovery_addresses ------------ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) + +------------ 20200519101057000005 - create_recovery_addresses ------------ +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) + +------------ 20200519101057000006 - create_recovery_addresses ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) + +------------ 20200519101057000007 - create_recovery_addresses ------------ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); + +------------ 20200601101000000000 - create_messages ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; + +------------ 20200601101000000001 - create_messages ------------ + + +------------ 20200601101000000002 - create_messages ------------ + + +------------ 20200601101000000003 - create_messages ------------ + + +------------ 20200605111551000000 - messages ------------ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT + +------------ 20200605111551000001 - messages ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT + +------------ 20200605111551000002 - messages ------------ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; + +------------ 20200605111551000003 - messages ------------ + + +------------ 20200605111551000004 - messages ------------ + + +------------ 20200605111551000005 - messages ------------ + + +------------ 20200605111551000006 - messages ------------ + + +------------ 20200605111551000007 - messages ------------ + + +------------ 20200605111551000008 - messages ------------ + + +------------ 20200605111551000009 - messages ------------ + + +------------ 20200605111551000010 - messages ------------ + + +------------ 20200605111551000011 - messages ------------ + + +------------ 20200607165100000000 - settings ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form' + +------------ 20200607165100000001 - settings ------------ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200607165100000002 - settings ------------ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" + +------------ 20200607165100000003 - settings ------------ + +DROP TABLE "selfservice_settings_requests" + +------------ 20200607165100000004 - settings ------------ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; + +------------ 20200705105359000000 - rename_identities_schema ------------ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; + +------------ 20200810141652000000 - flow_type ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000001 - flow_type ------------ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000002 - flow_type ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000003 - flow_type ------------ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000004 - flow_type ------------ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; + +------------ 20200810141652000005 - flow_type ------------ + + +------------ 20200810141652000006 - flow_type ------------ + + +------------ 20200810141652000007 - flow_type ------------ + + +------------ 20200810141652000008 - flow_type ------------ + + +------------ 20200810141652000009 - flow_type ------------ + + +------------ 20200810141652000010 - flow_type ------------ + + +------------ 20200810141652000011 - flow_type ------------ + + +------------ 20200810141652000012 - flow_type ------------ + + +------------ 20200810141652000013 - flow_type ------------ + + +------------ 20200810141652000014 - flow_type ------------ + + +------------ 20200810141652000015 - flow_type ------------ + + +------------ 20200810141652000016 - flow_type ------------ + + +------------ 20200810141652000017 - flow_type ------------ + + +------------ 20200810141652000018 - flow_type ------------ + + +------------ 20200810141652000019 - flow_type ------------ + + +------------ 20200810161022000000 - flow_rename ------------ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" + +------------ 20200810161022000001 - flow_rename ------------ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" + +------------ 20200810161022000002 - flow_rename ------------ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" + +------------ 20200810161022000003 - flow_rename ------------ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" + +------------ 20200810161022000004 - flow_rename ------------ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" + +------------ 20200810161022000005 - flow_rename ------------ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" + +------------ 20200810161022000006 - flow_rename ------------ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" + +------------ 20200810161022000007 - flow_rename ------------ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" + +------------ 20200810161022000008 - flow_rename ------------ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; + +------------ 20200810162450000000 - flow_fields_rename ------------ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" + +------------ 20200810162450000001 - flow_fields_rename ------------ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" + +------------ 20200810162450000002 - flow_fields_rename ------------ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" + +------------ 20200810162450000003 - flow_fields_rename ------------ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; + +------------ 20200812124254000000 - add_session_token ------------ +DELETE FROM sessions + +------------ 20200812124254000001 - add_session_token ------------ +ALTER TABLE "sessions" ADD COLUMN "token" TEXT + +------------ 20200812124254000002 - add_session_token ------------ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200812124254000003 - add_session_token ------------ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" + +------------ 20200812124254000004 - add_session_token ------------ +DROP TABLE "sessions" + +------------ 20200812124254000005 - add_session_token ------------ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions" + +------------ 20200812124254000006 - add_session_token ------------ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) + +------------ 20200812124254000007 - add_session_token ------------ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); + +------------ 20200812160551000000 - add_session_revoke ------------ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; + +------------ 20200812160551000001 - add_session_revoke ------------ + + +------------ 20200812160551000002 - add_session_revoke ------------ + + +------------ 20200812160551000003 - add_session_revoke ------------ + + +------------ 20200812160551000004 - add_session_revoke ------------ + + +------------ 20200812160551000005 - add_session_revoke ------------ + + +------------ 20200812160551000006 - add_session_revoke ------------ + + +------------ 20200812160551000007 - add_session_revoke ------------ + + +------------ 20200830121710000000 - update_recovery_token ------------ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; + +------------ 20200830130642000000 - add_verification_methods ------------ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; + +------------ 20200830130642000001 - add_verification_methods ------------ + + +------------ 20200830130642000002 - add_verification_methods ------------ + + +------------ 20200830130642000003 - add_verification_methods ------------ + + +------------ 20200830130642000004 - add_verification_methods ------------ + + +------------ 20200830130642000005 - add_verification_methods ------------ + + +------------ 20200830130642000006 - add_verification_methods ------------ + + +------------ 20200830130642000007 - add_verification_methods ------------ + + +------------ 20200830130642000008 - add_verification_methods ------------ + + +------------ 20200830130642000009 - add_verification_methods ------------ + + +------------ 20200830130642000010 - add_verification_methods ------------ + + +------------ 20200830130643000000 - add_verification_methods ------------ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; + +------------ 20200830130644000000 - add_verification_methods ------------ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20200830130644000001 - add_verification_methods ------------ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; + +------------ 20200830130645000000 - add_verification_methods ------------ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; + +------------ 20200830130646000000 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000001 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000002 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000003 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" + +------------ 20200830130646000004 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000005 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000006 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000007 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" + +------------ 20200830130646000008 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000009 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000010 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000011 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; + +------------ 20200830154602000000 - add_verification_token ------------ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +) + +------------ 20200830154602000001 - add_verification_token ------------ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) + +------------ 20200830154602000002 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) + +------------ 20200830154602000003 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) + +------------ 20200830154602000004 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); + +------------ 20200830172221000000 - recovery_token_expires ------------ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' + +------------ 20200830172221000001 - recovery_token_expires ------------ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' + +------------ 20200830172221000002 - recovery_token_expires ------------ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" + +------------ 20200830172221000003 - recovery_token_expires ------------ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" + +------------ 20200830172221000004 - recovery_token_expires ------------ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200830172221000005 - recovery_token_expires ------------ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) + +------------ 20200830172221000006 - recovery_token_expires ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) + +------------ 20200830172221000007 - recovery_token_expires ------------ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" + +------------ 20200830172221000008 - recovery_token_expires ------------ +DROP TABLE "identity_recovery_tokens" + +------------ 20200830172221000009 - recovery_token_expires ------------ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; + +------------ 20200830172221000010 - recovery_token_expires ------------ + + +------------ 20200830172221000011 - recovery_token_expires ------------ + + +------------ 20200830172221000012 - recovery_token_expires ------------ + + +------------ 20200830172221000013 - recovery_token_expires ------------ + + +------------ 20200830172221000014 - recovery_token_expires ------------ + + +------------ 20200830172221000015 - recovery_token_expires ------------ + + +------------ 20200830172221000016 - recovery_token_expires ------------ + + +------------ 20200830172221000017 - recovery_token_expires ------------ + + +------------ 20200830172221000018 - recovery_token_expires ------------ + + +------------ 20200830172221000019 - recovery_token_expires ------------ + + +------------ 20200830172221000020 - recovery_token_expires ------------ + + +------------ 20200830172221000021 - recovery_token_expires ------------ + + +------------ 20200830172221000022 - recovery_token_expires ------------ + + +------------ 20200830172221000023 - recovery_token_expires ------------ + + +------------ 20200830172221000024 - recovery_token_expires ------------ + + +------------ 20200831110752000000 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" + +------------ 20200831110752000001 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" + +------------ 20200831110752000002 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" + +------------ 20200831110752000003 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" + +------------ 20200831110752000004 - identity_verifiable_address_remove_code ------------ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200831110752000005 - identity_verifiable_address_remove_code ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000006 - identity_verifiable_address_remove_code ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000007 - identity_verifiable_address_remove_code ------------ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" + +------------ 20200831110752000008 - identity_verifiable_address_remove_code ------------ + +DROP TABLE "identity_verifiable_addresses" + +------------ 20200831110752000009 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" + +------------ 20200831110752000010 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" + +------------ 20200831110752000011 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" + +------------ 20200831110752000012 - identity_verifiable_address_remove_code ------------ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200831110752000013 - identity_verifiable_address_remove_code ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000014 - identity_verifiable_address_remove_code ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000015 - identity_verifiable_address_remove_code ------------ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" + +------------ 20200831110752000016 - identity_verifiable_address_remove_code ------------ + +DROP TABLE "identity_verifiable_addresses" + +------------ 20200831110752000017 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') + +------------ 20201201161451000001 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); + +------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt new file mode 100644 index 0000000..9ca2705 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt new file mode 100644 index 0000000..1f2f2fc --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Pending +20191100000001000001 identities Pending +20191100000001000002 identities Pending +20191100000001000003 identities Pending +20191100000001000004 identities Pending +20191100000001000005 identities Pending +20191100000002000000 requests Pending +20191100000002000001 requests Pending +20191100000002000002 requests Pending +20191100000002000003 requests Pending +20191100000002000004 requests Pending +20191100000003000000 sessions Pending +20191100000004000000 errors Pending +20191100000006000000 courier Pending +20191100000007000000 errors Pending +20191100000007000001 errors Pending +20191100000007000002 errors Pending +20191100000007000003 errors Pending +20191100000008000000 selfservice_verification Pending +20191100000008000001 selfservice_verification Pending +20191100000008000002 selfservice_verification Pending +20191100000008000003 selfservice_verification Pending +20191100000008000004 selfservice_verification Pending +20191100000008000005 selfservice_verification Pending +20191100000010000000 errors Pending +20191100000010000001 errors Pending +20191100000010000002 errors Pending +20191100000010000003 errors Pending +20191100000010000004 errors Pending +20191100000011000000 courier_body_type Pending +20191100000011000001 courier_body_type Pending +20191100000011000002 courier_body_type Pending +20191100000011000003 courier_body_type Pending +20191100000012000000 login_request_forced Pending +20191100000012000001 login_request_forced Pending +20191100000012000002 login_request_forced Pending +20191100000012000003 login_request_forced Pending +20200317160354000000 create_profile_request_forms Pending +20200317160354000001 create_profile_request_forms Pending +20200317160354000002 create_profile_request_forms Pending +20200317160354000003 create_profile_request_forms Pending +20200317160354000004 create_profile_request_forms Pending +20200317160354000005 create_profile_request_forms Pending +20200317160354000006 create_profile_request_forms Pending +20200401183443000000 continuity_containers Pending +20200402142539000000 rename_profile_flows Pending +20200402142539000001 rename_profile_flows Pending +20200402142539000002 rename_profile_flows Pending +20200519101057000000 create_recovery_addresses Pending +20200519101057000001 create_recovery_addresses Pending +20200519101057000002 create_recovery_addresses Pending +20200519101057000003 create_recovery_addresses Pending +20200519101057000004 create_recovery_addresses Pending +20200519101057000005 create_recovery_addresses Pending +20200519101057000006 create_recovery_addresses Pending +20200519101057000007 create_recovery_addresses Pending +20200601101000000000 create_messages Pending +20200601101000000001 create_messages Pending +20200601101000000002 create_messages Pending +20200601101000000003 create_messages Pending +20200605111551000000 messages Pending +20200605111551000001 messages Pending +20200605111551000002 messages Pending +20200605111551000003 messages Pending +20200605111551000004 messages Pending +20200605111551000005 messages Pending +20200605111551000006 messages Pending +20200605111551000007 messages Pending +20200605111551000008 messages Pending +20200605111551000009 messages Pending +20200605111551000010 messages Pending +20200605111551000011 messages Pending +20200607165100000000 settings Pending +20200607165100000001 settings Pending +20200607165100000002 settings Pending +20200607165100000003 settings Pending +20200607165100000004 settings Pending +20200705105359000000 rename_identities_schema Pending +20200810141652000000 flow_type Pending +20200810141652000001 flow_type Pending +20200810141652000002 flow_type Pending +20200810141652000003 flow_type Pending +20200810141652000004 flow_type Pending +20200810141652000005 flow_type Pending +20200810141652000006 flow_type Pending +20200810141652000007 flow_type Pending +20200810141652000008 flow_type Pending +20200810141652000009 flow_type Pending +20200810141652000010 flow_type Pending +20200810141652000011 flow_type Pending +20200810141652000012 flow_type Pending +20200810141652000013 flow_type Pending +20200810141652000014 flow_type Pending +20200810141652000015 flow_type Pending +20200810141652000016 flow_type Pending +20200810141652000017 flow_type Pending +20200810141652000018 flow_type Pending +20200810141652000019 flow_type Pending +20200810161022000000 flow_rename Pending +20200810161022000001 flow_rename Pending +20200810161022000002 flow_rename Pending +20200810161022000003 flow_rename Pending +20200810161022000004 flow_rename Pending +20200810161022000005 flow_rename Pending +20200810161022000006 flow_rename Pending +20200810161022000007 flow_rename Pending +20200810161022000008 flow_rename Pending +20200810162450000000 flow_fields_rename Pending +20200810162450000001 flow_fields_rename Pending +20200810162450000002 flow_fields_rename Pending +20200810162450000003 flow_fields_rename Pending +20200812124254000000 add_session_token Pending +20200812124254000001 add_session_token Pending +20200812124254000002 add_session_token Pending +20200812124254000003 add_session_token Pending +20200812124254000004 add_session_token Pending +20200812124254000005 add_session_token Pending +20200812124254000006 add_session_token Pending +20200812124254000007 add_session_token Pending +20200812160551000000 add_session_revoke Pending +20200812160551000001 add_session_revoke Pending +20200812160551000002 add_session_revoke Pending +20200812160551000003 add_session_revoke Pending +20200812160551000004 add_session_revoke Pending +20200812160551000005 add_session_revoke Pending +20200812160551000006 add_session_revoke Pending +20200812160551000007 add_session_revoke Pending +20200830121710000000 update_recovery_token Pending +20200830130642000000 add_verification_methods Pending +20200830130642000001 add_verification_methods Pending +20200830130642000002 add_verification_methods Pending +20200830130642000003 add_verification_methods Pending +20200830130642000004 add_verification_methods Pending +20200830130642000005 add_verification_methods Pending +20200830130642000006 add_verification_methods Pending +20200830130642000007 add_verification_methods Pending +20200830130642000008 add_verification_methods Pending +20200830130642000009 add_verification_methods Pending +20200830130642000010 add_verification_methods Pending +20200830130643000000 add_verification_methods Pending +20200830130644000000 add_verification_methods Pending +20200830130644000001 add_verification_methods Pending +20200830130645000000 add_verification_methods Pending +20200830130646000000 add_verification_methods Pending +20200830130646000001 add_verification_methods Pending +20200830130646000002 add_verification_methods Pending +20200830130646000003 add_verification_methods Pending +20200830130646000004 add_verification_methods Pending +20200830130646000005 add_verification_methods Pending +20200830130646000006 add_verification_methods Pending +20200830130646000007 add_verification_methods Pending +20200830130646000008 add_verification_methods Pending +20200830130646000009 add_verification_methods Pending +20200830130646000010 add_verification_methods Pending +20200830130646000011 add_verification_methods Pending +20200830154602000000 add_verification_token Pending +20200830154602000001 add_verification_token Pending +20200830154602000002 add_verification_token Pending +20200830154602000003 add_verification_token Pending +20200830154602000004 add_verification_token Pending +20200830172221000000 recovery_token_expires Pending +20200830172221000001 recovery_token_expires Pending +20200830172221000002 recovery_token_expires Pending +20200830172221000003 recovery_token_expires Pending +20200830172221000004 recovery_token_expires Pending +20200830172221000005 recovery_token_expires Pending +20200830172221000006 recovery_token_expires Pending +20200830172221000007 recovery_token_expires Pending +20200830172221000008 recovery_token_expires Pending +20200830172221000009 recovery_token_expires Pending +20200830172221000010 recovery_token_expires Pending +20200830172221000011 recovery_token_expires Pending +20200830172221000012 recovery_token_expires Pending +20200830172221000013 recovery_token_expires Pending +20200830172221000014 recovery_token_expires Pending +20200830172221000015 recovery_token_expires Pending +20200830172221000016 recovery_token_expires Pending +20200830172221000017 recovery_token_expires Pending +20200830172221000018 recovery_token_expires Pending +20200830172221000019 recovery_token_expires Pending +20200830172221000020 recovery_token_expires Pending +20200830172221000021 recovery_token_expires Pending +20200830172221000022 recovery_token_expires Pending +20200830172221000023 recovery_token_expires Pending +20200830172221000024 recovery_token_expires Pending +20200831110752000000 identity_verifiable_address_remove_code Pending +20200831110752000001 identity_verifiable_address_remove_code Pending +20200831110752000002 identity_verifiable_address_remove_code Pending +20200831110752000003 identity_verifiable_address_remove_code Pending +20200831110752000004 identity_verifiable_address_remove_code Pending +20200831110752000005 identity_verifiable_address_remove_code Pending +20200831110752000006 identity_verifiable_address_remove_code Pending +20200831110752000007 identity_verifiable_address_remove_code Pending +20200831110752000008 identity_verifiable_address_remove_code Pending +20200831110752000009 identity_verifiable_address_remove_code Pending +20200831110752000010 identity_verifiable_address_remove_code Pending +20200831110752000011 identity_verifiable_address_remove_code Pending +20200831110752000012 identity_verifiable_address_remove_code Pending +20200831110752000013 identity_verifiable_address_remove_code Pending +20200831110752000014 identity_verifiable_address_remove_code Pending +20200831110752000015 identity_verifiable_address_remove_code Pending +20200831110752000016 identity_verifiable_address_remove_code Pending +20200831110752000017 identity_verifiable_address_remove_code Pending +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt new file mode 100644 index 0000000..04309e1 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt new file mode 100644 index 0000000..6961f8d --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/cmd.go b/oryx/popx/cmd.go new file mode 100644 index 0000000..321e876 --- /dev/null +++ b/oryx/popx/cmd.go @@ -0,0 +1,270 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/errorsx" + "github.com/ory/x/flagx" +) + +type MigrationProvider interface { + MigrationStatus(context.Context) (MigrationStatuses, error) + MigrateUp(context.Context) error + MigrateDown(context.Context, int) error +} + +type MigrationPreparer interface { + PrepareMigration(context.Context) error +} + +func RegisterMigrateSQLUpFlags(cmd *cobra.Command) *cobra.Command { + cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + return cmd +} + +func NewMigrateSQLUpCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateSQLUpFlags(&cobra.Command{ + Use: "up [database_url]", + Args: cobra.RangeArgs(0, 1), + Short: "Apply all pending SQL migrations", + Long: `This command applies all pending SQL migrations for Ory {{ title .Root.Name }}. + +:::warning + +Before running this command, create a backup of your database. This command can be destructive as it may apply changes that cannot be easily reverted. Run this command close to the SQL instance (same VPC / same machine). + +::: + +It is recommended to review the migrations before running them. You can do this by running the command without the --yes flag: + + DSN=... {{ .CommandPath }} -e`, + Example: `Apply all pending migrations: + DSN=... {{ .CommandPath }} -e + +Apply all pending migrations: + DSN=... {{ .CommandPath }} -e --yes`, + RunE: runE, + }) +} + +func MigrateSQLUp(cmd *cobra.Command, p MigrationProvider) (err error) { + // convert migration tables + if prep, ok := p.(MigrationPreparer); ok { + if err := prep.PrepareMigration(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + // print migration status + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The migration plan is as follows:") + + // print migration status + status, err := p.MigrationStatus(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + cmdx.PrintTable(cmd, status) + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe SQL statements to be executed from top to bottom are:\n\n") + for i := range status { + if status[i].State == Pending { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ %s - %s ------------\n", status[i].Version, status[i].Name) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", status[i].ContentUp) + } + } + + if !flagx.MustGetBool(cmd, "yes") { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).") + if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ WARNING ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") + return nil + } + } + + // apply migrations + if err := p.MigrateUp(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ ERROR ------------\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ SUCCESS ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!") + return nil +} + +func RegisterMigrateSQLDownFlags(cmd *cobra.Command) *cobra.Command { + cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + cmd.Flags().Int("steps", 0, "The number of migrations to roll back.") + return cmd +} + +func NewMigrateSQLDownCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateSQLDownFlags(&cobra.Command{ + Use: "down [database_url]", + Args: cobra.RangeArgs(0, 1), + Short: "Rollback the last applied SQL migrations", + Long: `This command rolls back the last applied SQL migrations for Ory {{ title .Root.Name }}. + +:::warning + +Before running this command, create a backup of your database. This command can be destructive as it may revert changes made by previous migrations. Run this command close to the SQL instance (same VPC / same machine). + +::: + +It is recommended to review the migrations before running them. You can do this by running the command without the --yes flag: + + DSN=... {{ .CommandPath }} -e`, + Example: `See the current migration status: + DSN=... {{ .CommandPath }} -e + +Rollback the last 10 migrations: + {{ .CommandPath }} $DSN --steps 10 + +Rollback the last 10 migrations without confirmation: + DSN=... {{ .CommandPath }} -e --yes --steps 10`, + RunE: runE, + }) +} + +func MigrateSQLDown(cmd *cobra.Command, p MigrationProvider) (err error) { + steps := flagx.MustGetInt(cmd, "steps") + if steps < 0 { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Flag --steps must be larger than 0.") + return cmdx.FailSilently(cmd) + } + + // convert migration tables + if prep, ok := p.(MigrationPreparer); ok { + if err := prep.PrepareMigration(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + status, err := p.MigrationStatus(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + // Now we need to rollback the last `steps` migrations that have a status of "Applied": + var count int + var rollingBack int + for i := len(status) - 1; i >= 0; i-- { + if status[i].State == Applied { + count++ + if steps > 0 && count <= steps { + status[i].State = "Rollback" + rollingBack++ + } + } + } + + // print migration status + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The migration plan is as follows:") + cmdx.PrintTable(cmd, status) + + if rollingBack < 1 { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "There are apparently no migrations to roll back.") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Please provide the --steps argument with a value larger than 0.") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "") + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe SQL statements to be executed from top to bottom are:\n\n") + + for i := len(status) - 1; i >= 0; i-- { + if status[i].State == "Rollback" { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ %s - %s ------------\n", status[i].Version, status[i].Name) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", status[i].ContentDown) + } + } + + if !flagx.MustGetBool(cmd, "yes") { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).") + if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ WARNING ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") + return nil + } + } + + // apply migrations + if err := p.MigrateDown(cmd.Context(), rollingBack); err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ ERROR ------------\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ SUCCESS ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!") + return nil +} + +func RegisterMigrateStatusFlags(cmd *cobra.Command) *cobra.Command { + cmdx.RegisterFormatFlags(cmd.PersistentFlags()) + cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") + cmd.Flags().Bool("block", false, "Block until all migrations have been applied") + return cmd +} + +func NewMigrateSQLStatusCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateStatusFlags(&cobra.Command{ + Use: "status [database_url]", + Short: "Display the current migration status", + Long: `This command shows the current migration status for Ory {{ title .Root.Name }}. + +You can use this command to check which migrations have been applied and which are pending. + +To block until all migrations are applied, use the --block flag: + + DSN=... {{ .CommandPath }} -e --block`, + Example: `See the current migration status: + DSN=... {{ .CommandPath }} -e + +Block until all migrations are applied: + DSN=... {{ .CommandPath }} -e --block`, + RunE: runE, + }) +} + +func MigrateStatus(cmd *cobra.Command, p MigrationProvider) (err error) { + block := flagx.MustGetBool(cmd, "block") + ctx := cmd.Context() + s, err := p.MigrationStatus(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + for block && s.HasPending() { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for migrations to finish...\n") + for _, m := range s { + if m.State == Pending { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", m.Name) + } + } + time.Sleep(time.Second) + s, err = p.MigrationStatus(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + cmdx.PrintTable(cmd, s) + return nil +} diff --git a/oryx/popx/db_columns.go b/oryx/popx/db_columns.go new file mode 100644 index 0000000..99132c1 --- /dev/null +++ b/oryx/popx/db_columns.go @@ -0,0 +1,60 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "fmt" + "strings" + + "github.com/ory/pop/v6" +) + +func DBColumns[T any](quoter Quoter) string { + return (&pop.Model{Value: new(T)}).Columns().QuotedString(quoter) +} + +// IndexHint returns the table name including the index hint, if the database +// supports it. +func IndexHint(conn *pop.Connection, table string, index string) string { + if conn.Dialect.Name() == "cockroach" { + return table + "@" + index + } + // PostgreSQL and YugabyteDB do not accept CockroachDB's @index syntax. + return table +} + +func DBColumnsExcluding[T any](quoter Quoter, exclude ...string) string { + cols := (&pop.Model{Value: new(T)}).Columns() + for _, e := range exclude { + cols.Remove(e) + } + return cols.QuotedString(quoter) +} + +type ( + AliasQuoter struct { + Alias string + Quoter Quoter + } + Quoter interface { + Quote(key string) string + } +) + +func (pq *AliasQuoter) Quote(key string) string { + return fmt.Sprintf("%s.%s", pq.Quoter.Quote(pq.Alias), pq.Quoter.Quote(key)) +} + +// Placeholders returns `num` placeholders ?, e.g. "?, ?, ?". +// Placeholders(0) returns "". +func Placeholders(num int) string { + switch num { + case 0: + return "" + case 1: + return "?" + default: + return strings.Repeat("?, ", num-1) + "?" + } +} diff --git a/oryx/popx/dialect_check.go b/oryx/popx/dialect_check.go new file mode 100644 index 0000000..76ba47d --- /dev/null +++ b/oryx/popx/dialect_check.go @@ -0,0 +1,78 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" +) + +// VerifyDialect makes sure the declared SQL dialect matches the actual +// database server. It distinguishes PostgreSQL, CockroachDB, and YugabyteDB: +// all three speak the Postgres wire protocol, so an operator can easily point +// a service configured for one at a server running another. That mismatch +// silently selects the wrong dialect-specific migrations and produces broken +// schemas. +// +// Other dialects (MySQL, SQLite) are skipped. +func VerifyDialect(ctx context.Context, conn *pop.Connection) error { + declared := conn.Dialect.Name() + if declared != namePostgres && declared != dbal.DriverCockroachDB && declared != dbal.DriverYugabyteDB { + return nil + } + + var version string + if err := conn.WithContext(ctx).RawQuery("SELECT version() AS version").First(&version); err != nil { + return errors.Wrap(err, "could not query database version to verify dialect") + } + return checkDialect(declared, version, pop.DialectSupported(dbal.DriverYugabyteDB)) +} + +const ( + namePostgres = "postgres" + + // yugabyteVersionMarker identifies a YugabyteDB server in the output of + // SELECT version() (e.g. "PostgreSQL 11.2-YB-2025.2.4.0-b122 ..."). + yugabyteVersionMarker = "-YB-" +) + +func checkDialect(declared, version string, yugabyteDialectSupported bool) error { + detectedCockroach := strings.Contains(version, "CockroachDB") + detectedYugabyte := strings.Contains(version, yugabyteVersionMarker) + switch { + case declared == dbal.DriverCockroachDB && !detectedCockroach: + return errors.Errorf( + "DSN scheme cockroach:// declares a CockroachDB database but the server is not CockroachDB. Server reported: %q. Use a postgres:// DSN, or point the service at a CockroachDB cluster.", + firstLine(version), + ) + case declared == dbal.DriverYugabyteDB && !detectedYugabyte: + return errors.Errorf( + "DSN scheme yugabyte:// declares a YugabyteDB database but the server is not YugabyteDB. Server reported: %q. Use a DSN scheme matching the actual server, or point the service at a YugabyteDB cluster.", + firstLine(version), + ) + case declared == namePostgres && detectedCockroach: + return errors.Errorf( + "DSN scheme postgres:// declares a PostgreSQL database but the server is CockroachDB. Replace the scheme with cockroach:// so that the service picks the correct migrations and SQL dialect. Server reported: %q", + firstLine(version), + ) + case declared == namePostgres && detectedYugabyte && yugabyteDialectSupported: + return errors.Errorf( + "DSN scheme postgres:// declares a PostgreSQL database but the server is YugabyteDB. Replace the scheme with yugabyte:// (supported in Ory Enterprise License builds) so that the service picks the correct migrations. Server reported: %q", + firstLine(version), + ) + } + return nil +} + +func firstLine(s string) string { + if i := strings.IndexByte(s, '\n'); i >= 0 { + return s[:i] + } + return s +} diff --git a/oryx/popx/loggers.go b/oryx/popx/loggers.go new file mode 100644 index 0000000..d992190 --- /dev/null +++ b/oryx/popx/loggers.go @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "fmt" + "testing" + + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/logging" +) + +func formatter(lvl logging.Level, s string, args ...interface{}) string { + if pop.Debug == false { + return "" + } + + if lvl == logging.SQL { + if len(args) > 0 { + xargs := make([]string, len(args)) + for i, a := range args { + switch a.(type) { + case string: + xargs[i] = fmt.Sprintf("%q", a) + default: + xargs[i] = fmt.Sprintf("%v", a) + } + } + s = fmt.Sprintf("%s - %s | %s", lvl, s, xargs) + } else { + s = fmt.Sprintf("%s - %s", lvl, s) + } + } else { + s = fmt.Sprintf(s, args...) + s = fmt.Sprintf("%s - %s", lvl, s) + } + return s +} + +func TestingLogger(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + if line := formatter(lvl, s, args...); len(line) > 0 { + t.Log(line) + } + } +} + +func NullLogger() func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + // do nothing + } +} diff --git a/oryx/popx/match.go b/oryx/popx/match.go new file mode 100644 index 0000000..5d1631c --- /dev/null +++ b/oryx/popx/match.go @@ -0,0 +1,97 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "regexp" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" +) + +var errUnsupportedMigrationDialect = errors.New("unsupported dialect") + +var MigrationFileRegexp = regexp.MustCompile( + `^(\d+)_([^.]+)(\.[a-z0-9]+)?(\.autocommit)?\.(up|down)\.(sql)$`, +) + +const ( + // Human-readable constants for the regex capture groups + versionIdx = iota + 1 + nameIdx + dbTypeIdx + autocommitIdx + directionIdx + typeIdx +) + +// match holds the information parsed from a migration filename. +type match struct { + Version string + Name string + DBType string + Direction string + Type string + Autocommit bool +} + +// parseMigrationFilename parses a migration filename. +func parseMigrationFilename(filename string) (*match, error) { + matches := MigrationFileRegexp.FindAllStringSubmatch(filename, -1) + if len(matches) == 0 { + return nil, nil + } + m := matches[0] + + var ( + autocommit bool + dbType string + ) + + if m[dbTypeIdx] == ".autocommit" { + // A special case where autocommit group moves forward to the 3rd index. + autocommit = true + dbType = "all" + } else if m[dbTypeIdx] == "" { + dbType = "all" + } else { + dbType = strings.TrimPrefix(m[dbTypeIdx], ".") + // The commercial package registers this dialect from init(), but + // migration boxes can be constructed before that package enters the + // import graph. Canonicalize both accepted spellings independently of + // registration so migration selection is deterministic in every build. + switch dbType { + case dbal.DriverYugabyteDB, "yugabytedb": + dbType = dbal.DriverYugabyteDB + default: + dbType = pop.CanonicalDialect(dbType) + } + if dbType != dbal.DriverYugabyteDB && !pop.DialectSupported(dbType) { + return nil, errors.Wrapf(errUnsupportedMigrationDialect, "%s", dbType) + } + } + + if m[typeIdx] == "fizz" && dbType != "all" { + return nil, errors.Errorf("invalid database type %q, expected \"all\" because fizz is database type independent", dbType) + } + + if m[autocommitIdx] == ".autocommit" { + autocommit = true + } else if m[autocommitIdx] != "" { + return nil, errors.Errorf("invalid autocommit flag %q", m[autocommitIdx]) + } + + return &match{ + Version: m[versionIdx], + Name: m[nameIdx], + DBType: dbType, + Autocommit: autocommit, + Direction: m[directionIdx], + Type: m[typeIdx], + }, nil +} diff --git a/oryx/popx/migration_box.go b/oryx/popx/migration_box.go new file mode 100644 index 0000000..6ec4a40 --- /dev/null +++ b/oryx/popx/migration_box.go @@ -0,0 +1,371 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "database/sql" + "fmt" + "io/fs" + "path" + "regexp" + "slices" + "sort" + "strings" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" + "github.com/ory/x/logrusx" +) + +type ( + // MigrationBox is a embed migration box. + MigrationBox struct { + c *pop.Connection + migrationsUp Migrations + migrationsDown Migrations + perMigrationTimeout time.Duration + l *logrusx.Logger + migrationContent MigrationContent + disableGoldenDatabase bool + sqliteTemplateCacheDir string + } + MigrationContent func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) + MigrationBoxOption func(*MigrationBox) +) + +func WithTemplateValues(v map[string]interface{}) MigrationBoxOption { + return func(m *MigrationBox) { + m.migrationContent = ParameterizedMigrationContent(v) + } +} + +func WithMigrationContentMiddleware(middleware func(content string, err error) (string, error)) MigrationBoxOption { + return func(m *MigrationBox) { + prev := m.migrationContent + m.migrationContent = func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) { + return middleware(prev(mf, c, r, usingTemplate)) + } + } +} + +// WithGoMigrations adds migrations that have a custom migration runner. +// TEST THEM THOROUGHLY! +// It will be very hard to fix a buggy migration. +func WithGoMigrations(migrations Migrations) MigrationBoxOption { + return func(mb *MigrationBox) { + for _, m := range migrations { + switch m.Direction { + case "up": + mb.migrationsUp = append(mb.migrationsUp, m) + case "down": + mb.migrationsDown = append(mb.migrationsDown, m) + default: + panic(fmt.Sprintf("unknown migration direction %q for %q", m.Direction, m.Version)) + } + } + } +} + +func WithPerMigrationTimeout(timeout time.Duration) MigrationBoxOption { + return func(m *MigrationBox) { + m.perMigrationTimeout = timeout + } +} + +// WithoutGoldenDatabase disables the golden database optimisation for SQLite +// test databases. Use this when the caller asserts on migration execution +// side-effects (e.g. exact timing, call counts, or intermediate schema state). +func WithoutGoldenDatabase() MigrationBoxOption { + return func(m *MigrationBox) { + m.disableGoldenDatabase = true + } +} + +// WithSQLiteTemplateCacheDir stores pre-migrated SQLite test database +// templates in dir. By default, templates are stored in os.TempDir. +// +// This option only affects on-disk SQLite databases while running tests. +func WithSQLiteTemplateCacheDir(dir string) MigrationBoxOption { + return func(m *MigrationBox) { + m.sqliteTemplateCacheDir = dir + } +} + +var testdataPattern = regexp.MustCompile(`^(\d+)_testdata(|\.[a-zA-Z0-9]+).sql$`) + +// WithTestdata adds testdata to the migration box. +func WithTestdata(t *testing.T, testdata fs.FS) MigrationBoxOption { + return func(m *MigrationBox) { + // Testdata may not change the golden-database hash, + // so it should not share or create a golden database. + m.disableGoldenDatabase = true + require.NoError(t, fs.WalkDir(testdata, ".", func(path string, info fs.DirEntry, err error) error { + if err != nil { + return errors.WithStack(err) + } + if info.Name() == "." { + return nil + } + if !info.Type().IsRegular() { + t.Logf("skipping testdata entry that is not a file: %s", path) + return nil + } + + match := testdataPattern.FindStringSubmatch(info.Name()) + if len(match) != 2 && len(match) != 3 { + t.Logf(`WARNING! Found a test migration which does not match the test data pattern: %s`, info.Name()) + return nil + } + + version := match[1] + flavor := "all" + if len(match) == 3 && len(match[2]) > 0 { + flavor = pop.CanonicalDialect(strings.TrimPrefix(match[2], ".")) + } + + // t.Logf("Found test migration \"%s\" (%s, %+v): %s", flavor, match, err, info.Name()) + + m.migrationsUp = append(m.migrationsUp, Migration{ + Version: version + "9", // run testdata after version + Path: path, + Name: info.Name(), + DBType: flavor, + Direction: "up", + Type: "sql", + Runner: func(m Migration, c *pop.Connection) error { + b, err := fs.ReadFile(testdata, m.Path) + if err != nil { + return errors.WithStack(err) + } + if isMigrationEmpty(string(b)) { + return nil + } + _, err = c.Store.SQLDB().Exec(string(b)) + return errors.WithStack(err) + }, + }) + + m.migrationsDown = append(m.migrationsDown, Migration{ + Version: version + "9", // run testdata after version + Path: path, + Name: info.Name(), + DBType: flavor, + Direction: "down", + Type: "sql", + Runner: func(m Migration, _ *pop.Connection) error { return nil }, + }) + + return nil + })) + } +} + +var emptySQLReplace = regexp.MustCompile(`(?m)^(\s*--.*|\s*)$`) + +func isMigrationEmpty(content string) bool { + return len(strings.ReplaceAll(emptySQLReplace.ReplaceAllString(content, ""), "\n", "")) == 0 +} + +type queryExecutor interface { + Exec(query string, args ...any) (sql.Result, error) +} + +// NewMigrationBox creates a new migration box. +func NewMigrationBox(dir fs.FS, c *pop.Connection, l *logrusx.Logger, opts ...MigrationBoxOption) (*MigrationBox, error) { + mb := &MigrationBox{ + c: c, + l: l, + migrationContent: ParameterizedMigrationContent(nil), + } + + for _, o := range opts { + o(mb) + } + + txRunner := func(b []byte) func(Migration, *pop.Connection) error { + return func(mf Migration, c *pop.Connection) error { + content, err := mb.migrationContent(mf, c, b, true) + if err != nil { + return errors.Wrapf(err, "error processing %s", mf.Path) + } + if isMigrationEmpty(content) { + l.WithField("migration", mf.Path).Trace("This is usually ok - ignoring migration because content is empty. This is ok!") + return nil + } + + var q queryExecutor = c.Store.SQLDB() + if c.TX != nil { + q = c.TX + } + + if _, err = q.Exec(content); err != nil { + return errors.Wrapf(err, "error executing %s, sql: %s", mf.Path, content) + } + return nil + } + } + + err := mb.findMigrations(dir, txRunner) + if err != nil { + return mb, err + } + + if err := mb.check(); err != nil { + return nil, err + } + return mb, nil +} + +// migrationFallbacks returns the ordered dialects migration selection falls +// back to before the generic ("all") file (.yugabyte. → .postgres. → generic +// for YugabyteDB, no fallbacks otherwise). +func (mb *MigrationBox) migrationFallbacks() []string { + if mb.c.Dialect.Name() == dbal.DriverYugabyteDB { + return []string{dbal.DriverPostgreSQL} + } + return nil +} + +// SelectedMigrations returns a copy of the migrations selected for the +// connection's dialect and direction. It uses the same exact-match and fallback +// ranking as Up, Down, and Status so callers can audit the effective migration +// set without duplicating filename-selection logic. +func (mb *MigrationBox) SelectedMigrations(direction string) (Migrations, error) { + dialect := mb.c.Dialect.Name() + fallbacks := mb.migrationFallbacks() + switch direction { + case "up": + return slices.Clone(mb.migrationsUp.sortAndFilter(dialect, fallbacks...)), nil + case "down": + return slices.Clone(mb.migrationsDown.sortAndFilter(dialect, fallbacks...)), nil + default: + return nil, errors.Errorf("unknown migration direction %q", direction) + } +} + +// noTxDDL reports whether the dialect cannot run DDL inside a transaction, so +// both migrations and the migration-status table setup must run in autocommit +// mode. CockroachDB and MySQL auto-commit each DDL statement; YugabyteDB +// restricts DDL inside transactions. Because there is no surrounding +// transaction to roll back a partial failure, new YugabyteDB migrations must +// contain one idempotent statement per file. YugabyteDB also inherits older +// PostgreSQL migration files that predate this rule; service-level guard tests +// track that replay risk, and a dedicated .yugabyte. override is required when +// a new unsafe inherited migration appears. +func (mb *MigrationBox) noTxDDL() bool { + switch mb.c.Dialect.Name() { + case dbal.DriverCockroachDB, dbal.DriverMySQL, dbal.DriverYugabyteDB: + return true + } + return false +} + +func (mb *MigrationBox) findMigrations( + dir fs.FS, + runner func([]byte) func(m Migration, c *pop.Connection) error, +) error { + err := fs.WalkDir(dir, ".", func(p string, info fs.DirEntry, err error) error { + if err != nil { + return errors.WithStack(err) + } + + if !info.Type().IsRegular() { + mb.l.Tracef("ignoring non file: %s", info.Name()) + return nil + } + + if path.Ext(info.Name()) != ".sql" { + mb.l.Tracef("ignoring non SQL file: %s", info.Name()) + return nil + } + + details, err := parseMigrationFilename(info.Name()) + if err != nil { + if errors.Is(err, errUnsupportedMigrationDialect) { + mb.l.Debugf("Ignoring migration file %s because its dialect is not supported: %s", info.Name(), err.Error()) + return nil + } + return errors.WithStack(err) + } + + if details == nil { + return errors.Errorf("Found a migration file that does not match the file pattern: filename=%s pattern=%s", info.Name(), MigrationFileRegexp) + } + + content, err := fs.ReadFile(dir, p) + if err != nil { + return errors.WithStack(err) + } + + mf := Migration{ + Path: p, + Version: details.Version, + Name: details.Name, + DBType: details.DBType, + Direction: details.Direction, + Type: details.Type, + Content: string(content), + Autocommit: details.Autocommit, + } + + mf.Runner = runner(content) + + switch details.Direction { + case "up": + mb.migrationsUp = append(mb.migrationsUp, mf) + case "down": + mb.migrationsDown = append(mb.migrationsDown, mf) + default: + return errors.Errorf("unknown migration direction %q for %q", details.Direction, info.Name()) + } + return nil + }) + + // Sort descending. + sort.Sort(mb.migrationsDown) + slices.Reverse(mb.migrationsDown) + + // Sort ascending. + sort.Sort(mb.migrationsUp) + + return errors.WithStack(err) +} + +// hasDownMigrationWithVersion checks if there is a migration with the given +// version. +func (mb *MigrationBox) hasDownMigrationWithVersion(version string) bool { + for _, down := range mb.migrationsDown { + if version == down.Version { + return true + } + } + return false +} + +// check checks that every "up" migration has a corresponding "down" migration. +func (mb *MigrationBox) check() error { + for _, up := range mb.migrationsUp { + if !mb.hasDownMigrationWithVersion(up.Version) { + return errors.Errorf("migration %s has no corresponding down migration", up.Version) + } + } + + for _, n := range mb.migrationsUp { + if err := n.Valid(); err != nil { + return errors.WithStack(err) + } + } + for _, n := range mb.migrationsDown { + if err := n.Valid(); err != nil { + return errors.WithStack(err) + } + } + return nil +} diff --git a/oryx/popx/migration_content.go b/oryx/popx/migration_content.go new file mode 100644 index 0000000..a310b28 --- /dev/null +++ b/oryx/popx/migration_content.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "bytes" + "text/template" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" +) + +func ParameterizedMigrationContent(params map[string]interface{}) func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) { + return func(mf Migration, c *pop.Connection, b []byte, usingTemplate bool) (string, error) { + content := "" + if usingTemplate { + t := template.New("migration") + t.Funcs(SQLTemplateFuncs) + t, err := t.Parse(string(b)) + if err != nil { + return "", errors.Wrapf(err, "could not parse template %s", mf.Path) + } + var bb bytes.Buffer + err = t.Execute(&bb, struct { + IsSQLite bool + IsCockroach bool + IsMySQL bool + IsMariaDB bool + IsPostgreSQL bool + DialectDetails *pop.ConnectionDetails + Parameters map[string]interface{} + }{ + IsSQLite: c.Dialect.Name() == "sqlite3", + IsCockroach: c.Dialect.Name() == "cockroach", + IsMySQL: c.Dialect.Name() == "mysql", + IsMariaDB: c.Dialect.Name() == "mariadb", + IsPostgreSQL: dbal.IsPostgresCompatible(c.Dialect.Name()), + DialectDetails: c.Dialect.Details(), + Parameters: params, + }) + if err != nil { + return "", errors.Wrapf(err, "could not execute migration template %s", mf.Path) + } + content = bb.String() + } else { + content = string(b) + } + + return content, nil + } +} diff --git a/oryx/popx/migration_info.go b/oryx/popx/migration_info.go new file mode 100644 index 0000000..9b93fa6 --- /dev/null +++ b/oryx/popx/migration_info.go @@ -0,0 +1,130 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "sort" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" +) + +// Migration handles the data for a given database migration +type Migration struct { + // Path to the migration (./migrations/123_create_widgets.up.sql) + Path string + // Version of the migration (123) + Version string + // Name of the migration (create_widgets) + Name string + // Direction of the migration (up|down) + Direction string + // Type of migration (sql|go) + Type string + // DB type (all|postgres|mysql...) + DBType string + // Runner function to run/execute the migration. Will be wrapped in a + // database transaction. Mutually exclusive with RunnerNoTx + Runner func(Migration, *pop.Connection) error + // Content is the raw content of the migration file + Content string + // Autocommit indicates whether the migration should be run in autocommit mode + Autocommit bool +} + +func (m Migration) Valid() error { + if m.Runner == nil { + return errors.Errorf("no runner defined for %s", m.Path) + } + + return nil +} + +// Migrations is a collection of Migration +type Migrations []Migration + +func (mfs Migrations) Len() int { return len(mfs) } +func (mfs Migrations) Less(i, j int) bool { return compareMigration(mfs[i], mfs[j]) < 0 } +func (mfs Migrations) Swap(i, j int) { mfs[i], mfs[j] = mfs[j], mfs[i] } + +func compareMigration(a, b Migration) int { + if a.Version != b.Version { + return strings.Compare(a.Version, b.Version) + } + // Force "all" to be greater. + if a.DBType == "all" && b.DBType != "all" { + return 1 + } else if a.DBType != "all" && b.DBType == "all" { + return -1 + } + return strings.Compare(a.DBType, b.DBType) +} + +// migrationRank ranks a migration's DBType for the given dialect and its ordered +// fallbacks. Higher numbers indicate a better match: +// +// -1 not applicable +// 0 "all" (generic, dialect-independent) +// 1..n a fallback dialect (earlier fallbacks rank higher) +// n+1 the exact dialect +// +// This lets a migration box prefer a dialect-specific override, then a fallback +// dialect (e.g. postgres for a postgres-wire-compatible database), then the +// generic file. +func migrationRank(dbType, dialect string, fallbacks []string) int { + switch dbType { + case dialect: + return len(fallbacks) + 1 + case "all": + return 0 + } + for i, fb := range fallbacks { + if dbType == fb { + return len(fallbacks) - i + } + } + return -1 +} + +func (mfs Migrations) sortAndFilter(dialect string, fallbacks ...string) Migrations { + byVersion := make(map[string]Migration, len(mfs)) + for _, migration := range mfs { + rank := migrationRank(migration.DBType, dialect, fallbacks) + if rank < 0 { + // Not applicable to this dialect. + continue + } + if previousMigration, ok := byVersion[migration.Version]; ok { + if rank < migrationRank(previousMigration.DBType, dialect, fallbacks) { + // Previous migration is a better match, skip this one. + continue + } + } + byVersion[migration.Version] = migration + } + + filtered := make(Migrations, 0, len(byVersion)) + for k := range byVersion { + filtered = append(filtered, byVersion[k]) + } + sort.Sort(filtered) + return filtered +} + +func (mfs Migrations) find(version, dialect string, fallbacks ...string) *Migration { + var candidate *Migration + bestRank := -1 + for i := range mfs { + if mfs[i].Version != version { + continue + } + if rank := migrationRank(mfs[i].DBType, dialect, fallbacks); rank > bestRank { + bestRank = rank + candidate = &mfs[i] + } + } + return candidate +} diff --git a/oryx/popx/migrator.go b/oryx/popx/migrator.go new file mode 100644 index 0000000..efdb37f --- /dev/null +++ b/oryx/popx/migrator.go @@ -0,0 +1,724 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "crypto/sha256" + "database/sql" + "database/sql/driver" + "encoding/hex" + "fmt" + "math" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "testing" + "time" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/gofrs/flock" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + moderncsqlite "modernc.org/sqlite" + + "github.com/ory/pop/v6" + "github.com/ory/x/cmdx" + "github.com/ory/x/dbal" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +const ( + Pending = "Pending" + Applied = "Applied" + tracingComponent = "github.com/ory/x/popx" +) + +func (mb *MigrationBox) shouldNotUseTransaction(m Migration) bool { + return m.Autocommit || mb.noTxDDL() +} + +// Up runs pending "up" migrations and applies them to the database. +func (mb *MigrationBox) Up(ctx context.Context) error { + _, err := mb.UpTo(ctx, 0) + return errors.WithStack(err) +} + +// sqliteTemplatePath computes the path of the SQLite template for this +// migration set. The path is deterministic and content-addressed, so a changed +// migration set cannot reuse an older template. +func (mb *MigrationBox) sqliteTemplatePath() string { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + h := sha256.New() + for _, mi := range mfs { + for _, part := range []string{ + mi.Version, + mi.Path, + mi.Name, + mi.Direction, + mi.Type, + mi.DBType, + mi.Content, + } { + _, _ = h.Write([]byte(part)) + _, _ = h.Write([]byte{0}) + } + } + dir := mb.sqliteTemplateCacheDir + if dir == "" { + dir = os.TempDir() + } + return filepath.Join(dir, "ory-popx-sqlite-template-"+hex.EncodeToString(h.Sum(nil))+".sqlite") +} + +// restoreSQLiteOnline streams the contents of srcPath into the database +// represented by db using SQLite's online backup API. The destination +// connection stays open throughout, so the *sql.DB and any pop.Connection +// holding it remain valid. +// +// pop wraps the modernc.org/sqlite driver with otelsql for tracing; we +// unwrap via the otelsql Raw() accessor before reaching the driver-specific +// NewRestore method on the underlying *sqlite.conn. +func restoreSQLiteOnline(ctx context.Context, db *sql.DB, srcPath string) error { + conn, err := db.Conn(ctx) + if err != nil { + return errors.Wrap(err, "failed to acquire sql.Conn for restore") + } + defer func() { _ = conn.Close() }() + + return conn.Raw(func(driverConn any) error { + if w, ok := driverConn.(interface{ Raw() driver.Conn }); ok { + driverConn = w.Raw() + } + restorer, ok := driverConn.(interface { + NewRestore(srcUri string) (*moderncsqlite.Backup, error) + }) + if !ok { + return errors.Errorf("driver %T does not support online restore", driverConn) + } + // See: https://sqlite.org/backup.html . + backup, err := restorer.NewRestore(srcPath) + if err != nil { + return errors.Wrap(err, "NewRestore failed") + } + // Step(-1) copies all remaining pages in one call. + for { + more, stepErr := backup.Step(-1) + if stepErr != nil { + _ = backup.Finish() + return errors.Wrap(stepErr, "backup.Step failed") + } + if !more { + break + } + } + return errors.Wrap(backup.Finish(), "backup.Finish failed") + }) +} + +// UpTo runs up to step "up" migrations and applies them to the database. +// If step <= 0 all pending migrations are run. +func (mb *MigrationBox) UpTo(ctx context.Context, step int) (applied int, err error) { + ctx, span := startSpan(ctx, MigrationUpOpName, trace.WithAttributes(attribute.Int("step", step))) + defer otelx.End(span, &err) + + c := mb.c.WithContext(ctx) + + newDbFileName, isOnDiskSQLite := sqliteFilePath(mb.c.URL()) + isSQLite := mb.c.Dialect.Name() == "sqlite3" + isOnDiskSQLite = isSQLite && isOnDiskSQLite + + // For test SQLite databases, try to restore a pre-migrated template + // using SQLite's online backup API. The restore streams pages directly into + // the open connection, so mb.c stays valid throughout and any holders of + // it (including WithContext copies) keep working. + if testing.Testing() && isOnDiskSQLite && step <= 0 && !mb.disableGoldenDatabase { + templatePath := mb.sqliteTemplatePath() + // Only restore onto a fresh (uninitialized) database. If the + // migration table already exists, migrations were previously applied + // — either directly or via an earlier template restore. + mtn := sanitizedMigrationTableName(mb.c) + var existingTableCount int + alreadyMigrated := mb.c.Store.SQLDB().QueryRow( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", mtn, + ).Scan(&existingTableCount) == nil && existingTableCount > 0 + if !alreadyMigrated { + if mkdirErr := os.MkdirAll(filepath.Dir(templatePath), 0o750); mkdirErr != nil { + mb.l.Errorf("Failed to create SQLite template cache directory, applying migrations: path=%s err=%v", filepath.Dir(templatePath), mkdirErr) + } else { + // Test packages run as separate processes. Serialize the first + // template build so only one of them pays the migration cost; + // waiters restore the completed template after acquiring the + // lock. The template itself is still installed atomically. + lock := flock.New(templatePath + ".lock") + if lockErr := lock.Lock(); lockErr != nil { + mb.l.Errorf("Failed to lock SQLite template, applying migrations: path=%s err=%v", templatePath, lockErr) + } else { + defer func() { + if unlockErr := lock.Unlock(); unlockErr != nil { + mb.l.Errorf("Failed to unlock SQLite template: path=%s err=%v", templatePath, unlockErr) + } + }() + } + } + + if _, statErr := os.Stat(templatePath); statErr == nil { + if restoreErr := restoreSQLiteOnline(ctx, mb.c.Store.SQLDB(), templatePath); restoreErr != nil { + // Restore failed; log and fall through to the normal migration path. + mb.l.Errorf("Failed to restore SQLite template, applying migrations: src=%s dst=%s err=%v", templatePath, newDbFileName, restoreErr) + } else { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + mb.l.Infof("Skipped applying %d migrations using SQLite template: path=%s", len(mfs), templatePath) + return len(mfs), nil + } + } else if !errors.Is(statErr, os.ErrNotExist) { + mb.l.Errorf("Failed to inspect SQLite template, applying migrations: path=%s err=%v", templatePath, statErr) + } + } + } + + // Keep applied across whole-run retry attempts. Every migration counted here + // has already committed its schema_migration row, so it remains part of this + // UpTo call's step budget even when a later migration retries. + err = mb.exec(ctx, func() error { + mtn := sanitizedMigrationTableName(c) + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + for _, mi := range mfs { + l := mb.l.WithField("version", mi.Version).WithField("migration_name", mi.Name).WithField("migration_file", mi.Path) + + appliedMigrations := make([]string, 0, 2) + legacyVersion := mi.Version + if len(legacyVersion) > 14 { + legacyVersion = legacyVersion[:14] + } + err := c.RawQuery(fmt.Sprintf("SELECT version FROM %s WHERE version IN (?, ?)", mtn), mi.Version, legacyVersion).All(&appliedMigrations) + if err != nil { + return errors.Wrapf(err, "problem checking for migration version %s", mi.Version) + } + + if slices.Contains(appliedMigrations, mi.Version) { + l.Debug("Migration has already been applied, skipping.") + continue + } + + if slices.Contains(appliedMigrations, legacyVersion) { + l.WithField("legacy_version", legacyVersion).WithField("migration_table", mtn).Debug("Migration has already been applied in a legacy migration run. Updating version in migration table.") + if err := mb.isolatedTransaction(ctx, "init-migrate", func(conn *pop.Connection) error { + // We do not want to remove the legacy migration version or subsequent migrations might be applied twice. + // + // Do not activate the following - it is just for reference. + // + // if _, err := tx.Store.Exec(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), legacyVersion); err != nil { + // return errors.Wrapf(err, "problem removing legacy version %s", mi.Version) + // } + + // #nosec G201 - mtn is a system-wide const + err := conn.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec() + return errors.Wrapf(err, "problem inserting migration version %s", mi.Version) + }); err != nil { + return errors.WithStack(err) + } + continue + } + + l.Info("Migration has not yet been applied, running migration.") + + if err := mi.Valid(); err != nil { + return errors.WithStack(err) + } + + noTx := mb.shouldNotUseTransaction(mi) + if noTx { + l.Info("NOT running migrations inside a transaction") + if err := mi.Runner(mi, c); err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := c.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem inserting migration version %s. YOUR DATABASE MAY BE IN AN INCONSISTENT STATE! MANUAL INTERVENTION REQUIRED!", mi.Version) + } + } else { + if err := mb.isolatedTransaction(ctx, "up", func(conn *pop.Connection) error { + if err := mi.Runner(mi, conn); err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := conn.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem inserting migration version %s", mi.Version) + } + return nil + }); err != nil { + return errors.WithStack(err) + } + } + + l.WithField("autocommit", noTx).Infof("> %s applied successfully", mi.Name) + applied++ + if step > 0 && applied >= step { + break + } + } + if applied == 0 { + mb.l.Infof("Migrations already up to date, nothing to apply") + } else { + mb.l.Infof("Successfully applied %d migrations.", applied) + } + + // Save a SQLite template after applying all migrations (not for + // partial step runs) so future test runs can skip migrations. + // + // Only save when at least one migration was applied. Saving when + // applied == 0 either captures an empty DB or an already-migrated + // one that may contain test data inserted by an earlier registry + // (e.g. the Hydra janitor reuses the same DSN), which would + // pollute the shared template. + // + // Use VACUUM INTO to produce a clean, fully-checkpointed copy + // without a separate wal_checkpoint step. Write to a temp path + // unique to this test's database, then atomically rename to the + // shared template path. This prevents concurrent saves from + // multiple parallel tests from corrupting the template: + // os.Rename is atomic on the same filesystem, so the last writer + // wins with valid content and no reader ever sees a partial file. + if isOnDiskSQLite && step <= 0 && !mb.disableGoldenDatabase && applied > 0 { + templatePath := mb.sqliteTemplatePath() + tmp, err := os.CreateTemp(filepath.Dir(templatePath), ".sqlite-template-*.sqlite") + if err != nil { + mb.l.Errorf("Failed to allocate SQLite template file: err=%v", err) + return nil + } + tmpPath := tmp.Name() + if closeErr := tmp.Close(); closeErr != nil { + _ = os.Remove(tmpPath) + mb.l.Errorf("Failed to close SQLite template file: err=%v", closeErr) + return nil + } + // VACUUM INTO requires that the destination does not exist. + if removeErr := os.Remove(tmpPath); removeErr != nil { + mb.l.Errorf("Failed to prepare SQLite template file: err=%v", removeErr) + return nil + } + defer func() { _ = os.Remove(tmpPath) }() + // #nosec G201 - tmpPath is under os.TempDir() with a + // hex-hash filename, never derived from user input. + vacuumSQL := fmt.Sprintf("VACUUM INTO '%s'", strings.ReplaceAll(tmpPath, "'", "''")) + if err := mb.c.RawQuery(vacuumSQL).Exec(); err != nil { + mb.l.Errorf("Failed to create SQLite template copy: err=%v", err) + } else if err := os.Rename(tmpPath, templatePath); err != nil { + _ = os.Remove(tmpPath) + mb.l.Errorf("Failed to install SQLite template: src=%s dst=%s err=%v", tmpPath, templatePath, err) + } else { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + mb.l.Infof("SQLite template saved, subsequent test runs will skip %d migrations: path=%s", len(mfs), templatePath) + } + } + return nil + }) + return applied, errors.WithStack(err) +} + +// Down runs pending "down" migrations and rolls back the +// database by the specified number of steps. +// If step <= 0, all down migrations are run. +func (mb *MigrationBox) Down(ctx context.Context, steps int) (err error) { + ctx, span := startSpan(ctx, MigrationDownOpName, trace.WithAttributes(attribute.Int("steps", steps))) + defer otelx.End(span, &err) + + if steps <= 0 { + steps = math.MaxInt + } + remainingSteps := steps + + c := mb.c.WithContext(ctx) + return errors.WithStack(mb.exec(ctx, func() (err error) { + mtn := sanitizedMigrationTableName(c) + count, err := c.Count(mtn) + if err != nil { + return errors.Wrap(err, "migration down: unable count existing migration") + } + attemptSteps := min(remainingSteps, count) + + mfs := mb.migrationsDown.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + slices.Reverse(mfs) + if len(mfs) > count { + // skip all migrations that were not yet applied + mfs = mfs[len(mfs)-count:] + } + + reverted := 0 + defer func() { + migrationsToRevertCount := min(attemptSteps, len(mfs)) + mb.l.Debugf("Successfully reverted %d/%d migrations.", reverted, migrationsToRevertCount) + if err != nil { + mb.l.WithError(err).Error("Problem reverting migrations.") + } + }() + for i, mi := range mfs { + if i >= attemptSteps { + break + } + l := mb.l.WithField("version", mi.Version).WithField("migration_name", mi.Name).WithField("migration_file", mi.Path) + l.Debugf("handling migration %s", mi.Name) + exists, err := c.Where("version = ?", mi.Version).Exists(mtn) + if err != nil { + return errors.Wrapf(err, "problem checking for migration version %s", mi.Version) + } + + if !exists && len(mi.Version) > 14 { + legacyVersion := mi.Version[:14] + legacyVersionExists, err := c.Where("version = ?", legacyVersion).Exists(mtn) + if err != nil { + return errors.Wrapf(err, "problem checking for legacy migration version %s", legacyVersion) + } + + if !legacyVersionExists { + return errors.Errorf("neither normal (%s) nor legacy migration (%s) exist", mi.Version, legacyVersion) + } + } else if !exists { + return errors.Errorf("migration version %s does not exist", mi.Version) + } + + if err := mi.Valid(); err != nil { + return errors.WithStack(err) + } + + if mb.shouldNotUseTransaction(mi) { + err := mi.Runner(mi, c) + if err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := c.RawQuery(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem deleting migration version %s. YOUR DATABASE MAY BE IN AN INCONSISTENT STATE! MANUAL INTERVENTION REQUIRED!", mi.Version) + } + } else { + if err := mb.isolatedTransaction(ctx, "down", func(conn *pop.Connection) error { + err := mi.Runner(mi, conn) + if err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := conn.RawQuery(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem deleting migration version %s", mi.Version) + } + + return nil + }); err != nil { + return errors.WithStack(err) + } + } + + l.Infof("%s applied successfully", mi.Name) + reverted++ + remainingSteps-- + } + return nil + })) +} + +func (mb *MigrationBox) createTransactionalMigrationTable(ctx context.Context, c *pop.Connection, l *logrusx.Logger) error { + mtn := sanitizedMigrationTableName(c) + + if err := mb.createMigrationStatusTableTransaction(ctx, []string{ + fmt.Sprintf(`CREATE TABLE %s (version VARCHAR (48) NOT NULL, version_self INT NOT NULL DEFAULT 0)`, mtn), + fmt.Sprintf(`CREATE UNIQUE INDEX %s_version_idx ON %s (version)`, mtn, mtn), + fmt.Sprintf(`CREATE INDEX %s_version_self_idx ON %s (version_self)`, mtn, mtn), + }); err != nil { + return errors.WithStack(err) + } + + l.WithField("migration_table", mtn).Debug("Transactional migration table created successfully.") + + return nil +} + +func (mb *MigrationBox) migrateToTransactionalMigrationTable(ctx context.Context, c *pop.Connection, l *logrusx.Logger) error { + // This means the new pop migrator has also not yet been applied, do that now. + mtn := sanitizedMigrationTableName(c) + + withOn := fmt.Sprintf(" ON %s", mtn) + if c.Dialect.Name() != "mysql" { + withOn = "" + } + + interimTable := fmt.Sprintf("%s_transactional", mtn) + workload := [][]string{ + { + fmt.Sprintf(`DROP INDEX %s_version_idx%s`, mtn, withOn), + fmt.Sprintf(`CREATE TABLE %s (version VARCHAR (48) NOT NULL, version_self INT NOT NULL DEFAULT 0)`, interimTable), + fmt.Sprintf(`CREATE UNIQUE INDEX %s_version_idx ON %s (version)`, mtn, interimTable), + fmt.Sprintf(`CREATE INDEX %s_version_self_idx ON %s (version_self)`, mtn, interimTable), + // #nosec G201 - mtn is a system-wide const + fmt.Sprintf(`INSERT INTO %s (version) SELECT version FROM %s`, interimTable, mtn), + fmt.Sprintf(`ALTER TABLE %s RENAME TO %s_pop_legacy`, mtn, mtn), + }, + { + fmt.Sprintf(`ALTER TABLE %s RENAME TO %s`, interimTable, mtn), + }, + } + + if err := mb.createMigrationStatusTableTransaction(ctx, workload...); err != nil { + return errors.WithStack(err) + } + + l.WithField("migration_table", mtn).Debug("Successfully migrated legacy schema_migration to new transactional schema_migration table.") + + return nil +} + +func (mb *MigrationBox) isolatedTransaction(ctx context.Context, direction string, fn func(c *pop.Connection) error) (err error) { + ctx, span := startSpan(ctx, MigrationRunTransactionOpName, trace.WithAttributes(attribute.String("migration_direction", direction))) + defer otelx.End(span, &err) + + if mb.perMigrationTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeoutCause(ctx, mb.perMigrationTimeout, errors.Errorf("failed to run all SQL migrations: direction=%s timeout=%s", direction, mb.perMigrationTimeout)) + defer cancel() + } + + return Transaction(ctx, mb.c.WithContext(ctx), func(ctx context.Context, connection *pop.Connection) error { + return errors.WithStack(fn(connection)) + }) +} + +func (mb *MigrationBox) createMigrationStatusTableTransaction(ctx context.Context, transactions ...[]string) error { + for _, statements := range transactions { + // CockroachDB does not support transactional schema changes, so we have to run + // the statements outside of a transaction. The same applies to any dialect + // that opts into autocommit DDL (see noTxDDL). + if mb.noTxDDL() { + for _, statement := range statements { + if err := mb.c.WithContext(ctx).RawQuery(statement).Exec(); err != nil { + return errors.Wrapf(err, "unable to execute statement: %s", statement) + } + } + } else { + if err := mb.isolatedTransaction(ctx, "init", func(conn *pop.Connection) error { + for _, statement := range statements { + if err := conn.WithContext(ctx).RawQuery(statement).Exec(); err != nil { + return errors.Wrapf(err, "unable to execute statement: %s", statement) + } + } + return nil + }); err != nil { + return errors.WithStack(err) + } + } + } + + return nil +} + +// CreateSchemaMigrations sets up a table to track migrations. This is an idempotent +// operation. +func (mb *MigrationBox) CreateSchemaMigrations(ctx context.Context) error { + ctx, span := startSpan(ctx, MigrationInitOpName) + defer span.End() + + c := mb.c.WithContext(ctx) + + mtn := sanitizedMigrationTableName(c) + mb.l.WithField("migration_table", mtn).Debug("Checking if legacy migration table exists.") + _, err := c.Store.Exec(fmt.Sprintf("select version from %s", mtn)) + if err != nil { + mb.l.WithError(err).WithField("migration_table", mtn).Debug("An error occurred while checking for the legacy migration table, maybe it does not exist yet? Trying to create.") + // This means that the legacy pop migrator has not yet been applied + return errors.WithStack(mb.createTransactionalMigrationTable(ctx, c, mb.l)) + } + + mb.l.WithField("migration_table", mtn).Debug("A migration table exists, checking if it is a transactional migration table.") + _, err = c.Store.Exec(fmt.Sprintf("select version, version_self from %s", mtn)) + if err != nil { + mb.l.WithError(err).WithField("migration_table", mtn).Debug("An error occurred while checking for the transactional migration table, maybe it does not exist yet? Trying to create.") + return errors.WithStack(mb.migrateToTransactionalMigrationTable(ctx, c, mb.l)) + } + + mb.l.WithField("migration_table", mtn).Debug("Migration tables exist and are up to date.") + return nil +} + +type MigrationStatus struct { + State string `json:"state"` + Version string `json:"version"` + Name string `json:"name"` + ContentUp string `json:"content"` + ContentDown string `json:"content_down"` +} + +type MigrationStatuses []MigrationStatus + +var _ cmdx.Table = (MigrationStatuses)(nil) + +func (m MigrationStatuses) Header() []string { + return []string{"Version", "Name", "Status"} +} + +func (m MigrationStatuses) Table() [][]string { + t := make([][]string, len(m)) + for i, s := range m { + t[i] = []string{s.Version, s.Name, s.State} + } + return t +} + +func (m MigrationStatuses) Interface() interface{} { + return m +} + +func (m MigrationStatuses) Len() int { + return len(m) +} + +func (m MigrationStatuses) IDs() []string { + ids := make([]string, len(m)) + for i, s := range m { + ids[i] = s.Version + } + return ids +} + +func (m MigrationStatuses) HasPending() bool { + for _, mm := range m { + if mm.State == Pending { + return true + } + } + return false +} + +func sanitizedMigrationTableName(con *pop.Connection) string { + return regexp.MustCompile(`\W`).ReplaceAllString(con.MigrationTableName(), "") +} + +func errIsTableNotFound(err error) bool { + return strings.Contains(err.Error(), "no such table:") || // sqlite + strings.Contains(err.Error(), "Error 1146") || // MySQL + strings.Contains(err.Error(), "SQLSTATE 42P01") // PostgreSQL / CockroachDB +} + +// Status prints out the status of applied/pending migrations. +func (mb *MigrationBox) Status(ctx context.Context) (MigrationStatuses, error) { + ctx, span := startSpan(ctx, MigrationStatusOpName) + defer span.End() + + con := mb.c.WithContext(ctx) + + dialect := mb.c.Dialect.Name() + fallbacks := mb.migrationFallbacks() + migrationsUp := mb.migrationsUp.sortAndFilter(dialect, fallbacks...) + + if len(migrationsUp) == 0 { + return nil, errors.Errorf("unable to find any migrations for dialect: %s", dialect) + } + + alreadyApplied := make([]string, 0, len(migrationsUp)) + err := con.RawQuery(fmt.Sprintf("SELECT version FROM %s", sanitizedMigrationTableName(con))).All(&alreadyApplied) + if err != nil { + if errIsTableNotFound(err) { + // This means that no migrations have been applied and we need to apply all of them first! + // + // It also means that we can ignore this state and act as if no migrations have been applied yet. + } else { + // On any other error, we fail. + return nil, errors.Wrap(err, "problem with migration") + } + } + + statuses := make(MigrationStatuses, len(migrationsUp)) + for k, mf := range migrationsUp { + downContent := "-- error: no down migration defined for this migration" + if mDown := mb.migrationsDown.find(mf.Version, dialect, fallbacks...); mDown != nil { + downContent = mDown.Content + } + statuses[k] = MigrationStatus{ + State: Pending, + Version: mf.Version, + Name: mf.Name, + ContentUp: mf.Content, + ContentDown: downContent, + } + + if slices.ContainsFunc(alreadyApplied, func(applied string) bool { + return applied == mf.Version || (len(mf.Version) > 14 && applied == mf.Version[:14]) + }) { + statuses[k].State = Applied + continue + } + } + + return statuses, nil +} + +// DumpMigrationSchema will generate a file of the current database schema +func (mb *MigrationBox) DumpMigrationSchema(ctx context.Context) error { + c := mb.c.WithContext(ctx) + schema := "schema.sql" + f, err := os.Create(schema) //#nosec:G304 + if err != nil { + return errors.WithStack(err) + } + err = c.Dialect.DumpSchema(f) + if err != nil { + _ = os.RemoveAll(schema) + return errors.WithStack(err) + } + return nil +} + +func (mb *MigrationBox) exec(ctx context.Context, fn func() error) error { + now := time.Now() + defer mb.printTimer(now) + + err := mb.CreateSchemaMigrations(ctx) + if err != nil { + return errors.Wrap(err, "migrator: problem creating schema migrations") + } + + if mb.c.Dialect.Name() == "sqlite3" { + if err := mb.c.RawQuery("PRAGMA foreign_keys=OFF").Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + + switch mb.c.Dialect.Name() { + case dbal.DriverCockroachDB, dbal.DriverYugabyteDB: + outer := fn + fn = func() error { + // CreateSchemaMigrations runs before this wrapper. YugabyteDB uses + // autocommit DDL, but its pgwire errors expose the same retryable + // SQLSTATEs that crdb.Execute classifies for whole-run retries. + return errors.WithStack(crdb.Execute(outer)) + } + } + + if err := fn(); err != nil { + return errors.WithStack(err) + } + + if mb.c.Dialect.Name() == "sqlite3" { + if err := mb.c.RawQuery("PRAGMA foreign_keys=ON").Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + + return nil +} + +func (mb *MigrationBox) printTimer(timerStart time.Time) { + diff := time.Since(timerStart).Seconds() + if diff > 60 { + mb.l.Debugf("%.4f minutes", diff/60) + } else { + mb.l.Debugf("%.4f seconds", diff) + } +} diff --git a/oryx/popx/span.go b/oryx/popx/span.go new file mode 100644 index 0000000..9aef3c6 --- /dev/null +++ b/oryx/popx/span.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + + "go.opentelemetry.io/otel/trace" +) + +const ( + MigrationStatusOpName = "migration-status" + MigrationInitOpName = "migration-init" + MigrationUpOpName = "migration-up" + MigrationRunTransactionOpName = "migration-run-transaction" + MigrationDownOpName = "migration-down" +) + +func startSpan(ctx context.Context, opName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return trace.SpanFromContext(ctx).TracerProvider().Tracer(tracingComponent).Start(ctx, opName, opts...) +} diff --git a/oryx/popx/sql_template_funcs.go b/oryx/popx/sql_template_funcs.go new file mode 100644 index 0000000..32aa24f --- /dev/null +++ b/oryx/popx/sql_template_funcs.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "regexp" + + "github.com/pkg/errors" +) + +var SQLTemplateFuncs = map[string]interface{}{ + "identifier": Identifier, +} + +var identifierPattern = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_]*$") + +func Identifier(i string) (string, error) { + if !identifierPattern.MatchString(i) { + return "", errors.Errorf("invalid SQL identifier '%s'", i) + } + return i, nil +} diff --git a/oryx/popx/sqlite_template.go b/oryx/popx/sqlite_template.go new file mode 100644 index 0000000..56ebb22 --- /dev/null +++ b/oryx/popx/sqlite_template.go @@ -0,0 +1,88 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/logrusx" +) + +// sqliteFilePath returns the absolute file path for an on-disk SQLite DSN. +// SQLite memory databases and non-SQLite DSNs return ("", false). +func sqliteFilePath(dsn string) (string, bool) { + rest := dsn + if scheme, afterScheme, ok := strings.Cut(dsn, "://"); ok { + if scheme != "sqlite" && scheme != "sqlite3" { + return "", false + } + rest = afterScheme + } + if strings.Contains(rest, ":memory:") || strings.Contains(rest, "mode=memory") { + return "", false + } + rest = strings.TrimPrefix(rest, "file:") + if i := strings.IndexByte(rest, '?'); i >= 0 { + rest = rest[:i] + } + if !filepath.IsAbs(rest) { + return "", false + } + return rest, true +} + +// PrepareTestSQLiteDatabase migrates a fresh on-disk SQLite test database +// before the application opens it. MigrationBox's shared template cache makes +// this a cheap template restore after the first migration set has been built. +// +// This is useful when an application normally applies multiple migration sets +// in separate passes: pass their merged filesystem here so the cached template +// represents the final combined schema. Non-SQLite DSNs, memory databases, and +// existing database files are left untouched. +func PrepareTestSQLiteDatabase( + ctx context.Context, + dsn string, + migrations fs.FS, + logger *logrusx.Logger, + opts ...MigrationBoxOption, +) error { + if !testing.Testing() { + return nil + } + path, ok := sqliteFilePath(dsn) + if !ok { + return nil + } + if _, err := os.Stat(path); err == nil { + return nil + } else if !errors.Is(err, os.ErrNotExist) { + return errors.WithStack(err) + } + + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dsn}) + if err != nil { + return errors.WithStack(err) + } + if err := c.Open(); err != nil { + return errors.WithStack(err) + } + + mb, err := NewMigrationBox(migrations, c, logger, opts...) + if err == nil { + err = mb.Up(ctx) + } + closeErr := c.Close() + if err != nil { + return errors.WithStack(err) + } + return errors.WithStack(closeErr) +} diff --git a/oryx/popx/stub/migrations/check/valid/123_a.down.sql b/oryx/popx/stub/migrations/check/valid/123_a.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/check/valid/123_a.mysql.up.sql b/oryx/popx/stub/migrations/check/valid/123_a.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/check/valid/123_a.postgres.up.sql b/oryx/popx/stub/migrations/check/valid/123_a.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql new file mode 100644 index 0000000..30e62aa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credentials";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credential_types";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identities";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql new file mode 100644 index 0000000..638b1b7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql new file mode 100644 index 0000000..fcf2432 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_credential_identifiers`; +DROP TABLE `identity_credentials`; +DROP TABLE `identity_credential_types`; +DROP TABLE `identities`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql new file mode 100644 index 0000000..35d1c2a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`); +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql new file mode 100644 index 0000000..fec4915 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..bf912cb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql new file mode 100644 index 0000000..14ba150 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql new file mode 100644 index 0000000..1bca145 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql new file mode 100644 index 0000000..8aac48a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql @@ -0,0 +1,5 @@ +DROP TABLE `selfservice_login_request_methods`; +DROP TABLE `selfservice_login_requests`; +DROP TABLE `selfservice_registration_request_methods`; +DROP TABLE `selfservice_registration_requests`; +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql new file mode 100644 index 0000000..9894e39 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql new file mode 100644 index 0000000..d24b8b0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..d265527 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql @@ -0,0 +1,50 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql new file mode 100644 index 0000000..b7ffdf0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql new file mode 100644 index 0000000..c2b8ea0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql new file mode 100644 index 0000000..3081431 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql new file mode 100644 index 0000000..4e6d1a9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql new file mode 100644 index 0000000..139e50a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); diff --git a/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql new file mode 100644 index 0000000..8069ee9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql new file mode 100644 index 0000000..efa6f4e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql new file mode 100644 index 0000000..5b10f19 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql new file mode 100644 index 0000000..3b38079 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql new file mode 100644 index 0000000..434d2ef --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT '';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql new file mode 100644 index 0000000..af1b234 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors"; + +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..ba30655 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_verifiable_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..5e030a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..2e080c8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql @@ -0,0 +1,2 @@ +DROP TABLE `selfservice_verification_requests`; +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..5951c67 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..419ec91 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..a12f20a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql new file mode 100644 index 0000000..f8a7e0f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); diff --git a/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql new file mode 100644 index 0000000..c42e3bd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql @@ -0,0 +1,5 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql new file mode 100644 index 0000000..e9b8fc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql new file mode 100644 index 0000000..525b179 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql new file mode 100644 index 0000000..ccf29d7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql new file mode 100644 index 0000000..52ef938 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql @@ -0,0 +1,13 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql new file mode 100644 index 0000000..fc59202 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..53dd25f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ADD COLUMN "body" text;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "courier_messages" SET "body" = "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..abd09ec --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages"; +DROP TABLE "courier_messages"; +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..f6ee1d9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..56056e9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..a4db2cc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..aa1ddb2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..5601793 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..d2e92cd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON; +DROP TABLE `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..1c15ee7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..a800a7b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e5b9297 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..c1e707f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP TABLE "selfservice_profile_management_request_methods"; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..f8c1bd8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..11b99bc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..36a86ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..2d7a810 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..90b3c60 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..dba65df --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..7d66d48 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..32088d9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_recovery_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..f6c8b50 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..888e504 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_recovery_tokens`; +DROP TABLE `selfservice_recovery_request_methods`; +DROP TABLE `selfservice_recovery_requests`; +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..a935381 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`); +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..5b3fa1c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..2901619 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,48 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..54c99e1 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); diff --git a/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..7972b34 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..35028f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..127f682 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..346124a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql new file mode 100644 index 0000000..3bf20de --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql new file mode 100644 index 0000000..80601b5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql new file mode 100644 index 0000000..a9cfe75 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql new file mode 100644 index 0000000..427a387 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql new file mode 100644 index 0000000..21085a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql new file mode 100644 index 0000000..7c73915 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql new file mode 100644 index 0000000..4ac597f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql new file mode 100644 index 0000000..e1d0c11 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql @@ -0,0 +1,44 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql new file mode 100644 index 0000000..cc2a606 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql new file mode 100644 index 0000000..5877dc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql new file mode 100644 index 0000000..5848a58 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql new file mode 100644 index 0000000..c43477e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state`; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql new file mode 100644 index 0000000..00b29fa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql new file mode 100644 index 0000000..eaee899 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql new file mode 100644 index 0000000..4c2ac98 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql new file mode 100644 index 0000000..7366ef2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql @@ -0,0 +1,17 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql new file mode 100644 index 0000000..3892e62 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql @@ -0,0 +1,18 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..07a1c56 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..fc35d52 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql new file mode 100644 index 0000000..10fcdad --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql new file mode 100644 index 0000000..b1edbc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql new file mode 100644 index 0000000..6d503a7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql new file mode 100644 index 0000000..2953d00 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql new file mode 100644 index 0000000..e36b97a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql new file mode 100644 index 0000000..74d85bc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..238eea6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql @@ -0,0 +1,82 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at FROM "selfservice_recovery_requests"; + +DROP TABLE "selfservice_recovery_requests"; +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests"; +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..85a72ea --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..7b665f0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..9b60677 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql new file mode 100644 index 0000000..6ba6c69 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests`; +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods`; +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods`; +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests`; +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests`; +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods`; +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests`; +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql new file mode 100644 index 0000000..28508f8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods`; +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows`; +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods`; +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows`; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows`; +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods`; +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows`; +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..0ccaced --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..9f7efff --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..9a54733 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..93844b8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..f9e4897 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..2549800 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql @@ -0,0 +1,8 @@ +DELETE FROM sessions;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "sessions" SET "token" = "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" DROP COLUMN "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "sessions_token_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql new file mode 100644 index 0000000..fed24fb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions; +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32); +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32); +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`); +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql new file mode 100644 index 0000000..77edf55 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..75b3650 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +DROP INDEX IF EXISTS "sessions_token_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..a9f847a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +DELETE FROM sessions; +ALTER TABLE "sessions" ADD COLUMN "token" TEXT; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..f51c199 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..cc90f31 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9e7bfcb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,19 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..180633b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..b254b6e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..25bc849 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_verification_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..c325dcb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..fd97b4c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON; +DROP TABLE `selfservice_verification_flow_methods`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state`; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..adbf65d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_verification_flow_methods"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..27a7bfa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,34 @@ +DROP TABLE "selfservice_verification_flow_methods"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..34053ed --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..2dfbfde --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e8761c6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6677fbd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..fdccf04 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..496f1eb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..3aa5b8e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..db1391d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..374a2cf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..12d59e2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..6050119 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`); +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..a171839 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..9250c46 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..9a992a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql @@ -0,0 +1,10 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..adb3e1d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..696bde3 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at`; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e5f6e9d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..38e7b8b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at"; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..79c2e87 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ea864bb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,63 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..7ad4a61 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,23 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..932fd71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1,15 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..24a61df --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..9cb6530 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32); +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME; +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..91dafe3 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1,4 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses`; +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..6425e54 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..840985e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1,4 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX "identity_verifiable_addresses_code_idx"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f79f141 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,48 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME; +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..1279dc6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,43 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql new file mode 100644 index 0000000..8ea7e8f --- /dev/null +++ b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql @@ -0,0 +1 @@ +BEGIN;ROLLBACK; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql new file mode 100644 index 0000000..8ea7e8f --- /dev/null +++ b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql @@ -0,0 +1 @@ +BEGIN;ROLLBACK; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz b/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz new file mode 100644 index 0000000..149f0fd --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz @@ -0,0 +1,4 @@ +drop_table("identity_credential_identifiers") +drop_table("identity_credentials") +drop_table("identity_credential_types") +drop_table("identities") diff --git a/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz b/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz new file mode 100644 index 0000000..ee11525 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz @@ -0,0 +1,34 @@ +create_table("identities") { + t.Column("id", "uuid", {primary: true}) + t.Column("traits_schema_id", "string", {"size": 2048}) + t.Column("traits", "json") +} + +create_table("identity_credential_types") { + t.Column("id", "uuid", {primary: true}) + t.Column("name", "string", { "size": 32 }) + + t.DisableTimestamps() +} + +add_index("identity_credential_types", "name", {"unique": true}) + +create_table("identity_credentials") { + t.Column("id", "uuid", {primary: true}) + t.Column("config", "json") + + t.Column("identity_credential_type_id", "uuid") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) + t.ForeignKey("identity_credential_type_id", {"identity_credential_types": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("identity_credential_identifiers") { + t.Column("id", "uuid", {primary: true}) + t.Column("identifier", "string", {"size": 255}) + t.Column("identity_credential_id", "uuid") + t.ForeignKey("identity_credential_id", {"identity_credentials": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_credential_identifiers", "identifier", {"unique": true}) diff --git a/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz b/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz new file mode 100644 index 0000000..d8a2fe2 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz @@ -0,0 +1,7 @@ +drop_table("selfservice_login_request_methods") +drop_table("selfservice_login_requests") + +drop_table("selfservice_registration_request_methods") +drop_table("selfservice_registration_requests") + +drop_table("selfservice_profile_management_requests") diff --git a/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz b/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz new file mode 100644 index 0000000..2823b61 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz @@ -0,0 +1,47 @@ +create_table("selfservice_login_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + t.Column("csrf_token", "string") +} + +create_table("selfservice_login_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_login_request_id", "uuid") + t.Column("config", "json") + + t.ForeignKey("selfservice_login_request_id", {"selfservice_login_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_registration_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + t.Column("csrf_token", "string") +} + +create_table("selfservice_registration_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_registration_request_id", "uuid") + t.Column("config", "json") + + t.ForeignKey("selfservice_registration_request_id", {"selfservice_registration_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_profile_management_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("form", "json") + t.Column("update_successful", "bool") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz b/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz new file mode 100644 index 0000000..dc5c982 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz @@ -0,0 +1 @@ +drop_table("sessions") diff --git a/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz b/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz new file mode 100644 index 0000000..f0eb2f3 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz @@ -0,0 +1,9 @@ +create_table("sessions") { + t.Column("id", "uuid", {primary: true}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("authenticated_at", "timestamp") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz new file mode 100644 index 0000000..9ada90a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz @@ -0,0 +1 @@ +drop_table("selfservice_errors") diff --git a/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz new file mode 100644 index 0000000..2911b5e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz @@ -0,0 +1,6 @@ +create_table("selfservice_errors") { + t.Column("id", "uuid", {primary: true}) + t.Column("errors", "json") + t.Column("seen_at", "timestamp") + t.Column("was_seen", "bool") +} diff --git a/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql new file mode 100644 index 0000000..139e50a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); diff --git a/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql new file mode 100644 index 0000000..8069ee9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz b/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz new file mode 100644 index 0000000..2da9c63 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz @@ -0,0 +1 @@ +drop_table("courier_messages") diff --git a/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz b/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz new file mode 100644 index 0000000..5f6fda1 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz @@ -0,0 +1,10 @@ +create_table("courier_messages") { + t.Column("id", "uuid", {primary: true}) + + t.Column("type", "int") + t.Column("status", "int") + + t.Column("body", "string") + t.Column("subject", "string") + t.Column("recipient", "string") +} diff --git a/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz new file mode 100644 index 0000000..6f093e7 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_errors", "csrf_token") diff --git a/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz new file mode 100644 index 0000000..b5aa72a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_errors", "csrf_token", "string", {"default": ""}) diff --git a/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz new file mode 100644 index 0000000..48fd423 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz @@ -0,0 +1,2 @@ +drop_table("selfservice_verification_requests") +drop_table("identity_verifiable_addresses") diff --git a/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz new file mode 100644 index 0000000..3f58ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz @@ -0,0 +1,35 @@ +create_table("identity_verifiable_addresses") { + t.Column("id", "uuid", {primary: true}) + + t.Column("code", "string", {"size": 32}) + t.Column("status", "string", {"size": 16}) + t.Column("via", "string", {"size": 16}) + t.Column("verified", "bool") + + t.Column("value", "string", {"size": 400}) + + t.Column("verified_at", "timestamp", {"null": true}) + t.Column("expires_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + + t.Column("identity_id", "uuid") + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_verifiable_addresses", ["code"], { "unique": true, "name": "identity_verifiable_addresses_code_uq_idx" }) +add_index("identity_verifiable_addresses", ["code"], { "name": "identity_verifiable_addresses_code_idx" }) + +add_index("identity_verifiable_addresses", ["via", "value"], { "unique": true, "name": "identity_verifiable_addresses_status_via_uq_idx" }) +add_index("identity_verifiable_addresses", ["via", "value"], { "name": "identity_verifiable_addresses_status_via_idx" }) + +create_table("selfservice_verification_requests") { + t.Column("id", "uuid", {primary: true}) + + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + + t.Column("form", "json") + t.Column("via", "string", {"size": 16}) + t.Column("csrf_token", "string") + t.Column("success", "bool") +} diff --git a/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql new file mode 100644 index 0000000..f8a7e0f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); diff --git a/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz new file mode 100644 index 0000000..daaf4c0 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz @@ -0,0 +1,2 @@ +sql("UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL;") +change_column("selfservice_errors", "seen_at", "timestamp", { null: false }) diff --git a/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz new file mode 100644 index 0000000..542c123 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz @@ -0,0 +1 @@ +change_column("selfservice_errors", "seen_at", "timestamp", { "null": true }) diff --git a/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz new file mode 100644 index 0000000..178c60c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz @@ -0,0 +1,7 @@ +<%# + +Do nothing because the change will not be able to preserve data and the change is insignificant as it's compatible +with both code bases (prior and after this change). + +WARNING: https://github.com/gobuffalo/fizz/issues/45#issuecomment-586833728 +%> diff --git a/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz new file mode 100644 index 0000000..3ca90e2 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz @@ -0,0 +1 @@ +change_column("courier_messages", "body", "text", {}) diff --git a/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz new file mode 100644 index 0000000..43e866f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_login_requests", "forced") diff --git a/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz new file mode 100644 index 0000000..66fcd59 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_login_requests", "forced", "bool", {"default": false}) diff --git a/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz new file mode 100644 index 0000000..946a0d6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz @@ -0,0 +1,12 @@ +{{ if or .IsPostgreSQL .IsMySQL .IsMariaDB }} + add_column("selfservice_profile_management_requests", "form", "json", { "null": true }) + sql("UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t);") + change_column("selfservice_profile_management_requests", "form", "json", { "null": false }) +{{ end }} + +{{ if .IsCockroach }} + add_column("selfservice_profile_management_requests", "form", "json", { "default": "{}" }) +{{ end }} + +drop_table("selfservice_profile_management_request_methods") +drop_column("selfservice_profile_management_requests", "active_method") diff --git a/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz new file mode 100644 index 0000000..276dca6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz @@ -0,0 +1,12 @@ +create_table("selfservice_profile_management_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_profile_management_request_id", "uuid") + t.Column("config", "json") +} + +add_column("selfservice_profile_management_requests", "active_method", "string", {"size": 32, null: true}) + +sql("INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests;") + +drop_column("selfservice_profile_management_requests", "form") diff --git a/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz new file mode 100644 index 0000000..956151d --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz @@ -0,0 +1 @@ +drop_table("continuity_containers") diff --git a/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz new file mode 100644 index 0000000..efaff42 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz @@ -0,0 +1,11 @@ +create_table("continuity_containers") { + t.Column("id", "uuid", {primary: true}) + + t.Column("identity_id", "uuid", {null: true}) + + t.Column("name", "string") + t.Column("payload", "json", {null: true}) + t.Column("expires_at", "timestamp") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz new file mode 100644 index 0000000..cbf6f98 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz @@ -0,0 +1,5 @@ +rename_column("selfservice_settings_request_methods", "selfservice_settings_request_id", "selfservice_profile_management_request_id") + +rename_table("selfservice_settings_request_methods", "selfservice_profile_management_request_methods") +rename_table("selfservice_settings_requests", "selfservice_profile_management_requests") + diff --git a/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz new file mode 100644 index 0000000..4b0132b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz @@ -0,0 +1,4 @@ +rename_column("selfservice_profile_management_request_methods", "selfservice_profile_management_request_id", "selfservice_settings_request_id") + +rename_table("selfservice_profile_management_request_methods", "selfservice_settings_request_methods") +rename_table("selfservice_profile_management_requests", "selfservice_settings_requests") diff --git a/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz new file mode 100644 index 0000000..04b5fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz @@ -0,0 +1,4 @@ +drop_table("identity_recovery_tokens") +drop_table("selfservice_recovery_request_methods") +drop_table("selfservice_recovery_requests") +drop_table("identity_recovery_addresses") diff --git a/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz new file mode 100644 index 0000000..b037114 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz @@ -0,0 +1,52 @@ +create_table("identity_recovery_addresses") { + t.Column("id", "uuid", {primary: true}) + + t.Column("via", "string", {"size": 16}) + t.Column("value", "string", {"size": 400}) + + t.Column("identity_id", "uuid") + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_recovery_addresses", ["via", "value"], { "unique": true, "name": "identity_recovery_addresses_status_via_uq_idx" }) +add_index("identity_recovery_addresses", ["via", "value"], { "name": "identity_recovery_addresses_status_via_idx" }) + +create_table("selfservice_recovery_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("messages", "json", {"null": true}) + t.Column("active_method", "string", {"size": 32, "null": true}) + t.Column("csrf_token", "string") + t.Column("state", "string", {"size": 32}) + + t.Column("recovered_identity_id", "uuid", { "null": true }) + t.ForeignKey("recovered_identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_recovery_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("config", "json") + + t.Column("selfservice_recovery_request_id", "uuid") + t.ForeignKey("selfservice_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("identity_recovery_tokens") { + t.Column("id", "uuid", {primary: true}) + + t.Column("token", "string", {"size": 64}) + t.Column("used", "bool", {"default": false}) + t.Column("used_at", "timestamp", {"null": true}) + + t.Column("identity_recovery_address_id", "uuid") + t.ForeignKey("identity_recovery_address_id", {"identity_recovery_addresses": ["id"]}, {"on_delete": "cascade"}) + + t.Column("selfservice_recovery_request_id", "uuid") + t.ForeignKey("selfservice_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_recovery_tokens", ["token"], { "unique": true, "name": "identity_recovery_addresses_code_uq_idx" }) +add_index("identity_recovery_tokens", ["token"], { "name": "identity_recovery_addresses_code_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..54c99e1 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); diff --git a/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..7972b34 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz b/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz new file mode 100644 index 0000000..602d6ec --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_settings_requests", "messages") diff --git a/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz b/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz new file mode 100644 index 0000000..a4e0d5f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_settings_requests", "messages", "json", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql new file mode 100644 index 0000000..3bf20de --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz b/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz new file mode 100644 index 0000000..81d91db --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz @@ -0,0 +1,3 @@ +drop_column("selfservice_verification_requests", "messages") +drop_column("selfservice_login_requests", "messages") +drop_column("selfservice_registration_requests", "messages") diff --git a/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz b/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz new file mode 100644 index 0000000..23704c0 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz @@ -0,0 +1,3 @@ +add_column("selfservice_verification_requests", "messages", "json", {"null": true}) +add_column("selfservice_login_requests", "messages", "json", {"null": true}) +add_column("selfservice_registration_requests", "messages", "json", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz b/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz new file mode 100644 index 0000000..89b26ed --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz @@ -0,0 +1,2 @@ +drop_column("selfservice_settings_requests", "state") +add_column("selfservice_settings_requests", "update_successful", "bool", {"default": false}) diff --git a/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz b/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz new file mode 100644 index 0000000..c7f3607 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz @@ -0,0 +1,2 @@ +add_column("selfservice_settings_requests", "state", "string", {"default": "show_form"}) +drop_column("selfservice_settings_requests", "update_successful") diff --git a/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz new file mode 100644 index 0000000..ed0715f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz @@ -0,0 +1 @@ +rename_column("identities", "schema_id", "traits_schema_id") diff --git a/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz new file mode 100644 index 0000000..5a9159b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz @@ -0,0 +1 @@ +rename_column("identities", "traits_schema_id", "schema_id") diff --git a/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz b/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz new file mode 100644 index 0000000..eee4e4e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz @@ -0,0 +1,5 @@ +drop_column("selfservice_login_requests", "type") +drop_column("selfservice_registration_requests", "type") +drop_column("selfservice_settings_requests", "type") +drop_column("selfservice_recovery_requests", "type") +drop_column("selfservice_verification_requests", "type") diff --git a/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz b/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz new file mode 100644 index 0000000..90c2a76 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz @@ -0,0 +1,5 @@ +add_column("selfservice_login_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_registration_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_settings_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_recovery_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_verification_requests", "type", "string", {"default": "browser", "size": 16}) diff --git a/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz new file mode 100644 index 0000000..3ddf846 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz @@ -0,0 +1,13 @@ +rename_table("selfservice_login_flows", "selfservice_login_requests") +rename_table("selfservice_login_flow_methods", "selfservice_login_request_methods") + +rename_table("selfservice_registration_flow_methods", "selfservice_registration_request_methods") +rename_table("selfservice_registration_flows", "selfservice_registration_requests") + +rename_table("selfservice_settings_flow_methods", "selfservice_settings_request_methods") +rename_table("selfservice_settings_flows", "selfservice_settings_requests") + +rename_table("selfservice_recovery_flow_methods", "selfservice_recovery_request_methods") +rename_table("selfservice_recovery_flows", "selfservice_recovery_requests") + +rename_table("selfservice_verification_flows", "selfservice_verification_requests") diff --git a/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz new file mode 100644 index 0000000..469afdd --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz @@ -0,0 +1,13 @@ +rename_table("selfservice_login_request_methods", "selfservice_login_flow_methods") +rename_table("selfservice_login_requests", "selfservice_login_flows") + +rename_table("selfservice_registration_request_methods", "selfservice_registration_flow_methods") +rename_table("selfservice_registration_requests", "selfservice_registration_flows") + +rename_table("selfservice_settings_request_methods", "selfservice_settings_flow_methods") +rename_table("selfservice_settings_requests", "selfservice_settings_flows") + +rename_table("selfservice_recovery_request_methods", "selfservice_recovery_flow_methods") +rename_table("selfservice_recovery_requests", "selfservice_recovery_flows") + +rename_table("selfservice_verification_requests", "selfservice_verification_flows") diff --git a/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz new file mode 100644 index 0000000..86a600c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz @@ -0,0 +1,7 @@ +rename_column("selfservice_login_flow_methods", "selfservice_login_flow_id", "selfservice_login_request_id") + +rename_column("selfservice_registration_flow_methods", "selfservice_registration_flow_id", "selfservice_registration_request_id") + +rename_column("selfservice_settings_flow_methods", "selfservice_settings_flow_id", "selfservice_settings_request_id") + +rename_column("selfservice_recovery_flow_methods", "selfservice_recovery_flow_id", "selfservice_recovery_request_id") diff --git a/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz new file mode 100644 index 0000000..bc24ef3 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz @@ -0,0 +1,7 @@ +rename_column("selfservice_login_flow_methods", "selfservice_login_request_id", "selfservice_login_flow_id") + +rename_column("selfservice_registration_flow_methods", "selfservice_registration_request_id", "selfservice_registration_flow_id") + +rename_column("selfservice_recovery_flow_methods", "selfservice_recovery_request_id", "selfservice_recovery_flow_id") + +rename_column("selfservice_settings_flow_methods", "selfservice_settings_request_id", "selfservice_settings_flow_id") diff --git a/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz new file mode 100644 index 0000000..a25137a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz @@ -0,0 +1 @@ +drop_column("sessions", "token") diff --git a/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz new file mode 100644 index 0000000..3a9141a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz @@ -0,0 +1,7 @@ +sql("DELETE FROM sessions") + +add_column("sessions", "token", "string", {"size": 32, "null": true}) +change_column("sessions", "token", "string", {"size": 32, "null": false}) + +add_index("sessions", "token", {"unique": true, "name": "sessions_token_uq_idx"}) +add_index("sessions", "token", {"name": "sessions_token_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz new file mode 100644 index 0000000..23e604c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz @@ -0,0 +1 @@ +drop_column("sessions", "active") diff --git a/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz new file mode 100644 index 0000000..f858882 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz @@ -0,0 +1 @@ +add_column("sessions", "active", "boolean", {"null": false, "default": false}) diff --git a/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz new file mode 100644 index 0000000..a05f0d5 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz @@ -0,0 +1 @@ +rename_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "selfservice_recovery_request_id") diff --git a/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz new file mode 100644 index 0000000..8601646 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz @@ -0,0 +1 @@ +rename_column("identity_recovery_tokens", "selfservice_recovery_request_id", "selfservice_recovery_flow_id") diff --git a/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz new file mode 100644 index 0000000..2bb6500 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz @@ -0,0 +1,16 @@ +{{ if or .IsPostgreSQL .IsMySQL .IsMariaDB }} + add_column("selfservice_verification_flows", "form", "json", { "null": true }) + sql("UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t);") + change_column("selfservice_verification_flows", "form", "json", { "null": false }) +{{ end }} + +{{ if .IsCockroach }} + add_column("selfservice_verification_flows", "form", "json", { "default": "{}" }) +{{ end }} + +drop_table("selfservice_verification_flow_methods") +drop_column("selfservice_verification_flows", "active_method") +drop_column("selfservice_verification_flows", "state") + +add_column("selfservice_verification_flows", "via", "string", {"size": 16, "default": "email"}) +add_column("selfservice_verification_flows", "success", "bool", {"default_raw": "FALSE"}) diff --git a/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz new file mode 100644 index 0000000..2819d93 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_verification_flows", "state", "string", {"default": "show_form"}) diff --git a/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz new file mode 100644 index 0000000..376ec63 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz @@ -0,0 +1 @@ +sql("UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE") diff --git a/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz new file mode 100644 index 0000000..250846b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz @@ -0,0 +1,8 @@ +create_table("selfservice_verification_flow_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_verification_flow_id", "uuid") + t.Column("config", "json") +} + +add_column("selfservice_verification_flows", "active_method", "string", {"size": 32, null: true}) diff --git a/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz new file mode 100644 index 0000000..acad208 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz @@ -0,0 +1 @@ +sql("INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows;") diff --git a/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz new file mode 100644 index 0000000..ee0a577 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz @@ -0,0 +1,3 @@ +drop_column("selfservice_verification_flows", "form") +drop_column("selfservice_verification_flows", "via") +drop_column("selfservice_verification_flows", "success") diff --git a/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz new file mode 100644 index 0000000..beb5a42 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz @@ -0,0 +1 @@ +drop_table("identity_verification_tokens") diff --git a/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz new file mode 100644 index 0000000..c6182dc --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz @@ -0,0 +1,21 @@ +create_table("identity_verification_tokens") { + t.Column("id", "uuid", {primary: true}) + + t.Column("token", "string", {"size": 64}) + t.Column("used", "bool", {"default": false}) + t.Column("used_at", "timestamp", {"null": true}) + t.Column("expires_at", "timestamp") + t.Column("issued_at", "timestamp") + + t.Column("identity_verifiable_address_id", "uuid") + t.ForeignKey("identity_verifiable_address_id", {"identity_verifiable_addresses": ["id"]}, {"on_delete": "cascade"}) + + t.Column("selfservice_verification_flow_id", "uuid", {"null": true}) + t.ForeignKey("selfservice_verification_flow_id", {"selfservice_verification_flows": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_verification_tokens", ["token"], { "unique": true, "name": "identity_verification_tokens_token_uq_idx" }) +add_index("identity_verification_tokens", ["token"], { "name": "identity_verification_tokens_token_idx" }) + +add_index("identity_verification_tokens", ["identity_verifiable_address_id"], { "name": "identity_verification_tokens_verifiable_address_id_idx" }) +add_index("identity_verification_tokens", ["selfservice_verification_flow_id"], { "name": "identity_verification_tokens_verification_flow_id_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz new file mode 100644 index 0000000..ef694c9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz @@ -0,0 +1,4 @@ +sql("DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL") +change_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "uuid") +drop_column("identity_recovery_tokens", "expires_at") +drop_column("identity_recovery_tokens", "issued_at") diff --git a/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz new file mode 100644 index 0000000..aa8546e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz @@ -0,0 +1,3 @@ +add_column("identity_recovery_tokens", "expires_at", "timestamp", { "default": "2000-01-01 00:00:00" }) +add_column("identity_recovery_tokens", "issued_at", "timestamp", { "default": "2000-01-01 00:00:00" }) +change_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "uuid", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz new file mode 100755 index 0000000..d0a9216 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz @@ -0,0 +1,28 @@ +add_column("identity_verifiable_addresses", "code", "string", {"size": 32, "null": true}) +add_column("identity_verifiable_addresses", "expires_at", "timestamp", { "null": true }) + +{{ if .IsSQLite }} + sql("UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if or .IsMySQL .IsMariaDB }} + sql("UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if .IsPostgreSQL }} + sql("UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if .IsCockroach }} + sql("UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +change_column("identity_verifiable_addresses", "code", "string", {"size": 32}) +change_column("identity_verifiable_addresses", "expires_at", "timestamp", { "null": false }) + +add_index("identity_verifiable_addresses", ["code"], { "unique": true, "name": "identity_verifiable_addresses_code_uq_idx" }) +add_index("identity_verifiable_addresses", ["code"], { "name": "identity_verifiable_addresses_code_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz new file mode 100755 index 0000000..4a1d779 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz @@ -0,0 +1,5 @@ +drop_index("identity_verifiable_addresses", "identity_verifiable_addresses_code_uq_idx") +drop_index("identity_verifiable_addresses", "identity_verifiable_addresses_code_idx") + +drop_column("identity_verifiable_addresses", "code") +drop_column("identity_verifiable_addresses", "expires_at") diff --git a/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz new file mode 100644 index 0000000..ba68093 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz @@ -0,0 +1 @@ +sql("DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'") diff --git a/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz new file mode 100644 index 0000000..66512ad --- /dev/null +++ b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz @@ -0,0 +1,3 @@ +sql("INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password')") +sql("INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc')") + diff --git a/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.down.sql b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql new file mode 100644 index 0000000..f73626d --- /dev/null +++ b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql @@ -0,0 +1 @@ +CREATE TABLE {{ identifier .Parameters.tableName }} ( "id" UUID NOT NULL, PRIMARY KEY ("id")); diff --git a/oryx/popx/stub/migrations/testdata/20220513_testdata.invalid b/oryx/popx/stub/migrations/testdata/20220513_testdata.invalid new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata/20220513_testdata.sql b/oryx/popx/stub/migrations/testdata/20220513_testdata.sql new file mode 100644 index 0000000..6687fec --- /dev/null +++ b/oryx/popx/stub/migrations/testdata/20220513_testdata.sql @@ -0,0 +1 @@ +INSERT INTO testdata (Data) VALUES ('testdata'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/testdata/20220514_testdata.sql b/oryx/popx/stub/migrations/testdata/20220514_testdata.sql new file mode 100644 index 0000000..56d7b98 --- /dev/null +++ b/oryx/popx/stub/migrations/testdata/20220514_testdata.sql @@ -0,0 +1 @@ +-- empty migrations should not error \ No newline at end of file diff --git a/oryx/popx/stub/migrations/testdata/invalid b/oryx/popx/stub/migrations/testdata/invalid new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata/invalid_testdata.sql b/oryx/popx/stub/migrations/testdata/invalid_testdata.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.down.sql b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql new file mode 100644 index 0000000..59c85da --- /dev/null +++ b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql @@ -0,0 +1,3 @@ +CREATE TABLE "testdata" ( + "data" character varying(255) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql new file mode 100644 index 0000000..ae0df01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql new file mode 100644 index 0000000..ae2d9ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identities`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql new file mode 100644 index 0000000..f257ad0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql new file mode 100644 index 0000000..4fab90d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql new file mode 100644 index 0000000..7448d69 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql new file mode 100644 index 0000000..a5245e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql new file mode 100644 index 0000000..0440ced --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_types` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql new file mode 100644 index 0000000..52f95d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql new file mode 100644 index 0000000..a5245e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..a48493a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql new file mode 100644 index 0000000..6884f06 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credentials` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql new file mode 100644 index 0000000..4770736 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql new file mode 100644 index 0000000..25bb5e5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql new file mode 100644 index 0000000..b96b95d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_identifiers` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql new file mode 100644 index 0000000..ad1aa8a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql new file mode 100644 index 0000000..3bdf1c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql new file mode 100644 index 0000000..190bcc0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql new file mode 100644 index 0000000..5ebd7af --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql new file mode 100644 index 0000000..a32c587 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql new file mode 100644 index 0000000..2b20a63 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql new file mode 100644 index 0000000..e7ba7d7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql new file mode 100644 index 0000000..759def9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql new file mode 100644 index 0000000..2627dfe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql new file mode 100644 index 0000000..77db2f7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql new file mode 100644 index 0000000..5e7e552 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql new file mode 100644 index 0000000..2627dfe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql new file mode 100644 index 0000000..f7b5788 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql new file mode 100644 index 0000000..14c44bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql new file mode 100644 index 0000000..0cc2d40 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql new file mode 100644 index 0000000..fbbcafb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql new file mode 100644 index 0000000..fb69b6b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql new file mode 100644 index 0000000..48c9c7c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql new file mode 100644 index 0000000..4c08df7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql new file mode 100644 index 0000000..eb5929d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql new file mode 100644 index 0000000..596c6f4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql new file mode 100644 index 0000000..4c08df7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..0342d26 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql new file mode 100644 index 0000000..6adfd9f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql new file mode 100644 index 0000000..481215f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql new file mode 100644 index 0000000..da52b0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql new file mode 100644 index 0000000..d293ae9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql new file mode 100644 index 0000000..68d1d38 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql new file mode 100644 index 0000000..d073080 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql new file mode 100644 index 0000000..2f7a60a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql new file mode 100644 index 0000000..1fc559a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql new file mode 100644 index 0000000..7530465 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql new file mode 100644 index 0000000..48fa320 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql new file mode 100644 index 0000000..9cf7fc2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql new file mode 100644 index 0000000..a920e94 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql new file mode 100644 index 0000000..13fd1fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql new file mode 100644 index 0000000..0dc4431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql new file mode 100644 index 0000000..13fd1fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql new file mode 100644 index 0000000..0dc4431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..c1af035 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..1a2d145 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..11afafe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..83c7bd3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..10d3639 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..207fc03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..300e534 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..e920a7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..da1b36a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3f787b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3df0610 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3deaf4e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..5380dd4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..e843d51 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..a3dcda2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..86d22fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..c4c063f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql new file mode 100644 index 0000000..4520233 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql new file mode 100644 index 0000000..4520233 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql new file mode 100644 index 0000000..9687536 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..1ab82fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql new file mode 100644 index 0000000..3fd6e5e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql new file mode 100644 index 0000000..670dfb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..ffaad71 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..f8924c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql new file mode 100644 index 0000000..670dfb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql new file mode 100644 index 0000000..3fd6e5e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..f8924c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..ffaad71 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql new file mode 100644 index 0000000..9687536 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..1ab82fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..046714a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..fe38552 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..88ff829 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "body" text \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..ca6ae21 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..0c6a568 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "courier_messages" SET "body" = "_body_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..0623a36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..fe0b4ca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..228eae4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..fd575c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..7fd3f62 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..9d051c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..04ae974 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..0e22af6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..9cc49fa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..bc0b322 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..566fcf6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..b10ec65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..9867e64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..e392ad8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..b10ec65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f61bf8d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..5366d7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..edfcfd4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..e4dbeac --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..f7e14b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f071ec2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful FROM "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..7793d13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..adbcdb4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..7793d13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..669e51a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..c82eda8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..f4861a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..0c541b8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..ecd6b61 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..f61bf8d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..cf9678d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..c0c0ace --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..81040d7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..39fefdb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..cf51250 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..978cf22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..29362dd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..0d18953 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..d79504e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..432c984 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..b6ba272 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..7663c75 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..9372cdd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..665e86f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..9843693 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..9235aca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..91fe16e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..34a95c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_tokens` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..ecd5ac3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..ff29b1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..4f5b77b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..ae150ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..0b84cbc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..bb577b3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..832fb04 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..8ce83be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..346f257 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..d00c2ba --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..0b57fdc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..a04f5b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..0fb0af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e620ccb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..893940c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e620ccb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..893940c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..d1e8b79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..02da0e4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..ba66094 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql new file mode 100644 index 0000000..7457218 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql new file mode 100644 index 0000000..7457218 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql new file mode 100644 index 0000000..49925a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql new file mode 100644 index 0000000..81f1b3b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql new file mode 100644 index 0000000..6ebd845 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql new file mode 100644 index 0000000..afd33ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql new file mode 100644 index 0000000..0ff2399 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql new file mode 100644 index 0000000..f012c22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql new file mode 100644 index 0000000..8d44e29 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql new file mode 100644 index 0000000..d70352b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql new file mode 100644 index 0000000..06ca9ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql new file mode 100644 index 0000000..4e73ab7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql new file mode 100644 index 0000000..8d44e29 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql new file mode 100644 index 0000000..e5a4417 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql new file mode 100644 index 0000000..52b78bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql new file mode 100644 index 0000000..3354242 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql new file mode 100644 index 0000000..352e5f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql new file mode 100644 index 0000000..3d9176a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql new file mode 100644 index 0000000..5363bf6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql new file mode 100644 index 0000000..d67d0f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql new file mode 100644 index 0000000..352e5f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql new file mode 100644 index 0000000..41236ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql new file mode 100644 index 0000000..87ef40b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql new file mode 100644 index 0000000..a99388b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql new file mode 100644 index 0000000..42808eb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql new file mode 100644 index 0000000..d5fb51f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql new file mode 100644 index 0000000..c1f05c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql new file mode 100644 index 0000000..b458c0b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql new file mode 100644 index 0000000..ee747b8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql new file mode 100644 index 0000000..96225c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql new file mode 100644 index 0000000..2203ba5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success FROM "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql new file mode 100644 index 0000000..d9ad85e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql new file mode 100644 index 0000000..c387b8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql new file mode 100644 index 0000000..8455290 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql new file mode 100644 index 0000000..fcf5408 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql new file mode 100644 index 0000000..c387b8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql new file mode 100644 index 0000000..97a3f7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql new file mode 100644 index 0000000..1300963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql new file mode 100644 index 0000000..bec242d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql new file mode 100644 index 0000000..b922439 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql new file mode 100644 index 0000000..1300963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql new file mode 100644 index 0000000..3e88236 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql new file mode 100644 index 0000000..b246a8b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql new file mode 100644 index 0000000..cb697e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql new file mode 100644 index 0000000..56700ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql new file mode 100644 index 0000000..37abfb0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql new file mode 100644 index 0000000..8e01074 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql new file mode 100644 index 0000000..5fae17d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql new file mode 100644 index 0000000..b2a3fd7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql new file mode 100644 index 0000000..8e01074 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..f8c6563 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e98be86 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql new file mode 100644 index 0000000..e843d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql new file mode 100644 index 0000000..18cff22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql new file mode 100644 index 0000000..18aefc6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql new file mode 100644 index 0000000..272b917 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql new file mode 100644 index 0000000..e843d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql new file mode 100644 index 0000000..18cff22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..96225c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..5a7f522 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql new file mode 100644 index 0000000..b178dfb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql new file mode 100644 index 0000000..b8909ab --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql new file mode 100644 index 0000000..15060f4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql new file mode 100644 index 0000000..13b7c0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql new file mode 100644 index 0000000..b178dfb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql new file mode 100644 index 0000000..b8909ab --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..da7567f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success FROM "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..2e42a66 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql new file mode 100644 index 0000000..57ed47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql new file mode 100644 index 0000000..5f675f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql new file mode 100644 index 0000000..c97f5c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql new file mode 100644 index 0000000..57e979d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql new file mode 100644 index 0000000..57ed47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql new file mode 100644 index 0000000..5f675f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ae4ffd5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..8e5b854 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql new file mode 100644 index 0000000..f0fd55f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql new file mode 100644 index 0000000..c6195cd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql new file mode 100644 index 0000000..c1282d8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql new file mode 100644 index 0000000..f0fd55f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..e5c12a7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..3bf96ef --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..90bee92 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ed36b70 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at FROM "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ede1b13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..3e88236 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..cb697e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..b246a8b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..6ff6407 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..52b78bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..0aa91e5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..7a7e430 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..d5fb51f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..7ea2d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..2de4018 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql new file mode 100644 index 0000000..dd9aedc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql new file mode 100644 index 0000000..137df74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql new file mode 100644 index 0000000..694cabe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql new file mode 100644 index 0000000..ce602c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql new file mode 100644 index 0000000..322ca14 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql new file mode 100644 index 0000000..970fd35 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql new file mode 100644 index 0000000..763c5b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql new file mode 100644 index 0000000..8a04ed3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql new file mode 100644 index 0000000..ebb598e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql new file mode 100644 index 0000000..2215df5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql new file mode 100644 index 0000000..54935f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql new file mode 100644 index 0000000..bf9ed12 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql new file mode 100644 index 0000000..932e415 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql new file mode 100644 index 0000000..264b7e8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql new file mode 100644 index 0000000..630e470 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql new file mode 100644 index 0000000..406ca1d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql new file mode 100644 index 0000000..0175446 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql new file mode 100644 index 0000000..dc3fdb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..dcc2ab2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..3455d87 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..e1cf4cc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..712063c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..18f0622 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..3084591 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..b630499 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..78ea09e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql new file mode 100644 index 0000000..9581d45 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..4822fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..2472546 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..bdf13df --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql new file mode 100644 index 0000000..f8ee5fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql new file mode 100644 index 0000000..86c4207 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..c633c75 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..84df028 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql new file mode 100644 index 0000000..c8e8a19 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..0fef07d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..38d1dfa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..516cec3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "token" = "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql new file mode 100644 index 0000000..b4c20a1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..9db98fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..9d2a3fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..14772cc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..b83a7e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..961bbb6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..4822fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..38d1dfa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..1905c70 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..37fa47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..84df028 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..b83a7e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9db98fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..970590a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..4ea5af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..76acb84 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..4ea5af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e340455 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..9cb268b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..cb69380 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..9cb268b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..554453e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..18701c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..554453e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..0bcec61 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..cb5cafc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..6636766 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..5a06270 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..649bea3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..d401b5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..9a097ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..9a097ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..a27a777 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..8ac44ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..99f1a46 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..9f9be3b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..a1a5596 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..5a36bae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..c4234d0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ca15b0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..621021c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..2568649 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..690a58c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6cb7200 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..690a58c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..90f5503 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..00117a6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e5375ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..00117a6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..02c3c0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ae68024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..5289902 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..7fe65c3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e7c586c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1e7cac1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..4ea42da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..b582095 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..4a50778 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..cab945e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..1227a1c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..d0650f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..c3a3c47 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..e134442 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..ed9ee13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..80ec024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..0da7c73 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..ed9ee13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..b227755 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..425c694 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..74e835a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..91dd9b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..5e69d7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..425c694 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..74e835a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d5b864c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..cdb8f9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..0ada192 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..c8953a2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..712d8b2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..0fe490d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..10e628a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..a4605d0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..6670ad2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..7ddc5ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..d3ad85b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..e09ac77 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..479627d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..55082d4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..ab73f48 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..ab73f48 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..7059164 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..479627d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..7ddc5ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..d3ad85b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..c15f380 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..6557e4c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..c8953a2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..c4ea5d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d5b864c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ab19747 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..92460b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..c4ea5d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..6557e4c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..4c6931a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..037a822 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..788e152 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..9ac004d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..fcf23d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..037a822 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..ab06acf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..3f787b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..6f6853c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..16f5505 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..ab06acf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..a9426a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..b1d2ffc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..1ca3bf9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..4cde445 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..2042f8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..b1d2ffc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..86dce13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..76be8ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e58903e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..47f8bb6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f093d32 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..07227f5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e9f0577 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..98f6fd8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..717d0e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..999d350 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..2e16a02 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..a005a81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..b96c559 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..8c367bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..baf8813 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..289889f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..ab970e1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..b9252d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..6fb5802 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f093d32 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..d496bf4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..07227f5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e5b27b4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..baf8813 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..759f782 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..b9252d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..a8693e6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..21462d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..b73f215 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/transaction.go b/oryx/popx/transaction.go new file mode 100644 index 0000000..0e69c70 --- /dev/null +++ b/oryx/popx/transaction.go @@ -0,0 +1,255 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "database/sql" + "fmt" + "math/rand/v2" + "runtime" + "strings" + "time" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/jmoiron/sqlx" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/prometheusx" + + "github.com/ory/pop/v6" + "github.com/ory/x/sqlcon" +) + +type transactionContextKey int + +const transactionKey transactionContextKey = 0 + +func WithTransaction(ctx context.Context, tx *pop.Connection) context.Context { + return context.WithValue(ctx, transactionKey, tx) +} + +func InTransaction(ctx context.Context) bool { + return ctx.Value(transactionKey) != nil +} + +func Transaction(ctx context.Context, connection *pop.Connection, callback func(context.Context, *pop.Connection) error) error { + return TransactionWithOptions(ctx, connection, nil, callback) +} + +// TransactionWithOptions opens the transaction with given sql.TxOptions, allowing isolation level to be set. +func TransactionWithOptions(ctx context.Context, connection *pop.Connection, opts *sql.TxOptions, callback func(context.Context, *pop.Connection) error) error { + c := ctx.Value(transactionKey) + if c != nil { + if conn, ok := c.(*pop.Connection); ok { + return errors.WithStack(callback(ctx, conn.WithContext(ctx))) + } + } + + conn := connection.WithContext(ctx) + + switch conn.Dialect.Name() { + case "cockroach": + return conn.Dialect.Lock(func() error { + tx, err := conn.NewTransactionContextOptions(ctx, opts) + if err != nil { + return errors.WithStack(err) + } + attempt := 0 + // A transaction's retries are recorded in a single increment so + // that the trace exemplar's value equals the transaction's retry + // count, which Grafana renders as the exemplar marker's position. + // Deferred so that retries are recorded even when the callback + // panics. + defer func() { + if retries := attempt - 1; retries > 0 { + recordRetries(ctx, retries) + } + }() + return errors.WithStack(crdb.ExecuteInTx(ctx, sqlxTxAdapter{tx.TX.Tx}, func() error { + attempt++ + return errors.WithStack(callback(WithTransaction(ctx, tx), tx)) + })) + }) + case "postgres", "mysql", "yugabyte": + // YugabyteDB speaks the Postgres wire protocol and, like Postgres, may + // surface serialization failures (SQLSTATE 40001) that the retry loop + // below handles through sqlcon.ErrConcurrentUpdate. + // + // Mirrors pop's Connection#Transaction with opts passed to NewTransactionContextOptions. + // https://github.com/ory/pop/blob/89126558d36911217a1cc70172ba94ee32692cad/connection.go#L148 + return conn.Dialect.Lock(func() error { + var err error + for range MaxTransactionRetries { + err = func() error { + cn, err := conn.NewTransactionContextOptions(ctx, opts) + if err != nil { + return errors.WithStack(err) + } + defer func() { + if ex := recover(); ex != nil { + _ = cn.TX.Rollback() + panic(ex) + } + }() + err = callback(WithTransaction(ctx, cn), cn) + var dberr error + if err != nil { + dberr = cn.TX.Rollback() + if errors.Is(dberr, sql.ErrTxDone) { + // Already rolled back by the database (e.g. context cancelled). + return err + } + if dberr != nil && dberr.Error() == "conn closed" { + // pgx closes the connection on context cancellation before + // database/sql gets a chance to roll back. + // See https://github.com/jackc/pgx/issues/2551 + return err + } + } else { + dberr = cn.TX.Commit() + } + if dberr != nil { + return fmt.Errorf("database error on committing or rolling back transaction: %w", dberr) + } + return err + }() + if err == nil || !errors.Is(sqlcon.HandleError(err), sqlcon.ErrConcurrentUpdate()) { + return err + } + } + return err + }) + } + + // SQLite and unknown dialects: opts are ignored; use pop's default + // transaction path with concurrent-update retry handling. + var err error + for attempt := range MaxTransactionRetries { + err = conn.Transaction(func(tx *pop.Connection) error { + return callback(WithTransaction(ctx, tx), tx) + }) + if err == nil { + return nil + } + if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrConcurrentUpdate()) { + return err + } + // Back off with jitter before retrying. SQLite in WAL mode fails a + // deferred transaction's read-to-write upgrade with + // SQLITE_BUSY_SNAPSHOT immediately (busy_timeout cannot help a stale + // snapshot), so an immediate retry under write contention can livelock + // through the whole retry budget. + select { + case <-ctx.Done(): + return err + case <-time.After(time.Duration(attempt+1) * time.Duration(1+rand.IntN(3)) * time.Millisecond): + } + } + return err +} + +func GetConnection(ctx context.Context, connection *pop.Connection) *pop.Connection { + c := ctx.Value(transactionKey) + if c != nil { + if conn, ok := c.(*pop.Connection); ok { + return conn.WithContext(ctx) + } + } + return connection.WithContext(ctx) +} + +type sqlxTxAdapter struct { + *sqlx.Tx +} + +var _ crdb.Tx = sqlxTxAdapter{} + +func (s sqlxTxAdapter) Exec(ctx context.Context, query string, args ...any) error { + _, err := s.Tx.ExecContext(ctx, query, args...) + return errors.WithStack(err) +} + +func (s sqlxTxAdapter) Commit(ctx context.Context) error { + return errors.WithStack(s.Tx.Commit()) +} + +func (s sqlxTxAdapter) Rollback(ctx context.Context) error { + return errors.WithStack(s.Tx.Rollback()) +} + +// MaxTransactionRetries is the number of times a transaction is retried on +// a concurrent-update conflict before the error is returned to the caller. +const MaxTransactionRetries = 10 + +var ( + transactionRetries = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "ory_x_popx_cockroach_transaction_retries_total", + Help: "Counts the number of automatic CockroachDB transaction retries", + }, []string{"caller"}) + TransactionRetries prometheus.Collector = transactionRetries + _ = transactionRetries.WithLabelValues(unknownCaller) // make sure the metric is always present + unknownCaller = "unknown" +) + +// recordRetries counts a transaction's automatic CockroachDB restarts. When +// the surrounding request is traced, it also attaches the trace to the span +// (as an event) and to the counter (as an exemplar whose value is the retry +// count) so that retry spikes can be linked to concrete requests. +func recordRetries(ctx context.Context, retries int) { + c := caller() + counter := transactionRetries.WithLabelValues(c) + span := trace.SpanFromContext(ctx) + span.AddEvent("db.transaction.retry", trace.WithAttributes( + attribute.String("caller", c), + attribute.Int("retries", retries), + )) + prometheusx.AddWithExemplar(ctx, counter, float64(retries)) +} + +// caller returns the function that opened the transaction: the innermost +// frame that is neither transaction plumbing (this package, pop, and +// crdb.ExecuteInTx) nor one of the thin wrapper methods that services layer +// over popx, so that a retry is attributed to the business operation rather +// than to a generic Transaction wrapper. +func caller() string { + pc := make([]uintptr, 32) + n := runtime.Callers(2, pc) + frames := runtime.CallersFrames(pc[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && !skipCallerFrame(frame.Function) { + return frame.Function + } + if !more { + return unknownCaller + } + } +} + +// skipCallerFrame reports whether a stack frame identifies transaction +// plumbing rather than the transaction's origin: this package and the +// libraries under it, and the thin wrapper methods services layer over popx, +// e.g. hydra's BasePersister.Transaction and RegistrySQL.Transaction +// (fosite's Transactional interface), keto's WithLatestSnapshot, and +// backoffice's WithFollowerReads. +func skipCallerFrame(fn string) bool { + return strings.HasPrefix(fn, "github.com/ory/x/popx.") || + strings.HasPrefix(fn, "github.com/ory/pop/") || + strings.HasPrefix(fn, "github.com/cockroachdb/cockroach-go/v2/crdb.ExecuteInTx") || + // Deferred recording runs with runtime frames (e.g. runtime.gopanic) + // on the stack when the transaction callback panicked. + strings.HasPrefix(fn, "runtime.") || + // Wrapper matching is deliberately coarse: a frame that merely + // mentions one of these names is skipped and the retry is attributed + // one frame further up, which is still a useful label. "Transaction" + // also covers TransactionWithOptions and method-value "-fm" symbols. + strings.Contains(fn, "Transaction") || + strings.Contains(fn, "WithLatestSnapshot") || + strings.Contains(fn, "WithFollowerReads") +} diff --git a/oryx/profilex/profiling.go b/oryx/profilex/profiling.go new file mode 100644 index 0000000..3e143e5 --- /dev/null +++ b/oryx/profilex/profiling.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profilex + +import ( + "os" + + "github.com/pkg/profile" +) + +type noop struct{} + +// Stop is a noop. +func (p *noop) Stop() {} + +// Profile parses the PROFILING environment variable and executes the proper profiling task. +func Profile() interface { + Stop() +} { + switch os.Getenv("PROFILING") { + case "cpu": + return profile.Start(profile.CPUProfile, profile.NoShutdownHook) + case "mem": + return profile.Start(profile.MemProfile, profile.NoShutdownHook) + case "mutex": + return profile.Start(profile.MutexProfile, profile.NoShutdownHook) + case "block": + return profile.Start(profile.BlockProfile, profile.NoShutdownHook) + } + return new(noop) +} + +// HelpMessage returns a string explaining how profiling works. +func HelpMessage() string { + return `- PROFILING: Set "PROFILING=cpu" to enable cpu profiling and "PROFILING=mem" to enable memory profiling. + It is not possible to do both at the same time. Profiling is disabled per default. + + Example: PROFILING=cpu` +} diff --git a/oryx/prometheusx/exemplar.go b/oryx/prometheusx/exemplar.go new file mode 100644 index 0000000..e9b87b3 --- /dev/null +++ b/oryx/prometheusx/exemplar.go @@ -0,0 +1,61 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" +) + +// ExemplarFromContext returns exemplar labels (trace_id, span_id) for the +// sampled trace span in ctx, or nil when there is none. The nil return makes +// it directly usable with promhttp.WithExemplarFromContext. +func ExemplarFromContext(ctx context.Context) prometheus.Labels { + sc := trace.SpanContextFromContext(ctx) + if !sc.IsValid() || !sc.IsSampled() { + return nil + } + return prometheus.Labels{ + "trace_id": sc.TraceID().String(), + "span_id": sc.SpanID().String(), + } +} + +// AddWithExemplar adds v to counter. When ctx carries a sampled trace span, +// the addition carries a trace exemplar so dashboards can link the counter +// to the trace. Any panic from exemplar validation is swallowed: a lost +// exemplar must never break the recording path, and client_golang applies +// the addition before validating the exemplar, so the count is recorded +// either way. +func AddWithExemplar(ctx context.Context, counter prometheus.Counter, v float64) { + exemplar := ExemplarFromContext(ctx) + adder, ok := counter.(prometheus.ExemplarAdder) + if exemplar == nil || !ok { + counter.Add(v) + return + } + defer func() { + _ = recover() + }() + adder.AddWithExemplar(v, exemplar) +} + +// ObserveWithExemplar records v on obs. When ctx carries a sampled trace +// span, the observation carries a trace exemplar on the histogram bucket it +// falls into. The same panic-swallowing rationale as AddWithExemplar +// applies. +func ObserveWithExemplar(ctx context.Context, obs prometheus.Observer, v float64) { + exemplar := ExemplarFromContext(ctx) + observer, ok := obs.(prometheus.ExemplarObserver) + if exemplar == nil || !ok { + obs.Observe(v) + return + } + defer func() { + _ = recover() + }() + observer.ObserveWithExemplar(v, exemplar) +} diff --git a/oryx/prometheusx/handler.go b/oryx/prometheusx/handler.go new file mode 100644 index 0000000..aac59e7 --- /dev/null +++ b/oryx/prometheusx/handler.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + MetricsPrometheusPath = "/metrics/prometheus" +) + +type muxrouter interface { + Handle(path string, handle http.Handler) +} + +// Handler returns the metrics handler. It mirrors promhttp.Handler() but +// additionally serves the OpenMetrics format when the scraper negotiates it, +// because that is the only exposition format that carries exemplars (trace +// links on counters and histograms). It is safe to call multiple times. +func Handler() http.Handler { + return promhttp.InstrumentMetricHandler( + prometheus.DefaultRegisterer, + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ + EnableOpenMetrics: true, + }), + ) +} + +// SetMuxRoutes registers the prometheus handler. +func SetMuxRoutes(mux muxrouter) { + mux.Handle("GET "+MetricsPrometheusPath, Handler()) +} diff --git a/oryx/prometheusx/metrics.go b/oryx/prometheusx/metrics.go new file mode 100644 index 0000000..669c9d9 --- /dev/null +++ b/oryx/prometheusx/metrics.go @@ -0,0 +1,205 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "net/http" + "regexp" + "strings" + "sync" + "time" + + grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/urfave/negroni" + + "github.com/ory/x/httprouterx" +) + +type HTTPMetrics struct { + responseTime *prometheus.HistogramVec + totalRequests *prometheus.CounterVec + duration *prometheus.HistogramVec + responseSize *prometheus.HistogramVec + requestSize *prometheus.HistogramVec + handlerStatuses *prometheus.CounterVec +} + +const HTTPPrefix = "http" + +func NewHTTPMetrics(app, metricsPrefix, version, hash, date string) *HTTPMetrics { + labels := map[string]string{ + "app": app, + "version": version, + "hash": hash, + "buildTime": date, + } + + if metricsPrefix != "" { + metricsPrefix += "_" + } + + pm := &HTTPMetrics{ + responseTime: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "response_time_seconds", + Help: "Description", + ConstLabels: labels, + }, []string{"endpoint"}), + totalRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: metricsPrefix + "requests_total", + Help: "number of requests", + ConstLabels: labels, + }, []string{"code", "method", "endpoint"}), + duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "requests_duration_seconds", + Help: "duration of a requests in seconds", + ConstLabels: labels, + }, []string{"code", "method", "endpoint"}), + responseSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "response_size_bytes", + Help: "size of the responses in bytes", + ConstLabels: labels, + }, []string{"code", "method"}), + requestSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "requests_size_bytes", + Help: "size of the requests in bytes", + ConstLabels: labels, + }, []string{"code", "method"}), + handlerStatuses: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: metricsPrefix + "requests_statuses_total", + Help: "count number of responses per status", + ConstLabels: labels, + }, []string{"method", "status_bucket"}), + } + + err := prometheus.Register(pm) + if e := new(prometheus.AlreadyRegisteredError); errors.As(err, e) { + return pm + } else if err != nil { + panic(err) + } + + grpcPrometheus.EnableHandlingTimeHistogram() + + return pm +} + +// Describe implements prometheus Collector interface. +func (h *HTTPMetrics) Describe(in chan<- *prometheus.Desc) { + h.duration.Describe(in) + h.totalRequests.Describe(in) + h.requestSize.Describe(in) + h.responseSize.Describe(in) + h.handlerStatuses.Describe(in) + h.responseTime.Describe(in) +} + +// Collect implements prometheus Collector interface. +func (h *HTTPMetrics) Collect(in chan<- prometheus.Metric) { + h.duration.Collect(in) + h.totalRequests.Collect(in) + h.requestSize.Collect(in) + h.responseSize.Collect(in) + h.handlerStatuses.Collect(in) + h.responseTime.Collect(in) +} + +func (h *HTTPMetrics) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + rr := negroni.NewResponseWriter(rw) + start := time.Now() + + var pattern string + next(rr, httprouterx.WithAfterMatchHook(r, func(r *http.Request) { + pattern = r.Pattern + })) + + latency := time.Since(start) + code := StatusCodeToString(rr.Status()) + method := sanitizeMethod(r.Method) + endpoint := getLabelForPattern(pattern) + + // Latency histograms and request counters carry trace exemplars so that + // dashboards can link latency outliers and error spikes to traces. The + // request's span is still open here: the tracing middleware wraps this + // one. + ctx := r.Context() + h.responseSize.WithLabelValues(code, method).Observe(float64(rr.Size())) + AddWithExemplar(ctx, h.totalRequests.WithLabelValues(code, method, endpoint), 1) + ObserveWithExemplar(ctx, h.duration.WithLabelValues(code, method, endpoint), latency.Seconds()) + ObserveWithExemplar(ctx, h.responseTime.WithLabelValues(endpoint), latency.Seconds()) + h.requestSize.WithLabelValues(code, method).Observe(float64(computeApproximateRequestSize(r))) + + statusBucket := "unknown" + switch status := rr.Status(); { + case status >= 200 && status <= 299: + statusBucket = "2xx" + case status >= 300 && status <= 399: + statusBucket = "3xx" + case status >= 400 && status <= 499: + statusBucket = "4xx" + case status >= 500 && status <= 599: + statusBucket = "5xx" + } + + AddWithExemplar(ctx, h.handlerStatuses.WithLabelValues(r.Method, statusBucket), 1) +} + +var ( + paramPlaceHolderRE = regexp.MustCompile(`\{[a-zA-Z0-9_-]+}`) + // patternLabelCache is used to cache the generated label for a given pattern to avoid recomputing it on every request. + // We use a sync.Map over a ristretto cache as the number of unique patterns is expected to be low and the cache will not grow indefinitely. + // + // > The sync.Map type is optimized for two common use cases: (1) when the entry for a given + // > key is only ever written once but read many times, as in caches that only grow, + // > or (2) when multiple goroutines read, write, and overwrite entries for disjoint + // > sets of keys. In these two cases, use of a Map may significantly reduce lock + // > contention compared to a Go map paired with a separate [Mutex] or [RWMutex]. + // https://pkg.go.dev/sync#Map + patternLabelCache = sync.Map{} +) + +func getLabelForPattern(pattern string) string { + if label, ok := patternLabelCache.Load(pattern); ok { + return label.(string) + } + + cleanedPattern := pattern + // remove the method if it is included + if _, pattern, ok := strings.Cut(cleanedPattern, " "); ok { + cleanedPattern = pattern + } + // trim space, just to be sure + cleanedPattern = strings.TrimSpace(cleanedPattern) + + label := paramPlaceHolderRE.ReplaceAllString(strings.TrimSuffix(cleanedPattern, "/{$}"), "{param}") + // Cache the generated label for the pattern to avoid recomputing it on every request. + patternLabelCache.Store(pattern, label) + return label +} + +// computeApproximateRequestSize is copied from the promhttp package as it is not exported +func computeApproximateRequestSize(r *http.Request) int { + s := 0 + if r.URL != nil { + s += len(r.URL.String()) + } + + s += len(r.Method) + s += len(r.Proto) + for name, values := range r.Header { + s += len(name) + for _, value := range values { + s += len(value) + } + } + s += len(r.Host) + + // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. + + if r.ContentLength != -1 { + s += int(r.ContentLength) + } + return s +} diff --git a/oryx/prometheusx/sanitize_method.go b/oryx/prometheusx/sanitize_method.go new file mode 100644 index 0000000..85905ff --- /dev/null +++ b/oryx/prometheusx/sanitize_method.go @@ -0,0 +1,31 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +func sanitizeMethod(m string) string { + switch m { + case "GET", "get": + return "get" + case "PUT", "put": + return "put" + case "HEAD", "head": + return "head" + case "POST", "post": + return "post" + case "DELETE", "delete": + return "delete" + case "CONNECT", "connect": + return "connect" + case "OPTIONS", "options": + return "options" + case "NOTIFY", "notify": + return "notify" + case "TRACE", "trace": + return "trace" + case "PATCH", "patch": + return "patch" + default: + return "unknown" + } +} diff --git a/oryx/prometheusx/status_code_strings.go b/oryx/prometheusx/status_code_strings.go new file mode 100644 index 0000000..7545217 --- /dev/null +++ b/oryx/prometheusx/status_code_strings.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import "strconv" + +var toString = [600]string{ + 100: "100", 101: "101", 102: "102", 103: "103", + + 200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206", 207: "207", 208: "208", + 226: "226", + + 300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307", 308: "308", + 400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406", 407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413", 414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 421: "421", 422: "422", 423: "423", 424: "424", 425: "425", 426: "426", 428: "428", 429: "429", + 431: "431", + 451: "451", + 499: "499", + + 500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 506: "506", 507: "507", 508: "508", 510: "510", 511: "511", +} + +// StatusCodeToString converts an HTTP status code to its string representation. +// It is highly optimized for the well-known HTTP status codes. +func StatusCodeToString(code int) string { + if code >= 0 && code < len(toString) { + if s := toString[code]; s != "" { + return s + } + } + return strconv.Itoa(code) +} diff --git a/oryx/proxy/proxy.go b/oryx/proxy/proxy.go new file mode 100644 index 0000000..f379baf --- /dev/null +++ b/oryx/proxy/proxy.go @@ -0,0 +1,321 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package proxy + +import ( + "context" + "log" + "net/http" + "net/http/httputil" + + "github.com/pkg/errors" + + "github.com/rs/cors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type ( + RespMiddleware func(resp *http.Response, config *HostConfig, body []byte) ([]byte, error) + ReqMiddleware func(req *httputil.ProxyRequest, config *HostConfig, body []byte) ([]byte, error) + HostMapper func(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error) + options struct { + hostMapper HostMapper + onResError func(*http.Response, error) error + onReqError func(*http.Request, error) + respMiddlewares []RespMiddleware + reqMiddlewares []ReqMiddleware + transport http.RoundTripper + errHandler func(http.ResponseWriter, *http.Request, error) + } + HostConfig struct { + // CorsEnabled is a flag to enable or disable CORS + // Default: false + CorsEnabled bool + // CorsOptions allows to configure CORS + // If left empty, no CORS headers will be set even when CorsEnabled is true + CorsOptions *cors.Options + // CookieDomain is the host under which cookies are set. + // If left empty, no cookie domain will be set + CookieDomain string + // UpstreamHost is the next upstream host the proxy will pass the request to. + // e.g. fluffy-bear-afiu23iaysd.oryapis.com + UpstreamHost string + // UpstreamScheme is the protocol used by the upstream service. + UpstreamScheme string + // TargetHost is the final target of the request. Should be the same as UpstreamHost + // if the request is directly passed to the target service. + TargetHost string + // TargetScheme is the final target's scheme + // (i.e. the scheme the target thinks it is running under) + TargetScheme string + // PathPrefix is a prefix that is prepended on the original host, + // but removed before forwarding. + PathPrefix string + // TrustForwardedHosts is a flag that indicates whether the proxy should trust the + // X-Forwarded-* headers or not. + TrustForwardedHeaders bool + // originalHost the original hostname the request is coming from. + // This value will be maintained internally by the proxy. + originalHost string + // originalScheme is the original scheme of the request. + // This value will be maintained internally by the proxy. + originalScheme string + // ForceOriginalSchemeHTTP forces the original scheme to be https if enabled. + ForceOriginalSchemeHTTPS bool + } + Options func(*options) + contextKey string +) + +const ( + hostConfigKey contextKey = "host config" + serverSpanKey contextKey = "server span" +) + +func (c *HostConfig) setScheme(r *httputil.ProxyRequest) { + if c.ForceOriginalSchemeHTTPS { + c.originalScheme = "https" + } else if forwardedProto := r.In.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { + c.originalScheme = forwardedProto + } else if r.In.TLS == nil { + c.originalScheme = "http" + } else { + c.originalScheme = "https" + } +} + +func (c *HostConfig) setHost(r *httputil.ProxyRequest) { + if forwardedHost := r.In.Header.Get("X-Forwarded-Host"); forwardedHost != "" { + c.originalHost = forwardedHost + } else { + c.originalHost = r.In.Host + } +} + +// rewriter is a custom internal function for altering a http.Request +func rewriter(o *options) func(*httputil.ProxyRequest) { + return func(r *httputil.ProxyRequest) { + // Stash the server-side span so that modifyResponse can restore it as + // the active span before the response middlewares run. Without a valid + // span there is nothing worth restoring: replacing the transport's + // client span with a noop span would orphan the middleware spans. + ctx := r.Out.Context() + if s := trace.SpanFromContext(ctx); s.SpanContext().IsValid() { + ctx = context.WithValue(ctx, serverSpanKey, s) + r.Out = r.Out.WithContext(ctx) + } + + ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "x.proxy") + defer span.End() + + _, c, err := o.getHostConfig(ctx, r.In) + if err != nil { + o.onReqError(r.Out, err) + return + } + + if c.TrustForwardedHeaders { + headers := []string{ + "X-Forwarded-Host", + "X-Forwarded-Proto", + "X-Forwarded-For", + } + for _, h := range headers { + if v := r.In.Header.Get(h); v != "" { + r.Out.Header.Set(h, v) + } + } + } + + c.setScheme(r) + c.setHost(r) + + headerRequestRewrite(r.Out, c) + + var body []byte + var cb *compressableBody + + if r.Out.ContentLength != 0 { + body, cb, err = readBody(r.Out.Header, r.Out.Body) + if err != nil { + o.onReqError(r.Out, err) + return + } + } + + for _, m := range o.reqMiddlewares { + if body, err = m(r, c, body); err != nil { + o.onReqError(r.Out, err) + return + } + } + + n, err := cb.Write(body) + if err != nil { + o.onReqError(r.Out, err) + return + } + + r.Out.Header.Del("Content-Length") + r.Out.ContentLength = int64(n) + r.Out.Body = cb + } +} + +// modifyResponse is a custom internal function for altering a http.Response +func modifyResponse(o *options) func(*http.Response) error { + return func(r *http.Response) error { + // An instrumented transport (otelhttp) clones the outbound request with + // its HTTP client span as the active span and ends that span once the + // response body is consumed. Restore the server-side span stashed in + // rewriter so that spans started by the response middlewares are + // parented to the still-open server span instead of the ended client + // span. + if span, ok := r.Request.Context().Value(serverSpanKey).(trace.Span); ok { + r.Request = r.Request.WithContext(trace.ContextWithSpan(r.Request.Context(), span)) + } + + _, c, err := o.getHostConfig(r.Request.Context(), r.Request) + if err != nil { + return err + } + + if err := headerResponseRewrite(r, c); err != nil { + return o.onResError(r, err) + } + + body, cb, err := bodyResponseRewrite(r, c) + if err != nil { + return o.onResError(r, err) + } + + for _, m := range o.respMiddlewares { + if body, err = m(r, c, body); err != nil { + return o.onResError(r, err) + } + } + + n, err := cb.Write(body) + if err != nil { + return o.onResError(r, err) + } + + n, t, err := handleWebsocketResponse(n, cb, r.Body) + if err != nil { + return err + } + + r.Header.Del("Content-Length") + r.ContentLength = int64(n) + r.Body = t + return nil + } +} + +func WithOnError(onReqErr func(*http.Request, error), onResErr func(*http.Response, error) error) Options { + return func(o *options) { + o.onReqError = onReqErr + o.onResError = onResErr + } +} + +func WithReqMiddleware(middlewares ...ReqMiddleware) Options { + return func(o *options) { + o.reqMiddlewares = append(o.reqMiddlewares, middlewares...) + } +} + +func WithRespMiddleware(middlewares ...RespMiddleware) Options { + return func(o *options) { + o.respMiddlewares = append(o.respMiddlewares, middlewares...) + } +} + +func WithTransport(t http.RoundTripper) Options { + return func(o *options) { + o.transport = t + } +} + +func WithErrorHandler(eh func(w http.ResponseWriter, r *http.Request, err error)) Options { + return func(o *options) { + o.errHandler = eh + } +} + +func (o *options) getHostConfig(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error) { + if cached, ok := ctx.Value(hostConfigKey).(*HostConfig); ok && cached != nil { + return ctx, cached, nil + } + ctx, c, err := o.hostMapper(ctx, r) + if err != nil { + return nil, nil, err + } + // cache the host config in the request context + // this will be passed on to the request and response proxy functions + ctx = context.WithValue(ctx, hostConfigKey, c) + return ctx, c, nil +} + +func (o *options) beforeProxyMiddleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + // get the hostmapper configurations before the request is proxied + ctx, c, err := o.getHostConfig(request.Context(), request) + if err != nil { + o.onReqError(request, err) + return + } + + // Add our Cors middleware. + // This middleware will only trigger if the host config has cors enabled on that request. + if c.CorsEnabled && c.CorsOptions != nil { + cors.New(*c.CorsOptions).HandlerFunc(writer, request) + } + + h.ServeHTTP(writer, request.WithContext(ctx)) + }) +} + +func defaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, context.Canceled): + w.WriteHeader(499) // http://nginx.org/en/docs/dev/development_guide.html + case isTimeoutError(err): + w.WriteHeader(http.StatusGatewayTimeout) + default: + log.Printf("http: proxy error: %v", err) + w.WriteHeader(http.StatusBadGateway) + } +} + +func isTimeoutError(err error) bool { + var te interface{ Timeout() bool } = nil + return errors.As(err, &te) && te.Timeout() || errors.Is(err, context.DeadlineExceeded) +} + +// New creates a new Proxy +// A Proxy sets up a middleware with custom request and response modification handlers +func New(hostMapper HostMapper, opts ...Options) http.Handler { + o := &options{ + hostMapper: hostMapper, + onReqError: func(*http.Request, error) {}, + onResError: func(_ *http.Response, err error) error { return err }, + transport: http.DefaultTransport, + errHandler: defaultErrorHandler, + } + + for _, op := range opts { + op(o) + } + + rp := &httputil.ReverseProxy{ + Rewrite: rewriter(o), + ModifyResponse: modifyResponse(o), + Transport: o.transport, + ErrorHandler: o.errHandler, + } + + return o.beforeProxyMiddleware(rp) +} diff --git a/oryx/proxy/rewrites.go b/oryx/proxy/rewrites.go new file mode 100644 index 0000000..ddd6d1d --- /dev/null +++ b/oryx/proxy/rewrites.go @@ -0,0 +1,163 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package proxy + +import ( + "bytes" + "compress/gzip" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/pkg/errors" +) + +type compressableBody struct { + buf bytes.Buffer + w io.WriteCloser +} + +// we require a read and write for websocket connections +var _ io.ReadWriteCloser = new(compressableBody) + +func (b *compressableBody) Close() error { + if b != nil { + b.buf.Reset() + if b.w != nil { + return b.w.Close() + } + } + return nil +} + +func (b *compressableBody) Write(d []byte) (int, error) { + if b == nil { + // this happens when the body is empty + return 0, nil + } + + var w io.Writer = &b.buf + if b.w != nil { + w = b.w + defer b.w.Close() + } + return w.Write(d) +} + +func (b *compressableBody) Read(p []byte) (n int, err error) { + if b == nil { + // this happens when the body is empty + return 0, io.EOF + } + return b.buf.Read(p) +} + +func headerRequestRewrite(req *http.Request, c *HostConfig) { + req.URL.Scheme = c.UpstreamScheme + req.URL.Host = c.UpstreamHost + req.URL.Path = strings.TrimPrefix(req.URL.Path, c.PathPrefix) + + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } +} + +func headerResponseRewrite(resp *http.Response, c *HostConfig) error { + redir, err := resp.Location() + if err != nil { + if !errors.Is(err, http.ErrNoLocation) { + return errors.WithStack(err) + } + } else if redir.Host == c.TargetHost { + redir.Scheme = c.originalScheme + redir.Host = c.originalHost + redir.Path = path.Join(c.PathPrefix, redir.Path) + resp.Header.Set("Location", redir.String()) + } + + ReplaceCookieDomainAndSecure(resp, c.TargetHost, c.CookieDomain, c.originalScheme == "https") + + return nil +} + +// ReplaceCookieDomainAndSecure replaces the domain of all matching Set-Cookie headers in the response. +func ReplaceCookieDomainAndSecure(resp *http.Response, original, replacement string, secure bool) { + original, replacement = stripPort(original), stripPort(replacement) // cookies don't distinguish ports + + cookies := resp.Cookies() + resp.Header.Del("Set-Cookie") + for _, co := range cookies { + co.Domain = replacement + co.Secure = secure + if !secure { + co.SameSite = http.SameSiteLaxMode + } + resp.Header.Add("Set-Cookie", co.String()) + } +} + +func bodyResponseRewrite(resp *http.Response, c *HostConfig) ([]byte, *compressableBody, error) { + if resp.ContentLength == 0 { + return nil, nil, nil + } + + body, cb, err := readBody(resp.Header, resp.Body) + if err != nil { + return nil, nil, err + } + + if c.TargetScheme == "" { + c.TargetScheme = "https" + } + + return bytes.ReplaceAll(body, []byte(c.TargetScheme+"://"+c.TargetHost), []byte(c.originalScheme+"://"+c.originalHost+c.PathPrefix)), cb, nil +} + +func readBody(h http.Header, body io.ReadCloser) ([]byte, *compressableBody, error) { + defer body.Close() + + cb := &compressableBody{} + + switch h.Get("Content-Encoding") { + case "gzip": + var err error + body, err = gzip.NewReader(body) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + cb.w = gzip.NewWriter(&cb.buf) + default: + // do nothing, we can read directly + } + + b, err := io.ReadAll(body) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return b, cb, nil +} + +func handleWebsocketResponse(n int, cb *compressableBody, body io.ReadCloser) (int, io.ReadWriteCloser, error) { + var err error + readWriteCloser, ok := body.(io.ReadWriteCloser) + if ok { + if cb != nil { + n, err = readWriteCloser.Write(cb.buf.Bytes()) + if err != nil { + return 0, nil, errors.WithStack(err) + } + } + return n, readWriteCloser, nil + } + return n, cb, nil +} + +// stripPort removes the optional port from the host. +func stripPort(host string) string { + return (&url.URL{Host: host}).Hostname() +} diff --git a/oryx/proxy/stubs/auth.example.com.json b/oryx/proxy/stubs/auth.example.com.json new file mode 100644 index 0000000..2dcdc3b --- /dev/null +++ b/oryx/proxy/stubs/auth.example.com.json @@ -0,0 +1,9 @@ +{ + "ui": { + "action": "https://auth.example.com" + }, + "callbacks": [ + "https://auth.example.com/path/to/resource", + "https://auth.example.com/path?q=https://localhost:8000" + ] +} diff --git a/oryx/randx/README.md b/oryx/randx/README.md new file mode 100644 index 0000000..fb8c504 --- /dev/null +++ b/oryx/randx/README.md @@ -0,0 +1,10 @@ +`randx.RuneSequence` generates even distributions for the given character set +and length. All results are therefore also evenly distributed. + +## AlphaNum + +[Alphabet and Numeric](../docs/alpha_num.png) + +## AlphaNum + +[Alphabet and Numeric](../docs/num.png) diff --git a/oryx/randx/sequence.go b/oryx/randx/sequence.go new file mode 100644 index 0000000..f41b22e --- /dev/null +++ b/oryx/randx/sequence.go @@ -0,0 +1,75 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package randx + +import ( + "crypto/rand" + "math/big" + + "github.com/pkg/errors" +) + +var rander = rand.Reader // random function + +var ( + // AlphaNum contains runes [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]. + AlphaNum = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + // Alpha contains runes [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]. + Alpha = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + // AlphaLowerNum contains runes [abcdefghijklmnopqrstuvwxyz0123456789]. + AlphaLowerNum = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + // AlphaUpperNum contains runes [ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]. + AlphaUpperNum = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + // AlphaLower contains runes [abcdefghijklmnopqrstuvwxyz]. + AlphaLower = []rune("abcdefghijklmnopqrstuvwxyz") + // AlphaUpperVowels contains runes [AEIOUY]. + AlphaUpperVowels = []rune("AEIOUY") + // AlphaUpperNoVowels contains runes [BCDFGHJKLMNPQRSTVWXZ]. + AlphaUpperNoVowels = []rune("BCDFGHJKLMNPQRSTVWXZ") + // AlphaUpper contains runes [ABCDEFGHIJKLMNOPQRSTUVWXYZ]. + AlphaUpper = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + // Numeric contains runes [0123456789]. + Numeric = []rune("0123456789") + // AlphaNumNoAmbiguous is equivalent to AlphaNum but without visually ambiguous characters [0Oo1IlB8S5Z2]. + AlphaNumNoAmbiguous = []rune("abcdefghijkmnpqrstuvwxyzACDEFGHJKLMNPQRTUVWXY34679") +) + +// RuneSequence returns a random sequence using the defined allowed runes. +func RuneSequence(l int, allowedRunes []rune) (seq []rune, err error) { + c := big.NewInt(int64(len(allowedRunes))) + seq = make([]rune, l) + + for i := 0; i < l; i++ { + r, err := rand.Int(rander, c) + if err != nil { + return seq, err + } + rn := allowedRunes[r.Uint64()] + seq[i] = rn + } + + return seq, nil +} + +// MustString returns a random string sequence using the defined runes. Panics on error. +func MustString(l int, allowedRunes []rune) string { + seq, err := RuneSequence(l, allowedRunes) + if err != nil { + panic(err) + } + return string(seq) +} + +func GenerateNonce() ([]byte, error) { + const challengeSize = 32 + + challenge := make([]byte, challengeSize) + + _, err := rand.Read(challenge) + if err != nil { + return nil, errors.WithStack(err) + } + + return challenge, nil +} diff --git a/oryx/randx/strength/go.mod b/oryx/randx/strength/go.mod new file mode 100644 index 0000000..968295e --- /dev/null +++ b/oryx/randx/strength/go.mod @@ -0,0 +1,22 @@ +module github.com/ory/x/randx/strength + +go 1.26 + +replace github.com/ory/x => ../.. + +require ( + github.com/ory/x v0.0.0-00010101000000-000000000000 + gonum.org/v1/plot v0.17.0 +) + +require ( + codeberg.org/go-fonts/liberation v0.5.0 // indirect + codeberg.org/go-latex/latex v0.2.0 // indirect + codeberg.org/go-pdf/fpdf v0.11.1 // indirect + git.sr.ht/~sbinet/gg v0.7.0 // indirect + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/image v0.41.0 // indirect + golang.org/x/text v0.39.0 // indirect +) diff --git a/oryx/randx/strength/go.sum b/oryx/randx/strength/go.sum new file mode 100644 index 0000000..811ddce --- /dev/null +++ b/oryx/randx/strength/go.sum @@ -0,0 +1,65 @@ +codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I= +codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg= +codeberg.org/go-fonts/latin-modern v0.4.0 h1:vkRCc1y3whKA7iL9Ep0fSGVuJfqjix0ica9UflHORO8= +codeberg.org/go-fonts/latin-modern v0.4.0/go.mod h1:BF68mZznJ9QHn+hic9ks2DaFl4sR5YhfM6xTYaP9vNw= +codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI= +codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y= +codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs= +codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= +git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= +git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM= +git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4= +gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/oryx/randx/strength/main.go b/oryx/randx/strength/main.go new file mode 100644 index 0000000..26c877c --- /dev/null +++ b/oryx/randx/strength/main.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "sort" + + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/plotutil" + "gonum.org/v1/plot/vg" + + "github.com/ory/x/randx" +) + +const iterations = 1000 * 100 + +type generate func(int, []rune) ([]rune, error) + +func main() { + draw(measureDistribution(iterations, randx.AlphaNum, randx.RuneSequence), "AlphaNum Distribution", "docs/alpha_num.png") + draw(measureDistribution(iterations, randx.Numeric, randx.RuneSequence), "Num Distribution", "docs/num.png") + draw(measureResultDistribution(100, 6, randx.Numeric, randx.RuneSequence), "Num Distribution", "docs/result_num.png") +} + +func measureResultDistribution(iterations int, length int, characters []rune, fn generate) map[string]int { + dist := make(map[string]int) + for index := 1; index <= iterations; index++ { + // status output to cli + if index%1000 == 0 { + fmt.Printf("\r%d / %d", index, iterations) + } + raw, err := fn(length, characters) + if err != nil { + panic(err) + } + dist[string(raw)] = dist[string(raw)] + 1 + } + return dist +} + +func measureDistribution(iterations int, characters []rune, fn generate) map[string]int { + dist := make(map[string]int) + for index := 1; index <= iterations; index++ { + // status output to cli + if index%1000 == 0 { + fmt.Printf("\r%d / %d", index, iterations) + } + raw, err := fn(100, characters) + if err != nil { + panic(err) + } + for _, s := range raw { + c := string(s) + i := dist[c] + dist[c] = i + 1 + } + } + return dist +} + +func draw(distribution map[string]int, title, filename string) { + keys, values := orderMap(distribution) + group := plotter.Values{} + for _, v := range values { + group = append(group, float64(v)) + } + + p := plot.New() + p.Title.Text = title + p.Y.Label.Text = "N" + + bars, err := plotter.NewBarChart(group, vg.Points(4)) + if err != nil { + panic(err) + } + bars.LineStyle.Width = vg.Length(0) + bars.Color = plotutil.Color(0) + + p.Add(bars) + p.NominalX(keys...) + + if err := p.Save(300*vg.Millimeter, 150*vg.Millimeter, filename); err != nil { + panic(err) + } +} + +func orderMap(m map[string]int) (keys []string, values []int) { + keys = []string{} + values = []int{} + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + values = append(values, m[key]) + } + return keys, values +} diff --git a/oryx/region/region.go b/oryx/region/region.go new file mode 100644 index 0000000..dd7dc13 --- /dev/null +++ b/oryx/region/region.go @@ -0,0 +1,142 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package region defines the Ory Network region enumeration. Lives in x/ so +// kratos-oss and other OSS layers can use it without importing cloudlib. +package region + +import ( + "database/sql/driver" + + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +// Region is an Ory Network region. Specific regions map to a single CRDB +// region; super-regions (EU, Asia, US, Global) group specific regions that +// share a data-residency boundary. +// +// swagger:enum Region +type Region string + +const ( + EUCentral Region = "eu-central" + AsiaNorthEast Region = "asia-northeast" + USEast Region = "us-east" + USWest Region = "us-west" + + EU Region = "eu" + Asia Region = "asia" + US Region = "us" + Global Region = "global" +) + +// All returns every valid region in stable order: specific regions first, +// then super-regions. +func All() []Region { + return []Region{EUCentral, AsiaNorthEast, USEast, USWest, EU, Asia, US, Global} +} + +func (r Region) String() string { + return string(r) +} + +// Valid reports whether r is a known region. +func (r Region) Valid() bool { + switch r { + case EUCentral, AsiaNorthEast, USEast, USWest, EU, Asia, US, Global: + return true + } + return false +} + +// IsSuperRegion reports whether r is a super-region. +func (r Region) IsSuperRegion() bool { + switch r { + case EU, Asia, US, Global: + return true + } + return false +} + +// Contains reports whether r contains other: +// - Global contains every valid region. +// - EU contains EUCentral; Asia contains AsiaNorthEast; US contains USEast and USWest. +// - Every region contains itself. +func (r Region) Contains(other Region) bool { + if !r.Valid() || !other.Valid() { + return false + } + if r == Global || r == other { + return true + } + switch r { + case EU: + return other == EUCentral + case Asia: + return other == AsiaNorthEast + case US: + return other == USEast || other == USWest + } + return false +} + +// Scan implements sql.Scanner. NULL and empty scan to the zero value; +// validate via Region.Valid if "unset" must be rejected. +func (r *Region) Scan(src any) error { + switch s := src.(type) { + case nil: + *r = "" + case string: + *r = Region(s) + case []byte: + *r = Region(s) + default: + return errors.Errorf("cannot scan %T into region.Region", src) + } + return nil +} + +// Value implements driver.Valuer. The empty Region writes as "". +func (r Region) Value() (driver.Value, error) { + return string(r), nil +} + +// IsEqual compares two nullable *Region pointers (both nil = equal). +func IsEqual(a, b *Region) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return *a == *b +} + +// ErrInvalid is wrapped when a submitted region is not a known value. +// Use NewErrInvalid for a herodot 400; use errors.Is for chain checks. +var ErrInvalid = errors.New("the provided region is not a valid Ory region") + +// ErrNotAllowed is wrapped when a valid region is outside the project's +// home_region constraint. +var ErrNotAllowed = errors.New("the provided region is not allowed by this project's home region") + +// NewErrInvalid returns a fresh herodot 400 wrapping ErrInvalid. +func NewErrInvalid() error { + return errors.WithStack( + herodot.ErrBadRequest(). + WithReason(ErrInvalid.Error()). + WithDebug(`region must be one of eu-central, asia-northeast, us-east, us-west, eu, asia, us, global`). + WithWrap(ErrInvalid), + ) +} + +// NewErrNotAllowed returns a fresh herodot 400 wrapping ErrNotAllowed. +func NewErrNotAllowed() error { + return errors.WithStack( + herodot.ErrBadRequest(). + WithReason(ErrNotAllowed.Error()). + WithWrap(ErrNotAllowed), + ) +} diff --git a/oryx/reqlog/LICENSE b/oryx/reqlog/LICENSE new file mode 100644 index 0000000..638544b --- /dev/null +++ b/oryx/reqlog/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dan Buch and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/oryx/reqlog/external_latency.go b/oryx/reqlog/external_latency.go new file mode 100644 index 0000000..53edda9 --- /dev/null +++ b/oryx/reqlog/external_latency.go @@ -0,0 +1,34 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package reqlog + +import ( + "context" + "sync/atomic" + "time" +) + +func withEnableExternalLatencyMeasurement(ctx context.Context) context.Context { + return context.WithValue(ctx, externalLatencyKey, new(int64)) +} + +func AccumulateExternalLatency(ctx context.Context, dur time.Duration) { + total, ok := ctx.Value(externalLatencyKey).(*int64) + if !ok { + return + } + atomic.AddInt64(total, int64(dur)) +} + +func getExternalLatency(ctx context.Context) time.Duration { + total, ok := ctx.Value(externalLatencyKey).(*int64) + if !ok { + return 0 + } + return time.Duration(atomic.LoadInt64(total)) +} + +type contextKey int + +const externalLatencyKey contextKey = 1 diff --git a/oryx/reqlog/middleware.go b/oryx/reqlog/middleware.go new file mode 100644 index 0000000..59dc281 --- /dev/null +++ b/oryx/reqlog/middleware.go @@ -0,0 +1,191 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package reqlog + +import ( + "net/http" + "sync" + "time" + + "github.com/sirupsen/logrus" + "github.com/urfave/negroni" + + "github.com/ory/x/logrusx" +) + +type timer interface { + Now() time.Time + Since(time.Time) time.Duration +} + +type realClock struct{} + +func (rc *realClock) Now() time.Time { + return time.Now() +} + +func (rc *realClock) Since(t time.Time) time.Duration { + return time.Since(t) +} + +// Middleware is a middleware handler that logs the request as it goes in and the response as it goes out. +type Middleware struct { + // Logger is the log.Logger instance used to log messages with the Logger middleware + Logger *logrusx.Logger + // Name is the name of the application as recorded in latency metrics + Name string + Before func(*logrusx.Logger, *http.Request, string) *logrusx.Logger + After func(*logrusx.Logger, *http.Request, negroni.ResponseWriter, time.Duration, string) *logrusx.Logger + + logStarting bool + + clock timer + + logLevel logrus.Level + + // Silence log for specific URL paths + silencePaths map[string]bool + + sync.RWMutex +} + +// NewMiddleware returns a reqlog middleware with default settings +func NewMiddleware() *Middleware { + return NewCustomMiddleware(logrus.InfoLevel, &logrus.TextFormatter{}, "web") +} + +// NewCustomMiddleware returns a reqlog middleware with the given level and formatter +func NewCustomMiddleware(level logrus.Level, formatter logrus.Formatter, name string) *Middleware { + log := logrusx.New(name, "", logrusx.ForceFormatter(formatter), logrusx.ForceLevel(level)) + return &Middleware{ + Logger: log, + Name: name, + Before: DefaultBefore, + After: DefaultAfter, + + logLevel: logrus.InfoLevel, + logStarting: true, + clock: &realClock{}, + silencePaths: map[string]bool{}, + } +} + +// NewMiddlewareFromLogger returns a reqlog middleware which writes to a given logrus logger. +func NewMiddlewareFromLogger(logger *logrusx.Logger, name string) *Middleware { + return &Middleware{ + Logger: logger, + Name: name, + Before: DefaultBefore, + After: DefaultAfter, + + logLevel: logrus.InfoLevel, + logStarting: true, + clock: &realClock{}, + silencePaths: map[string]bool{}, + } +} + +// SetLogStarting accepts a bool to control the logging of "started handling +// request" prior to passing to the next middleware +func (m *Middleware) SetLogStarting(v bool) { + m.logStarting = v +} + +// ExcludePaths adds new URL paths to be ignored during logging. The URL u is parsed, hence the returned error +func (m *Middleware) ExcludePaths(paths ...string) *Middleware { + for _, path := range paths { + m.Lock() + m.silencePaths[path] = true + m.Unlock() + } + return m +} + +func (m *Middleware) Wrap(handler http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + m.ServeHTTP(rw, r, handler.ServeHTTP) + }) +} + +func (m *Middleware) WrapFunc(handler http.HandlerFunc) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + m.ServeHTTP(rw, r, handler) + } +} + +func (m *Middleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if m.Before == nil { + m.Before = DefaultBefore + } + + if m.After == nil { + m.After = DefaultAfter + } + + logLevel := m.logLevel + m.RLock() + if _, ok := m.silencePaths[r.URL.Path]; ok { + logLevel = logrus.TraceLevel + } + m.RUnlock() + + start := m.clock.Now() + + // Try to get the real IP + remoteAddr := r.RemoteAddr + if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + remoteAddr = realIP + } + + entry := m.Logger.NewEntry() + + entry = m.Before(entry, r, remoteAddr) + + if m.logStarting { + entry.Log(logLevel, "started handling request") + } + + nrw, ok := rw.(negroni.ResponseWriter) + if !ok { + nrw = negroni.NewResponseWriter(rw) + } + + r = r.WithContext(withEnableExternalLatencyMeasurement(r.Context())) + next(nrw, r) + + latency := m.clock.Since(start) + + m.After(entry, r, nrw, latency, m.Name).Log(logLevel, "completed handling request") +} + +// BeforeFunc is the func type used to modify or replace the *logrusx.Logger prior +// to calling the next func in the middleware chain +type BeforeFunc func(*logrusx.Logger, *http.Request, string) *logrusx.Logger + +// AfterFunc is the func type used to modify or replace the *logrusx.Logger after +// calling the next func in the middleware chain +type AfterFunc func(*logrusx.Logger, negroni.ResponseWriter, time.Duration, string) *logrusx.Logger + +// DefaultBefore is the default func assigned to *Middleware.Before +func DefaultBefore(entry *logrusx.Logger, req *http.Request, remoteAddr string) *logrusx.Logger { + return entry.WithRequest(req) +} + +// DefaultAfter is the default func assigned to *Middleware.After +func DefaultAfter(entry *logrusx.Logger, req *http.Request, res negroni.ResponseWriter, latency time.Duration, name string) *logrusx.Logger { + e := entry.WithRequest(req).WithField("http_response", map[string]any{ + "status": res.Status(), + "size": res.Size(), + "text_status": http.StatusText(res.Status()), + "took": latency, + "headers": entry.HTTPHeadersRedacted(res.Header()), + }) + if el := getExternalLatency(req.Context()); el > 0 { + e = e.WithFields(map[string]any{ + "took_internal": latency - el, + "took_external": el, + }) + } + return e +} diff --git a/oryx/resilience/retry.go b/oryx/resilience/retry.go new file mode 100644 index 0000000..8ca6511 --- /dev/null +++ b/oryx/resilience/retry.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package resilience provides helpers for dealing with resilience. +package resilience + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/logrusx" +) + +// Retry executes a f until no error is returned or failAfter is reached. +func Retry(logger *logrusx.Logger, maxWait time.Duration, failAfter time.Duration, f func() error) (err error) { + var lastStart time.Time + err = errors.New("did not connect") + loopWait := time.Millisecond * 100 + retryStart := time.Now().UTC() + for retryStart.Add(failAfter).After(time.Now().UTC()) { + lastStart = time.Now().UTC() + if err = f(); err == nil { + return nil + } + + if lastStart.Add(maxWait * 2).Before(time.Now().UTC()) { + retryStart = time.Now().UTC() + } + + logger.WithError(err).Infof("Retrying in %f seconds...", loopWait.Seconds()) + time.Sleep(loopWait) + loopWait = loopWait * time.Duration(int64(2)) + if loopWait > maxWait { + loopWait = maxWait + } + } + return err +} diff --git a/oryx/safecast/safecast.go b/oryx/safecast/safecast.go new file mode 100644 index 0000000..947b517 --- /dev/null +++ b/oryx/safecast/safecast.go @@ -0,0 +1,14 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package safecast + +import "math" + +// Clamp if needed. +func Uint64ToInt64(in uint64) int64 { + if in > math.MaxInt64 { + return math.MaxInt64 + } + return int64(in) +} diff --git a/oryx/serverx/404.go b/oryx/serverx/404.go new file mode 100644 index 0000000..5e5a68b --- /dev/null +++ b/oryx/serverx/404.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package serverx + +import ( + _ "embed" + "net/http" + + "github.com/ory/herodot/httputil" +) + +//go:embed 404.html +var page404HTML string + +//go:embed 404.json +var page404JSON string + +// DefaultNotFoundHandler is a default handler for handling 404 errors. +var DefaultNotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var contentType, body string + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "text/plain", + "application/json", + }, "text/html") { + case "text/plain": + contentType = "text/plain" + body = "Error 404 - The requested route does not exist. Make sure you are using the right path, domain, and port." + case "application/json": + contentType = "application/json" + body = page404JSON + default: + fallthrough + case "text/html": + contentType = "text/html" + body = page404HTML + } + + w.Header().Set("Content-Type", contentType+"; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(body)) +}) diff --git a/oryx/serverx/404.html b/oryx/serverx/404.html new file mode 100644 index 0000000..d2031d1 --- /dev/null +++ b/oryx/serverx/404.html @@ -0,0 +1,56 @@ + + + + + 404 - Route not found + + + +
+
+

Error 404

+

+ The requested route does not exist. Make sure you are using the right + path, domain, and port. +

+
+
+ + diff --git a/oryx/serverx/404.json b/oryx/serverx/404.json new file mode 100644 index 0000000..5f46c1c --- /dev/null +++ b/oryx/serverx/404.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 404, + "message": "Not Found", + "reason": "The requested route does not exist. Make sure you are using the right path, domain, and port." + } +} diff --git a/oryx/servicelocatorx/options.go b/oryx/servicelocatorx/options.go new file mode 100644 index 0000000..9609bbc --- /dev/null +++ b/oryx/servicelocatorx/options.go @@ -0,0 +1,83 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package servicelocatorx + +import ( + "github.com/urfave/negroni" + "google.golang.org/grpc" + + "github.com/ory/x/contextx" + "github.com/ory/x/logrusx" +) + +type ( + Options struct { + logger *logrusx.Logger + contextualizer contextx.Contextualizer + httpMiddlewares []negroni.Handler + grpcUnaryInterceptors []grpc.UnaryServerInterceptor + grpcStreamInterceptors []grpc.StreamServerInterceptor + } + Option func(o *Options) +) + +func WithLogger(l *logrusx.Logger) Option { + return func(o *Options) { + o.logger = l + } +} + +func WithContextualizer(ctxer contextx.Contextualizer) Option { + return func(o *Options) { + o.contextualizer = ctxer + } +} + +func WithHTTPMiddlewares(m ...negroni.Handler) Option { + return func(o *Options) { + o.httpMiddlewares = m + } +} + +func WithGRPCUnaryInterceptors(i ...grpc.UnaryServerInterceptor) Option { + return func(o *Options) { + o.grpcUnaryInterceptors = i + } +} + +func WithGRPCStreamInterceptors(i ...grpc.StreamServerInterceptor) Option { + return func(o *Options) { + o.grpcStreamInterceptors = i + } +} + +func (o *Options) Logger() *logrusx.Logger { + return o.logger +} + +func (o *Options) Contextualizer() contextx.Contextualizer { + return o.contextualizer +} + +func (o *Options) HTTPMiddlewares() []negroni.Handler { + return o.httpMiddlewares +} + +func (o *Options) GRPCUnaryInterceptors() []grpc.UnaryServerInterceptor { + return o.grpcUnaryInterceptors +} + +func (o *Options) GRPCStreamInterceptors() []grpc.StreamServerInterceptor { + return o.grpcStreamInterceptors +} + +func NewOptions(options ...Option) *Options { + o := &Options{ + contextualizer: &contextx.Default{}, + } + for _, opt := range options { + opt(o) + } + return o +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json new file mode 100644 index 0000000..4bc224c --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json @@ -0,0 +1,27 @@ +{ + "foo": { + "other": "fdsa" + }, + "nested": { + "nested": { + "arr": [ + { + }, + { + } + ] + } + }, + "arr": [ + { + }, + { + "arr": [ + { + }, + { + } + ] + } + ] +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json new file mode 100644 index 0000000..9926439 --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json @@ -0,0 +1,34 @@ +{ + "created_at": "1234", + "updated_at": "1234", + "nested": { + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json new file mode 100644 index 0000000..5e9b1c9 --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json @@ -0,0 +1,28 @@ +{ + "updated_at": "1234", + "nested": { + "nested": { + "arr": [ + { + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + }, + { + "updated_at": 1234, + "arr": [ + { + }, + { + "updated_at": 1234 + } + ] + } + ] +} diff --git a/oryx/snapshotx/fixtures/1.json b/oryx/snapshotx/fixtures/1.json new file mode 100644 index 0000000..a0d0535 --- /dev/null +++ b/oryx/snapshotx/fixtures/1.json @@ -0,0 +1,47 @@ +{ + "ignore_nested": [ + "updated_at", + "created_at" + ], + "ignore_exact": [ + "foo.id" + ], + "content": { + "foo": { + "id": "asdf", + "other": "fdsa" + }, + "created_at": "1234", + "updated_at": "1234", + "nested":{ + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/fixtures/2.json b/oryx/snapshotx/fixtures/2.json new file mode 100644 index 0000000..dbe84c0 --- /dev/null +++ b/oryx/snapshotx/fixtures/2.json @@ -0,0 +1,38 @@ +{ + "ignore_nested": [ + ], + "content": { + "created_at": "1234", + "updated_at": "1234", + "nested":{ + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/fixtures/3.json b/oryx/snapshotx/fixtures/3.json new file mode 100644 index 0000000..bc58d3f --- /dev/null +++ b/oryx/snapshotx/fixtures/3.json @@ -0,0 +1,39 @@ +{ + "ignore_nested": [ + "created_at" + ], + "content": { + "created_at": "1234", + "updated_at": "1234", + "nested": { + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/snapshot.go b/oryx/snapshotx/snapshot.go new file mode 100644 index 0000000..e8b0b6d --- /dev/null +++ b/oryx/snapshotx/snapshot.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package snapshotx + +import ( + "bytes" + "encoding/json" + "fmt" + "slices" + "strings" + "testing" + + "github.com/bradleyjkemp/cupaloy/v2" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type ( + Opt = func(*options) + options struct { + modifiers []func(t *testing.T, raw []byte) []byte + name string + } +) + +func ExceptPaths(keys ...string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + for _, key := range keys { + var err error + raw, err = sjson.DeleteBytes(raw, key) + require.NoError(t, err) + } + return raw + }) + } +} + +func ExceptNestedKeys(nestedKeys ...string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + parsed := gjson.ParseBytes(raw) + require.True(t, parsed.IsObject() || parsed.IsArray()) + return deleteMatches(t, "", parsed, nestedKeys, []string{}, raw) + }) + } +} + +func WithReplacement(str, replace string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + return bytes.ReplaceAll(raw, []byte(str), []byte(replace)) + }) + } +} + +func WithName(name string) Opt { + return func(o *options) { + o.name = name + } +} + +func newOptions(opts ...Opt) *options { + o := &options{} + for _, opt := range opts { + opt(o) + } + return o +} + +func (o *options) applyModifiers(t *testing.T, compare []byte) []byte { + for _, modifier := range o.modifiers { + compare = modifier(t, compare) + } + return compare +} + +var snapshot = cupaloy.New(cupaloy.SnapshotFileExtension(".json")) + +func SnapshotTJSON[C ~string | ~[]byte](t *testing.T, compare C, opts ...Opt) { + SnapshotT(t, json.RawMessage(compare), opts...) +} + +func SnapshotT(t *testing.T, actual any, opts ...Opt) { + t.Helper() + compare, err := json.MarshalIndent(actual, "", " ") + require.NoErrorf(t, err, "%+v", actual) + + o := newOptions(opts...) + compare = o.applyModifiers(t, compare) + + if o.name == "" { + snapshot.SnapshotT(t, compare) + } else { + name := strings.ReplaceAll(t.Name()+"_"+o.name, "/", "-") + require.NoError(t, snapshot.SnapshotWithName(name, compare)) + } +} + +// SnapshotTExcept is deprecated in favor of SnapshotT with Opt. +// +// DEPRECATED: please use SnapshotT instead +func SnapshotTExcept(t *testing.T, actual interface{}, except []string) { + t.Helper() + compare, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err, "%+v", actual) + for _, e := range except { + compare, err = sjson.DeleteBytes(compare, e) + require.NoError(t, err, "%s", e) + } + + snapshot.SnapshotT(t, compare) +} + +func deleteMatches(t *testing.T, key string, result gjson.Result, matches []string, parents []string, content []byte) []byte { + path := parents + if key != "" { + path = append(parents, key) + } + + if result.IsObject() { + result.ForEach(func(key, value gjson.Result) bool { + content = deleteMatches(t, key.String(), value, matches, path, content) + return true + }) + } else if result.IsArray() { + var i int + result.ForEach(func(_, value gjson.Result) bool { + content = deleteMatches(t, fmt.Sprintf("%d", i), value, matches, path, content) + i++ + return true + }) + } + + if slices.Contains(matches, key) { + content, err := sjson.DeleteBytes(content, strings.Join(path, ".")) + require.NoError(t, err) + return content + } + + return content +} diff --git a/oryx/sqlcon/connector.go b/oryx/sqlcon/connector.go new file mode 100644 index 0000000..a21539c --- /dev/null +++ b/oryx/sqlcon/connector.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package sqlcon provides helpers for dealing with SQL connectivity. +package sqlcon + +import ( + "runtime" + "strings" +) + +// GetDriverName returns the driver name of a given DSN. +func GetDriverName(dsn string) string { + return strings.Split(dsn, "://")[0] +} +func maxParallelism() int { + maxProcs := runtime.GOMAXPROCS(0) + numCPU := runtime.NumCPU() + if maxProcs < numCPU { + return maxProcs + } + return numCPU +} diff --git a/oryx/sqlcon/dockertest/cockroach.go b/oryx/sqlcon/dockertest/cockroach.go new file mode 100644 index 0000000..c2d1820 --- /dev/null +++ b/oryx/sqlcon/dockertest/cockroach.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dockertest + +import ( + "testing" + + "github.com/cockroachdb/cockroach-go/v2/testserver" + "github.com/stretchr/testify/require" +) + +func NewLocalTestCRDBServer(t testing.TB) string { + ts, err := testserver.NewTestServer(testserver.CustomVersionOpt("25.3.3")) + require.NoError(t, err) + t.Cleanup(ts.Stop) + + require.NoError(t, ts.WaitForInit()) + + ts.PGURL().Scheme = "cockroach" + return ts.PGURL().String() +} diff --git a/oryx/sqlcon/dockertest/test_helper.go b/oryx/sqlcon/dockertest/test_helper.go new file mode 100644 index 0000000..4b0c237 --- /dev/null +++ b/oryx/sqlcon/dockertest/test_helper.go @@ -0,0 +1,195 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dockertest + +import ( + "cmp" + "fmt" + "io" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/moby/moby/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + + "github.com/ory/dockertest/v4" + "github.com/ory/x/stringsx" +) + +func ConnectPop(t require.TestingT, url string) (c *pop.Connection) { + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + c, err = pop.NewConnection(&pop.ConnectionDetails{ + URL: url, + }) + require.NoError(t, err) + require.NoError(t, c.Open()) + require.NoError(t, c.RawQuery("select version()").Exec()) + }, 5*time.Minute, time.Second*5, "could not connect to database at %s", url) + return +} + +// ## PostgreSQL ## + +func startPostgreSQL(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "postgres", + dockertest.WithTag(cmp.Or(version, "16")), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{"PGUSER=postgres", "POSTGRES_PASSWORD=secret", "POSTGRES_DB=postgres"}), + ) +} + +// RunTestPostgreSQL runs a PostgreSQL database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestPostgreSQL(t testing.TB) string { + if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_POSTGRESQL is set to: %s", dsn) + return dsn + } + r := startPostgreSQL(t, "") + return fmt.Sprintf("postgres://postgres:secret@127.0.0.1:%s/postgres?sslmode=disable", r.GetPort("5432/tcp")) +} + +// RunTestPostgreSQLWithVersion connects to a PostgreSQL database. +func RunTestPostgreSQLWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" { + return dsn + } + r := startPostgreSQL(t, version) + return fmt.Sprintf("postgres://postgres:secret@127.0.0.1:%s/postgres?sslmode=disable", r.GetPort("5432/tcp")) +} + +// ## MySQL ## + +func startMySQL(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "mysql", + dockertest.WithTag(cmp.Or(version, "8.0")), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{ + "MYSQL_ROOT_PASSWORD=secret", + "MYSQL_ROOT_HOST=%", + }), + ) +} + +// RunTestMySQL runs a MySQL database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestMySQL(t testing.TB) string { + if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_MYSQL is set to: %s", dsn) + return dsn + } + r := startMySQL(t, "") + return fmt.Sprintf("mysql://root:secret@tcp(localhost:%s)/mysql?parseTime=true&multiStatements=true", r.GetPort("3306/tcp")) +} + +// RunTestMySQLWithVersion runs a MySQL database in the specified version and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestMySQLWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_MYSQL is set to: %s", dsn) + return dsn + } + r := startMySQL(t, version) + return fmt.Sprintf("mysql://root:secret@tcp(localhost:%s)/mysql?parseTime=true&multiStatements=true", r.GetPort("3306/tcp")) +} + +// ## CockroachDB + +func startCockroachDB(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "cockroachdb/cockroach", + dockertest.WithTag(cmp.Or(version, "latest-v25.4")), + dockertest.WithoutReuse(), + dockertest.WithCmd([]string{"start-single-node", "--insecure"}), + ) +} + +// RunTestCockroachDB runs a CockroachDB database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestCockroachDB(t testing.TB) string { + return RunTestCockroachDBWithVersion(t, "") +} + +// RunTestCockroachDBWithVersion runs a CockroachDB database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestCockroachDBWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_COCKROACHDB"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_COCKROACHDB is set to: %s", dsn) + return dsn + } + r := startCockroachDB(t, version) + return fmt.Sprintf("cockroach://root@localhost:%s/defaultdb?sslmode=disable", r.GetPort("26257/tcp")) +} + +func DumpSchema(t testing.TB, c *pop.Connection) string { + name, database, port := c.Dialect.Name(), c.Dialect.Details().Database, c.Dialect.Details().Port + t.Logf("Dumping schema for dialect %s, database %s on port %s", name, database, port) + + var cmd []string + var appendToDump string + switch dialects := stringsx.SwitchExact(name); { + case dialects.AddCase("sqlite3"): + return dumpSQLiteSchema(t, c) + case dialects.AddCase("postgres"): + cmd = []string{"pg_dump", "--username", "postgres", "--schema-only", "--dbname", database} + // we need to set the search path because the postgres dump always unsets it + appendToDump = "SET search_path TO public;\n" + case dialects.AddCase("mysql"): + cmd = []string{"mysqldump", "--user", "root", "--password=secret", "--no-data", database} + case dialects.AddCase("cockroach"): + cmd = []string{"cockroach", "sql", "--insecure", "--database", database, "--execute", "SHOW CREATE ALL TABLES; SHOW CREATE ALL TYPES;", "--format", "raw"} + default: + t.Log(dialects.ToUnknownCaseErr()) + t.FailNow() + return "" + } + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + list, err := cli.ContainerList(t.Context(), client.ContainerListOptions{ + Filters: client.Filters{}.Add("publish", port), + }) + require.NoError(t, err) + require.Lenf(t, list.Items, 1, "expected exactly one %s container with port %s", name, port) + + process, err := cli.ExecCreate(t.Context(), list.Items[0].ID, client.ExecCreateOptions{ + TTY: true, + AttachStdout: true, + Cmd: cmd, + }) + require.NoError(t, err) + + resp, err := cli.ExecAttach(t.Context(), process.ID, client.ExecAttachOptions{ + TTY: true, + }) + require.NoError(t, err) + dump, err := io.ReadAll(resp.Reader) + require.NoErrorf(t, err, "%s", dump) + + d := string(dump) + appendToDump + d = regexp.MustCompile(`(--|#|\\|mysqldump|SHOW CREATE)[^\n]*\n`).ReplaceAllLiteralString(d, "") // comments and other non-schema lines + d = strings.ReplaceAll(d, "\r\n", "\n") + d = regexp.MustCompile(`\n\n+`).ReplaceAllLiteralString(d, "\n\n") + return d +} + +func dumpSQLiteSchema(t testing.TB, c *pop.Connection) string { + var sqls []string + require.NoError(t, c.RawQuery("SELECT sql FROM sqlite_master WHERE type IN ('table', 'index', 'trigger', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").All(&sqls)) + return strings.Join(sqls, ";\n") + ";\n" +} diff --git a/oryx/sqlcon/error.go b/oryx/sqlcon/error.go new file mode 100644 index 0000000..d7c913e --- /dev/null +++ b/oryx/sqlcon/error.go @@ -0,0 +1,150 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "database/sql" + stderrs "errors" + "net/http" + + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "modernc.org/sqlite" + + "github.com/ory/herodot" +) + +// ErrUniqueViolation is returned when a SQL INSERT / UPDATE command returns a conflict. +func ErrUniqueViolation() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusConflict, + GRPCCodeField: codes.AlreadyExists, + StatusField: http.StatusText(http.StatusConflict), + ErrorField: "Unable to insert or update resource because a resource with that value exists already", + } +} + +// ErrNoRows is returned when a SQL SELECT statement returns no rows. +func ErrNoRows() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusNotFound, + GRPCCodeField: codes.NotFound, + StatusField: http.StatusText(http.StatusNotFound), + ErrorField: "Unable to locate the resource", + } +} + +// ErrConcurrentUpdate is returned when the database is unable to serialize access due to a concurrent update. +func ErrConcurrentUpdate() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusBadRequest, + GRPCCodeField: codes.Aborted, + StatusField: http.StatusText(http.StatusBadRequest), + ErrorField: "Unable to serialize access due to a concurrent update in another session", + } +} + +func ErrNoSuchTable() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusInternalServerError, + GRPCCodeField: codes.Internal, + StatusField: http.StatusText(http.StatusInternalServerError), + ErrorField: "Unable to locate the table", + } +} + +func ErrNoSuchColumn() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusInternalServerError, + GRPCCodeField: codes.Internal, + StatusField: http.StatusText(http.StatusInternalServerError), + ErrorField: "Unable to locate the column", + } +} + +func handlePostgres(err error, sqlState string) error { + switch sqlState { + case "23505": // "unique_violation" + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case "40001", // "serialization_failure" in CRDB + "CR000": // "serialization_failure" + return errors.WithStack(ErrConcurrentUpdate().WithWrap(err)) + case "42P01": // "no such table" + return errors.WithStack(ErrNoSuchTable().WithWrap(err)) + case "42703": // "no such column" + return errors.WithStack(ErrNoSuchColumn().WithWrap(err)) + } + return errors.WithStack(err) +} + +// HandleError returns the right sqlcon.Err* depending on the input error. +func HandleError(err error) error { + if err == nil { + return nil + } + + type stater = interface { + error + SQLState() string + } + + if stderrs.Is(err, sql.ErrNoRows) { + return errors.WithStack(ErrNoRows()) + } + if e, ok := stderrs.AsType[stater](err); ok { + return errors.WithStack(handlePostgres(err, e.SQLState())) + } + if e, ok := stderrs.AsType[*pgconn.PgError](err); ok { + return errors.WithStack(handlePostgres(err, e.Code)) + } + if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok { + switch e.Number { + case 1062: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case 1146: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_no_such_table + return errors.WithStack(ErrNoSuchTable().WithWrap(e)) + case 1054: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_bad_field_error + return errors.WithStack(ErrNoSuchColumn().WithWrap(e)) + case 1213: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_lock_deadlock + return errors.WithStack(ErrConcurrentUpdate().WithWrap(e)) + } + } + if e, ok := stderrs.AsType[*sqlite.Error](err); ok { + return handleSqlite(e, err) + } + + return errors.WithStack(err) +} + +// IsForeignKeyViolation reports whether err is a foreign-key constraint +// violation, e.g. deleting a parent row that a child row still references +// under ON DELETE RESTRICT or NO ACTION. +func IsForeignKeyViolation(err error) bool { + if err == nil { + return false + } + + type stater = interface { + error + SQLState() string + } + + if e, ok := stderrs.AsType[stater](err); ok { + return e.SQLState() == "23503" // "foreign_key_violation" in Postgres / CockroachDB + } + if e, ok := stderrs.AsType[*pgconn.PgError](err); ok { + return e.Code == "23503" + } + if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok { + // 1451: cannot delete or update a parent row (a foreign key constraint fails). + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_row_is_referenced_2 + return e.Number == 1451 + } + if e, ok := stderrs.AsType[*sqlite.Error](err); ok { + return isSqliteForeignKeyViolation(e) + } + return false +} diff --git a/oryx/sqlcon/error_sqlite.go b/oryx/sqlcon/error_sqlite.go new file mode 100644 index 0000000..cdd075f --- /dev/null +++ b/oryx/sqlcon/error_sqlite.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "strings" + + "github.com/pkg/errors" + "modernc.org/sqlite" + sqlite3lib "modernc.org/sqlite/lib" +) + +// isSqliteForeignKeyViolation reports whether e is an SQLite foreign-key +// constraint failure. SQLite's extended result code for this case is not +// reported consistently by the driver, so we match on the stable error +// message instead. +func isSqliteForeignKeyViolation(e *sqlite.Error) bool { + return strings.Contains(e.Error(), "FOREIGN KEY constraint failed") +} + +// handleSqlite handles the error iff (if and only if) it is an sqlite error +func handleSqlite(e *sqlite.Error, err error) error { + // Code() returns the full extended error code. + switch e.Code() { + case sqlite3lib.SQLITE_CONSTRAINT_UNIQUE, + sqlite3lib.SQLITE_CONSTRAINT_PRIMARYKEY: + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case sqlite3lib.SQLITE_ERROR: + if strings.Contains(e.Error(), "no such table") { + return errors.WithStack(ErrNoSuchTable().WithWrap(err)) + } + case sqlite3lib.SQLITE_LOCKED, + sqlite3lib.SQLITE_BUSY, + sqlite3lib.SQLITE_BUSY_RECOVERY, + sqlite3lib.SQLITE_BUSY_SNAPSHOT, + sqlite3lib.SQLITE_BUSY_TIMEOUT: + return errors.WithStack(ErrConcurrentUpdate().WithWrap(err)) + } + + if strings.HasPrefix(e.Error(), "no such column:") || + strings.Contains(e.Error(), "has no column named") { + return errors.WithStack(ErrNoSuchColumn().WithWrap(err)) + } + + return errors.WithStack(err) +} diff --git a/oryx/sqlcon/message.go b/oryx/sqlcon/message.go new file mode 100644 index 0000000..d6b44e8 --- /dev/null +++ b/oryx/sqlcon/message.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +// HelpMessage returns a string explaining how to set up SQL using environment variables. +func HelpMessage() string { + return `- DATABASE_URL: A DSN to a persistent backend. Various backends are supported: + + - Changes are lost on process death (ephemeral storage): + + - Memory: If DATABASE_URL is "memory", data will be written to memory and is lost when you restart this instance. + Example: DATABASE_URL=memory + + - Changes are kept after process death (persistent storage): + + - SQL Databases: Officially, PostgreSQL, MySQL and CockroachDB are supported. This project works best with PostgreSQL. + + - PostgreSQL: If DATABASE_URL is a DSN starting with postgres://, PostgreSQL will be used as storage backend. + Example: DATABASE_URL=postgres://user:password@host:123/database + + Additionally, the following query/DSN parameters are supported: + + * max_conns (number): Sets the maximum number of open connections to the database. Defaults to the number of CPU cores times 2. + * max_idle_conns (number): Sets the maximum number of connections in the idle. Defaults to the number of CPU cores. + * max_conn_lifetime (duratino): Sets the maximum amount of time ("ms", "s", "m", "h") a connection may be reused. + Defaults to 0s (disabled). + * sslmode (string): Whether or not to use SSL (default is require) + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + * fallback_application_name (string): An application_name to fall back to if one isn't provided. + * connect_timeout (number): Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + * sslcert (string): Cert file location. The file must contain PEM encoded data. + * sslkey (string): Key file location. The file must contain PEM encoded data. + * sslrootcert (string): The location of the root certificate file. The file + must contain PEM encoded data. + Example: DATABASE_URL=postgres://user:password@host:123/database?sslmode=verify-full + + - MySQL: If DATABASE_URL is a DSN starting with mysql:// MySQL will be used as storage backend. + Be aware that the ?parseTime=true parameter is mandatory, or timestamps will not work. + Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true + + Additionally, the following query/DSN parameters are supported: + * collation (string): Sets the collation used for client-server interaction on connection. In contrast to charset, + collation does not issue additional queries. If the specified collation is unavailable on the target server, + the connection will fail. + * loc (string): Sets the location for time.Time values. Note that this sets the location for time.Time values + but does not change MySQL's time_zone setting. For that set the time_zone DSN parameter. Please keep in mind, + that param values must be url.QueryEscape'ed. Alternatively you can manually replace the / with %2F. + For example US/Pacific would be loc=US%2FPacific. + * maxAllowedPacket (number): Max packet size allowed in bytes. The default value is 4 MiB and should be + adjusted to match the server settings. maxAllowedPacket=0 can be used to automatically fetch the max_allowed_packet variable from server on every connection. + * readTimeout (duration): I/O read timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + * timeout (duration): Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + * tls (bool / string): tls=true enables TLS / SSL encrypted connection to the server. Use skip-verify if + you want to use a self-signed or invalid certificate (server side). + * writeTimeout (duration): I/O write timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true&writeTimeout=123s + + - CockroachDB: If DATABASE_URL is a DSN starting with cockroach://, CockroachDB will be used as storage backend. + Example: DATABASE_URL=cockroach://user:password@host:123/database + + Additionally, the following query/DSN parameters are supported: + * sslmode (string): Whether or not to use SSL (default is require) + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + * application_name (string): An initial value for the application_name session variable. + * sslcert (string): Cert file location. The file must contain PEM encoded data. + * sslkey (string): Key file location. The file must contain PEM encoded data. + * sslrootcert (string): The location of the root certificate file. The file + must contain PEM encoded data. + Example: DATABASE_URL=cockroach://user:password@host:123/database?sslmode=verify-full` +} diff --git a/oryx/sqlcon/parse_opts.go b/oryx/sqlcon/parse_opts.go new file mode 100644 index 0000000..b8adccd --- /dev/null +++ b/oryx/sqlcon/parse_opts.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/ory/x/logrusx" +) + +// ParseConnectionOptions parses values for max_conns, max_idle_conns, max_conn_lifetime from DSNs. +// It also returns the URI without those query parameters. +func ParseConnectionOptions(l *logrusx.Logger, dsn string) (maxConns int, maxIdleConns int, maxConnLifetime, maxIdleConnTime time.Duration, cleanedDSN string) { + maxConns = maxParallelism() * 2 + maxIdleConns = maxParallelism() + cleanedDSN = dsn + + hostPath, rawQuery, ok := strings.Cut(dsn, "?") + if !ok { + l. + WithField("sql_max_connections", maxConns). + WithField("sql_max_idle_connections", maxIdleConns). + WithField("sql_max_connection_lifetime", maxConnLifetime). + WithField("sql_max_idle_connection_time", maxIdleConnTime). + Debugf("No SQL connection options have been defined, falling back to default connection options.") + return + } + + query, err := url.ParseQuery(rawQuery) + if err != nil { + l. + WithField("sql_max_connections", maxConns). + WithField("sql_max_idle_connections", maxIdleConns). + WithField("sql_max_connection_lifetime", maxConnLifetime). + WithField("sql_max_idle_connection_time", maxIdleConnTime). + WithError(err). + Warnf("Unable to parse SQL DSN query, falling back to default connection options.") + return + } + + if v := query.Get("max_conns"); v != "" { + s, err := strconv.ParseInt(v, 10, 64) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conns" value %v could not be parsed to int, falling back to default value %d`, v, maxConns) + } else { + maxConns = int(s) + } + query.Del("max_conns") + } + + if v := query.Get("max_idle_conns"); v != "" { + s, err := strconv.ParseInt(v, 10, 64) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_idle_conns" value %v could not be parsed to int, falling back to default value %d`, v, maxIdleConns) + } else { + maxIdleConns = int(s) + } + query.Del("max_idle_conns") + } + + if v := query.Get("max_conn_lifetime"); v != "" { + s, err := time.ParseDuration(v) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conn_lifetime" value %v could not be parsed to duration, falling back to default value %d`, v, maxConnLifetime) + } else { + maxConnLifetime = s + } + query.Del("max_conn_lifetime") + } + + if v := query.Get("max_conn_idle_time"); v != "" { + s, err := time.ParseDuration(v) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conn_idle_time" value %v could not be parsed to duration, falling back to default value %d`, v, maxIdleConnTime) + } else { + maxIdleConnTime = s + } + query.Del("max_conn_idle_time") + } + cleanedDSN = fmt.Sprintf("%s?%s", hostPath, query.Encode()) + + return +} + +// FinalizeDSN will return a finalized DSN URI. +func FinalizeDSN(l *logrusx.Logger, dsn string) string { + if !strings.HasPrefix(dsn, "mysql://") { + return dsn + } + + var q url.Values + hostPath, query, ok := strings.Cut(dsn, "?") + + if !ok { + q = make(url.Values) + } else { + var err error + q, err = url.ParseQuery(query) + if err != nil { + l.WithError(err).Warnf("Unable to parse SQL DSN query, could not finalize the DSN URI.") + return dsn + } + } + + q.Set("multiStatements", "true") + q.Set("parseTime", "true") + + // This causes an UPDATE to return the number of matching rows instead of + // the number of rows changed. This ensures compatibility with PostgreSQL and SQLite behavior. + q.Set("clientFoundRows", "true") + + return fmt.Sprintf("%s?%s", hostPath, q.Encode()) +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json new file mode 100644 index 0000000..51b3ae7 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json @@ -0,0 +1,14 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)" +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json new file mode 100644 index 0000000..db458b9 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json @@ -0,0 +1,14 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)" +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json new file mode 100644 index 0000000..c5bdc38 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json @@ -0,0 +1,16 @@ +[ + "0001-01-01T00:00:00Z", + "0001-01-01T00:00:00Z", + "string", + 42, + null, + { + "ID": "00000000-0000-0000-0000-000000000000", + "NID": "00000000-0000-0000-0000-000000000000", + "String": "string", + "Int": 42, + "NullTimePtr": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +] diff --git a/oryx/sqlxx/batch/create.go b/oryx/sqlxx/batch/create.go new file mode 100644 index 0000000..ae01482 --- /dev/null +++ b/oryx/sqlxx/batch/create.go @@ -0,0 +1,298 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "github.com/jmoiron/sqlx/reflectx" + + "github.com/ory/x/dbal" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + + "github.com/ory/x/sqlxx" +) + +type ( + insertQueryArgs struct { + TableName string + ColumnsDecl string + Columns []string + Placeholders string + } + quoter interface { + Quote(key string) string + } + TracerConnection struct { + Tracer *otelx.Tracer + Connection *pop.Connection + } +) + +func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *reflectx.Mapper, quoter quoter, models []*T) insertQueryArgs { + var ( + v T + model = pop.NewModel(v, ctx) + + columns []string + quotedColumns []string + placeholders []string + placeholderRow []string + ) + + for _, col := range model.Columns().Cols { + columns = append(columns, col.Name) + placeholderRow = append(placeholderRow, "?") + } + + // We sort for the sole reason that the test snapshots are deterministic. + sort.Strings(columns) + + for _, col := range columns { + quotedColumns = append(quotedColumns, quoter.Quote(col)) + } + + // We generate a list (for every row one) of VALUE statements here that + // will be substituted by their column values later: + // + // (?, ?, ?, ?), + // (?, ?, ?, ?), + // (?, ?, ?, ?) + for _, m := range models { + m := reflect.ValueOf(m) + + pl := make([]string, len(placeholderRow)) + copy(pl, placeholderRow) + + // PostgreSQL-compatible databases can generate UUIDs using + // "gen_random_uuid()", which ends up in a VALUE statement of: + // + // (gen_random_uuid(), ?, ?, ?), + for k := range placeholderRow { + if columns[k] != "id" { + continue + } + + field := mapper.FieldByName(m, columns[k]) + val, ok := field.Interface().(uuid.UUID) + if !ok { + continue + } + + if val == uuid.Nil && dbal.IsPostgresCompatible(dialect) { + pl[k] = "gen_random_uuid()" + break + } + } + + placeholders = append(placeholders, fmt.Sprintf("(%s)", strings.Join(pl, ", "))) + } + + return insertQueryArgs{ + TableName: 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) { + for _, m := range models { + m := reflect.ValueOf(m) + + now := nowFunc() + // Append model fields to args + for _, c := range columns { + field := mapper.FieldByName(m, c) + + switch c { + case "created_at": + if pop.IsZeroOfUnderlyingType(field.Interface()) { + field.Set(reflect.ValueOf(now)) + } + case "updated_at": + field.Set(reflect.ValueOf(now)) + case "id": + if value, ok := field.Interface().(uuid.UUID); ok && value != uuid.Nil { + break // breaks switch, not for + } else if value, ok := field.Interface().(string); ok && len(value) > 0 { + break // breaks switch, not for + } else if dbal.IsPostgresCompatible(dialect) { + // This is a special case: + // 1. We're using a PostgreSQL-compatible database. + // 2. It's the primary key field ("ID") + // 3. A UUID was not yet set. + // + // If all these conditions meet, the VALUE statement will look as such: + // + // (gen_random_uuid(), ?, ?, ?, ...) + // + // For that reason, we do not add the ID value to the list of arguments, + // because one of the arguments is using a built-in and thus doesn't need a value. + continue // break switch, not for + } + + id, err := uuid.NewV4() + if err != nil { + return nil, err + } + field.Set(reflect.ValueOf(id)) + } + + values = append(values, field.Interface()) + + // Special-handling for *sqlxx.NullTime: mapper.FieldByName sets this to a zero time.Time, + // but we want a nil pointer instead. + if i, ok := field.Interface().(*sqlxx.NullTime); ok { + if time.Time(*i).IsZero() { + field.Set(reflect.Zero(field.Type())) + } + } + } + } + + return values, nil +} + +type createOptions struct { + onConflict string +} + +type option func(*createOptions) + +func OnConflictDoNothing() func(*createOptions) { + return func(o *createOptions) { + o.onConflict = "ON CONFLICT DO NOTHING" + } +} + +// CreateFromSlice is a helper around Create that accepts a slice of models +// instead of a slice of model pointers. +func CreateFromSlice[T any](ctx context.Context, p *TracerConnection, models []T, opts ...option) (err error) { + var ptrs []*T + for k := range models { + ptrs = append(ptrs, &models[k]) + } + return Create(ctx, p, ptrs, opts...) +} + +// 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, opts ...option) (err error) { + ctx, span := p.Tracer.Tracer().Start(ctx, "persistence.sql.batch.Create") + defer otelx.End(span, &err) + + if len(models) == 0 { + return nil + } + + options := &createOptions{} + for _, opt := range opts { + opt(options) + } + + var v T + 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) + } + + 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) }) + if err != nil { + return err + } + + var returningClause string + if conn.Dialect.Name() != dbal.DriverMySQL { + // PostgreSQL, CockroachDB, YugabyteDB, and SQLite support RETURNING. + returningClause = fmt.Sprintf("RETURNING %s", model.IDField()) + } + + query := conn.Dialect.TranslateSQL(fmt.Sprintf( + "INSERT INTO %s (%s) VALUES\n%s\n%s\n%s", + queryArgs.TableName, + queryArgs.ColumnsDecl, + queryArgs.Placeholders, + options.onConflict, + returningClause, + )) + + rows, err := conn.TX.QueryContext(ctx, query, values...) + if err != nil { + return sqlcon.HandleError(err) + } + defer rows.Close() + + // Hydrate the models from the RETURNING clause. + // + // Databases not supporting RETURNING will just return 0 rows. + count := 0 + for rows.Next() { + if err := setModelID(rows, pop.NewModel(models[count], ctx)); err != nil { + return err + } + count++ + } + + if err := rows.Err(); err != nil { + return sqlcon.HandleError(err) + } + + return sqlcon.HandleError(err) +} + +// 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") + } + + pkt, err := model.PrimaryKeyType() + if err != nil { + return errors.WithStack(err) + } + + 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)) + } + } + + return nil +} diff --git a/oryx/sqlxx/expand.go b/oryx/sqlxx/expand.go new file mode 100644 index 0000000..8f9020d --- /dev/null +++ b/oryx/sqlxx/expand.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +// Expandable controls what fields to expand for projects. +type Expandable string + +// Expandables is a list of Expandable values. +type Expandables []Expandable + +// String returns a string representation of the Expandable. +func (e Expandable) String() string { + return string(e) +} + +// ToEager returns the fields used by pop's Eager command. +func (e Expandables) ToEager() []string { + var s []string + for _, e := range e { + s = append(s, e.String()) + } + return s +} + +// Has returns true if the Expandable is in the list. +func (e Expandables) Has(search Expandable) bool { + for _, e := range e { + if e == search { + return true + } + } + return false +} diff --git a/oryx/sqlxx/sqlxx.go b/oryx/sqlxx/sqlxx.go new file mode 100644 index 0000000..e472095 --- /dev/null +++ b/oryx/sqlxx/sqlxx.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +import ( + "fmt" + "net/url" + "reflect" + "slices" + "strings" + + "github.com/pkg/errors" +) + +func keys(t any, exclude []string) []string { + tt := reflect.TypeOf(t) + if tt.Kind() == reflect.Pointer { + tt = tt.Elem() + } + ks := make([]string, 0, tt.NumField()) + for i := range tt.NumField() { + f := tt.Field(i) + key, _, _ := strings.Cut(f.Tag.Get("db"), ",") + if key != "" && key != "-" && !slices.Contains(exclude, key) { + ks = append(ks, key) + } + } + return ks +} + +// NamedInsertArguments returns columns and arguments for SQL INSERT statements based on a struct's tags. Does +// not work with nested structs or maps! +// +// type st struct { +// Foo string `db:"foo"` +// Bar string `db:"bar,omitempty"` +// Baz string `db:"-"` +// Zab string +// } +// columns, arguments := NamedInsertArguments(new(st)) +// query := fmt.Sprintf("INSERT INTO foo (%s) VALUES (%s)", columns, arguments) +// // INSERT INTO foo (foo, bar) VALUES (:foo, :bar) +func NamedInsertArguments(t any, exclude ...string) (columns string, arguments string) { + keys := keys(t, exclude) + return strings.Join(keys, ", "), + ":" + strings.Join(keys, ", :") +} + +// NamedUpdateArguments returns columns and arguments for SQL UPDATE statements based on a struct's tags. Does +// not work with nested structs or maps! +// +// type st struct { +// Foo string `db:"foo"` +// Bar string `db:"bar,omitempty"` +// Baz string `db:"-"` +// Zab string +// } +// query := fmt.Sprintf("UPDATE foo SET %s", NamedUpdateArguments(new(st))) +// // UPDATE foo SET foo=:foo, bar=:bar +func NamedUpdateArguments(t any, exclude ...string) string { + keys := keys(t, exclude) + statements := make([]string, len(keys)) + + for k, key := range keys { + statements[k] = fmt.Sprintf("%s=:%s", key, key) + } + + return strings.Join(statements, ", ") +} + +func OnConflictDoNothing(dialect string, columnNoop string) string { + if dialect == "mysql" { + return fmt.Sprintf(" ON DUPLICATE KEY UPDATE `%s` = `%s` ", columnNoop, columnNoop) + } else { + return ` ON CONFLICT DO NOTHING ` + } +} + +// ExtractSchemeFromDSN returns the scheme (e.g. `mysql`, `postgres`, etc) component in a DSN string, +// as well as the remaining part of the DSN after the scheme separator. +// It is an error to not have a scheme present. +// This makes sense in the context of a DSN to be able to identify which database is in use. +func ExtractSchemeFromDSN(dsn string) (string, string, error) { + scheme, afterSchemeSeparator, schemeSeparatorFound := strings.Cut(dsn, "://") + if !schemeSeparatorFound { + return "", "", errors.New("invalid DSN: missing scheme separator") + } + if scheme == "" { + return "", "", errors.New("invalid DSN: empty scheme") + } + + return scheme, afterSchemeSeparator, nil +} + +// ExtractDbNameFromDSN returns the database name component in a DSN string. +func ExtractDbNameFromDSN(dsn string) (string, error) { + _, afterScheme, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", err + } + + _, afterSlash, slashFound := strings.Cut(afterScheme, "/") + if !slashFound { + return "", nil + } + + dbName, _, _ := strings.Cut(afterSlash, "?") + return dbName, nil +} + +// ReplaceSchemeInDSN replaces the scheme (e.g. `mysql`, `postgres`, etc) in a DSN string with another one. +// This is necessary for example when using `cockroach` as a scheme, but using the postgres driver to connect to the database, +// and this driver only accepts `postgres` as a scheme. +func ReplaceSchemeInDSN(dsn string, newScheme string) (string, error) { + _, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", errors.WithStack(err) + } + + return newScheme + "://" + afterSchemeSeparator, nil +} + +// DSNRedacted parses a database DSN and returns a redacted form as a string. +// It replaces any password with "xxxxx" just like `url.Redacted()`. +// Only the password is redacted, not the username. +// This function is necessary because MySQL uses a DSN format not compatible with `url.Parse`. +// Additionally and as a consequence of the point above, the scheme is expected to be present and non-empty. +// This function is less strict that `url.Parse` in the case of MySQL. +// It also does not escape any characters in the username, whereas `url.String()`/`url.Redacted` does. +func DSNRedacted(dsn string) (string, error) { + scheme, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", errors.WithStack(err) + } + + // If this is not MySQL, we simply delegate the work to `url.Parse`. + if scheme != "mysql" { + u, err := url.Parse(dsn) + if err != nil { + return "", errors.WithStack(err) + } + return u.Redacted(), nil + } + + // MySQL has a weird DSN syntax not conforming to a standard URL, of the form: + // `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + // We only need to parse up to `@` in order to redact the password. The rest is left as-is. + + usernamePassword, afterUsernamePassword, usernamePasswordSeparatorFound := strings.Cut(afterSchemeSeparator, "@") + if !usernamePasswordSeparatorFound { + afterUsernamePassword = afterSchemeSeparator + } + + username, password, hasPassword := strings.Cut(usernamePassword, ":") + // We only insert a redacted password in the final result if a password was provided in the input. + // This behavior matches the one of `url.Redacted()`. + if hasPassword { + password = ":xxxxx" + } + + res := scheme + "://" + if usernamePasswordSeparatorFound { + res += username + password + "@" + } + res += afterUsernamePassword + return res, nil +} + +func StripQueryParamsFromDSN(dsn string) string { + before, _, _ := strings.Cut(dsn, "?") + return before +} diff --git a/oryx/sqlxx/types.go b/oryx/sqlxx/types.go new file mode 100644 index 0000000..5f6f4eb --- /dev/null +++ b/oryx/sqlxx/types.go @@ -0,0 +1,632 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "slices" + "strings" + "time" + + "github.com/tidwall/gjson" + + "github.com/pkg/errors" +) + +// Duration represents a JSON and SQL compatible time.Duration. +// swagger:type string +type Duration time.Duration + +// MarshalJSON returns m as the JSON encoding of m. +func (ns Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(ns).String()) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := time.ParseDuration(s) + if err != nil { + return err + } + + *ns = Duration(p) + return nil +} + +// StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. +// swagger:type array +type StringSliceJSONFormat []string + +// Scan implements the Scanner interface. +func (m *StringSliceJSONFormat) Scan(value interface{}) error { + var val string + switch v := value.(type) { + case nil: + *m = StringSliceJSONFormat{} + return nil + case string: + val = v + case []byte: + val = string(v) + default: + return errors.Errorf("cannot scan %#v into StringSliceJSONFormat", value) + } + if len(val) == 0 { + val = "[]" + } + + if parsed := gjson.Parse(val); parsed.Type == gjson.Null { + val = "[]" + } else if !parsed.IsArray() { + return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + return errors.WithStack(json.Unmarshal([]byte(val), &m)) +} + +// Value implements the driver Valuer interface. +func (m StringSliceJSONFormat) Value() (driver.Value, error) { + if len(m) == 0 { + return "[]", nil + } + + encoded, err := json.Marshal(&m) + return string(encoded), errors.WithStack(err) +} + +// StringSlicePipeDelimiter de/encodes the string slice to/from a SQL string. +type StringSlicePipeDelimiter []string + +// Scan implements the Scanner interface. +func (n *StringSlicePipeDelimiter) Scan(value interface{}) error { + var s sql.NullString + if err := s.Scan(value); err != nil { + return err + } + *n = scanStringSlice('|', s.String) + return nil +} + +// Value implements the driver Valuer interface. +func (n StringSlicePipeDelimiter) Value() (driver.Value, error) { + return valueStringSlice('|', n), nil +} + +func scanStringSlice(delimiter rune, value interface{}) []string { + escaped := false + s := fmt.Sprintf("%s", value) + splitted := strings.FieldsFunc(s, func(r rune) bool { + if r == '\\' { + escaped = !escaped + } else if escaped && r != delimiter { + escaped = false + } + return !escaped && r == delimiter + }) + for k, v := range splitted { + splitted[k] = strings.ReplaceAll(v, "\\"+string(delimiter), string(delimiter)) + } + return splitted +} + +func valueStringSlice(delimiter rune, value []string) string { + replace := make([]string, len(value)) + for k, v := range value { + replace[k] = strings.ReplaceAll(v, string(delimiter), "\\"+string(delimiter)) + } + return strings.Join(replace, string(delimiter)) +} + +// NullBool represents a bool that may be null. +// NullBool implements the Scanner interface so +// swagger:type bool +// swagger:model nullBool +type NullBool struct { + Bool bool + Valid bool // Valid is true if Bool is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullBool) Scan(value interface{}) error { + d := sql.NullBool{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Bool = d.Bool + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullBool) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Bool, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullBool) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + return json.Marshal(ns.Bool) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullBool) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Bool)) +} + +// FalsyNullBool represents a bool that may be null. +// It JSON decodes to false if null. +// +// swagger:type bool +// swagger:model falsyNullBool +type FalsyNullBool struct { + Bool bool + Valid bool // Valid is true if Bool is not NULL +} + +// Scan implements the Scanner interface. +func (ns *FalsyNullBool) Scan(value interface{}) error { + d := sql.NullBool{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Bool = d.Bool + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns FalsyNullBool) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Bool, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns FalsyNullBool) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("false"), nil + } + return json.Marshal(ns.Bool) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *FalsyNullBool) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Bool)) +} + +// swagger:type string +// swagger:model nullString +type NullString string + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullString) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ns)) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullString) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal(data, (*string)(ns))) +} + +// Scan implements the Scanner interface. +func (ns *NullString) Scan(value interface{}) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *ns = NullString(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullString) Value() (driver.Value, error) { + if len(ns) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(ns)}.Value() +} + +// String implements the Stringer interface. +func (ns NullString) String() string { + return string(ns) +} + +// NullTime implements sql.NullTime functionality. +// +// swagger:model nullTime +// required: false +type NullTime time.Time + +// Scan implements the Scanner interface. +func (ns *NullTime) Scan(value interface{}) error { + var v sql.NullTime + if err := (&v).Scan(value); err != nil { + return err + } + *ns = NullTime(v.Time.UTC()) + return nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullTime) MarshalJSON() ([]byte, error) { + var t *time.Time + if !time.Time(ns).IsZero() { + tt := time.Time(ns) + t = &tt + } + return json.Marshal(t) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullTime) UnmarshalJSON(data []byte) error { + var t time.Time + if err := json.Unmarshal(data, &t); err != nil { + return err + } + *ns = NullTime(t) + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullTime) Value() (driver.Value, error) { + return sql.NullTime{Valid: !time.Time(ns).IsZero(), Time: time.Time(ns).UTC()}.Value() +} + +// DeepCopy implements github.com/mohae/deepcopy.Interface. +// +// NullTime wraps time.Time, whose fields are unexported. The default +// reflection-based deep copy in mohae/deepcopy skips unexported fields, which +// would zero out the timestamp. Returning a pointer to a copy preserves the +// value. The method uses a pointer receiver so that mohae/deepcopy returns +// the matching *NullTime type when it finds one as a struct field. +func (ns *NullTime) DeepCopy() any { + if ns == nil { + return (*NullTime)(nil) + } + cpy := *ns + return &cpy +} + +// MapStringInterface represents a map[string]interface that works well with JSON, SQL, and Swagger. +type MapStringInterface map[string]interface{} + +// Scan implements the Scanner interface. +func (n *MapStringInterface) Scan(value interface{}) error { + v := fmt.Sprintf("%s", value) + if len(v) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal([]byte(v), n)) +} + +// Value implements the driver Valuer interface. +func (n MapStringInterface) Value() (driver.Value, error) { + value, err := json.Marshal(n) + if err != nil { + return nil, errors.WithStack(err) + } + return string(value), nil +} + +// JSONArrayRawMessage represents a json.RawMessage which only accepts arrays that works well with JSON, SQL, and Swagger. +type JSONArrayRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *JSONArrayRawMessage) Scan(value interface{}) error { + val := fmt.Sprintf("%s", value) + if len(val) == 0 { + val = "[]" + } + + if parsed := gjson.Parse(val); parsed.Type == gjson.Null { + val = "[]" + } else if !parsed.IsArray() { + return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + *m = []byte(val) + return nil +} + +// Value implements the driver Valuer interface. +func (m JSONArrayRawMessage) Value() (driver.Value, error) { + if len(m) == 0 { + return "[]", nil + } + + if parsed := gjson.ParseBytes(m); parsed.Type == gjson.Null { + return "[]", nil + } else if !parsed.IsArray() { + return nil, errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + return string(m), nil +} + +// JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. +type JSONRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *JSONRawMessage) Scan(value interface{}) error { + switch v := value.(type) { + case []byte: + *m = slices.Clone(v) + case string: + *m = JSONRawMessage(v) + case nil: + *m = JSONRawMessage("null") + default: + return errors.Errorf("cannot scan %T into JSONRawMessage", value) + } + return nil +} + +// Value implements the driver Valuer interface. +func (m JSONRawMessage) Value() (driver.Value, error) { + if len(m) == 0 { + return "null", nil + } + return string(m), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m JSONRawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *JSONRawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} + +// NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- +// +// swagger:model nullJsonRawMessage +type NullJSONRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *NullJSONRawMessage) Scan(value any) error { + return (*JSONRawMessage)(m).Scan(value) +} + +// Value implements the driver Valuer interface. +func (m NullJSONRawMessage) Value() (driver.Value, error) { + if len(m) == 0 || string(m) == "null" { + return nil, nil + } + return string(m), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m NullJSONRawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *NullJSONRawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} + +// NullJSONObject represents a nullable JSON object (always key-value pairs). +// Use this for fields that are always objects (e.g., HTTP headers). +// For arbitrary JSON that can be strings, arrays, or objects, use NullJSONRawMessage. +// +// swagger:type object +type NullJSONObject json.RawMessage + +// Scan implements the Scanner interface. +func (m *NullJSONObject) Scan(value any) error { + return (*NullJSONRawMessage)(m).Scan(value) +} + +// Value implements the driver Valuer interface. +func (m NullJSONObject) Value() (driver.Value, error) { + return NullJSONRawMessage(m).Value() +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m NullJSONObject) MarshalJSON() ([]byte, error) { + return NullJSONRawMessage(m).MarshalJSON() +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *NullJSONObject) UnmarshalJSON(data []byte) error { + return (*NullJSONRawMessage)(m).UnmarshalJSON(data) +} + +// JSONScan is a generic helper for retrieving a SQL JSON-encoded value. +func JSONScan(dst, value any) error { + // Note: raw is a string (not []byte) because the MySQL driver reuses byte slices across scans. + // Using strings avoids the need to manually copy the byte slice. + var raw string + switch v := value.(type) { + case nil: + raw = "null" + case string: + raw = v + case []byte: + raw = string(v) + default: + return fmt.Errorf("unable to scan type %T as JSON into %T", value, dst) + } + if err := json.Unmarshal([]byte(raw), dst); err != nil { + return fmt.Errorf("unable to decode JSON payload into %T: %w", dst, err) + } + return nil +} + +// NullInt64 represents an int64 that may be null. +// swagger:type int64 +// swagger:model nullInt64 +type NullInt64 struct { + Int int64 + Valid bool // Valid is true if Duration is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullInt64) Scan(value interface{}) error { + d := sql.NullInt64{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Int = d.Int64 + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullInt64) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Int, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullInt64) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + return json.Marshal(ns.Int) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullInt64) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Int)) +} + +// NullDuration represents a nullable JSON and SQL compatible time.Duration. +// +// swagger:type string +// swagger:model nullDuration +type NullDuration struct { + Duration time.Duration + Valid bool +} + +// Scan implements the Scanner interface. +func (ns *NullDuration) Scan(value interface{}) error { + d := sql.NullInt64{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Duration = time.Duration(d.Int64) + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullDuration) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return int64(ns.Duration), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullDuration) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + + return json.Marshal(ns.Duration.String()) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullDuration) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + + if len(data) == 0 || string(data) == "null" { + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := time.ParseDuration(s) + if err != nil { + return err + } + + ns.Duration = p + ns.Valid = true + return nil +} + +func (ns Duration) IsZero() bool { return ns == 0 } +func (m StringSliceJSONFormat) IsZero() bool { return len(m) == 0 } +func (n StringSlicePipeDelimiter) IsZero() bool { return len(n) == 0 } +func (ns NullBool) IsZero() bool { return !ns.Valid || !ns.Bool } +func (ns FalsyNullBool) IsZero() bool { return !ns.Valid || !ns.Bool } +func (ns NullString) IsZero() bool { return len(ns) == 0 } +func (ns NullTime) IsZero() bool { return time.Time(ns).IsZero() } +func (n MapStringInterface) IsZero() bool { return len(n) == 0 } +func (m JSONArrayRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "[]" } +func (m JSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" } +func (m NullJSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" } +func (ns NullInt64) IsZero() bool { return !ns.Valid || ns.Int == 0 } +func (ns NullDuration) IsZero() bool { return !ns.Valid || ns.Duration == 0 } diff --git a/oryx/stringslice/unique.go b/oryx/stringslice/unique.go new file mode 100644 index 0000000..7a649d4 --- /dev/null +++ b/oryx/stringslice/unique.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringslice + +// Unique returns the given string slice with unique values, preserving order. +// Consider using slices.Compact with slices.Sort instead when you don't care about order. +func Unique(i []string) []string { + u := make([]string, 0, len(i)) + m := make(map[string]struct{}, len(i)) + + for _, val := range i { + if _, ok := m[val]; !ok { + m[val] = struct{}{} + u = append(u, val) + } + } + + return u +} diff --git a/oryx/stringsx/case.go b/oryx/stringsx/case.go new file mode 100644 index 0000000..45048b3 --- /dev/null +++ b/oryx/stringsx/case.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "unicode" + +// ToLowerInitial converts a string's first character to lower case. +func ToLowerInitial(s string) string { + if s == "" { + return "" + } + a := []rune(s) + a[0] = unicode.ToLower(a[0]) + return string(a) +} + +// ToUpperInitial converts a string's first character to upper case. +func ToUpperInitial(s string) string { + if s == "" { + return "" + } + a := []rune(s) + a[0] = unicode.ToUpper(a[0]) + return string(a) +} diff --git a/oryx/stringsx/split.go b/oryx/stringsx/split.go new file mode 100644 index 0000000..132d20b --- /dev/null +++ b/oryx/stringsx/split.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "strings" + +// Splitx is a special case of strings.Split +// which returns an empty slice if the string is empty +func Splitx(s, sep string) []string { + if s == "" { + return []string{} + } + + return strings.Split(s, sep) +} diff --git a/oryx/stringsx/switch_case.go b/oryx/stringsx/switch_case.go new file mode 100644 index 0000000..dc5cb7f --- /dev/null +++ b/oryx/stringsx/switch_case.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import ( + "fmt" + "slices" + "strings" +) + +type ( + RegisteredCases struct { + cases []string + actual string + } + errUnknownCase struct { + *RegisteredCases + } + RegisteredPrefixes struct { + prefixes []string + actual string + } + errUnknownPrefix struct { + *RegisteredPrefixes + } +) + +var ( + ErrUnknownCase = errUnknownCase{} + ErrUnknownPrefix = errUnknownPrefix{} +) + +func SwitchExact(actual string) *RegisteredCases { + return &RegisteredCases{ + actual: actual, + } +} + +func SwitchPrefix(actual string) *RegisteredPrefixes { + return &RegisteredPrefixes{ + actual: actual, + } +} + +func (r *RegisteredCases) AddCase(cases ...string) bool { + r.cases = append(r.cases, cases...) + return slices.Contains(cases, r.actual) +} + +func (r *RegisteredPrefixes) HasPrefix(prefixes ...string) bool { + r.prefixes = append(r.prefixes, prefixes...) + return slices.ContainsFunc(prefixes, func(s string) bool { + return strings.HasPrefix(r.actual, s) + }) +} + +func (r *RegisteredCases) String() string { + return "[" + strings.Join(r.cases, ", ") + "]" +} + +func (r *RegisteredPrefixes) String() string { + return "[" + strings.Join(r.prefixes, ", ") + "]" +} + +func (r *RegisteredCases) ToUnknownCaseErr() error { + return errUnknownCase{r} +} + +func (r *RegisteredPrefixes) ToUnknownPrefixErr() error { + return errUnknownPrefix{r} +} + +func (e errUnknownCase) Error() string { + return fmt.Sprintf("expected one of %s but got %s", e.String(), e.actual) +} + +func (e errUnknownCase) Is(err error) bool { + _, ok := err.(errUnknownCase) + return ok +} + +func (e errUnknownPrefix) Error() string { + return fmt.Sprintf("expected %s to have one of the prefixes %s", e.actual, e.String()) +} + +func (e errUnknownPrefix) Is(err error) bool { + _, ok := err.(errUnknownPrefix) + return ok +} diff --git a/oryx/stringsx/truncate.go b/oryx/stringsx/truncate.go new file mode 100644 index 0000000..86c1021 --- /dev/null +++ b/oryx/stringsx/truncate.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "unicode/utf8" + +// TruncateByteLen returns string truncated at the end with the length specified +func TruncateByteLen(s string, length int) string { + if length <= 0 || len(s) <= length { + return s + } + + res := s[:length] + + // in case we cut in the middle of an utf8 rune, we have to remove the last byte as well until it fits + for !utf8.ValidString(res) { + res = res[:len(res)-1] + } + return res +} diff --git a/oryx/swaggerx/error.go b/oryx/swaggerx/error.go new file mode 100644 index 0000000..e825145 --- /dev/null +++ b/oryx/swaggerx/error.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package swaggerx + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/go-openapi/runtime" +) + +func FormatSwaggerError(err error) string { + var e *runtime.APIError + if errors.As(err, &e) { + body, err := json.MarshalIndent(e, "\t", " ") + if err != nil { + body = []byte(fmt.Sprintf("%+v", e.Response)) + } + + switch e.Code { + case http.StatusForbidden: + return fmt.Sprintf("The service responded with status code 403 indicating that you lack permission to access the resource. The full error details are:\n\n\t%s\n\n", body) + case http.StatusUnauthorized: + return fmt.Sprintf("The service responded with status code 401 indicating that you forgot to include credentials (e.g. token, TLS certificate, ...) in the HTTP request. The full error details are:\n\n\t%s\n\n", body) + case http.StatusNotFound: + return fmt.Sprintf("The service responded with status code 404 indicating that the resource does not exist. Check that the URL is correct (are you using the correct admin/public/... endpoint?) and that the resource exists. The full error details are:\n\n\t%s\n\n", body) + default: + return fmt.Sprintf("Unable to complete operation %s because the server responded with status code %d:\n\n\t%s\n", e.OperationName, e.Code, body) + } + } + return fmt.Sprintf("%+v", err) +} diff --git a/oryx/testingx/helpers.go b/oryx/testingx/helpers.go new file mode 100644 index 0000000..a778efd --- /dev/null +++ b/oryx/testingx/helpers.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package testingx contains helper functions and extensions used when writing tests in Ory. +package testingx + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/stretchr/testify/require" +) + +// RepoRootPath returns the absolute path of the closest parent directory that has a go.mod file relative to the caller. +func RepoRootPath(t require.TestingT) (repoRoot string) { + _, fpath, _, _ := runtime.Caller(1) + for dir := filepath.Dir(filepath.FromSlash(fpath)); dir != filepath.Dir(dir); dir = filepath.Dir(dir) { + modPath := filepath.Join(dir, "go.mod") + if _, err := os.Stat(modPath); err == nil { + repoRoot = dir + break + } + } + require.NotEmptyf(t, repoRoot, "could not determine repo root using path: %q", fpath) + return repoRoot +} diff --git a/oryx/tlsx/cert.go b/oryx/tlsx/cert.go new file mode 100644 index 0000000..aa0a28f --- /dev/null +++ b/oryx/tlsx/cert.go @@ -0,0 +1,401 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tlsx + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "math/big" + "os" + "path/filepath" + "slices" + "sync/atomic" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/x/watcherx" +) + +// ErrNoCertificatesConfigured is returned when no TLS configuration was found. +var ErrNoCertificatesConfigured = errors.New("no tls configuration was found") + +// ErrInvalidCertificateConfiguration is returned when an invalid TLS configuration was found. +var ErrInvalidCertificateConfiguration = errors.New("tls configuration is invalid") + +// HTTPSCertificate returns loads a HTTP over TLS Certificate by looking at environment variables. +func HTTPSCertificate() ([]tls.Certificate, error) { + prefix := "HTTPS_TLS" + return Certificate( + os.Getenv(prefix+"_CERT"), os.Getenv(prefix+"_KEY"), + os.Getenv(prefix+"_CERT_PATH"), os.Getenv(prefix+"_KEY_PATH"), + ) +} + +// HTTPSCertificateHelpMessage returns a help message for configuring HTTP over TLS Certificates. +func HTTPSCertificateHelpMessage() string { + return CertificateHelpMessage("HTTPS_TLS") +} + +// CertificateHelpMessage returns a help message for configuring TLS Certificates. +func CertificateHelpMessage(prefix string) string { + return `- ` + prefix + `_CERT_PATH: The path to the TLS certificate (pem encoded). + Example: ` + prefix + `_CERT_PATH=~/cert.pem + +- ` + prefix + `_KEY_PATH: The path to the TLS private key (pem encoded). + Example: ` + prefix + `_KEY_PATH=~/key.pem + +- ` + prefix + `_CERT: Base64 encoded (without padding) string of the TLS certificate (PEM encoded) to be used for HTTP over TLS (HTTPS). + Example: ` + prefix + `_CERT="-----BEGIN CERTIFICATE-----\nMIIDZTCCAk2gAwIBAgIEV5xOtDANBgkqhkiG9w0BAQ0FADA0MTIwMAYDVQQDDClP..." + +- ` + prefix + `_KEY: Base64 encoded (without padding) string of the private key (PEM encoded) to be used for HTTP over TLS (HTTPS). + Example: ` + prefix + `_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDg..." +` +} + +// CertificateFromBase64 loads a TLS certificate from a base64-encoded string of +// the PEM representations of the cert and key. +func CertificateFromBase64(certBase64, keyBase64 string) (tls.Certificate, error) { + certPEM, err := base64.StdEncoding.DecodeString(certBase64) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to base64 decode the TLS certificate: %v", err) + } + keyPEM, err := base64.StdEncoding.DecodeString(keyBase64) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to base64 decode the TLS private key: %v", err) + } + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to load X509 key pair: %v", err) + } + return cert, nil +} + +// [deprecated] Certificate returns a TLS Certificate by looking at its +// arguments. If both certPEMBase64 and keyPEMBase64 are not empty and contain +// base64-encoded PEM representations of a cert and key, respectively, that key +// pair is returned. Otherwise, if certPath and keyPath point to PEM files, the +// key pair is loaded from those. Returns ErrNoCertificatesConfigured if all +// arguments are empty, and ErrInvalidCertificateConfiguration if the arguments +// are inconsistent. +// +// This function is deprecated. Use CertificateFromBase64 or GetCertificate +// instead. +func Certificate( + certPEMBase64, keyPEMBase64 string, + certPath, keyPath string, +) ([]tls.Certificate, error) { + if certPEMBase64 == "" && keyPEMBase64 == "" && certPath == "" && keyPath == "" { + return nil, errors.WithStack(ErrNoCertificatesConfigured) + } + + if certPEMBase64 != "" && keyPEMBase64 != "" { + cert, err := CertificateFromBase64(certPEMBase64, keyPEMBase64) + if err != nil { + return nil, errors.WithStack(err) + } + return []tls.Certificate{cert}, nil + } + + if certPath != "" && keyPath != "" { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("unable to load X509 key pair from files: %v", err) + } + return []tls.Certificate{cert}, nil + } + + return nil, errors.WithStack(ErrInvalidCertificateConfiguration) +} + +type CertFunc = func(*tls.ClientHelloInfo) (*tls.Certificate, error) + +// GetCertificate returns a function for use with +// "net/tls".Config.GetCertificate. +// +// The certificate and private key are read from the specified filesystem paths. +// The certificate file is watched for changes, upon which the cert+key are +// reloaded in the background. Errors during reloading are deduplicated and +// reported through the errs channel if it is not nil. When the provided context +// is canceled, background reloading stops and the errs channel is closed. +// +// The returned function always yields the latest successfully loaded +// certificate; ClientHelloInfo is unused. +func GetCertificate( + ctx context.Context, + certPath, keyPath string, + errs chan<- error, +) (CertFunc, error) { + if certPath == "" || keyPath == "" { + return nil, errors.WithStack(ErrNoCertificatesConfigured) + } + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, errors.WithStack(fmt.Errorf("unable to load X509 key pair from files: %v", err)) + } + var store atomic.Value + store.Store(&cert) + + events := make(chan watcherx.Event) + // The cert could change without the key changing, but not the other way around. + // Hence, we only watch the cert. + _, err = watcherx.WatchFile(ctx, certPath, events) + if err != nil { + return nil, errors.WithStack(err) + } + go func() { + if errs != nil { + defer close(errs) + } + var lastReportedError string + for { + select { + case <-ctx.Done(): + return + + case event := <-events: + var err error + switch event := event.(type) { + case *watcherx.ChangeEvent: + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(certPath, keyPath) + if err == nil { + store.Store(&cert) + lastReportedError = "" + continue + } + err = fmt.Errorf("unable to load X509 key pair from files: %v", err) + + case *watcherx.ErrorEvent: + err = fmt.Errorf("file watch: %v", event) + default: + continue + } + + if err.Error() == lastReportedError { // same message as before: don't spam the error channel + continue + } + // fresh error + select { + case errs <- errors.WithStack(err): + lastReportedError = err.Error() + case <-time.After(500 * time.Millisecond): + } + } + } + }() + + return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + if cert, ok := store.Load().(*tls.Certificate); ok { + return cert, nil + } + return nil, errors.WithStack(ErrNoCertificatesConfigured) + }, nil +} + +// PublicKey returns the public key for a given private key, or nil. +func PublicKey(key crypto.PrivateKey) interface{ Equal(x crypto.PublicKey) bool } { + switch k := key.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} + +// CreateSelfSignedTLSCertificate creates a self-signed TLS certificate. +func CreateSelfSignedTLSCertificate(key interface{}, opts ...CertificateOpts) (*tls.Certificate, error) { + c, err := CreateSelfSignedCertificate(key, opts...) + if err != nil { + return nil, err + } + + block, err := PEMBlockForKey(key) + if err != nil { + return nil, err + } + + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) + pemKey := pem.EncodeToMemory(block) + cert, err := tls.X509KeyPair(pemCert, pemKey) + if err != nil { + return nil, err + } + + return &cert, nil +} + +// CreateSelfSignedCertificate creates a self-signed x509 certificate. +func CreateSelfSignedCertificate(key interface{}, opts ...CertificateOpts) (cert *x509.Certificate, err error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return cert, errors.Errorf("failed to generate serial number: %s", err) + } + + certificate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"ORY GmbH"}, + CommonName: "ORY", + }, + Issuer: pkix.Name{ + Organization: []string{"ORY GmbH"}, + CommonName: "ORY", + }, + NotBefore: time.Now().UTC(), + NotAfter: time.Now().UTC().Add(time.Hour * 24 * 31), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + DNSNames: []string{"localhost"}, + } + for _, opt := range opts { + opt(certificate) + } + + der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, PublicKey(key), key) + if err != nil { + return cert, errors.Errorf("failed to create certificate: %s", err) + } + + cert, err = x509.ParseCertificate(der) + if err != nil { + return cert, errors.Errorf("failed to encode private key: %s", err) + } + return cert, nil +} + +// PEMBlockForKey returns a PEM-encoded block for key. +func PEMBlockForKey(key interface{}) (*pem.Block, error) { + b, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, errors.WithStack(err) + } + return &pem.Block{Type: "PRIVATE KEY", Bytes: b}, nil +} + +// NewClientCert creates a new client TLS certificate signed by the given CA. +func NewClientCert(CAcert *x509.Certificate, CAkey crypto.PrivateKey, opts ...CertificateOpts) (*tls.Certificate, error) { + if !slices.Contains(CAcert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) { + return nil, errors.Errorf("the CA certificate does not have the client authentication extended key usage (OID 1.3.6.1.5.5.7.3.2) set") + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, errors.Errorf("failed to generate serial number: %s", err) + } + + key, err := rsa.GenerateKey(rand.Reader, 3072) + if err != nil { + return nil, errors.Errorf("failed to generate private key: %s", err) + } + + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Ory GmbH"}, + CommonName: "ORY", + }, + Issuer: CAcert.Subject, + NotBefore: time.Now().UTC(), + NotAfter: CAcert.NotAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: false, + } + for _, opt := range opts { + opt(template) + } + + der, err := x509.CreateCertificate(rand.Reader, template, CAcert, PublicKey(key), CAkey) + if err != nil { + return nil, errors.Errorf("failed to create certificate: %s", err) + } + + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + pemBlock, err := PEMBlockForKey(key) + if err != nil { + return nil, err + } + pemKey := pem.EncodeToMemory(pemBlock) + + cert, err := tls.X509KeyPair(pemCert, pemKey) + if err != nil { + return nil, errors.WithStack(err) + } + return &cert, nil +} + +type CertificateOpts func(*x509.Certificate) + +// CreateSelfSignedCertificateForTest writes a new, self-signed TLS +// certificate+key (in PEM format) to a temporary location on disk and returns +// the paths to both, and the respective contents in base64 encoding. The +// files are automatically cleaned up when the given *testing.T concludes its +// tests. +func CreateSelfSignedCertificateForTest(t testing.TB) (certPath, keyPath, certBase64, keyBase64 string) { + tmpDir := t.TempDir() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + cert, err := CreateSelfSignedCertificate(privateKey) + require.NoError(t, err) + + // write cert + certFile, err := os.Create(filepath.Join(tmpDir, "cert.pem")) + require.NoError(t, err) + certPath = certFile.Name() + + var buf bytes.Buffer + enc := base64.NewEncoder(base64.StdEncoding, &buf) + require.NoErrorf(t, pem.Encode( + io.MultiWriter(enc, certFile), + &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}, + ), "Failed to write data to %q", certPath) + require.NoError(t, enc.Close()) + require.NoErrorf(t, certFile.Close(), "Error closing %q", certPath) + certBase64 = buf.String() + + // write key + keyFile, err := os.Create(filepath.Join(tmpDir, "key.pem")) + require.NoError(t, err) + keyPath = keyFile.Name() + buf.Reset() + enc = base64.NewEncoder(base64.StdEncoding, &buf) + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + require.NoErrorf(t, pem.Encode( + io.MultiWriter(enc, keyFile), + &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}, + ), "Failed to write data to %q", keyPath) + require.NoError(t, enc.Close()) + require.NoErrorf(t, keyFile.Close(), "Error closing %q", keyPath) + keyBase64 = buf.String() + + return +} diff --git a/oryx/tlsx/termination.go b/oryx/tlsx/termination.go new file mode 100644 index 0000000..7e684a4 --- /dev/null +++ b/oryx/tlsx/termination.go @@ -0,0 +1,95 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tlsx + +import ( + "net" + "net/http" + "strings" + + "github.com/pkg/errors" + "github.com/urfave/negroni" + + "github.com/ory/x/healthx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/prometheusx" +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider +} + +// EnforceTLSRequests creates a middleware that enforces TLS for incoming HTTP requests. +// It allows termination (non-HTTPS traffic) from specific CIDR ranges provided in the `allowTerminationFrom` slice. +// If the request is not secure and does not match the allowed CIDR ranges, an error response is returned. +// The middleware also validates the `X-Forwarded-Proto` header to ensure it is set to "https". +func EnforceTLSRequests(d dependencies, allowTerminationFrom []string) (negroni.Handler, error) { + networks := make([]*net.IPNet, 0, len(allowTerminationFrom)) + for _, rn := range allowTerminationFrom { + _, network, err := net.ParseCIDR(rn) + if err != nil { + return nil, errors.WithStack(err) + } + networks = append(networks, network) + } + + return negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.TLS != nil || + r.URL.Path == healthx.AliveCheckPath || + r.URL.Path == healthx.ReadyCheckPath || + r.URL.Path == prometheusx.MetricsPrometheusPath { + next(rw, r) + return + } + + if len(networks) == 0 { + d.Logger().WithRequest(r).WithError(errors.New("TLS termination is not enabled")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } + + if err := matchesRange(r, networks); err != nil { + d.Logger().WithRequest(r).WithError(err).Warnln("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } + + proto := r.Header.Get("X-Forwarded-Proto") + if proto == "" { + d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } else if proto != "https" { + d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.Errorf("expected X-Forwarded-Proto header to be https but got: %s", proto)) + return + } + + next(rw, r) + }), nil +} + +func matchesRange(r *http.Request, networks []*net.IPNet) error { + remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return errors.WithStack(err) + } + + check := []string{remoteIP} + for fwd := range strings.SplitSeq(r.Header.Get("X-Forwarded-For"), ",") { + check = append(check, strings.TrimSpace(fwd)) + } + + for _, ipNet := range networks { + for _, ip := range check { + addr := net.ParseIP(ip) + if ipNet.Contains(addr) { + return nil + } + } + } + return errors.Errorf("neither remote address nor any x-forwarded-for values match CIDR ranges %+v: %v, ranges, check)", networks, check) +} diff --git a/oryx/urlx/copy.go b/oryx/urlx/copy.go new file mode 100644 index 0000000..2e558cb --- /dev/null +++ b/oryx/urlx/copy.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import "net/url" + +// Copy returns a copy of the input url. +func Copy(src *url.URL) *url.URL { + var out = new(url.URL) + *out = *src + return out +} + +// CopyWithQuery returns a copy of the input url with the given query parameters +func CopyWithQuery(src *url.URL, query url.Values) *url.URL { + out := Copy(src) + q := out.Query() + for k := range query { + q.Set(k, query.Get(k)) + } + out.RawQuery = q.Encode() + return out +} diff --git a/oryx/urlx/extract.go b/oryx/urlx/extract.go new file mode 100644 index 0000000..6081832 --- /dev/null +++ b/oryx/urlx/extract.go @@ -0,0 +1,139 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "context" + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/pkg/errors" +) + +// hostCache caches DNS lookup results for hostnames to avoid repeated lookups. +// The cache is thread-safe and stores true/false whether a hostname resolves to public IPs. +type hostCache struct { + mu sync.RWMutex + cache map[string]bool +} + +// get retrieves a cached value for a hostname. Returns value and whether it was found. +func (hc *hostCache) get(hostname string) (bool, bool) { + hc.mu.RLock() + defer hc.mu.RUnlock() + isPublic, found := hc.cache[hostname] + return isPublic, found +} + +// set stores the lookup result for a hostname. +func (hc *hostCache) set(hostname string, isPublic bool) { + hc.mu.Lock() + defer hc.mu.Unlock() + hc.cache[hostname] = isPublic +} + +// localCache lives for the lifetime of the main process. The cache +// size is not expected to grow more than a few hundred bytes. +var localCache = &hostCache{ + cache: make(map[string]bool), +} + +// ExtractPublicAddress iterates over parameters and extracts the first public +// address found. Parameter values are assumed to be in priority order. Returns +// an empty string if only private addresses are available. +func ExtractPublicAddress(values ...string) string { + for _, value := range values { + if value == "" || value == "*" { + continue + } + host := value + + // parse URL addresses + if u, err := url.Parse(value); err == nil && len(u.Host) > 1 { + host = removeWildcardsFromHostname(u.Host) + } + + // strip port on both URL and non-URL addresses + hostname, _, err := net.SplitHostPort(host) + if err != nil { + hostname = host + } + + // for IP addresses + if ip := net.ParseIP(hostname); ip != nil { + if !isPrivateIP(ip) { + return host + } + continue + } + + // for hostnames, first check cache + if isPublic, found := localCache.get(hostname); found { + if isPublic { + return host + } + continue + } + + // otherwise, perform DNS lookup & cache result + isPublic := isPublicHostname(hostname) + localCache.set(hostname, isPublic) + if isPublic { + return host + } + } + + return "" +} + +// isPrivateIP checks if an IP address is private (RFC 1918/4193). +func isPrivateIP(ip net.IP) bool { + return ip.IsPrivate() || + ip.IsLoopback() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsUnspecified() // 0.0.0.0 or :: +} + +// isPublicHostname performs DNS lookup to determine if hostname resolves to public IPs. +// Returns true if at least one resolved IP is public, false if all are private or lookup fails. +func isPublicHostname(hostname string) bool { + // avoid DNS lookup if localhost + lower := strings.ToLower(hostname) + if lower == "localhost" { + return false + } + + ctx, cancel := context.WithTimeoutCause(context.Background(), 2*time.Second, errors.Errorf("failed to resolve DNS for %s within 2s", hostname)) + defer cancel() + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) + if err != nil { + return false + } + + for _, ip := range ips { + if !isPrivateIP(ip.IP) { + return true + } + } + + return false +} + +// removeWildcardsFromHostname removes wildcard segments from a hostname string +// by splitting on dots and filtering out asterisk-only segments. +func removeWildcardsFromHostname(hostname string) string { + sep := strings.Split(hostname, ".") + clean := make([]string, 0, len(sep)) + for _, s := range sep { + if s != "*" && s != "" { + clean = append(clean, s) + } + } + return strings.Join(clean, ".") +} diff --git a/oryx/urlx/join.go b/oryx/urlx/join.go new file mode 100644 index 0000000..90eeb15 --- /dev/null +++ b/oryx/urlx/join.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "net/url" + "path" + + "github.com/ory/x/cmdx" +) + +// MustJoin joins the paths of two URLs. Fatals if first is not a DSN. +func MustJoin(first string, parts ...string) string { + u, err := url.Parse(first) + if err != nil { + cmdx.Fatalf("Unable to parse %s: %s", first, err) + } + return AppendPaths(u, parts...).String() +} + +// AppendPaths appends the provided paths to the url. +// Paths are intentionally *not* URL encoded. +// The caller is responsible for url encoding, possibly selectively, the required path components with `url.PathEscape`. +func AppendPaths(u *url.URL, paths ...string) (ep *url.URL) { + ep = Copy(u) + if len(paths) == 0 { + return ep + } + + ep.Path = path.Join(append([]string{ep.Path}, paths...)...) + + last := paths[len(paths)-1] + if last != "" && last[len(last)-1] == '/' { + ep.Path = ep.Path + "/" + } + + return ep +} + +// SetQuery appends the provided url values to the DSN's query string. +func SetQuery(u *url.URL, query url.Values) (ep *url.URL) { + ep = Copy(u) + q := ep.Query() + + for k := range query { + q.Set(k, query.Get(k)) + } + + ep.RawQuery = q.Encode() + return ep +} diff --git a/oryx/urlx/parse.go b/oryx/urlx/parse.go new file mode 100644 index 0000000..d6a7706 --- /dev/null +++ b/oryx/urlx/parse.go @@ -0,0 +1,119 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "net/url" + "regexp" + "strings" + + "github.com/ory/x/logrusx" +) + +// winPathRegex is a regex for [DRIVE-LETTER]: +var winPathRegex = regexp.MustCompile("^[A-Za-z]:.*") + +// Parse parses rawURL into a URL structure with special handling for file:// URLs +// +// File URLs with relative paths (file://../file, ../file) will be returned as a +// url.URL object without the Scheme set to "file". This is because the file +// scheme does not support relative paths. Make sure to check for +// both "file" or "" (an empty string) in URL.Scheme if you are looking for +// a file path. +// +// Use the companion function GetURLFilePath() to get a file path suitable +// for the current operating system. +func Parse(rawURL string) (*url.URL, error) { + lcRawURL := strings.ToLower(rawURL) + if strings.HasPrefix(lcRawURL, "file:///") { + return url.Parse(rawURL) + } + + // Normally the first part after file:// is a hostname, but since + // this is often misused we interpret the URL like a normal path + // by removing the "file://" from the beginning (if it exists) + rawURL = trimPrefixIC(rawURL, "file://") + + if winPathRegex.MatchString(rawURL) { + // Windows path + return url.Parse("file:///" + rawURL) + } + + if strings.HasPrefix(lcRawURL, "\\\\") { + // Windows UNC path + // We extract the hostname and create an appropriate file:// URL + // based on the hostname and the path + host, path := extractUNCPathParts(rawURL) + // It is safe to replace the \ with / here because this is POSIX style path + return url.Parse("file://" + host + strings.ReplaceAll(path, "\\", "/")) + } + + parsed, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + // Since go1.19: + // + // > The URL type now distinguishes between URLs with no authority and URLs with an empty authority. + // > For example, http:///path has an empty authority (host), while http:/path has none. + // + // See https://golang.org/doc/go1.19#net/url for more details. + parsed.OmitHost = false + return parsed, nil +} + +// ParseOrPanic parses a url or panics. +func ParseOrPanic(in string) *url.URL { + out, err := url.Parse(in) + if err != nil { + panic(err.Error()) + } + return out +} + +// ParseOrFatal parses a url or fatals. +func ParseOrFatal(l *logrusx.Logger, in string) *url.URL { + out, err := url.Parse(in) + if err != nil { + l.WithError(err).Fatalf("Unable to parse url: %s", in) + } + return out +} + +// ParseRequestURIOrPanic parses a request uri or panics. +func ParseRequestURIOrPanic(in string) *url.URL { + out, err := url.ParseRequestURI(in) + if err != nil { + panic(err.Error()) + } + return out +} + +// ParseRequestURIOrFatal parses a request uri or fatals. +func ParseRequestURIOrFatal(l *logrusx.Logger, in string) *url.URL { + out, err := url.ParseRequestURI(in) + if err != nil { + l.WithError(err).Fatalf("Unable to parse url: %s", in) + } + return out +} + +func extractUNCPathParts(uncPath string) (host, path string) { + parts := strings.Split(strings.TrimPrefix(uncPath, "\\\\"), "\\") + host = parts[0] + if len(parts) > 0 { + path = "\\" + strings.Join(parts[1:], "\\") + } + return host, path +} + +// trimPrefixIC returns s without the provided leading prefix string using +// case insensitive matching. +// If s doesn't start with prefix, s is returned unchanged. +func trimPrefixIC(s, prefix string) string { + if strings.HasPrefix(strings.ToLower(s), prefix) { + return s[len(prefix):] + } + return s +} diff --git a/oryx/urlx/path.go b/oryx/urlx/path.go new file mode 100644 index 0000000..8ceaddc --- /dev/null +++ b/oryx/urlx/path.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !windows +// +build !windows + +package urlx + +import ( + "net/url" +) + +// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem +func GetURLFilePath(u *url.URL) string { + if u == nil { + return "" + } + return u.Path +} diff --git a/oryx/urlx/path_windows.go b/oryx/urlx/path_windows.go new file mode 100644 index 0000000..cf100d2 --- /dev/null +++ b/oryx/urlx/path_windows.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows +// +build windows + +package urlx + +import ( + "net/url" + "path/filepath" + "strings" +) + +// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem +func GetURLFilePath(u *url.URL) string { + if u == nil { + return "" + } + if !(u.Scheme == "file" || u.Scheme == "") { + return u.Path + } + + fPath := u.Path + if u.Host != "" { + // Make UNC Path + fPath = "\\\\" + u.Host + filepath.FromSlash(fPath) + return fPath + } + fPathTrimmed := strings.TrimLeft(fPath, "/") + if winPathRegex.MatchString(fPathTrimmed) { + // On Windows we should remove the initial path separator in case this + // is a normal path (for example: "\c:\" -> "c:\"") + fPath = fPathTrimmed + } + return filepath.FromSlash(fPath) +} diff --git a/oryx/uuidx/uuid.go b/oryx/uuidx/uuid.go new file mode 100644 index 0000000..e405746 --- /dev/null +++ b/oryx/uuidx/uuid.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package uuidx + +import "github.com/gofrs/uuid" + +// NewV4 returns a new randomly generated UUID or panics. +func NewV4() uuid.UUID { + return uuid.Must(uuid.NewV4()) +} diff --git a/oryx/watcherx/definitions.go b/oryx/watcherx/definitions.go new file mode 100644 index 0000000..ff94079 --- /dev/null +++ b/oryx/watcherx/definitions.go @@ -0,0 +1,78 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "fmt" + "net/url" + + "github.com/pkg/errors" +) + +type ( + errSchemeUnknown struct { + scheme string + } + EventChannel chan Event + Watcher interface { + // DispatchNow fires the watcher and causes an event. + // + // WARNING: The returned channel must be read or no further events will + // be propagated due to a deadlock. + DispatchNow() (<-chan int, error) + } + dispatcher struct { + ctx context.Context + trigger chan struct{} + done chan int + } +) + +var ( + // ErrSchemeUnknown is just for checking with errors.Is() + ErrSchemeUnknown = &errSchemeUnknown{} + ErrWatcherNotRunning = fmt.Errorf("watcher is not running") +) + +func (e *errSchemeUnknown) Is(other error) bool { + _, ok := other.(*errSchemeUnknown) + return ok +} + +func (e *errSchemeUnknown) Error() string { + return fmt.Sprintf("unknown scheme '%s' to watch", e.scheme) +} + +func newDispatcher(ctx context.Context) *dispatcher { + return &dispatcher{ + ctx: ctx, + trigger: make(chan struct{}), + done: make(chan int), + } +} + +func (d *dispatcher) DispatchNow() (<-chan int, error) { + if d.trigger == nil { + return nil, ErrWatcherNotRunning + } + // The trigger send must respect cancellation. Once the watcher's context is + // done its receiver goroutine has returned, so a bare send would block + // forever and wedge the caller. + select { + case d.trigger <- struct{}{}: + return d.done, nil + case <-d.ctx.Done(): + return nil, errors.WithStack(ErrWatcherNotRunning) + } +} + +func Watch(ctx context.Context, u *url.URL, c EventChannel) (Watcher, error) { + switch u.Scheme { + // see urlx.Parse for why the empty string is also file + case "file", "": + return WatchFile(ctx, u.Path, c) + } + return nil, &errSchemeUnknown{u.Scheme} +} diff --git a/oryx/watcherx/directory.go b/oryx/watcherx/directory.go new file mode 100644 index 0000000..f65d0b5 --- /dev/null +++ b/oryx/watcherx/directory.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" +) + +func WatchDirectory(ctx context.Context, dir string, c EventChannel) (Watcher, error) { + w, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithStack(err) + } + subDirs := make(map[string]struct{}) + if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return errors.WithStack(err) + } + if info.IsDir() { + if err := w.Add(path); err != nil { + return errors.WithStack(err) + } + subDirs[path] = struct{}{} + } + return nil + }); err != nil { + return nil, err + } + + dw := &directoryWatcher{ + dispatcher: newDispatcher(ctx), + c: c, + dir: dir, + subDirs: subDirs, + w: w, + } + go dw.streamDirectoryEvents(ctx) + return dw, nil +} + +type directoryWatcher struct { + *dispatcher + c EventChannel + dir string + // subDirsMtx guards subDirs, which is read by the watcher goroutine and + // potentially by external callers (e.g. tests) concurrently. + subDirsMtx sync.RWMutex + subDirs map[string]struct{} + w *fsnotify.Watcher +} + +func (w *directoryWatcher) handleEvent(ctx context.Context, e fsnotify.Event) { + if e.Has(fsnotify.Remove) { + w.subDirsMtx.Lock() + _, isSubDir := w.subDirs[e.Name] + if isSubDir { + // we do not want any event on deletion of a directory + delete(w.subDirs, e.Name) + } + w.subDirsMtx.Unlock() + if isSubDir { + return + } + w.maybeSend(ctx, &RemoveEvent{ + source: source(e.Name), + }) + return + } else if e.Has(fsnotify.Write | fsnotify.Create) { + if stats, err := os.Stat(e.Name); err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: errors.WithStack(err), + source: source(e.Name), + }) + return + } else if stats.IsDir() { + if err := w.w.Add(e.Name); err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: errors.WithStack(err), + source: source(e.Name), + }) + } + w.subDirsMtx.Lock() + w.subDirs[e.Name] = struct{}{} + w.subDirsMtx.Unlock() + return + } + + //#nosec G304 -- false positive + data, err := os.ReadFile(e.Name) + if err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(e.Name), + }) + } else { + w.maybeSend(ctx, &ChangeEvent{ + data: data, + source: source(e.Name), + }) + } + } +} + +func (w *directoryWatcher) maybeSend(ctx context.Context, e Event) bool { + select { + case <-ctx.Done(): + return false + case w.c <- e: + return true + } +} + +func (w *directoryWatcher) streamDirectoryEvents(ctx context.Context) { + defer func() { + close(w.done) + close(w.c) + _ = w.w.Close() + }() + for { + select { + case <-ctx.Done(): + return + case e := <-w.w.Events: + w.handleEvent(ctx, e) + case <-w.trigger: + var eventsSent int + + if err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + //#nosec G304 -- false positive + data, err := os.ReadFile(path) + if err != nil { + if !w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(path), + }) { + return errors.WithStack(context.Canceled) + } + } else { + if !w.maybeSend(ctx, &ChangeEvent{ + data: data, + source: source(path), + }) { + return errors.WithStack(context.Canceled) + } + } + eventsSent++ + } + return nil + }); err != nil { + if !w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(w.dir), + }) { + return + } + eventsSent++ + } + + w.done <- eventsSent + } + } +} diff --git a/oryx/watcherx/event.go b/oryx/watcherx/event.go new file mode 100644 index 0000000..a4065c0 --- /dev/null +++ b/oryx/watcherx/event.go @@ -0,0 +1,109 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +type ( + Event interface { + // MarshalJSON is required to work multiple times + json.Marshaler + + Reader() io.Reader + Source() string + String() string + setSource(string) + } + source string + ErrorEvent struct { + error + source + } + ChangeEvent struct { + data []byte + source + } + RemoveEvent struct { + source + } + serialEventType string + serialEvent struct { + Type serialEventType `json:"type"` + Data []byte `json:"data"` + Source source `json:"source"` + } +) + +func NewErrorEvent(err error, source_ string) *ErrorEvent { + return &ErrorEvent{ + error: err, + source: source(source_), + } +} + +const ( + serialTypeChange serialEventType = "change" + serialTypeRemove serialEventType = "remove" + serialTypeError serialEventType = "error" +) + +func (e *ErrorEvent) Reader() io.Reader { + return bytes.NewBufferString(e.Error()) +} + +func (e *ErrorEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeError, + Data: []byte(e.Error()), + Source: e.source, + }) +} + +func (e *ErrorEvent) String() string { + return fmt.Sprintf("error: %+v; source: %s", e.error, e.source) +} + +func (e source) Source() string { + return string(e) +} + +func (e *source) setSource(nsrc string) { + *e = source(nsrc) +} + +func (e *ChangeEvent) Reader() io.Reader { + return bytes.NewBuffer(e.data) +} + +func (e *ChangeEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeChange, + Data: e.data, + Source: e.source, + }) +} + +func (e *ChangeEvent) String() string { + return fmt.Sprintf("data: %s; source: %s", e.data, e.source) +} + +func (e *RemoveEvent) Reader() io.Reader { + return nil +} + +func (e *RemoveEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeRemove, + Source: e.source, + }) +} + +func (e *RemoveEvent) String() string { + return fmt.Sprintf("removed source: %s", e.source) +} diff --git a/oryx/watcherx/file.go b/oryx/watcherx/file.go new file mode 100644 index 0000000..ecb4419 --- /dev/null +++ b/oryx/watcherx/file.go @@ -0,0 +1,174 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" +) + +// WatchFile spawns a background goroutine to watch file, reporting any changes +// to c. Watching stops when ctx is canceled. +func WatchFile(ctx context.Context, file string, c EventChannel) (Watcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithStack(err) + } + dir := filepath.Dir(file) + if err := watcher.Add(dir); err != nil { + return nil, errors.WithStack(err) + } + resolvedFile, err := filepath.EvalSymlinks(file) + if err != nil { + if pathError := new(os.PathError); !errors.As(err, &pathError) { + return nil, errors.WithStack(err) + } + // The file does not exist. The watcher should still watch the directory + // to get notified about file creation. + resolvedFile = "" + } else if resolvedFile != file { + // If `resolvedFile` != `file` then `file` is a symlink and we have to explicitly watch the referenced file. + // This is because fsnotify follows symlinks and watches the destination file, not the symlink + // itself. That is at least the case for unix systems. See: https://github.com/fsnotify/fsnotify/issues/199 + if err := watcher.Add(file); err != nil { + return nil, errors.WithStack(err) + } + } + d := newDispatcher(ctx) + go streamFileEvents(ctx, watcher, c, d.trigger, d.done, file, resolvedFile) + return d, nil +} + +// streamFileEvents watches for file changes and supports symlinks which requires several workarounds due to limitations of fsnotify. +// Argument `resolvedFile` is the resolved symlink path of the file, or it is the watchedFile name itself. If `resolvedFile` is empty, then the watchedFile does not exist. +func streamFileEvents(ctx context.Context, watcher *fsnotify.Watcher, c EventChannel, sendNow <-chan struct{}, sendNowDone chan<- int, watchedFile, resolvedFile string) { + eventSource := source(watchedFile) + removeDirectFileWatcher := func() { + _ = watcher.Remove(watchedFile) + } + addDirectFileWatcher := func() { + // check if the watchedFile (symlink) exists + // if it does not the dir watcher will notify us when it gets created + if _, err := os.Lstat(watchedFile); err == nil { + if err := watcher.Add(watchedFile); err != nil { + c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + } + } + } + } + defer watcher.Close() + for { + select { + case <-ctx.Done(): + return + case <-sendNow: + if resolvedFile == "" { + // The file does not exist. Announce this by sending a RemoveEvent. + c <- &RemoveEvent{eventSource} + } else { + // The file does exist. Announce the current content by sending a ChangeEvent. + // #nosec G304 -- false positive + data, err := os.ReadFile(watchedFile) + if err != nil { + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + select { + case c <- &ChangeEvent{ + data: data, + source: eventSource, + }: + case <-ctx.Done(): + return + } + } + + // in any of the above cases we send exactly one event + select { + case sendNowDone <- 1: + case <-ctx.Done(): + return + } + case e, ok := <-watcher.Events: + if !ok { + return + } + // filter events to only watch watchedFile + // e.Name contains the name of the watchedFile (regardless whether it is a symlink), not the resolved file name + if filepath.Clean(e.Name) == watchedFile { + recentlyResolvedFile, err := filepath.EvalSymlinks(watchedFile) + // when there is no error the file exists and any symlinks can be resolved + if err != nil { + // check if the watchedFile (or the file behind the symlink) was removed + if _, ok := err.(*os.PathError); ok { + select { + case c <- &RemoveEvent{eventSource}: + case <-ctx.Done(): + return + } + removeDirectFileWatcher() + continue + } + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + // This catches following three cases: + // 1. the watchedFile was written or created + // 2. the watchedFile is a symlink and has changed (k8s config map updates) + // 3. the watchedFile behind the symlink was written or created + switch { + case recentlyResolvedFile != resolvedFile: + resolvedFile = recentlyResolvedFile + // watch the symlink again to update the actually watched file + removeDirectFileWatcher() + addDirectFileWatcher() + // we fallthrough because we also want to read the file in this case + fallthrough + case e.Has(fsnotify.Write | fsnotify.Create): + // #nosec G304 -- false positive + data, err := os.ReadFile(watchedFile) + if err != nil { + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + select { + case c <- &ChangeEvent{ + data: data, + source: eventSource, + }: + case <-ctx.Done(): + return + } + } + } + } + } +} diff --git a/oryx/watcherx/integrationtest/.dockerignore b/oryx/watcherx/integrationtest/.dockerignore new file mode 100644 index 0000000..515faed --- /dev/null +++ b/oryx/watcherx/integrationtest/.dockerignore @@ -0,0 +1,7 @@ +event_logger.yml +configmap.yml + +Makefile +README.md + +eventlog_snapshot diff --git a/oryx/watcherx/integrationtest/.gitignore b/oryx/watcherx/integrationtest/.gitignore new file mode 100644 index 0000000..2533ea2 --- /dev/null +++ b/oryx/watcherx/integrationtest/.gitignore @@ -0,0 +1 @@ +tmp_snapshot diff --git a/oryx/watcherx/integrationtest/Dockerfile b/oryx/watcherx/integrationtest/Dockerfile new file mode 100644 index 0000000..07b9713 --- /dev/null +++ b/oryx/watcherx/integrationtest/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.26-alpine3.23 AS builder + +RUN apk -U --no-cache add build-base + +WORKDIR /go/src/github.com/ory/x + +ADD go.mod go.mod +ADD go.sum go.sum + +RUN go mod download + +ADD . . + +RUN go build -o /usr/bin/eventlogger ./watcherx/integrationtest + +FROM alpine:3.22 + +COPY --from=builder /usr/bin/eventlogger /usr/bin/eventlogger + +ENTRYPOINT ["eventlogger"] +CMD ["/etc/config/mock-config"] diff --git a/oryx/watcherx/integrationtest/Makefile b/oryx/watcherx/integrationtest/Makefile new file mode 100644 index 0000000..7725e79 --- /dev/null +++ b/oryx/watcherx/integrationtest/Makefile @@ -0,0 +1,65 @@ +SHELL=/bin/bash -euo pipefail + +CLUSTER_NAME=watcherx-integration-test +SNAPSHOT_FILE=eventlog_snapshot + +define generate_snapshot + sleep 5 + make update + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) + make apply + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) + make update + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) +endef + +.PHONY: build +build: + docker build -f Dockerfile -t eventlogger:latest ../.. + +.PHONY: create +create: + kind create cluster --name ${CLUSTER_NAME} --wait 1m || true + +.PHONY: load +load: + kind load docker-image eventlogger:latest --name ${CLUSTER_NAME} + +.PHONY: apply +apply: + kubectl apply -f configmap.yml -f event_logger.yml --context kind-${CLUSTER_NAME} + +.PHONY: delete +delete: + kind delete cluster --name ${CLUSTER_NAME} + +.PHONY: setup +setup: build create load apply + +.PHONY: snapshot +snapshot: setup container-restart + rm ${SNAPSHOT_FILE} + ${call generate_snapshot,$(SNAPSHOT_FILE)} + +.PHONY: check +check: setup container-restart + rm tmp_snapshot || true + ${call generate_snapshot,tmp_snapshot} + diff tmp_snapshot ${SNAPSHOT_FILE} + +.PHONY: logs +logs: + kubectl logs eventlogger --context kind-${CLUSTER_NAME} + +.PHONY: container-restart +container-restart: + kubectl delete -f event_logger.yml --context kind-${CLUSTER_NAME} + kubectl apply -f event_logger.yml --context kind-${CLUSTER_NAME} + +.PHONY: update +update: + cat configmap.yml | sed 's/somevalue/othervalue/' | kubectl apply -f - --context kind-${CLUSTER_NAME} + cat event_logger.yml | sed 's/somevalue/othervalue/' | kubectl apply -f - --context kind-${CLUSTER_NAME} diff --git a/oryx/watcherx/integrationtest/README.md b/oryx/watcherx/integrationtest/README.md new file mode 100644 index 0000000..4dedd3b --- /dev/null +++ b/oryx/watcherx/integrationtest/README.md @@ -0,0 +1,27 @@ +# Integration Test for watcherx/FileWatcher + +As kubernetes has a special way to change mounted config map values we want to +make sure our file watcher is compatible with that. + +## Perquisites + +The versions are the ones that definitely work. + +- kind (v0.8.1) +- kubectl (v1.18.5) +- docker (v19.03.12-ce) +- make (v4.3) + +## Structure + +The `main.go` just logs all events it gets. It is deployed to a kind kubernetes +cluster together with a configmap that gets updated during the test. For details +on the test steps have a look at the `Makefile`. + +## Running + +To generate the log snapshot run `make snapshot`. That snapshot should be +committed. To check if the FileWatcher works run `make check`. For debugging +purposes single steps of the setup have descriptive make target names and can be +run separately. It is safe to delete the cluster at any point or rerun snapshot +generation. diff --git a/oryx/watcherx/integrationtest/configmap.yml b/oryx/watcherx/integrationtest/configmap.yml new file mode 100644 index 0000000..b8c5cee --- /dev/null +++ b/oryx/watcherx/integrationtest/configmap.yml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: changing-config +data: + mock-config: somevalue diff --git a/oryx/watcherx/integrationtest/event_logger.yml b/oryx/watcherx/integrationtest/event_logger.yml new file mode 100644 index 0000000..acd9641 --- /dev/null +++ b/oryx/watcherx/integrationtest/event_logger.yml @@ -0,0 +1,19 @@ +kind: Pod +apiVersion: v1 +metadata: + name: eventlogger + annotations: + variant: somevalue +spec: + containers: + - name: eventlogger + image: eventlogger:latest + imagePullPolicy: Never + volumeMounts: + - name: changing-config + mountPath: /etc/config + restartPolicy: Never + volumes: + - name: changing-config + configMap: + name: changing-config diff --git a/oryx/watcherx/integrationtest/eventlog_snapshot b/oryx/watcherx/integrationtest/eventlog_snapshot new file mode 100644 index 0000000..c9ab6a7 --- /dev/null +++ b/oryx/watcherx/integrationtest/eventlog_snapshot @@ -0,0 +1,21 @@ +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +got change event: +Data: somevalue, +Src: /etc/config/mock-config +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +got change event: +Data: somevalue, +Src: /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config diff --git a/oryx/watcherx/integrationtest/main.go b/oryx/watcherx/integrationtest/main.go new file mode 100644 index 0000000..b66085b --- /dev/null +++ b/oryx/watcherx/integrationtest/main.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/ory/x/watcherx" +) + +func main() { + if len(os.Args) != 2 { + _, _ = fmt.Fprintf(os.Stderr, "expected 1 comand line argument but got %d\n", len(os.Args)-1) + os.Exit(1) + } + c := make(chan watcherx.Event) + _, err := watcherx.WatchFile(context.Background(), os.Args[1], c) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "could not initialize file watcher: %+v\n", err) + os.Exit(1) + } + fmt.Printf("watching file %s\n", os.Args[1]) + for { + switch e := (<-c).(type) { + case *watcherx.ChangeEvent: + var data []byte + data, err = io.ReadAll(e.Reader()) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "could not read data: %+v\n", err) + os.Exit(1) + } + fmt.Printf("got change event:\nData: %s,\nSrc: %s\n", data, e.Source()) + case *watcherx.RemoveEvent: + fmt.Printf("got remove event:\nSrc: %s\n", e.Source()) + case *watcherx.ErrorEvent: + fmt.Printf("got error event:\nError: %s\n", e.Error()) + default: + fmt.Println("got unknown event") + } + } +} diff --git a/oryx/watcherx/test_helpers.go b/oryx/watcherx/test_helpers.go new file mode 100644 index 0000000..8960458 --- /dev/null +++ b/oryx/watcherx/test_helpers.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "os" + "path" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func KubernetesAtomicWrite(t *testing.T, dir, fileName, content string) { + // atomic write according to https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/util/atomic_writer.go + const ( + dataDirName = "..data" + newDataDirName = "..data_tmp" + ) + // (2) + dataDirPath := filepath.Join(dir, dataDirName) + oldTsDir, err := os.Readlink(dataDirPath) + if err != nil { + require.True(t, os.IsNotExist(err), "%+v", err) + // although Readlink() returns "" on err, don't be fragile by relying on it (since it's not specified in docs) + // empty oldTsDir indicates that it didn't exist + oldTsDir = "" + } + oldTsPath := filepath.Join(dir, oldTsDir) + + // (3) we are not interested in the case where a file gets deleted as we just operate on one file + // (4) we assume the file needs an update + + // (5) + tsDir, err := os.MkdirTemp(dir, time.Now().UTC().Format("..2006_01_02_15_04_05.")) + require.NoError(t, err) + tsDirName := filepath.Base(tsDir) + + // (6) + require.NoError( + t, + os.WriteFile(path.Join(tsDir, fileName), []byte(content), 0600), + ) + + // (7) + _, err = os.Readlink(filepath.Join(dir, fileName)) + if err != nil && os.IsNotExist(err) { + // The link into the data directory for this path doesn't exist; create it + require.NoError( + t, + os.Symlink(filepath.Join(dataDirName, fileName), filepath.Join(dir, fileName)), + ) + } + + // (8) + newDataDirPath := filepath.Join(dir, newDataDirName) + require.NoError( + t, + os.Symlink(tsDirName, newDataDirPath), + ) + + // (9) + if runtime.GOOS == "windows" { + require.NoError(t, os.Remove(dataDirPath)) + require.NoError(t, os.Symlink(tsDirName, dataDirPath)) + require.NoError(t, os.Remove(newDataDirPath)) + } else { + require.NoError(t, os.Rename(newDataDirPath, dataDirPath)) + } + + // (10) in our case there is nothing to remove + + // (11) + if len(oldTsDir) > 0 { + require.NoError(t, os.RemoveAll(oldTsPath)) + } +} diff --git a/otp/otp.go b/otp/otp.go new file mode 100644 index 0000000..0d297fd --- /dev/null +++ b/otp/otp.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otp + +import ( + "github.com/pkg/errors" + + "github.com/ory/x/randx" +) + +// Entropy sets the number of characters used for generating verification codes. This must not be +// changed to another value as we only have 32 characters available in the SQL schema. +const Entropy = 32 + +func New() (string, error) { + code, err := randx.RuneSequence(Entropy, randx.AlphaNum) + if err != nil { + return "", errors.WithStack(err) + } + return string(code), nil +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..95995f2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2521 @@ +{ + "name": "kratos-oss", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@openapitools/openapi-generator-cli": "2.31.1", + "yamljs": "0.3.0" + }, + "devDependencies": { + "license-checker": "25.0.1", + "prettier": "3.8.2", + "prettier-plugin-packagejson": "3.0.2", + "process": "0.11.10", + "wait-on": "9.0.1" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@inquirer/core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", + "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.1.6", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.10.7", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "figures": "^3.2.0", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/select": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-1.3.3.tgz", + "integrity": "sha512-RzlRISXWqIKEf83FDC9ZtJ3JvuK1l7aGpretf41BCWYrvla2wU8W8MTRNMiPrPJ+1SIqrRC1nZdZ60hD9hRXLg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^6.0.0", + "@inquirer/type": "^1.1.6", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "figures": "^3.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", + "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.2", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.18.tgz", + "integrity": "sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@nuxtjs/opencollective/node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/@openapitools/openapi-generator-cli": { + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.31.1.tgz", + "integrity": "sha512-dPE+COjNLLTHFQ1lddUvpo+J8YQB1RD3/NVRJ3K+1hPZnyuxCURgOCmr7mXgHEyHmzWH8dKXWm/pD170iVR0vw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/select": "1.3.3", + "@nestjs/axios": "4.0.1", + "@nestjs/common": "11.1.17", + "@nestjs/core": "11.1.18", + "@nuxtjs/opencollective": "0.3.2", + "axios": "^1.14.0", + "chalk": "4.1.2", + "commander": "8.3.0", + "compare-versions": "6.1.1", + "concurrently": "9.2.1", + "console.table": "0.10.0", + "fs-extra": "11.3.4", + "glob": "13.0.6", + "proxy-agent": "6.5.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2", + "tslib": "2.8.1" + }, + "bin": { + "openapi-generator-cli": "main.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openapi_generator" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console.table": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", + "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==", + "license": "MIT", + "dependencies": { + "easy-table": "1.1.0" + }, + "engines": { + "node": "> 0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/easy-table": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", + "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==", + "license": "MIT", + "optionalDependencies": { + "wcwidth": ">=1.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/git-hooks-list": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.2.1.tgz", + "integrity": "sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ip-address": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", + "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-packagejson": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-3.0.2.tgz", + "integrity": "sha512-kmoj3hEynXwoHDo8ZhmWAIjRBoQWCDUVackiWfSDWdgD0rS3LGB61T9zoVbume/cotYdCoadUh4sqViAmXvpBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-package-json": "^3.6.0" + }, + "peerDependencies": { + "prettier": "^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sort-object-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.1.0.tgz", + "integrity": "sha512-SOiEnthkJKPv2L6ec6HMwhUcN0/lppkeYuN1x63PbyPRrgSPIuBJCiYxYyvWRTtjMlOi14vQUCGUJqS6PLVm8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sort-package-json": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.6.1.tgz", + "integrity": "sha512-Chgejw1+10p2D0U2tB7au1lHtz6TkFnxmvZktyBCRyV0GgmF6nl1IxXxAsPtJVsUyg/fo+BfCMAVVFUVRkAHrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.2", + "detect-newline": "^4.0.1", + "git-hooks-list": "^4.1.1", + "is-plain-obj": "^4.1.0", + "semver": "^7.7.3", + "sort-object-keys": "^2.0.1", + "tinyglobby": "^0.2.15" + }, + "bin": { + "sort-package-json": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sort-package-json/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wait-on": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.1.tgz", + "integrity": "sha512-noeCAI+XbqWMXY23sKril0BSURhuLYarkVXwJv1uUWwoojZJE7pmX3vJ7kh7SZaNgPGzfsCSQIZM/AGvu0Q9pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.2", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..44e97f0 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "private": true, + "scripts": { + "openapi-generator-cli": "openapi-generator-cli", + "wait-on": "wait-on" + }, + "prettier": { + "proseWrap": "always", + "semi": false, + "trailingComma": "all" + }, + "dependencies": { + "@openapitools/openapi-generator-cli": "2.31.1", + "yamljs": "0.3.0" + }, + "devDependencies": { + "license-checker": "25.0.1", + "prettier": "3.8.2", + "prettier-plugin-packagejson": "3.0.2", + "process": "0.11.10", + "wait-on": "9.0.1" + }, + "overrides": { + "axios": ">=1.15.2", + "glob": ">=11.1.0", + "shell-quote": "1.9.0" + } +} diff --git a/persistence/reference.go b/persistence/reference.go new file mode 100644 index 0000000..f656062 --- /dev/null +++ b/persistence/reference.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package persistence + +import ( + "context" + "time" + + "github.com/ory/kratos/x/transaction" + + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/x/networkx" + + "github.com/gofrs/uuid" + + "github.com/ory/pop/v6" + + "github.com/ory/x/popx" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/strategy/code" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/session" +) + +type Provider interface { + Persister() Persister + SetPersister(Persister) +} + +type Persister interface { + continuity.Persister + identity.PrivilegedPool + identity.PendingTraitsChangePersister + registration.FlowPersister + login.FlowPersister + settings.FlowPersister + courier.Persister + session.Persister + sessiontokenexchange.Persister + errorx.Persister + verification.FlowPersister + recovery.FlowPersister + link.RecoveryTokenPersister + link.VerificationTokenPersister + code.RecoveryCodePersister + code.VerificationCodePersister + code.RegistrationCodePersister + code.LoginCodePersister + + CleanupDatabase(context.Context, time.Duration, time.Duration, int) error + Close(context.Context) error + Ping(context.Context) error + MigrationStatus(context.Context) (popx.MigrationStatuses, error) + MigrateDown(ctx context.Context, steps int) error + MigrateUp(context.Context) error + MigrationBox() *popx.MigrationBox + GetConnection(context.Context) *pop.Connection + Connection(ctx context.Context) *pop.Connection + transaction.Persister + Networker +} + +type Networker interface { + WithNetworkID(nid uuid.UUID) Persister + NetworkID(ctx context.Context) uuid.UUID + DetermineNetwork(ctx context.Context) (*networkx.Network, error) +} diff --git a/persistence/sql/.soda.yml b/persistence/sql/.soda.yml new file mode 100644 index 0000000..f265223 --- /dev/null +++ b/persistence/sql/.soda.yml @@ -0,0 +1,14 @@ +development: + url: sqlite://a/b + +sqlite: + url: sqlite://a/b + +postgres: + url: postgres://a/b + +mysql: + url: mysql://tcp(a)/b?parseTime=true&multiStatements=true + +cockroach: + url: crdb://a/b diff --git a/persistence/sql/README.md b/persistence/sql/README.md new file mode 100644 index 0000000..ced33e8 --- /dev/null +++ b/persistence/sql/README.md @@ -0,0 +1,45 @@ +# SQL Migrations + +Migrations consist of one `up` and one `down` file. +To create these SQL migrations, copy the last migration in `./persistence/sql/migrations/sql` and change the timestamp to the current timestamp and the name to the desired name. + +If some logic is different for one of the database systems, add the id after the name to the file name. +The content of that file will override the content of the "general" file for that particular DB system. + +Example: + +`20220802103909000000_courier_send_count.up.sql` +and +`20220802103909000000_courier_send_count.down.sql` + +With for example cockroach specific behavior: + +`20220802103909000000_courier_send_count.cockroach.up.sql` +and +`20220802103909000000_courier_send_count.cockroach.down.sql` + +Replace `cockroach` with `mysql`, `postgres` or `sqlite` if applicable. + +## Old Way + +To create SQL migrations, target each database individually and run + +``` +$ dialect=mysql # or postgres|cockroach|sqlite +$ name= +$ ory dev pop migration create -d=$dialect ./persistence/sql/migrations/templates $name +$ soda generate sql -e mysql -c ./persistence/sql/.soda.yml -p ./persistence/sql/migrations/templates [name] +$ soda generate sql -e sqlite -c ./persistence/sql/.soda.yml -p ./persistence/sql/migrations/templates [name] +$ soda generate sql -e postgres -c ./persistence/sql/.soda.yml -p ./persistence/sql/migrations/templates [name] +$ soda generate sql -e cockroach -c ./persistence/sql/.soda.yml -p ./persistence/sql/migrations/templates [name] +``` + +and remove the `sqlite` part from the newly generated file to create a SQL migrations that works with all +aforementioned databases. + +## Rendering Migrations + +Because migrations needs to be backwards compatible, and because fizz migrations might change, we render +fizz migrations to raw SQL statements using `make migrations-render`. + +The concrete migrations being applied can be found in [`./migrations/sql`](./migrations/sql). diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=Identities.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=Identities.json new file mode 100644 index 0000000..49011b1 --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=Identities.json @@ -0,0 +1,20 @@ +{ + "TableName": "\"identities\"", + "ColumnsDecl": "\"available_aal\", \"created_at\", \"external_id\", \"id\", \"metadata_admin\", \"metadata_public\", \"nid\", \"organization_id\", \"schema_id\", \"state\", \"state_changed_at\", \"traits\", \"updated_at\"", + "Columns": [ + "available_aal", + "created_at", + "external_id", + "id", + "metadata_admin", + "metadata_public", + "nid", + "organization_id", + "schema_id", + "state", + "state_changed_at", + "traits", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" +} diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress#01.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress#01.json new file mode 100644 index 0000000..eb3617b --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress#01.json @@ -0,0 +1,15 @@ +{ + "TableName": "\"identity_recovery_addresses\"", + "ColumnsDecl": "\"break_glass_for_organization\", \"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"", + "Columns": [ + "break_glass_for_organization", + "created_at", + "id", + "identity_id", + "nid", + "updated_at", + "value", + "via" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)" +} diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress.json new file mode 100644 index 0000000..eb3617b --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=RecoveryAddress.json @@ -0,0 +1,15 @@ +{ + "TableName": "\"identity_recovery_addresses\"", + "ColumnsDecl": "\"break_glass_for_organization\", \"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"", + "Columns": [ + "break_glass_for_organization", + "created_at", + "id", + "identity_id", + "nid", + "updated_at", + "value", + "via" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)" +} diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json new file mode 100644 index 0000000..4fc722f --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json @@ -0,0 +1,15 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"traits\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "traits", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)" +} diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json new file mode 100644 index 0000000..ef25e16 --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json @@ -0,0 +1,15 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"traits\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "traits", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)" +} diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs_ExtraColumns-case=testModel.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs_ExtraColumns-case=testModel.json new file mode 100644 index 0000000..8e696c6 --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryArgs_ExtraColumns-case=testModel.json @@ -0,0 +1,16 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"traits\", \"updated_at\", \"crdb_region\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "traits", + "updated_at", + "crdb_region" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?, ?)" +} 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 new file mode 100644 index 0000000..04c9db3 --- /dev/null +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json @@ -0,0 +1,10 @@ +[ + "2023-01-01T00:00:00Z", + "2023-01-01T00:00:00Z", + "string", + 42, + null, + { + "foo": "bar" + } +] diff --git a/persistence/sql/batch/create.go b/persistence/sql/batch/create.go new file mode 100644 index 0000000..2e8b987 --- /dev/null +++ b/persistence/sql/batch/create.go @@ -0,0 +1,382 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "slices" + "sort" + "strings" + "time" + + "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/kratos/identity" + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +type ( + insertQueryArgs struct { + TableName string + ColumnsDecl string + Columns []string + Placeholders string + } + quoter interface { + Quote(key string) string + } + TracerConnection struct { + 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 (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) + + columns []string + quotedColumns []string + placeholders []string + placeholderRow []string + ) + + for _, col := range model.Columns().Cols { + columns = append(columns, col.Name) + placeholderRow = append(placeholderRow, "?") + } + + // We sort for the sole reason that the test snapshots are deterministic. + sort.Strings(columns) + + // Extra columns trail the sorted model columns; values are appended in + // the same order in buildInsertQueryValues. + for _, ec := range opts.extraColumns { + columns = append(columns, ec.K) + placeholderRow = append(placeholderRow, "?") + } + + for _, col := range columns { + quotedColumns = append(quotedColumns, opts.quoter.Quote(col)) + } + + // We generate a list (for every row one) of VALUE statements here that + // will be substituted by their column values later: + // + // (?, ?, ?, ?), + // (?, ?, ?, ?), + // (?, ?, ?, ?) + for _, m := range models { + m := reflect.ValueOf(m) + + pl := make([]string, len(placeholderRow)) + copy(pl, placeholderRow) + + // PostgreSQL-compatible databases can generate UUIDs using + // "gen_random_uuid()", which ends up in a VALUE statement of: + // + // (gen_random_uuid(), ?, ?, ?), + for k := range placeholderRow { + if columns[k] != "id" { + continue + } + + field := opts.mapper.FieldByName(m, columns[k]) + val, ok := field.Interface().(uuid.UUID) + if !ok { + continue + } + + if val == uuid.Nil && dbal.IsPostgresCompatible(opts.dialect) && !opts.partialInserts { + pl[k] = "gen_random_uuid()" + break + } + } + placeholders = append(placeholders, fmt.Sprintf("(%s)", strings.Join(pl, ", "))) + } + + return insertQueryArgs{ + TableName: opts.quoter.Quote(model.TableName()), + ColumnsDecl: strings.Join(quotedColumns, ", "), + Columns: columns, + Placeholders: strings.Join(placeholders, ",\n"), + } +} + +func buildInsertQueryValues[T any](columns []string, models []*T, opts *createOpts) (values []any, err error) { + for _, m := range models { + m := reflect.ValueOf(m) + + now := opts.now() + // Model fields only — extra columns aren't on the struct. + modelColumnCount := len(columns) - len(opts.extraColumns) + for _, c := range columns[:modelColumnCount] { + field := opts.mapper.FieldByName(m, c) + + switch c { + case "created_at": + if pop.IsZeroOfUnderlyingType(field.Interface()) { + field.Set(reflect.ValueOf(now)) + } + case "updated_at": + field.Set(reflect.ValueOf(now)) + case "id": + if field.Interface().(uuid.UUID) != uuid.Nil { + break // breaks switch, not for + } else if dbal.IsPostgresCompatible(opts.dialect) && !opts.partialInserts { + // This is a special case: + // 1. We're using a PostgreSQL-compatible database. + // 2. It's the primary key field ("ID") + // 3. A UUID was not yet set. + // + // If all these conditions meet, the VALUE statement will look as such: + // + // (gen_random_uuid(), ?, ?, ?, ...) + // + // For that reason, we do not add the ID value to the list of arguments, + // because one of the arguments is using a built-in and thus doesn't need a value. + continue // break switch, not for + } + + id, err := uuid.NewV4() + if err != nil { + return nil, err + } + field.Set(reflect.ValueOf(id)) + } + + values = append(values, field.Interface()) + + // Special-handling for *sqlxx.NullTime: mapper.FieldByName sets this to a zero time.Time, + // but we want a nil pointer instead. + if i, ok := field.Interface().(*sqlxx.NullTime); ok { + if time.Time(*i).IsZero() { + field.Set(reflect.Zero(field.Type())) + } + } + } + + for _, ec := range opts.extraColumns { + values = append(values, ec.V) + } + } + + return values, nil +} + +type createOpts struct { + partialInserts bool + dialect string + mapper *reflectx.Mapper + quoter quoter + now func() time.Time + extraColumns []identity.ExtraColumn +} + +type CreateOpts func(*createOpts) + +// WithExtraColumns appends fixed (K, V) columns to every row of the batch. +// All rows receive the same values; group rows by extra-column values first. +func WithExtraColumns(cols []identity.ExtraColumn) CreateOpts { + return func(o *createOpts) { + o.extraColumns = append(o.extraColumns, cols...) + } +} + +// 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) + + if len(models) == 0 { + return nil + } + + var v T + model := pop.NewModel(v, ctx) + + conn := p.Connection + options := newCreateOpts(conn, opts...) + + queryArgs := buildInsertQueryArgs(ctx, models, options) + values, err := buildInsertQueryValues(queryArgs.Columns, models, options) + if err != nil { + return err + } + + var returningClause string + if conn.Dialect.Name() != dbal.DriverMySQL { + // PostgreSQL, CockroachDB, YugabyteDB, and SQLite support RETURNING. + 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( + "INSERT INTO %s (%s) VALUES\n%s\n%s", + queryArgs.TableName, + queryArgs.ColumnsDecl, + queryArgs.Placeholders, + returningClause, + )) + + rows, err := conn.TX.QueryContext(ctx, query, values...) + if err != nil { + return sqlcon.HandleError(err) + } + defer func() { _ = 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. + for i := 0; rows.Next(); i++ { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return errors.WithStack(err) + } + if err := setModelID(id, models[i]); err != nil { + return err + } + } + if err := rows.Err(); err != nil { + return sqlcon.HandleError(err) + } + + return nil +} + +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) + } + + 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)) + } + + 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 new file mode 100644 index 0000000..9f41346 --- /dev/null +++ b/persistence/sql/batch/create_test.go @@ -0,0 +1,240 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "fmt" + "testing" + "time" + + "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" +) + +type ( + testModel struct { + ID uuid.UUID `db:"id"` + NID uuid.UUID `db:"nid"` + String string `db:"string"` + Int int `db:"int"` + Traits identity.Traits `db:"traits"` + NullTimePtr *sqlxx.NullTime `db:"null_time_ptr"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + } + testQuoter struct{} +) + +func (i testModel) TableName(ctx context.Context) string { + return "test_models" +} + +func (tq testQuoter) Quote(s string) string { return fmt.Sprintf("%q", s) } + +func makeModels[T any]() []*T { + models := make([]*T, 10) + for k := range models { + models[k] = new(T) + } + return models +} + +func Test_buildInsertQueryArgs(t *testing.T) { + ctx := context.Background() + t.Run("case=testModel", func(t *testing.T) { + models := makeModels[testModel]() + 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) + assert.Equal(t, `INSERT INTO "test_models" ("created_at", "id", "int", "nid", "null_time_ptr", "string", "traits", "updated_at") VALUES +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?), +(?, ?, ?, ?, ?, ?, ?, ?)`, query) + }) + + t.Run("case=Identities", func(t *testing.T) { + models := makeModels[identity.Identity]() + 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]() + 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]() + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db"), + } + args := buildInsertQueryArgs(ctx, models, opts) + snapshotx.SnapshotT(t, args) + }) + + t.Run("case=cockroach", func(t *testing.T) { + models := makeModels[testModel]() + for k := range models { + if k%3 == 0 { + models[k].ID = uuid.FromStringOrNil(fmt.Sprintf("ae0125a9-2786-4ada-82d2-d169cf75047%d", k)) + } + } + opts := &createOpts{ + dialect: dbal.DriverCockroachDB, + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db"), + } + args := buildInsertQueryArgs(ctx, models, opts) + snapshotx.SnapshotT(t, args) + }) + + for _, dialect := range []string{dbal.DriverPostgreSQL, dbal.DriverCockroachDB, dbal.DriverYugabyteDB} { + t.Run("case=database generated UUID/"+dialect, func(t *testing.T) { + models := makeModels[testModel]() + opts := &createOpts{ + dialect: dialect, + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db"), + } + args := buildInsertQueryArgs(ctx, models, opts) + assert.Contains(t, args.Placeholders, "gen_random_uuid()") + }) + } +} + +func Test_buildInsertQueryArgs_ExtraColumns(t *testing.T) { + ctx := context.Background() + t.Run("case=testModel", func(t *testing.T) { + models := makeModels[testModel]() + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db"), + extraColumns: []identity.ExtraColumn{{K: "crdb_region", V: "gcp-europe-west3"}}, + } + args := buildInsertQueryArgs(ctx, models, opts) + snapshotx.SnapshotT(t, args) + + assert.Contains(t, args.ColumnsDecl, `"crdb_region"`) + assert.Contains(t, args.Placeholders, "?") + }) +} + +func Test_buildInsertQueryValues_ExtraColumns(t *testing.T) { + t.Run("case=testModel", func(t *testing.T) { + model := &testModel{ + String: "string", + Int: 42, + Traits: []byte(`{"foo": "bar"}`), + } + + frozenTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + opts := &createOpts{ + dialect: "other", + mapper: reflectx.NewMapper("db"), + quoter: testQuoter{}, + now: func() time.Time { return frozenTime }, + extraColumns: []identity.ExtraColumn{{K: "crdb_region", V: "gcp-europe-west3"}}, + } + + // columns includes the extra column name, mirroring what buildInsertQueryArgs returns. + values, err := buildInsertQueryValues( + []string{"created_at", "updated_at", "id", "string", "int", "null_time_ptr", "traits", "crdb_region"}, + []*testModel{model}, + opts, + ) + require.NoError(t, err) + + // The last value should be the extra column value. + // 7 model fields + 1 extra column = 8 total. + require.Len(t, values, 8) + assert.Equal(t, "gcp-europe-west3", values[len(values)-1]) + }) +} + +func Test_buildInsertQueryValues(t *testing.T) { + t.Run("case=testModel", func(t *testing.T) { + model := &testModel{ + String: "string", + Int: 42, + Traits: []byte(`{"foo": "bar"}`), + } + + 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) { + 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) { + 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.Equal(t, frozenTime, model.CreatedAt) + assert.Equal(t, model.CreatedAt, values[0]) + + assert.Equal(t, frozenTime, model.UpdatedAt) + assert.Equal(t, model.UpdatedAt, values[1]) + + assert.NotZero(t, model.ID) + assert.Equal(t, model.ID, values[2]) + + assert.Equal(t, model.String, values[3]) + assert.Equal(t, model.Int, values[4]) + + assert.Nil(t, model.NullTimePtr) + }) + }) +} diff --git a/persistence/sql/batch/test_persister.go b/persistence/sql/batch/test_persister.go new file mode 100644 index 0000000..f15b467 --- /dev/null +++ b/persistence/sql/batch/test_persister.go @@ -0,0 +1,111 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "errors" + "testing" + + "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/pop/v6" + "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/devices/persister_devices.go b/persistence/sql/devices/persister_devices.go new file mode 100644 index 0000000..3f744b9 --- /dev/null +++ b/persistence/sql/devices/persister_devices.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package devices + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/session" + "github.com/ory/pop/v6" + "github.com/ory/x/contextx" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" +) + +var _ session.DevicePersister = (*DevicePersister)(nil) + +type DevicePersister struct { + ctxer contextx.Provider + c *pop.Connection + nid uuid.UUID +} + +func NewPersister(r contextx.Provider, c *pop.Connection) *DevicePersister { + return &DevicePersister{ + ctxer: r, + c: c, + } +} + +func (p *DevicePersister) NetworkID(ctx context.Context) uuid.UUID { + return p.ctxer.Contextualizer().Network(ctx, p.nid) +} + +func (p DevicePersister) WithNetworkID(nid uuid.UUID) session.DevicePersister { + p.nid = nid + return &p +} + +func (p *DevicePersister) CreateDevice(ctx context.Context, d *session.Device) error { + d.NID = p.NetworkID(ctx) + return sqlcon.HandleError(popx.GetConnection(ctx, p.c.WithContext(ctx)).Create(d)) +} diff --git a/persistence/sql/identity/partial_inserts_internal_test.go b/persistence/sql/identity/partial_inserts_internal_test.go new file mode 100644 index 0000000..d16157e --- /dev/null +++ b/persistence/sql/identity/partial_inserts_internal_test.go @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" + "github.com/ory/x/sqlxx" +) + +func TestIdentitiesNeedPartialInserts(t *testing.T) { + withExternalID := func(id string) *identity.Identity { + return &identity.Identity{ExternalID: sqlxx.NullString(id)} + } + + for _, tc := range []struct { + name string + identities []*identity.Identity + want bool + }{ + { + name: "empty batch", + identities: nil, + want: false, + }, + { + name: "single identity without external id", + identities: []*identity.Identity{{}}, + want: false, + }, + { + name: "single identity with external id", + identities: []*identity.Identity{withExternalID("ext-1")}, + want: false, + }, + { + // The identities insert cannot conflict without an external id (the + // only unique constraint), so partial inserts are unnecessary. + name: "many identities without external ids", + identities: []*identity.Identity{{}, {}, {}}, + want: false, + }, + { + name: "many identities, one with external id", + identities: []*identity.Identity{{}, withExternalID("ext-1"), {}}, + want: true, + }, + { + name: "many identities all with external ids", + identities: []*identity.Identity{withExternalID("ext-1"), withExternalID("ext-2")}, + want: true, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, identitiesNeedPartialInserts(tc.identities)) + }) + } +} diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go new file mode 100644 index 0000000..f8ce9e4 --- /dev/null +++ b/persistence/sql/identity/persister_identity.go @@ -0,0 +1,2188 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "cmp" + "context" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "maps" + "reflect" + "slices" + "sort" + "strings" + "sync/atomic" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/otp" + "github.com/ory/kratos/persistence/sql/batch" + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/pop/v6" + "github.com/ory/x/contextx" + "github.com/ory/x/crdbx" + "github.com/ory/x/errorsx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +var ( + _ identity.Pool = new(IdentityPersister) + _ identity.PrivilegedPool = new(IdentityPersister) +) + +type dependencies interface { + schema.IdentitySchemaProvider + identity.ValidationProvider + logrusx.Provider + config.Provider + contextx.Provider + otelx.Provider +} + +type IdentityPersister struct { + r dependencies + c *pop.Connection + nid uuid.UUID +} + +func NewPersister(r dependencies, c *pop.Connection) *IdentityPersister { + return &IdentityPersister{ + c: c, + r: r, + } +} + +func (p *IdentityPersister) NetworkID(ctx context.Context) uuid.UUID { + return p.r.Contextualizer().Network(ctx, p.nid) +} + +func (p IdentityPersister) WithNetworkID(nid uuid.UUID) identity.PrivilegedPool { + p.nid = nid + return &p +} + +func WithTransaction(ctx context.Context, tx *pop.Connection) context.Context { + return popx.WithTransaction(ctx, tx) +} + +func (p *IdentityPersister) Transaction(ctx context.Context, callback func(ctx context.Context, connection *pop.Connection) error) error { + return popx.Transaction(ctx, p.c.WithContext(ctx), callback) +} + +func (p *IdentityPersister) GetConnection(ctx context.Context) *pop.Connection { + return popx.GetConnection(ctx, p.c.WithContext(ctx)) +} + +func (p *IdentityPersister) ListVerifiableAddresses(ctx context.Context, page, itemsPerPage int) (a []identity.VerifiableAddress, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListVerifiableAddresses", + trace.WithAttributes( + attribute.Int("per_page", itemsPerPage), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + if err := p.GetConnection(ctx).Where("nid = ?", p.NetworkID(ctx)).Order("id DESC").Paginate(page, x.MaxItemsPerPage(itemsPerPage)).All(&a); err != nil { + return nil, sqlcon.HandleError(err) + } + + return a, nil +} + +func (p *IdentityPersister) ListRecoveryAddresses(ctx context.Context, page, itemsPerPage int) (a []identity.RecoveryAddress, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListRecoveryAddresses", + trace.WithAttributes( + attribute.Int("per_page", itemsPerPage), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + if err := p.GetConnection(ctx).Where("nid = ?", p.NetworkID(ctx)).Order("id DESC").Paginate(page, x.MaxItemsPerPage(itemsPerPage)).All(&a); err != nil { + return nil, sqlcon.HandleError(err) + } + + return a, nil +} + +// PreferExactMatch returns the element from results whose value (extracted by getValue) +// matches originalValue. If no exact match exists, the first element is returned. +// Used by IN(normalized, original) queries to prefer the non-normalized match. +func PreferExactMatch[T any](results []T, originalValue string, getValue func(T) string) T { + for _, r := range results { + if getValue(r) == originalValue { + return r + } + } + return results[0] +} + +// NormalizeIdentifier takes a credential type to determine the type of +// formatting to apply to the 'match' string. Password, Code, and +// WebAuthn types perform graceful normalization which include lower +// casing for email formats. +func NormalizeIdentifier(ct identity.CredentialsType, match string) string { + switch ct { + case identity.CredentialsTypeLookup: + // lookup credentials are case-sensitive + return match + case identity.CredentialsTypeTOTP: + // totp credentials are case-sensitive + return match + case identity.CredentialsTypeOIDC, identity.CredentialsTypeSAML: + // OIDC credentials are case-sensitive + return match + + case identity.CredentialsTypeDeviceAuthn: + return match + + case identity.CredentialsTypePassword, identity.CredentialsTypeCodeAuth, identity.CredentialsTypeWebAuthn: + return x.GracefulNormalization(match) + default: + return match + } +} + +func (p *IdentityPersister) FindIdentityByCredentialIdentifier(ctx context.Context, identifier string, caseSensitive bool, expand identity.Expandables) (_ *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindIdentityByCredentialIdentifier", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + var res []struct { + IdentityID uuid.UUID `db:"identity_id"` + Identifier string `db:"identifier"` + } + + var normalizedIdentifier string + if !caseSensitive { + normalizedIdentifier = NormalizeIdentifier(identity.CredentialsTypePassword, identifier) + } else { + normalizedIdentifier = identifier + } + + nid := p.NetworkID(ctx) + + // Query with both normalized and non-normalized + // identifiers for backward compatibility. + // LIMIT is 1 when both forms are identical, 2 when they differ. + limit := 1 + if normalizedIdentifier != identifier { + limit = 2 + } + err = p.GetConnection(ctx).RawQuery(` +SELECT ic.identity_id, ici.identifier +FROM identity_credentials ic +INNER JOIN identity_credential_identifiers ici +ON ic.id = ici.identity_credential_id +WHERE ici.identifier IN (?,?) +AND ic.nid = ? +AND ici.nid = ? +LIMIT ?`, + normalizedIdentifier, + identifier, + nid, + nid, + limit, + ).All(&res) + if err != nil { + return nil, sqlcon.HandleError(err) + } + + if len(res) == 0 { + return nil, sqlcon.HandleError(sqlcon.ErrNoRows()) + } + + result := PreferExactMatch(res, identifier, func(r struct { + IdentityID uuid.UUID `db:"identity_id"` + Identifier string `db:"identifier"` + }, + ) string { + return r.Identifier + }) + if len(res) > 1 { + for _, r := range res { + if r.IdentityID != result.IdentityID { + p.r.Logger().Warnf("Possible duplicate identities with IDs [%q, %q]", result.IdentityID, r.IdentityID) + break + } + } + } + + // Record result count for observability + span.SetAttributes( + attribute.String("identity.id", result.IdentityID.String()), + attribute.Int("identity.results", len(res)), + ) + + return p.GetIdentity(ctx, result.IdentityID, expand) +} + +func (p *IdentityPersister) FindByCredentialsIdentifier(ctx context.Context, ct identity.CredentialsType, match string) (_ *identity.Identity, _ *identity.Credentials, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindByCredentialsIdentifier", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + + var res []struct { + IdentityID uuid.UUID `db:"identity_id"` + Identifier string `db:"identifier"` + } + + // Force case-insensitivity and trimming for identifiers + normalizedMatch := NormalizeIdentifier(ct, match) + + credentialTypeID, err := FindIdentityCredentialsTypeByName(p.GetConnection(ctx), ct) + if err != nil { + return nil, nil, err + } + + // Query with both normalized and non-normalized + // identifiers for backward compatibility. + // LIMIT is 1 when both forms are identical, 2 when they differ. + limit := 1 + if normalizedMatch != match { + limit = 2 + } + if err := p.GetConnection(ctx).RawQuery(` + SELECT + ic.identity_id, ici.identifier + FROM identity_credentials ic + INNER JOIN identity_credential_identifiers ici + ON ic.id = ici.identity_credential_id AND ici.identity_credential_type_id = ? + WHERE ici.identifier IN (?, ?) + AND ic.nid = ? + AND ici.nid = ? + LIMIT ?`, + credentialTypeID, + normalizedMatch, + match, + nid, + nid, + limit, + ).All(&res); err != nil { + return nil, nil, sqlcon.HandleError(err) + } + + if len(res) == 0 { + return nil, nil, sqlcon.HandleError(sqlcon.ErrNoRows()) + } + + result := PreferExactMatch(res, match, func(r struct { + IdentityID uuid.UUID `db:"identity_id"` + Identifier string `db:"identifier"` + }, + ) string { + return r.Identifier + }) + if len(res) > 1 { + for _, r := range res { + if r.IdentityID != result.IdentityID { + p.r.Logger().Warnf("Possible duplicate identities with IDs [%q, %q]", result.IdentityID, r.IdentityID) + break + } + } + } + + // Record result count for observability + span.SetAttributes( + attribute.String("identity.id", result.IdentityID.String()), + attribute.Int("identity.results", len(res)), + ) + + i, err := p.GetIdentityConfidential(ctx, result.IdentityID) + if err != nil { + return nil, nil, err + } + + creds, ok := i.GetCredentials(ct) + if !ok { + 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, 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 + con := p.GetConnection(ctx) + switch con.Dialect.Name() { + case "sqlite", "mysql": + jsonPath = "$.user_handle" + default: + jsonPath = "user_handle" + } + + columns := popx.DBColumns[identity.Identity](&popx.AliasQuoter{Alias: "identities", Quoter: con.Dialect}) + + credTypeID, err := FindIdentityCredentialsTypeByName(con, identity.CredentialsTypeWebAuthn) + if err != nil { + return nil, err + } + + if err := con.RawQuery(fmt.Sprintf(` +SELECT %s +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 = ? +WHERE identity_credentials.config ->> '%s' = ? AND identity_credentials.config ->> '%s' IS NOT NULL + AND identities.nid = ? +LIMIT 1`, columns, + jsonPath, jsonPath), + credTypeID, + base64.StdEncoding.EncodeToString(userHandle), + p.NetworkID(ctx), + ).First(&id); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &id, nil +} + +func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, extraColumns []identity.ExtraColumn, identities ...*identity.Identity) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.createIdentityCredentials", + trace.WithAttributes( + attribute.Int("num_identities", len(identities)), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + type credentialSlot struct { + ident *identity.Identity + key identity.CredentialsType + } + + var ( + nid = p.NetworkID(ctx) + traceConn = &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: p.GetConnection(ctx)} + credentials []*identity.Credentials + slots = map[*identity.Credentials]credentialSlot{} + identifiers []*identity.CredentialIdentifier + ) + + var opts []batch.CreateOpts + if len(extraColumns) > 0 { + // Extra columns are values the caller already knows for columns that are + // not part of the OSS model (e.g. crdb_region on CockroachDB + // multi-region). Writing them with the insert avoids the server-side + // fallback (a column default or a lookup derived from a foreign key), + // which for region columns costs a cross-region round trip per statement. + opts = append(opts, batch.WithExtraColumns(extraColumns)) + } + if len(identities) > 1 { + opts = append(opts, batch.WithPartialInserts) + } + + for _, ident := range identities { + for k := range ident.Credentials { + cred := new(ident.Credentials[k]) + + if len(cred.Config) == 0 { + cred.Config = sqlxx.JSONRawMessage("{}") + } + + ct, err := FindIdentityCredentialsTypeByName(p.GetConnection(ctx), cred.Type) + if err != nil { + return err + } + + cred.IdentityID = ident.ID + cred.NID = nid + cred.IdentityCredentialTypeID = ct + + // TOTP and lookup-secret AAL2 logins resolve the credential by + // joining identity_credential_identifiers, using the identity ID + // as the identifier (see selfservice/strategy/{totp,lookup}/ + // login.go and settings.go). The admin import path does not + // provide an identifier, and at import time the identity ID may + // still be the zero value (CockroachDB assigns it via + // gen_random_uuid() during the identity insert). At this point + // ident.ID is the persisted identity ID, so default the + // identifier here to keep AAL2 login working for imported + // credentials. See https://github.com/ory/kratos/issues/4561. + if len(cred.Identifiers) == 0 && + (cred.Type == identity.CredentialsTypeTOTP || + cred.Type == identity.CredentialsTypeLookup) { + cred.Identifiers = []string{ident.ID.String()} + } + + credentials = append(credentials, cred) + slots[cred] = credentialSlot{ident: ident, key: k} + } + } + + err = batch.Create(ctx, traceConn, credentials, opts...) + + for _, cred := range credentials { + slot := slots[cred] + slot.ident.Credentials[slot.key] = *cred + } + + if err != nil { + return err + } + + for _, cred := range credentials { + for _, identifier := range cred.Identifiers { + // Force case-insensitivity and trimming for identifiers + identifier = NormalizeIdentifier(cred.Type, identifier) + + if identifier == "" { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf( + "Unable to create identity credentials with missing or empty identifier.")) + } + + ct, err := FindIdentityCredentialsTypeByName(p.GetConnection(ctx), cred.Type) + if err != nil { + return err + } + + identifiers = append(identifiers, &identity.CredentialIdentifier{ + Identifier: identifier, + IdentityID: new(cred.IdentityID), + IdentityCredentialsID: cred.ID, + IdentityCredentialsTypeID: ct, + NID: p.NetworkID(ctx), + }) + } + } + + if err = batch.Create(ctx, traceConn, identifiers, opts...); err != nil { + return err + } + + return nil +} + +func (p *IdentityPersister) createVerifiableAddresses(ctx context.Context, conn *pop.Connection, extraColumns []identity.ExtraColumn, identities ...*identity.Identity) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.createVerifiableAddresses", + trace.WithAttributes( + attribute.Int("num_identities", len(identities)), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + work := make([]*identity.VerifiableAddress, 0, len(identities)) + for _, id := range identities { + for i := range id.VerifiableAddresses { + work = append(work, &id.VerifiableAddresses[i]) + } + } + var opts []batch.CreateOpts + if len(extraColumns) > 0 { + // Extra columns are values the caller already knows for columns that are + // not part of the OSS model (e.g. crdb_region on CockroachDB + // multi-region). Writing them with the insert avoids the server-side + // fallback (a column default or a lookup derived from a foreign key), + // which for region columns costs a cross-region round trip per statement. + opts = append(opts, batch.WithExtraColumns(extraColumns)) + } + if len(identities) > 1 { + opts = append(opts, batch.WithPartialInserts) + } + + return batch.Create(ctx, &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: conn}, work, opts...) +} + +type differ interface { + Signature() string + GetID() uuid.UUID +} + +func updateAssociationWith[T differ](ctx context.Context, p *IdentityPersister, extraColumns []identity.ExtraColumn, fromDatabase, updateTo []T, +) (result []T, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.updateAssociationWith", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + toKeep, toCreate, toRemoveIDs := diffAssociations(fromDatabase, updateTo) + + // Subtle: we delete the old associations from the DB first, because else + // they could cause UNIQUE constraints to fail on insert. + // Foreign key cascade will take care of deleting dependent records. + if len(toRemoveIDs) > 0 { + if err := p.GetConnection(ctx).Where("id IN (?)", toRemoveIDs).Where("nid = ?", p.NetworkID(ctx)).Delete(new(T)); err != nil { + return nil, sqlcon.HandleError(err) + } + } + + if len(toCreate) > 0 { + var opts []batch.CreateOpts + if len(extraColumns) > 0 { + // Extra columns are values the caller already knows for columns that are + // not part of the OSS model (e.g. crdb_region on CockroachDB + // multi-region). Writing them with the insert avoids the server-side + // fallback (a column default or a lookup derived from a foreign key), + // which for region columns costs a cross-region round trip per statement. + opts = append(opts, batch.WithExtraColumns(extraColumns)) + } + if err := batch.Create(ctx, + &batch.TracerConnection{ + Tracer: p.r.Tracer(ctx), + Connection: p.GetConnection(ctx), + }, + toCreate, + opts..., + ); err != nil { + return nil, err + } + } + + result = make([]T, 0, len(toKeep)+len(toCreate)) + for _, v := range toKeep { + result = append(result, *v) + } + for _, v := range toCreate { + result = append(result, *v) + } + + return result, nil +} + +func updateAssociation[T differ](ctx context.Context, p *IdentityPersister, extraColumns []identity.ExtraColumn, i *identity.Identity, inID []T, +) (result []T, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.updateAssociation", + trace.WithAttributes( + attribute.Stringer("identity.id", i.ID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + var inDB []T + if err := p.GetConnection(ctx). + Where("identity_id = ? AND nid = ?", i.ID, p.NetworkID(ctx)). + All(&inDB); err != nil { + return nil, sqlcon.HandleError(err) + } + + return updateAssociationWith(ctx, p, extraColumns, inDB, inID) +} + +func (p *IdentityPersister) updateCredentialsAssociation(ctx context.Context, extraColumns []identity.ExtraColumn, identityID uuid.UUID, fromDatabase []identity.Credentials, updateTo []identity.Credentials) (result map[identity.CredentialsType]identity.Credentials, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.updateCredentialsAssociation", + trace.WithAttributes( + attribute.Stringer("identity.id", identityID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + + // Normalize new credentials by ensuring IdentityID, NID, and identifiers are set before hashing + for i := range updateTo { + updateTo[i].IdentityID = identityID + updateTo[i].NID = nid + // Normalize identifiers to match what's stored in the database (make a copy to avoid modifying original) + normalizedIdentifiers := make([]string, len(updateTo[i].Identifiers)) + for j, identifier := range updateTo[i].Identifiers { + normalizedIdentifiers[j] = NormalizeIdentifier(updateTo[i].Type, identifier) + } + updateTo[i].Identifiers = normalizedIdentifiers + } + + credsToKeep, newCreds, credsToDeleteIDs := diffAssociations(fromDatabase, updateTo) + + if len(credsToDeleteIDs) > 0 { + // Delete the credential and its identifiers. + conn := p.GetConnection(ctx) + q := "DELETE FROM identity_credentials WHERE nid = ? AND id IN (?)" + // This @index hint is CockroachDB-only; PostgreSQL and YugabyteDB use + // the unadorned table name. + if conn.Dialect.Name() == "cockroach" { + q = "DELETE FROM identity_credentials@primary WHERE nid = ? AND id IN (?)" + } + if err := conn.RawQuery(q, nid, credsToDeleteIDs).Exec(); err != nil { + return nil, sqlcon.HandleError(err) + } + } + + // Create new credentials that aren't already in the database + credsToCreate := make(map[identity.CredentialsType]identity.Credentials, len(newCreds)) + for _, c := range newCreds { + credsToCreate[c.Type] = *c + } + + if len(credsToCreate) > 0 { + if err := p.createIdentityCredentials(ctx, extraColumns, &identity.Identity{ + ID: identityID, + Credentials: credsToCreate, + }); err != nil { + return nil, err + } + } + + result = make(map[identity.CredentialsType]identity.Credentials, len(credsToKeep)+len(credsToCreate)) + for _, c := range credsToKeep { + result[c.Type] = *c + } + maps.Copy(result, credsToCreate) + + return result, nil +} + +func diffAssociations[T differ](fromDatabase, updateTo []T) (unchanged, toCreate []*T, toRemoveIDs []uuid.UUID) { + newAssocs := make(map[string]*T, len(updateTo)) + oldAssocs := make(map[string]*T, len(fromDatabase)) + for i, a := range updateTo { + newAssocs[a.Signature()] = &updateTo[i] + } + for i, a := range fromDatabase { + oldAssocs[a.Signature()] = &fromDatabase[i] + } + + toRemoveIDs = make([]uuid.UUID, 0, len(fromDatabase)) + toCreate = make([]*T, 0, len(updateTo)) + unchanged = make([]*T, 0, len(updateTo)) + + for h, a := range oldAssocs { + if _, found := newAssocs[h]; found { + delete(newAssocs, h) + unchanged = append(unchanged, a) + } else { + toRemoveIDs = append(toRemoveIDs, (*a).GetID()) + } + } + + for _, a := range newAssocs { + toCreate = append(toCreate, a) + } + + return +} + +func (p *IdentityPersister) normalizeAllAddresses(ctx context.Context, identities ...*identity.Identity) { + for _, id := range identities { + p.normalizeRecoveryAddresses(ctx, id) + p.normalizeVerifiableAddresses(ctx, id) + } +} + +func (p *IdentityPersister) normalizeVerifiableAddresses(ctx context.Context, id *identity.Identity) { + for k := range id.VerifiableAddresses { + v := id.VerifiableAddresses[k] + + v.IdentityID = id.ID + v.NID = p.NetworkID(ctx) + v.Value = x.GracefulNormalization(v.Value) + v.Via = cmp.Or(v.Via, identity.AddressTypeEmail) + if len(v.Status) == 0 { + if v.Verified { + v.Status = identity.VerifiableAddressStatusCompleted + } else { + v.Status = identity.VerifiableAddressStatusPending + } + } + + // If verified is true but no timestamp is set, we default to time.Now + if v.Verified && (v.VerifiedAt == nil || time.Time(*v.VerifiedAt).IsZero()) { + v.VerifiedAt = new(sqlxx.NullTime(time.Now())) + } + if !v.Verified { + v.VerifiedAt = nil + } + + id.VerifiableAddresses[k] = v + } +} + +func (p *IdentityPersister) normalizeRecoveryAddresses(ctx context.Context, id *identity.Identity) { + for k := range id.RecoveryAddresses { + id.RecoveryAddresses[k].IdentityID = id.ID + id.RecoveryAddresses[k].NID = p.NetworkID(ctx) + id.RecoveryAddresses[k].Value = x.GracefulNormalization(id.RecoveryAddresses[k].Value) + id.RecoveryAddresses[k].Via = cmp.Or(id.RecoveryAddresses[k].Via, identity.AddressTypeEmail) + } +} + +func (p *IdentityPersister) createRecoveryAddresses(ctx context.Context, conn *pop.Connection, extraColumns []identity.ExtraColumn, identities ...*identity.Identity) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.createRecoveryAddresses", + trace.WithAttributes( + attribute.Int("num_identities", len(identities)), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + // https://go.dev/play/p/b1kU5Bme2Fr + work := make([]*identity.RecoveryAddress, 0, len(identities)) + for _, id := range identities { + for i := range id.RecoveryAddresses { + work = append(work, &id.RecoveryAddresses[i]) + } + } + + var opts []batch.CreateOpts + if len(extraColumns) > 0 { + // Extra columns are values the caller already knows for columns that are + // not part of the OSS model (e.g. crdb_region on CockroachDB + // multi-region). Writing them with the insert avoids the server-side + // fallback (a column default or a lookup derived from a foreign key), + // which for region columns costs a cross-region round trip per statement. + opts = append(opts, batch.WithExtraColumns(extraColumns)) + } + 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) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CountIdentities", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + count, err := p.c.WithContext(ctx).Where("nid = ?", p.NetworkID(ctx)).Count(new(identity.Identity)) + if err != nil { + return 0, sqlcon.HandleError(err) + } + span.SetAttributes(attribute.Int("num_identities", count)) + return int64(count), nil +} + +func (p *IdentityPersister) CreateIdentity(ctx context.Context, ident *identity.Identity) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateIdentity", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + return p.CreateIdentities(ctx, []*identity.Identity{ident}) +} + +// identitiesNeedPartialInserts reports whether a batch identity insert must use +// partial inserts. Partial inserts pre-assign the ids so a conflicting row can +// be correlated back to its input, which keeps the id client-generated instead +// of database-generated. +// +// The only unique constraint on the identities table is on external_id (and it +// is partial: it applies only when external_id is set). An identity insert can +// therefore only conflict when an external_id is present, so partial inserts +// are only needed when the batch has more than one row and at least one carries +// an external_id. Otherwise a plain insert is enough; the downstream credential +// and address inserts still use partial inserts to report their own conflicts. +// +// On a transaction retry, the ids generated during the first attempt (by the +// database or by Go) remain on the in-memory structs, so the retried insert +// reuses them instead of generating new ones. The identity pool contract test +// covers this ("transaction retry" cases). +func identitiesNeedPartialInserts(identities []*identity.Identity) bool { + return len(identities) > 1 && slices.ContainsFunc(identities, func(i *identity.Identity) bool { + return i.ExternalID != "" + }) +} + +func (p *IdentityPersister) CreateIdentities(ctx context.Context, identities []*identity.Identity, opts ...identity.CreateIdentitiesModifier) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateIdentities", + trace.WithAttributes( + attribute.Int("identities.count", len(identities)), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + options := identity.NewCreateIdentitiesOptions(opts) + + for _, ident := range identities { + ident.NID = p.NetworkID(ctx) + + if ident.SchemaID == "" { + ident.SchemaID = p.r.Config().DefaultIdentityTraitsSchemaID(ctx) + } + + stateChangedAt := sqlxx.NullTime(time.Now().UTC()) + ident.StateChangedAt = &stateChangedAt + if ident.State == "" { + ident.State = identity.StateActive + } + + if len(ident.Traits) == 0 { + ident.Traits = identity.Traits("{}") + } + + if err = p.InjectTraitsSchemaURL(ctx, ident); err != nil { + return err + } + + if err = p.validateIdentity(ctx, ident); err != nil { + return err + } + } + + var succeededIDs []uuid.UUID + var partialErr *identity.CreateIdentitiesError + if err := 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{ created bool }) + partialErr = nil + createdIdentities := make([]*identity.Identity, 0, len(identities)) + + var batchOpts []batch.CreateOpts + if extras := options.ExtraColumns; len(extras) > 0 { + batchOpts = append(batchOpts, batch.WithExtraColumns(extras)) + } + if identitiesNeedPartialInserts(identities) { + batchOpts = append(batchOpts, batch.WithPartialInserts) + } + if err := batch.Create(ctx, conn, identities, batchOpts...); err != nil { + if partialErr := new(batch.PartialConflictError[identity.Identity]); errors.As(err, &partialErr) { + for _, k := range partialErr.Failed { + failedIdentityIDs[k.ID] = struct{ created bool }{false} + } + + // Mark all created identities that were not in the failed list as created. + for _, ident := range identities { + if _, ok := failedIdentityIDs[ident.ID]; !ok { + createdIdentities = append(createdIdentities, ident) + } + } + } else { + return sqlcon.HandleError(err) + } + } else { + // If no errors occurred, we can safely assume all identities were created. + createdIdentities = identities + } + + p.normalizeAllAddresses(ctx, createdIdentities...) + + if err = p.createVerifiableAddresses(ctx, tx, options.ExtraColumns, createdIdentities...); err != nil { + if partialErr := new(batch.PartialConflictError[identity.VerifiableAddress]); errors.As(err, &partialErr) { + for _, k := range partialErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{ created bool }{true} + } + } else { + return sqlcon.HandleError(err) + } + } + if err = p.createRecoveryAddresses(ctx, tx, options.ExtraColumns, createdIdentities...); err != nil { + if partialErr := new(batch.PartialConflictError[identity.RecoveryAddress]); errors.As(err, &partialErr) { + for _, k := range partialErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{ created bool }{true} + } + } else { + return sqlcon.HandleError(err) + } + } + if err = p.createIdentityCredentials(ctx, options.ExtraColumns, createdIdentities...); err != nil { + if partialErr := new(batch.PartialConflictError[identity.Credentials]); errors.As(err, &partialErr) { + for _, k := range partialErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{ created bool }{true} + } + } else if partialErr := new(batch.PartialConflictError[identity.CredentialIdentifier]); errors.As(err, &partialErr) { + for _, k := range partialErr.Failed { + // The failed identifier carries the owning identity ID + // directly, so map it back without scanning every + // identity's credentials by ID. + if k.IdentityID != nil { + failedIdentityIDs[*k.IdentityID] = struct{ created bool }{true} + } + } + } else { + return sqlcon.HandleError(err) + } + } + + // If any of the batch inserts failed on conflict, let's delete the corresponding + // identity and return a list of failed identities in the error. + if len(failedIdentityIDs) > 0 { + partialErr = identity.NewCreateIdentitiesError(len(failedIdentityIDs)) + idsToBeRemoved := make([]uuid.UUID, 0, len(failedIdentityIDs)) + + for _, ident := range identities { + if info, ok := failedIdentityIDs[ident.ID]; ok { + partialErr.AddFailedIdentity(ident, sqlcon.ErrUniqueViolation()) + if info.created { + idsToBeRemoved = append(idsToBeRemoved, 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, idsToBeRemoved); err != nil { + return sqlcon.HandleError(err) + } + + return nil + } else { + // No failures: report all identities as created. + for _, ident := range identities { + succeededIDs = append(succeededIDs, ident.ID) + } + } + + return nil + }); err != nil { + return err + } + + // Report succeeded identities as created. + for _, identID := range succeededIDs { + span.AddEvent(events.NewIdentityCreated(ctx, identID)) + } + + return partialErr.ErrOrNil() +} + +func (p *IdentityPersister) HydrateIdentityAssociations(ctx context.Context, i *identity.Identity, expand identity.Expandables) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.HydrateIdentityAssociations", + trace.WithAttributes( + attribute.Stringer("identity.id", i.ID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + + if expand.Has(identity.ExpandFieldRecoveryAddresses) { + if err := p.GetConnection(ctx). + Where("identity_id = ? AND nid = ?", i.ID, nid). + Order("id ASC"). + All(&i.RecoveryAddresses); err != nil { + return sqlcon.HandleError(err) + } + } + + if expand.Has(identity.ExpandFieldVerifiableAddresses) { + if err := p.GetConnection(ctx). + Order("id ASC"). + Where("identity_id = ? AND nid = ?", i.ID, nid). + All(&i.VerifiableAddresses); err != nil { + return sqlcon.HandleError(err) + } + } + + if expand.Has(identity.ExpandFieldCredentials) { + creds, err := QueryForCredentials(p.GetConnection(ctx), + Where{"identity_credentials.identity_id = ?", []interface{}{i.ID}}, + Where{"identity_credentials.nid = ?", []interface{}{nid}}) + if err != nil { + return err + } + i.Credentials = creds[i.ID] + } + + if err := i.Validate(); err != nil { + return err + } + + if err := identity.UpgradeCredentials(i); err != nil { + return err + } + + return p.InjectTraitsSchemaURL(ctx, i) +} + +type queryCredentials struct { + Identifier string `db:"cred_identifier"` + identity.Credentials +} + +func (queryCredentials) TableName() string { + return "identity_credentials" +} + +type Where struct { + Condition string + Args []interface{} +} + +// QueryForCredentials queries for identity credentials with custom WHERE +// clauses, returning the results resolved by the owning identity's UUID. +func QueryForCredentials(con *pop.Connection, where ...Where) (credentialsPerIdentity map[uuid.UUID](map[identity.CredentialsType]identity.Credentials), err error) { + // This query has been meticulously crafted to be as fast as possible. + // If you touch it, you will likely introduce a performance regression. + q := con.Select( + "COALESCE(identity_credential_identifiers.identifier, '') cred_identifier", + "identity_credentials.id", + "identity_credentials.identity_credential_type_id", + "identity_credentials.identity_id", + "identity_credentials.nid", + "identity_credentials.config", + "identity_credentials.version", + "identity_credentials.created_at", + "identity_credentials.updated_at", + ).LeftJoin(identifiersTableNameWithIndexHint(con), + "identity_credential_identifiers.identity_credential_id = identity_credentials.id AND identity_credential_identifiers.nid = identity_credentials.nid", + ) + for _, w := range where { + q = q.Where("("+w.Condition+")", w.Args...) + } + var results []queryCredentials + if err := q.All(&results); err != nil { + return nil, sqlcon.HandleError(err) + } + + // assemble + credentialsPerIdentity = map[uuid.UUID](map[identity.CredentialsType]identity.Credentials){} + for _, res := range results { + + res.Type, err = FindIdentityCredentialsTypeByID(con, res.IdentityCredentialTypeID) + if err != nil { + return nil, err + } + + credentials, ok := credentialsPerIdentity[res.IdentityID] + if !ok { + credentialsPerIdentity[res.IdentityID] = make(map[identity.CredentialsType]identity.Credentials) + credentials = credentialsPerIdentity[res.IdentityID] + } + identifiers := credentials[res.Type].Identifiers + if res.Identifier != "" { + identifiers = append(identifiers, res.Identifier) + } + if identifiers == nil { + identifiers = make([]string, 0) + } + res.Identifiers = identifiers + credentials[res.Type] = res.Credentials + } + + // We need deterministic ordering for testing, but sorting in the + // database can be expensive under certain circumstances. + for _, creds := range credentialsPerIdentity { + for k := range creds { + sort.Strings(creds[k].Identifiers) + } + } + return credentialsPerIdentity, nil +} + +func identifiersTableNameWithIndexHint(con *pop.Connection) string { + ici := "identity_credential_identifiers" + switch con.Dialect.Name() { + case "cockroach": + ici += "@identity_credential_identifiers_ici_nid_i_idx" + case "sqlite3": + ici += " INDEXED BY identity_credential_identifiers_ici_nid_i_idx" + case "mysql": + ici += " USE INDEX(identity_credential_identifiers_ici_nid_i_idx)" + default: + // PostgreSQL and YugabyteDB do not accept these dialect-specific index + // hint syntaxes. + } + return ici +} + +func paginationAttributes(params *identity.ListIdentityParameters, paginator *keysetpagination.Paginator) []attribute.KeyValue { + attrs := []attribute.KeyValue{ + attribute.StringSlice("expand", params.Expand.ToEager()), + attribute.Bool("use:credential_identifier_filter", params.CredentialsIdentifier != ""), + attribute.Bool("use:credential_identifier_similar_filter", params.CredentialsIdentifierSimilar != ""), + } + if params.PagePagination != nil { + attrs = append(attrs, + attribute.Int("page", params.PagePagination.Page), + attribute.Int("per_page", params.PagePagination.ItemsPerPage)) + } else { + attrs = append(attrs, + attribute.String("page_token", paginator.Token().Encode()), + attribute.Int("page_size", paginator.Size())) + } + return attrs +} + +// getCredentialTypeIDs returns a map of credential types to their respective IDs. +// +// If a credential type is not found, an error is returned. +func (p *IdentityPersister) getCredentialTypeIDs(ctx context.Context, credentialTypes []identity.CredentialsType) (map[identity.CredentialsType]uuid.UUID, error) { + result := map[identity.CredentialsType]uuid.UUID{} + + for _, ct := range credentialTypes { + typeID, err := FindIdentityCredentialsTypeByName(p.GetConnection(ctx), ct) + if err != nil { + return nil, err + } + result[ct] = typeID + } + + return result, nil +} + +func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity.ListIdentityParameters) (_ []identity.Identity, nextPage *keysetpagination.Paginator, err error) { + if (params.ColumnsTransformer == nil) != (params.RowScanner == nil) { + return nil, nil, errors.New("ListIdentityParameters: ColumnsTransformer and RowScanner must be set together") + } + + paginator := keysetpagination.GetPaginator(append( + params.KeySetPagination, + keysetpagination.WithDefaultToken(identity.DefaultPageToken()), + keysetpagination.WithDefaultSize(250), + keysetpagination.WithColumn("id", "ASC"))...) + + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListIdentities", trace.WithAttributes(append( + paginationAttributes(¶ms, paginator), + 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 + + if err = p.Transaction(ctx, func(ctx context.Context, con *pop.Connection) error { + is = make([]identity.Identity, 0) // Make sure we reset this to 0 in case of retries. + nextPage = nil + + if err := crdbx.SetTransactionReadOnly(con); err != nil { + return err + } + + // This transaction runs up to five statements (credential-type lookup, + // page query, expansion queries over the page's identities). At + // SERIALIZABLE, a concurrent write to any of those identities between + // statements forces a read-refresh over the whole page and retries the + // transaction when it fails; READ COMMITTED absorbs those conflicts + // per-statement on the server. + if err := crdbx.SetTransactionReadCommitted(con); err != nil { + return err + } + + if err := crdbx.SetTransactionConsistency(con, params.ConsistencyLevel, p.r.Config().DefaultConsistencyLevel(ctx)); err != nil { + return err + } + + joins := "" + wheres := "identities.nid = ? AND identities.id > ?" + args := []any{nid, paginator.Token().Encode()} + limit := fmt.Sprintf("LIMIT %d", paginator.Size()+1) + if params.PagePagination != nil { + wheres = "identities.nid = ?" + args = []any{nid} + paginator := pop.NewPaginator(params.PagePagination.Page+1, params.PagePagination.ItemsPerPage) + limit = fmt.Sprintf("LIMIT %d OFFSET %d", paginator.PerPage, paginator.Offset) + } + identifier := params.CredentialsIdentifier + identifierOperator := "=" + if identifier == "" && params.CredentialsIdentifierSimilar != "" { + identifier = x.EscapeLikePattern(params.CredentialsIdentifierSimilar) + "%" + identifierOperator = "LIKE" + } + + if len(identifier) > 0 { + types, err := p.getCredentialTypeIDs(ctx, []identity.CredentialsType{ + identity.CredentialsTypeWebAuthn, + identity.CredentialsTypePassword, + identity.CredentialsTypeCodeAuth, + identity.CredentialsTypeOIDC, + identity.CredentialsTypeSAML, + identity.CredentialsTypeDeviceAuthn, + }) + if err != nil { + return err + } + + // When filtering by credentials identifier, we most likely are looking for a username or email. It is therefore + // important to normalize the identifier before querying the database. + + joins = params.TransformStatement(` + INNER JOIN identity_credentials ic ON ic.identity_id = identities.id AND ic.nid = identities.nid + INNER JOIN identity_credential_identifiers ici ON ici.identity_credential_id = ic.id AND ici.nid = ic.nid +`) + + wheres += fmt.Sprintf(` + AND ic.nid = ? AND ici.nid = ? + AND ((ici.identity_credential_type_id IN (?, ?, ?) AND ici.identifier %s ?) + OR (ici.identity_credential_type_id IN (?, ?) AND ici.identifier %s ?)) + `, identifierOperator, identifierOperator) + args = append(args, + nid, nid, + types[identity.CredentialsTypeWebAuthn], types[identity.CredentialsTypePassword], types[identity.CredentialsTypeCodeAuth], + NormalizeIdentifier(identity.CredentialsTypePassword, identifier), + types[identity.CredentialsTypeOIDC], types[identity.CredentialsTypeSAML], + identifier, + ) + } + + if len(params.IdsFilter) > 0 { + wheres += ` + AND identities.id in (?) + ` + args = append(args, params.IdsFilter) + } else if !params.OrganizationID.IsNil() { + wheres += ` + AND identities.organization_id = ? + ` + args = append(args, params.OrganizationID.String()) + } + + columns := popx.DBColumns[identity.Identity](&popx.AliasQuoter{Alias: "identities", Quoter: con.Dialect}) + if params.ColumnsTransformer != nil { + columns = params.ColumnsTransformer(columns) + } + + // DISTINCT is only needed when the credentials-identifier filter adds the + // INNER JOINs below: a single identity can match multiple identifier rows, + // which would otherwise produce duplicates. Without the joins, identities.id + // is the primary key, so every row is already unique and DISTINCT is pure + // overhead (a distinct processor over the wide traits/metadata columns). + distinct := "" + if joins != "" { + distinct = "DISTINCT " + } + + query := fmt.Sprintf(` + SELECT %s%s + FROM identities AS identities + %s + WHERE + %s + ORDER BY identities.id ASC + %s`, + distinct, columns, + joins, wheres, limit) + + if params.RowScanner != nil { + is, err = params.RowScanner(con, query, args) + if err != nil { + return sqlcon.HandleError(err) + } + } else if err := con.RawQuery(query, args...).All(&is); err != nil { + return sqlcon.HandleError(err) + } + + if params.PagePagination == nil { + is, nextPage = keysetpagination.Result(is, paginator) + } + + if len(is) == 0 { + return nil + } + + identitiesByID := make(map[uuid.UUID]*identity.Identity, len(is)) + identityIDs := make([]any, len(is)) + for k := range is { + identitiesByID[is[k].ID] = &is[k] + identityIDs[k] = is[k].ID + } + + for _, e := range params.Expand { + switch e { + case identity.ExpandFieldCredentials: + creds, err := QueryForCredentials(con, + Where{"identity_credentials.nid = ?", []interface{}{nid}}, + Where{"identity_credentials.identity_id IN (?)", identityIDs}) + if err != nil { + return err + } + for k := range is { + is[k].Credentials = creds[is[k].ID] + } + case identity.ExpandFieldVerifiableAddresses: + addrs := make([]identity.VerifiableAddress, 0) + if err := con.Where("identity_id IN (?)", identityIDs).Where("nid = ?", nid).Order("id").All(&addrs); err != nil { + return sqlcon.HandleError(err) + } + for _, addr := range addrs { + identitiesByID[addr.IdentityID].VerifiableAddresses = append(identitiesByID[addr.IdentityID].VerifiableAddresses, addr) + } + case identity.ExpandFieldRecoveryAddresses: + addrs := make([]identity.RecoveryAddress, 0) + if err := con.Where("identity_id IN (?)", identityIDs).Where("nid = ?", nid).Order("id").All(&addrs); err != nil { + return sqlcon.HandleError(err) + } + for _, addr := range addrs { + identitiesByID[addr.IdentityID].RecoveryAddresses = append(identitiesByID[addr.IdentityID].RecoveryAddresses, addr) + } + } + } + + return nil + }); err != nil { + return nil, nil, err + } + + schemaCache := map[string]string{} + for k := range is { + i := &is[k] + + if u, ok := schemaCache[i.SchemaID]; ok { + i.SchemaURL = u + } else { + if err := p.InjectTraitsSchemaURL(ctx, i); err != nil { + return nil, nil, err + } + schemaCache[i.SchemaID] = i.SchemaURL + } + + if err := i.Validate(); err != nil { + return nil, nil, err + } + + if err := identity.UpgradeCredentials(i); err != nil { + return nil, nil, err + } + + is[k] = *i + } + + 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.UpdateIdentityColumns", + 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 +} + +// credentialsConfigLockTimeout bounds how long UpdateCredentialsConfig waits +// for the credential-row lock, so a flood of requests against one row cannot +// park waiters on pooled connections until the pool is exhausted. Enforced +// via context cancellation, which aborts a blocked statement and rolls back +// the transaction. It does not apply to SQLite (see UpdateCredentialsConfig). +const credentialsConfigLockTimeout = 5 * time.Second + +// UpdateCredentialsConfig atomically read-modify-writes a single +// identity_credentials row's config under READ COMMITTED, holding an exclusive +// row lock (SELECT ... FOR UPDATE) across the whole read-mutate-write cycle: +// concurrent updates to the same row serialize and mutate always observes the +// latest committed config. +// +// The isolation level is deliberate. Under CockroachDB SERIALIZABLE, +// SELECT ... FOR UPDATE is only best-effort — a waiter may not queue behind the +// holder — whereas under READ COMMITTED it is a durable, replicated lock that +// behaves like the textbook Postgres/MySQL row lock. So here the lock, not the +// isolation level, is the correctness mechanism: it is the sole defense against +// a lost update, and any change that weakens it (reintroducing a join, an index +// hint that changes the plan, dropping FOR UPDATE) is a silent correctness bug +// rather than a throughput regression. This is safe because the operation is a +// single-row read-modify-write with no cross-row invariant, so the other READ +// COMMITTED anomalies (read skew, phantoms, write skew) cannot arise. On a +// cluster without READ COMMITTED the request is upgraded back to SERIALIZABLE, +// which is fail-safe here; on SQLite pop serializes whole transactions on an +// in-process mutex, which subsumes the row lock, and the isolation option is +// ignored. +// +// mutate maps the current config JSON to the new one; a structurally equal +// result skips the write. It must be pure: it may run more than once if the +// database retries the transaction. Calls inside a surrounding transaction +// are rejected — the lock and its timeout must be scoped to the transaction +// opened here. +// +// opts may narrow the row set with ExtraColumns, applied as equality +// predicates to both statements. WithDerivedIdentifiers additionally syncs +// the credential's identifier rows to the set derived from the post-mutation +// config, inside the same transaction and lock; like mutate, derive may run +// more than once on database retries. +func (p *IdentityPersister) UpdateCredentialsConfig(ctx context.Context, identityID uuid.UUID, ct identity.CredentialsType, mutate func(config []byte) ([]byte, error), opts ...identity.UpdateCredentialsConfigModifier) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateCredentialsConfig", + trace.WithAttributes( + attribute.Stringer("identity.id", identityID), + attribute.Stringer("network.id", p.NetworkID(ctx)), + attribute.String("credentials.type", string(ct)))) + defer otelx.End(span, &err) + + // An ambient transaction would be reused by popx, extending the lock to + // its lifetime — fail loudly instead. + if popx.InTransaction(ctx) { + return errors.WithStack(herodot.ErrInternalServerError().WithReason("UpdateCredentialsConfig must not be called inside a surrounding transaction: its row lock and lock timeout are scoped to the transaction it opens itself")) + } + + // The lock-wait budget does not apply to SQLite: pop serializes whole + // SQLite transactions on an in-process per-connection mutex, so the row + // lock the budget bounds on the cluster databases does not exist — and a + // mutation queued on that mutex behind unrelated transactions on a loaded + // machine (parallel test runs) would burn the budget waiting without + // holding any pooled connection. The caller's context still cancels. + if p.c.Dialect.Name() != "sqlite3" { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, credentialsConfigLockTimeout) + defer cancel() + } + + nid := p.NetworkID(ctx) + + o := identity.NewUpdateCredentialsConfigOptions(opts) + var extraSQLBuilder strings.Builder + var extraArgs []any + for _, col := range o.ExtraColumns { + _, _ = fmt.Fprintf(&extraSQLBuilder, " AND %s = ?", col.K) + extraArgs = append(extraArgs, col.V) + } + extraSQL := extraSQLBuilder.String() + + // READ COMMITTED so SELECT ... FOR UPDATE takes a real, durable row lock on + // CockroachDB (see the method doc). popx applies the isolation for + // Postgres/MySQL/CockroachDB and ignores it for SQLite. + txOpts := &sql.TxOptions{Isolation: sql.LevelReadCommitted} + // Resolve the type id before the transaction (cached after the first + // load) so the locking SELECT touches only identity_credentials: a join + // under FOR UPDATE would lock the shared type row and serialize all + // updates of that type globally. Outside the transaction, because on + // SQLite a cache miss would take pop's statement mutex inside it (see + // the lock-order note below). + typeID, err := FindIdentityCredentialsTypeByName(p.c.WithContext(ctx), ct) + if err != nil { + return err + } + + var wrote bool + if err := popx.TransactionWithOptions(ctx, p.c.WithContext(ctx), txOpts, func(ctx context.Context, tx *pop.Connection) error { + wrote = false + + // No index hint: the locking SELECT matches + // identity_credentials_identity_id_idx. Forcing @primary would make + // it a full-scan locking read that locks every scanned row. + selectQuery := ` + SELECT ic.id, ic.config + FROM identity_credentials ic + WHERE ic.identity_id = ? + AND ic.nid = ? + AND ic.identity_credential_type_id = ?` + extraSQL + ` + FOR UPDATE` + selectArgs := append([]any{identityID, nid, typeID}, extraArgs...) + + var row struct { + ID uuid.UUID `db:"id"` + Config sqlxx.JSONRawMessage `db:"config"` + } + if tx.Dialect.Name() == "sqlite3" { + // SQLite has no FOR UPDATE; pop serializes whole SQLite + // transactions on an in-process mutex instead. That mutex does + // not cover plain single-statement writes (e.g. flow updates + // during a login burst), which commit freely while this + // transaction is open, and this read-then-write transaction is + // unsound against them in WAL mode: a deferred transaction takes + // its read snapshot at the first SELECT and upgrades to writer at + // the UPDATE, and if anything committed in between the upgrade + // fails immediately with SQLITE_BUSY_SNAPSHOT — busy_timeout + // cannot help a stale snapshot, and enough write traffic exhausts + // the retry budget in TransactionWithOptions. A no-op write as + // the transaction's first statement acquires the write lock up + // front (the in-transaction equivalent of BEGIN IMMEDIATE), so + // the snapshot is taken with the lock already held and concurrent + // writers wait on busy_timeout instead. + if err := tx.RawQuery("UPDATE identity_credentials SET id = id WHERE 1 = 0").Exec(); err != nil { + return sqlcon.HandleError(err) + } + + // Read via the underlying sqlx transaction, not pop's finders: + // pop wraps SQLite reads in a process-wide statement mutex, which + // a concurrent model write may hold while it busy-waits on the + // very write lock the barrier above just took. Taking that mutex + // here, with the write lock held, would invert the lock order and + // park both sides until the busy timeout expires. + if err := tx.TX.GetContext(ctx, &row, ` + SELECT ic.id, ic.config + FROM identity_credentials ic + WHERE ic.identity_id = ? + AND ic.nid = ? + AND ic.identity_credential_type_id = ?`+extraSQL, selectArgs...); err != nil { + return sqlcon.HandleError(err) + } + } else if err := tx.RawQuery(selectQuery, selectArgs...).First(&row); err != nil { + return sqlcon.HandleError(err) + } + + newConfig, err := mutate(row.Config) + if err != nil { + return err + } + + // A no-op mutation needs no write; the lock still linearizes it with + // concurrent writers. + if !jsonContentEqual(row.Config, newConfig) { + updateQuery := ` + UPDATE identity_credentials + SET config = ? + WHERE id = ? AND nid = ?` + extraSQL + + updateArgs := append([]any{sqlxx.JSONRawMessage(newConfig), row.ID, nid}, extraArgs...) + if err := tx.RawQuery(updateQuery, updateArgs...).Exec(); err != nil { + return sqlcon.HandleError(err) + } + wrote = true + } + + // The identifier rows are derived state of the config; sync them under + // the same lock even when the config write was skipped as a no-op, so + // a previously diverged set converges. + if o.DeriveIdentifiers != nil { + derived, err := o.DeriveIdentifiers(newConfig) + if err != nil { + return err + } + conn := &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: tx} + proto := identity.CredentialIdentifier{ + IdentityID: new(identityID), + IdentityCredentialsID: row.ID, + IdentityCredentialsTypeID: typeID, + NID: nid, + } + changed, err := syncDerivedIdentifiers(ctx, conn, ct, proto, derived, o.ExtraColumns) + if err != nil { + return err + } + // An identifier-only change still updates the identity. + wrote = wrote || changed + } + return nil + }); err != nil { + return err + } + + // Only a real write is an identity update; the no-op path changed nothing. + if wrote { + span.AddEvent(events.NewIdentityUpdated(ctx, identityID)) + } + return nil +} + +// jsonContentEqual reports whether a and b encode structurally equal JSON +// documents. Byte equality is not enough: databases normalize stored JSON +// (jsonb key order on PostgreSQL/CockroachDB, binary JSON on MySQL), so a +// re-marshaled but semantically unchanged config rarely matches byte-for-byte. +func jsonContentEqual(a, b []byte) bool { + var av, bv any + if json.Unmarshal(a, &av) != nil || json.Unmarshal(b, &bv) != nil { + return false + } + return reflect.DeepEqual(av, bv) +} + +// syncDerivedIdentifiers reconciles a credential row's identifier rows with +// the derived set, inside the caller's locked transaction, and reports +// whether any row changed. proto is the template for inserted rows: the +// credential/type/identity/network references are already set, and each +// insert copies it and fills in ID and Identifier. Identifiers are +// normalized like createIdentityCredentials does; an unchanged set writes +// nothing, a changed set is replaced wholesale. extra narrows the read and +// the delete like the caller's config statements and is written explicitly +// on the inserts (the cloud multi-region persister pins crdb_region; the +// explicit write spares the infer_rbr_region_col_using_constraint lookup, +// mirroring UpdateIdentity). +func syncDerivedIdentifiers(ctx context.Context, conn *batch.TracerConnection, ct identity.CredentialsType, proto identity.CredentialIdentifier, derived []string, extra []identity.ExtraColumn) (changed bool, err error) { + tx := conn.Connection + + var extraSQLBuilder strings.Builder + extraArgs := make([]any, 0, len(extra)) + for _, col := range extra { + _, _ = fmt.Fprintf(&extraSQLBuilder, " AND %s = ?", col.K) + extraArgs = append(extraArgs, col.V) + } + extraSQL := extraSQLBuilder.String() + + target := make([]string, 0, len(derived)) + for _, identifier := range derived { + identifier = NormalizeIdentifier(ct, identifier) + if identifier == "" { + return false, errors.WithStack(herodot.ErrMisconfiguration().WithReason("Unable to sync identity credential identifiers with missing or empty identifier.")) + } + target = append(target, identifier) + } + slices.Sort(target) + target = slices.Compact(target) + + var rows []struct { + Identifier string `db:"identifier"` + } + readQuery := `SELECT identifier FROM identity_credential_identifiers WHERE identity_credential_id = ? AND nid = ?` + extraSQL + readArgs := append([]any{proto.IdentityCredentialsID, proto.NID}, extraArgs...) + if tx.Dialect.Name() == "sqlite3" { + // Read via the underlying sqlx transaction, not pop's finders: this + // transaction already holds SQLite's write lock (the caller wrote the + // config, or took it up front via the write barrier), and pop's + // SQLite finders serialize on a process-wide statement mutex that a + // concurrent model write may hold while busy-waiting on that write + // lock — a lock-order inversion that parks both sides until the busy + // timeout expires. + if err := tx.TX.SelectContext(ctx, &rows, readQuery, readArgs...); err != nil { + return false, sqlcon.HandleError(err) + } + } else if err := tx.RawQuery(readQuery, readArgs...).All(&rows); err != nil { + return false, sqlcon.HandleError(err) + } + current := make([]string, len(rows)) + for i, r := range rows { + current[i] = r.Identifier + } + slices.Sort(current) + + if slices.Equal(current, target) { + return false, nil + } + + // Replace wholesale, deleting first so values kept across the change do + // not trip the (nid, type, identifier) unique index on insert. + if err := tx.RawQuery( + `DELETE FROM identity_credential_identifiers WHERE identity_credential_id = ? AND nid = ?`+extraSQL, + append([]any{proto.IdentityCredentialsID, proto.NID}, extraArgs...)...).Exec(); err != nil { + return false, sqlcon.HandleError(err) + } + identifiers := make([]*identity.CredentialIdentifier, len(target)) + for i, identifier := range target { + ci := proto + // The ID stays Nil so batch.Create generates it: gen_random_uuid() + // in the statement on CockroachDB, a client-side UUID elsewhere. + ci.Identifier = identifier + identifiers[i] = &ci + } + // One batched INSERT keeps the lock hold time flat in the identifier + // count. A duplicate identifier owned by another identity surfaces here + // as sqlcon.ErrUniqueViolation (batch.Create normalizes driver errors + // via sqlcon.HandleError) and rolls back the whole transaction. + if err := batch.Create(ctx, conn, identifiers, batch.WithExtraColumns(extra)); err != nil { + return false, err + } + return true, nil +} + +func (p *IdentityPersister) UpdateIdentity(ctx context.Context, i *identity.Identity, mods ...identity.UpdateIdentityModifier) (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.validateIdentity(ctx, i); err != nil { + return err + } + + o := identity.NewUpdateIdentityOptions(mods) + + span.SetAttributes(attribute.Bool("update.minimize_diff", o.FromDatabase() != nil)) + + i.NID = p.NetworkID(ctx) + i.UpdatedAt = time.Now().UTC().Truncate(time.Microsecond) + // external_id backs a unique index, so including it in the UPDATE ... SET + // list makes the database plan a uniqueness check for that index even when + // the value is unchanged. external_id almost never changes on an update, so + // when we have the previous row to compare against and it is unchanged, we + // leave it out of the SET list and the database skips the check. When we + // have no snapshot to compare against, or external_id did change, we write + // it and accept the check. + externalIDUnchanged := o.FromDatabase() != nil && o.FromDatabase().ExternalID == i.ExternalID + 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 externalIDUnchanged { + err = update.GenericExcept(WithTransaction(ctx, tx), tx, p.r.Tracer(ctx).Tracer(), i, "external_id") + } else { + err = update.Generic(WithTransaction(ctx, tx), tx, p.r.Tracer(ctx).Tracer(), i) + } + if err != nil { + return err + } + + var identityCreds map[identity.CredentialsType]identity.Credentials + p.normalizeAllAddresses(ctx, i) + if o.FromDatabase() != nil { + if o.FromDatabase().ID != i.ID { + return errors.New("mismatched identity ID: this is a bug") + } + var err error + i.RecoveryAddresses, err = updateAssociationWith(ctx, p, o.ExtraColumns(), o.FromDatabase().RecoveryAddresses, i.RecoveryAddresses) + if err != nil { + return err + } + i.VerifiableAddresses, err = updateAssociationWith(ctx, p, o.ExtraColumns(), o.FromDatabase().VerifiableAddresses, i.VerifiableAddresses) + if err != nil { + return err + } + identityCreds = o.FromDatabase().Credentials + } else { + i.RecoveryAddresses, err = updateAssociation(ctx, p, o.ExtraColumns(), i, i.RecoveryAddresses) + if err != nil { + return err + } + i.VerifiableAddresses, err = updateAssociation(ctx, p, o.ExtraColumns(), i, i.VerifiableAddresses) + if err != nil { + return err + } + + creds, err := QueryForCredentials(tx, + Where{"identity_credentials.identity_id = ?", []interface{}{i.ID}}, + Where{"identity_credentials.nid = ?", []interface{}{p.NetworkID(ctx)}}) + if err != nil { + return err + } + if c, found := creds[i.ID]; found { + identityCreds = c + } + + if len(identityCreds) > 0 { + // Create temporary identity to run migrations + tempIdentity := &identity.Identity{ + ID: i.ID, + Credentials: identityCreds, + } + if err := identity.UpgradeCredentials(tempIdentity); err != nil { + return err + } + identityCreds = tempIdentity.Credentials + } + } + + // Excluded credential types are invisible to the diff: their rows are + // neither deleted, recreated, nor updated. Their database entries are + // kept aside and merged back into the returned identity below. + excludedTypes := o.ExcludedCredentialTypes() + excludedCreds := make(map[identity.CredentialsType]identity.Credentials, len(excludedTypes)) + + oldCredentials := make([]identity.Credentials, 0, len(identityCreds)) + for ct, cred := range identityCreds { + if slices.Contains(excludedTypes, ct) { + excludedCreds[ct] = cred + continue + } + oldCredentials = append(oldCredentials, cred) + } + + // Upgrade incoming credentials to ensure proper comparison with upgraded DB credentials + if err := identity.UpgradeCredentials(i); err != nil { + return err + } + + // Convert new credentials map to slice + newCredentials := make([]identity.Credentials, 0, len(i.Credentials)) + for ct, cred := range i.Credentials { + if slices.Contains(excludedTypes, ct) { + continue + } + newCredentials = append(newCredentials, cred) + } + + updatedCreds, err := p.updateCredentialsAssociation(ctx, o.ExtraColumns(), i.ID, oldCredentials, newCredentials) + if err != nil { + return err + } + // The excluded types' rows were left untouched; surface their database + // state on the returned identity instead of the in-memory copy. + maps.Copy(updatedCreds, excludedCreds) + i.Credentials = updatedCreds + return nil + })); 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) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteIdentity", + trace.WithAttributes( + attribute.Stringer("identity.id", id), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + tableName := new(identity.Identity).TableName(ctx) + // This @index hint is CockroachDB-only; PostgreSQL and YugabyteDB use the + // unadorned table name. + 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 = ?", tableName), + id, + nid, + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + 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) { + // This function is only used internally to cleanup partially created identities, + // when creating a batch of identities at once and some failed to be fully created. + // This act should not be observable externally and thus we do not emit an event. + + if len(ids) == 0 { + return nil + } + + 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) + // This @index hint is CockroachDB-only; PostgreSQL and YugabyteDB use the + // unadorned table name. + 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 +} + +func (p *IdentityPersister) GetIdentity(ctx context.Context, id uuid.UUID, expand identity.Expandables) (_ *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetIdentity", + trace.WithAttributes( + attribute.Stringer("identity.id", id), + attribute.Stringer("network.id", p.NetworkID(ctx)), + attribute.StringSlice("expand", expand.ToEager()))) + defer otelx.End(span, &err) + + var i identity.Identity + if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&i); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := p.HydrateIdentityAssociations(ctx, &i, expand); err != nil { + return nil, err + } + + return &i, nil +} + +func (p *IdentityPersister) GetIdentityConfidential(ctx context.Context, id uuid.UUID) (res *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetIdentityConfidential") + defer otelx.End(span, &err) + + return p.GetIdentity(ctx, id, identity.ExpandEverything) +} + +func (p *IdentityPersister) FindIdentityByExternalID(ctx context.Context, externalID string, expand identity.Expandables) (res *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindIdentityByExternalID", + trace.WithAttributes( + attribute.String("identity.external_id", externalID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + var i identity.Identity + if err := p.GetConnection(ctx).Where("external_id = ? AND nid = ?", externalID, p.NetworkID(ctx)).First(&i); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := p.HydrateIdentityAssociations(ctx, &i, expand); err != nil { + return nil, err + } + + return &i, nil +} + +func (p *IdentityPersister) FindVerifiableAddressByValue(ctx context.Context, via string, value string) (_ *identity.VerifiableAddress, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindVerifiableAddressByValue", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + normalized := x.GracefulNormalization(value) + limit := 1 + if normalized != value { + limit = 2 + } + var addresses []identity.VerifiableAddress + if err := p.GetConnection(ctx).RawQuery( + "SELECT * FROM identity_verifiable_addresses WHERE nid = ? AND via = ? AND value IN (?,?) LIMIT ?", + p.NetworkID(ctx), via, value, normalized, limit, + ).All(&addresses); err != nil { + return nil, sqlcon.HandleError(err) + } + + if len(addresses) == 0 { + return nil, sqlcon.HandleError(sqlcon.ErrNoRows()) + } + + addr := PreferExactMatch(addresses, value, func(a identity.VerifiableAddress) string { return a.Value }) + return &addr, nil +} + +func (p *IdentityPersister) FindRecoveryAddressByValue(ctx context.Context, via, value string) (_ *identity.RecoveryAddress, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindRecoveryAddressByValue", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + normalized := x.GracefulNormalization(value) + limit := 1 + if normalized != value { + limit = 2 + } + var addresses []identity.RecoveryAddress + if err := p.GetConnection(ctx).RawQuery( + "SELECT * FROM identity_recovery_addresses WHERE nid = ? AND via = ? AND value IN (?,?) LIMIT ?", + p.NetworkID(ctx), via, value, normalized, limit, + ).All(&addresses); err != nil { + return nil, sqlcon.HandleError(err) + } + + if len(addresses) == 0 { + return nil, sqlcon.HandleError(sqlcon.ErrNoRows()) + } + + addr := PreferExactMatch(addresses, value, func(a identity.RecoveryAddress) string { return a.Value }) + return &addr, nil +} + +// FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue returns the +// values of all recovery addresses for an identity if at least one of those +// addresses matches the provided value. +func (p *IdentityPersister) FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) (recoveryAddresses []string, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + // SQL explanation: + // 1. Find a row (`B`) with the value matching `anyRecoveryAddress`. + // This row has an identity id (`B.identity_id`). + // 2. Find all rows (`A`) with this identity id. + // Meaning: find all recovery addresses for this identity. + // The result set includes the user provided address (`anyRecoveryAddress`). + // NOTE: Should we exclude from the result set the login address for more security? + // + // This is all done in one query with a self-join. + // We also bound the results for safety. + via := identity.AddressTypeSMS + if strings.ContainsRune(anyRecoveryAddress, '@') { + via = identity.AddressTypeEmail + } + + nid := p.NetworkID(ctx) + err = p.GetConnection(ctx).RawQuery(` +SELECT A.value +FROM identity_recovery_addresses A +JOIN identity_recovery_addresses B + ON A.identity_id = B.identity_id + AND A.nid = B.nid +WHERE A.nid = ? + AND B.via = ? + AND B.value IN (?, ?) +ORDER BY A.value +LIMIT 10 + `, + nid, + via, + x.GracefulNormalization(anyRecoveryAddress), + anyRecoveryAddress, + ). + All(&recoveryAddresses) + if err != nil { + return nil, sqlcon.HandleError(err) + } + return recoveryAddresses, nil +} + +func (p *IdentityPersister) VerifyAddress(ctx context.Context, code string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAddress", + trace.WithAttributes( + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + newCode, err := otp.New() + if err != nil { + return err + } + + count, err := p.GetConnection(ctx).RawQuery( + // #nosec G201 -- TableName is static + fmt.Sprintf( + "UPDATE %s SET status = ?, verified = true, verified_at = ?, code = ? WHERE nid = ? AND code = ? AND expires_at > ?", + new(identity.VerifiableAddress).TableName(), + ), + identity.VerifiableAddressStatusCompleted, + time.Now().UTC().Round(time.Second), + newCode, + p.NetworkID(ctx), + code, + time.Now().UTC(), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + + if count == 0 { + return sqlcon.HandleError(sqlcon.ErrNoRows()) + } + + return nil +} + +func (p *IdentityPersister) UpdateVerifiableAddress(ctx context.Context, address *identity.VerifiableAddress, updateColumns ...string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateVerifiableAddress", + trace.WithAttributes( + attribute.Stringer("identity.id", address.IdentityID), + attribute.Stringer("network.id", p.NetworkID(ctx)), + attribute.StringSlice("columns", updateColumns))) + defer otelx.End(span, &err) + + address.NID = p.NetworkID(ctx) + address.Value = x.GracefulNormalization(address.Value) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), address, updateColumns...) +} + +func (p *IdentityPersister) validateIdentity(ctx context.Context, i *identity.Identity) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.validateIdentity", + trace.WithAttributes( + attribute.Stringer("identity.id", i.ID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + if err := p.r.IdentityValidator().ValidateWithRunner(ctx, i); err != nil { + if _, ok := errorsx.Cause(err).(*jsonschema.ValidationError); ok { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + return err + } + + return nil +} + +func (p *IdentityPersister) InjectTraitsSchemaURL(ctx context.Context, i *identity.Identity) (err error) { + // This trace is more noisy than it's worth in diagnostic power. + // ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InjectTraitsSchemaURL") + // defer otelx.End(span, &err) + + ss, err := p.r.IdentityTraitsSchemas(ctx) + if err != nil { + return err + } + s, err := ss.GetByID(i.SchemaID) + if err != nil { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf( + `The JSON Schema "%s" for this identity's traits could not be found.`, i.SchemaID)) + } + i.SchemaURL = s.SchemaURL(p.r.Config().SelfPublicURL(ctx)).String() + return nil +} + +type credentialTypesCache struct { + byID map[uuid.UUID]identity.CredentialsType + byName map[identity.CredentialsType]uuid.UUID +} + +// credentialTypesCachePtr holds the loaded cache. It is nil until the first +// successful load. Reads and writes are lock-free (atomic.Pointer). +// In the rare case where multiple goroutines see a nil pointer simultaneously, +// they each load from the database independently and store the result; the +// last store wins, but all results are identical. After the first successful +// store, all subsequent calls take the fast path. +var credentialTypesCachePtr atomic.Pointer[credentialTypesCache] + +// ensureCredentialTypesLoaded loads the credential type mappings from the +// database on the first successful call. If the load fails the pointer stays +// nil and the next call retries, so a transient DB error does not permanently +// break the process. +func ensureCredentialTypesLoaded(con *pop.Connection) (*credentialTypesCache, error) { + if c := credentialTypesCachePtr.Load(); c != nil { + return c, nil + } + c, err := loadCredentialTypes(con) + if err != nil { + return nil, err + } + credentialTypesCachePtr.Store(c) + return c, nil +} + +func FindIdentityCredentialsTypeByID(con *pop.Connection, id uuid.UUID) (identity.CredentialsType, error) { + c, err := ensureCredentialTypesLoaded(con) + if err != nil { + return "", err + } + + result, found := c.byID[id] + if !found { + return "", errors.WithStack(herodot.ErrInternalServerError().WithReasonf(`No identity credential type id "%s" could be found, this is a code bug.`, id)) + } + + return result, nil +} + +func FindIdentityCredentialsTypeByName(con *pop.Connection, ct identity.CredentialsType) (uuid.UUID, error) { + c, err := ensureCredentialTypesLoaded(con) + if err != nil { + return uuid.Nil, err + } + + result, found := c.byName[ct] + if !found { + return uuid.Nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf(`No identity credential type "%s" could be found, this is a code bug.`, ct)) + } + + return result, nil +} + +func loadCredentialTypes(con *pop.Connection) (_ *credentialTypesCache, err error) { + ctx, span := trace.SpanFromContext(con.Context()).TracerProvider().Tracer("").Start(con.Context(), "persistence.sql.identity.loadCredentialTypes") + defer otelx.End(span, &err) + _ = ctx + + var tt []identity.CredentialsTypeTable + if err := con.WithContext(ctx).All(&tt); err != nil { + return nil, sqlcon.HandleError(err) + } + + if len(tt) == 0 { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("The SQL adapter failed to return any credentials_type. This is a bug in the code/db.")) + } + + c := &credentialTypesCache{ + byID: make(map[uuid.UUID]identity.CredentialsType, 16), + byName: make(map[identity.CredentialsType]uuid.UUID, 16), + } + for _, t := range tt { + c.byID[t.ID] = t.Name + c.byName[t.Name] = t.ID + } + + return c, nil +} diff --git a/persistence/sql/identity/persister_identity_test.go b/persistence/sql/identity/persister_identity_test.go new file mode 100644 index 0000000..5c8a10e --- /dev/null +++ b/persistence/sql/identity/persister_identity_test.go @@ -0,0 +1,122 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity_test + +// Since: +// - This is a separate test package +// - Go creates one executable per test package +// - Go global variables are isolated per process (i.e. executable) +// - This test uses its own database +// , it means that this test is independent from the others and cannot interfere with them. +// We do not need to clear the global maps at the start of the test: they are still pristine (unset). + +import ( + "fmt" + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + id "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/sql/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" + "github.com/ory/x/dbal" + "github.com/ory/x/sqlxx" +) + +// TestCreateIdentities_WritesExtraColumns verifies that WithExtraColumns is +// forwarded from CreateIdentities down to the batch INSERT. +func TestCreateIdentities_WritesExtraColumns(t *testing.T) { + _, reg := pkg.NewRegistryDefaultWithDSN(t, dbal.NewSQLiteTestDatabase(t)) + _, p := testhelpers.NewNetwork(t, t.Context(), reg.Persister()) + + require.NoError(t, + p.GetConnection(t.Context()).RawQuery(`ALTER TABLE identities ADD COLUMN extra_foo TEXT`).Exec(), + ) + + ctx := testhelpers.WithDefaultIdentitySchemaFromRaw(t.Context(), []byte(`{"$id":"test","type":"object"}`)) + + ident := id.NewIdentity("") + require.NoError(t, p.CreateIdentities(ctx, []*id.Identity{ident}, + id.WithExtraColumns([]id.ExtraColumn{{K: "extra_foo", V: "bar"}}), + )) + + var got string + require.NoError(t, + p.GetConnection(t.Context()).RawQuery( + `SELECT extra_foo FROM identities WHERE id = ?`, ident.ID, + ).First(&got), + ) + assert.Equal(t, "bar", got) +} + +func TestNonStandardCredentialTypes(t *testing.T) { + _, reg := pkg.NewRegistryDefaultWithDSN(t, dbal.NewSQLiteTestDatabase(t)) + _, p := testhelpers.NewNetwork(t, t.Context(), reg.Persister()) + + // Replace the content of the table with custom UUIDs. This relies on the + // process-global credential-types cache being pristine, which the header + // comment's package isolation guarantees — no other test in this package + // touches the cache first. + { + require.NoError(t, p.GetConnection(t.Context()).RawQuery(`DELETE FROM identity_credential_types`, x.NewUUID()).Exec()) + + // Test the case of empty credentials in the database, so that we can exercise the case of loading credentials failing, + // and then subsequent loads succeed, and the service does not get suck. + _, err := identity.FindIdentityCredentialsTypeByName(p.GetConnection(t.Context()), id.CredentialsTypeOIDC) + require.Error(t, err) + + // Now fill the table with non-standard UUIDs. + q := strings.Builder{} + q.WriteString(`INSERT INTO identity_credential_types (id, name) VALUES `) + for i, ct := range id.AllCredentialTypes { + fmt.Fprintf(&q, `('%s', '%s')`, x.NewUUID(), ct) + if i < len(id.AllCredentialTypes)-1 { + q.WriteRune(',') + } + } + require.NoError(t, p.GetConnection(t.Context()).RawQuery(q.String()).Exec()) + } + + // Valid credential types are found. + for _, ct := range id.AllCredentialTypes { + t.Run("type="+ct.String(), func(t *testing.T) { + id, err := identity.FindIdentityCredentialsTypeByName(p.GetConnection(t.Context()), ct) + require.NoError(t, err) + + require.NotEqual(t, uuid.Nil, id) + name, err := identity.FindIdentityCredentialsTypeByID(p.GetConnection(t.Context()), id) + require.NoError(t, err) + + assert.Equal(t, ct, name) + }) + } + + // Invalid credential types are not found. + _, err := identity.FindIdentityCredentialsTypeByName(p.GetConnection(t.Context()), "unknown") + require.Error(t, err) + + _, err = identity.FindIdentityCredentialsTypeByID(p.GetConnection(t.Context()), x.NewUUID()) + require.Error(t, err) + + // Create an identity and find it by credential identifier. + ctx := testhelpers.WithDefaultIdentitySchemaFromRaw(t.Context(), []byte(`{"$id":"test","type":"object"}`)) + email := "test-" + x.NewUUID().String() + "@example.com" + i := id.NewIdentity("") + i.SetCredentials(id.CredentialsTypePassword, id.Credentials{ + Type: id.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{}`), + }) + require.NoError(t, p.CreateIdentity(ctx, i)) + + actualIdentity, actualCreds, err := p.FindByCredentialsIdentifier(ctx, id.CredentialsTypePassword, email) + require.NoError(t, err) + assert.Equal(t, i.ID, actualIdentity.ID) + assert.Equal(t, []string{email}, actualCreds.Identifiers) +} diff --git a/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb6.json b/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb6.json new file mode 100644 index 0000000..ce936fc --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb6.json @@ -0,0 +1,18 @@ +{ + "id": "0149ce5f-76a8-4efe-b2e3-431b8c6cceb6", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb7.json b/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb7.json new file mode 100644 index 0000000..2cf7629 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/0149ce5f-76a8-4efe-b2e3-431b8c6cceb7.json @@ -0,0 +1,18 @@ +{ + "id": "0149ce5f-76a8-4efe-b2e3-431b8c6cceb7", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbarbar@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json new file mode 100644 index 0000000..252268b --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json @@ -0,0 +1,14 @@ +{ + "id": "196d8c1e-4f04-40f0-94b3-5ec43996b28a", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json new file mode 100644 index 0000000..cc42a52 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json @@ -0,0 +1,18 @@ +{ + "id": "28ff0031-190b-4253-bd15-14308dec013e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbarbarfoo@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json b/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json new file mode 100644 index 0000000..9a58022 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json @@ -0,0 +1,14 @@ +{ + "id": "2ae6a5a7-2983-49e7-a4d8-7740b37c88cb", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "d7b10@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/308929d3-41a2-43fe-a33c-75308539d841.json b/persistence/sql/migratest/fixtures/identity/308929d3-41a2-43fe-a33c-75308539d841.json new file mode 100644 index 0000000..798d0ef --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/308929d3-41a2-43fe-a33c-75308539d841.json @@ -0,0 +1,18 @@ +{ + "id": "308929d3-41a2-43fe-a33c-75308539d841", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json b/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json new file mode 100644 index 0000000..997e385 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json @@ -0,0 +1,14 @@ +{ + "id": "359963ec-b09b-4ea0-aece-fb4dd95f304a", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "d7b11@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json new file mode 100644 index 0000000..61caba8 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json @@ -0,0 +1,50 @@ +{ + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "foo2@ory.sh", + "foo@ory.sh" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=131072,t=2,p=1$lQFPaKxXqPL56/mU7vRi4w$6aldHyBnURt8sP8+xu41Ng" + }, + "version": 1, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + "totp": { + "type": "totp", + "identifiers": [], + "config": { + "totp_url": "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP\u0026issuer=Example" + }, + "version": 0, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + }, + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json new file mode 100644 index 0000000..5e96c83 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json @@ -0,0 +1,67 @@ +{ + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "foo1@ory.sh", + "foobar@ory.sh" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=131072,t=2,p=1$lQFPaKxXqPL56/mU7vRi4w$6aldHyBnURt8sP8+xu41Ng" + }, + "version": 1, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + }, + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json new file mode 100644 index 0000000..2591e1e --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json @@ -0,0 +1,14 @@ +{ + "id": "d7b9addb-ac15-4bc2-9fa5-562e0bf48755", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "d7b9@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json new file mode 100644 index 0000000..9a208bf --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json @@ -0,0 +1,14 @@ +{ + "id": "ed253b2c-48ed-4c58-9b6f-1dc963c30a66", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/identity_recovery_address/b8293f1c-010f-45d9-b809-f3fc5365ba80.json b/persistence/sql/migratest/fixtures/identity_recovery_address/b8293f1c-010f-45d9-b809-f3fc5365ba80.json new file mode 100644 index 0000000..5ae9735 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity_recovery_address/b8293f1c-010f-45d9-b809-f3fc5365ba80.json @@ -0,0 +1,7 @@ +{ + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/identity_verification_address/45e867e9-2745-4f16-8dd4-84334a252b61.json b/persistence/sql/migratest/fixtures/identity_verification_address/45e867e9-2745-4f16-8dd4-84334a252b61.json new file mode 100644 index 0000000..ae27511 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity_verification_address/45e867e9-2745-4f16-8dd4-84334a252b61.json @@ -0,0 +1,9 @@ +{ + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/identity_verification_address/b2d59320-8564-4400-a39f-a22a497a23f1.json b/persistence/sql/migratest/fixtures/identity_verification_address/b2d59320-8564-4400-a39f-a22a497a23f1.json new file mode 100644 index 0000000..5a672bd --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity_verification_address/b2d59320-8564-4400-a39f-a22a497a23f1.json @@ -0,0 +1,9 @@ +{ + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/identity_verification_address/c2427b6d-312b-46d9-9285-536db7ae11fd.json b/persistence/sql/migratest/fixtures/identity_verification_address/c2427b6d-312b-46d9-9285-536db7ae11fd.json new file mode 100644 index 0000000..7981cc2 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity_verification_address/c2427b6d-312b-46d9-9285-536db7ae11fd.json @@ -0,0 +1,9 @@ +{ + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/identity_verification_address/d4718a67-aec2-418d-8173-6ebc7bde3b86.json b/persistence/sql/migratest/fixtures/identity_verification_address/d4718a67-aec2-418d-8173-6ebc7bde3b86.json new file mode 100644 index 0000000..918a24a --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity_verification_address/d4718a67-aec2-418d-8173-6ebc7bde3b86.json @@ -0,0 +1,9 @@ +{ + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json new file mode 100644 index 0000000..e695ce9 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json @@ -0,0 +1,6 @@ +{ + "id": "bd292366-af32-4ba6-bdf0-11d6d1a217f3", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z", + "identity_id": "28ff0031-190b-4253-bd15-14308dec013e" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json new file mode 100644 index 0000000..837de1c --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json @@ -0,0 +1,19 @@ +{ + "id": "00b1517f-2467-4aaf-b0a5-82b4a27dcaf5", + "organization_id": null, + "oauth2_login_challenge": "challenge data", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json new file mode 100644 index 0000000..eeaa7f9 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json @@ -0,0 +1,18 @@ +{ + "id": "0bc96cc9-dda4-4700-9e42-35731f2af91e", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json new file mode 100644 index 0000000..cd6619e --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json @@ -0,0 +1,18 @@ +{ + "id": "1fb23c75-b809-42cc-8984-6ca2d0a1192f", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal2", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json new file mode 100644 index 0000000..d5c8bac --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json @@ -0,0 +1,18 @@ +{ + "id": "202c1981-1e25-47f0-8764-75ad506c2bec", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json new file mode 100644 index 0000000..5aca174 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json @@ -0,0 +1,18 @@ +{ + "id": "349c945a-60f8-436a-a301-7a42c92604f9", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login?login_challenge=3caddfd599034bce83ffcae36f42dff7", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal2", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json new file mode 100644 index 0000000..83a1fd6 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json @@ -0,0 +1,18 @@ +{ + "id": "38caf592-b042-4551-b92f-8d5223c2a4e2", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal2", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json new file mode 100644 index 0000000..7a56b04 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json @@ -0,0 +1,18 @@ +{ + "id": "3a9ea34f-0f12-469b-9417-3ae5795a7baa", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json new file mode 100644 index 0000000..392e31a --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json @@ -0,0 +1,19 @@ +{ + "id": "43c99182-bb67-47e1-b564-bb23bd8d4393", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login?prompt=login\u0026return_to=http%3A%2F%2F127.0.0.1%3A4455%2F.ory%2Fkratos%2Fpublic%2Fself-service%2Fbrowser%2Fflows%2Fsettings%2Fstrategies%2Fprofile%3Frequest%3D74fd6c53-7651-453e-90b8-2c5adbf911bb", + "return_to": "http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=74fd6c53-7651-453e-90b8-2c5adbf911bb", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": true, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json new file mode 100644 index 0000000..d9244b3 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json @@ -0,0 +1,18 @@ +{ + "id": "47edd3a8-0998-4779-9469-f4b8ee4430df", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json new file mode 100644 index 0000000..8307d8f --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json @@ -0,0 +1,18 @@ +{ + "id": "56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json new file mode 100644 index 0000000..114fc6b --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json @@ -0,0 +1,18 @@ +{ + "id": "6d387820-f2f4-4f9f-9980-a90d89e7811f", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json new file mode 100644 index 0000000..27c6540 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json @@ -0,0 +1,18 @@ +{ + "id": "916ded11-aa64-4a27-b06e-96e221a509d7", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json new file mode 100644 index 0000000..bfcc30b --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json @@ -0,0 +1,18 @@ +{ + "id": "99974ce6-388c-4669-a95a-7757ee724020", + "organization_id": null, + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json new file mode 100644 index 0000000..0023c80 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json @@ -0,0 +1,18 @@ +{ + "id": "b1fac7fb-d016-4a06-a7fe-e4eab2a0429f", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json new file mode 100644 index 0000000..e48d123 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json @@ -0,0 +1,19 @@ +{ + "id": "cccccccc-dda4-4700-9e42-35731f2af911", + "organization_id": null, + "oauth2_login_challenge": "challenge data", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json new file mode 100644 index 0000000..ca25b30 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json @@ -0,0 +1,19 @@ +{ + "id": "cccccccc-dda4-4700-9e42-35731f2af91e", + "organization_id": null, + "oauth2_login_challenge": "challenge data", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json new file mode 100644 index 0000000..6b60fce --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json @@ -0,0 +1,18 @@ +{ + "id": "d6aa1f23-88c9-4b9b-a850-392f48c7f9e8", + "organization_id": null, + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/recovery_code/8f75f5d9-9cb4-4848-9a73-9344f686f8a6.json b/persistence/sql/migratest/fixtures/recovery_code/8f75f5d9-9cb4-4848-9a73-9344f686f8a6.json new file mode 100644 index 0000000..0c2de81 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_code/8f75f5d9-9cb4-4848-9a73-9344f686f8a6.json @@ -0,0 +1,7 @@ +{ + "id": "8f75f5d9-9cb4-4848-9a73-9344f686f8a6", + "recovery_address": null, + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z", + "identity_id": "308929d3-41a2-43fe-a33c-75308539d841" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/0d14427f-e16d-43a5-8695-8278bf85d4eb.json b/persistence/sql/migratest/fixtures/recovery_flow/0d14427f-e16d-43a5-8695-8278bf85d4eb.json new file mode 100644 index 0000000..0d93c18 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/0d14427f-e16d-43a5-8695-8278bf85d4eb.json @@ -0,0 +1,14 @@ +{ + "id": "0d14427f-e16d-43a5-8695-8278bf85d4eb", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/13178936-095a-466b-abe0-36d977d3dc18.json b/persistence/sql/migratest/fixtures/recovery_flow/13178936-095a-466b-abe0-36d977d3dc18.json new file mode 100644 index 0000000..f07c2f2 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/13178936-095a-466b-abe0-36d977d3dc18.json @@ -0,0 +1,14 @@ +{ + "id": "13178936-095a-466b-abe0-36d977d3dc18", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/4963f305-e874-4a68-8424-a00bec679e7b.json b/persistence/sql/migratest/fixtures/recovery_flow/4963f305-e874-4a68-8424-a00bec679e7b.json new file mode 100644 index 0000000..04aa38a --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/4963f305-e874-4a68-8424-a00bec679e7b.json @@ -0,0 +1,14 @@ +{ + "id": "4963f305-e874-4a68-8424-a00bec679e7b", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/recovery", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee891.json b/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee891.json new file mode 100644 index 0000000..c6aa555 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee891.json @@ -0,0 +1,14 @@ +{ + "id": "68fb4010-84a9-4d1e-9f92-2705978ee891", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee89e.json b/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee89e.json new file mode 100644 index 0000000..85fdd92 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/68fb4010-84a9-4d1e-9f92-2705978ee89e.json @@ -0,0 +1,14 @@ +{ + "id": "68fb4010-84a9-4d1e-9f92-2705978ee89e", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/recovery", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_flow/87e871e1-a45f-4ed0-ba4e-a03063c774dc.json b/persistence/sql/migratest/fixtures/recovery_flow/87e871e1-a45f-4ed0-ba4e-a03063c774dc.json new file mode 100644 index 0000000..5c70779 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_flow/87e871e1-a45f-4ed0-ba4e-a03063c774dc.json @@ -0,0 +1,14 @@ +{ + "id": "87e871e1-a45f-4ed0-ba4e-a03063c774dc", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/recovery_token/1b667e6d-8fda-4194-a765-08185185d7e4.json b/persistence/sql/migratest/fixtures/recovery_token/1b667e6d-8fda-4194-a765-08185185d7e4.json new file mode 100644 index 0000000..3445efb --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_token/1b667e6d-8fda-4194-a765-08185185d7e4.json @@ -0,0 +1,7 @@ +{ + "id": "1b667e6d-8fda-4194-a765-08185185d7e4", + "recovery_address": null, + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "identity_id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e" +} diff --git a/persistence/sql/migratest/fixtures/recovery_token/5529d454-2946-404e-b681-d950f8657fd0.json b/persistence/sql/migratest/fixtures/recovery_token/5529d454-2946-404e-b681-d950f8657fd0.json new file mode 100644 index 0000000..0442a81 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_token/5529d454-2946-404e-b681-d950f8657fd0.json @@ -0,0 +1,7 @@ +{ + "id": "5529d454-2946-404e-b681-d950f8657fd0", + "recovery_address": null, + "expires_at": "2000-01-01T00:00:00Z", + "issued_at": "2000-01-01T00:00:00Z", + "identity_id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e" +} diff --git a/persistence/sql/migratest/fixtures/recovery_token/77ca3f5c-cd39-488b-9f1d-cc7166d14bdc.json b/persistence/sql/migratest/fixtures/recovery_token/77ca3f5c-cd39-488b-9f1d-cc7166d14bdc.json new file mode 100644 index 0000000..1f1c0a4 --- /dev/null +++ b/persistence/sql/migratest/fixtures/recovery_token/77ca3f5c-cd39-488b-9f1d-cc7166d14bdc.json @@ -0,0 +1,7 @@ +{ + "id": "77ca3f5c-cd39-488b-9f1d-cc7166d14bdc", + "recovery_address": null, + "expires_at": "2000-01-01T00:00:00Z", + "issued_at": "2000-01-01T00:00:00Z", + "identity_id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e" +} diff --git a/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json new file mode 100644 index 0000000..5e429ce --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json @@ -0,0 +1,5 @@ +{ + "id": "f1f66a69-ce02-4a12-9591-9e02dda30a0d", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json new file mode 100644 index 0000000..beb3815 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json @@ -0,0 +1,14 @@ +{ + "id": "05a7f09d-4ef3-41fb-958a-6ad74584b36a", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json new file mode 100644 index 0000000..2255949 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json @@ -0,0 +1,14 @@ +{ + "id": "22d58184-b97d-44a5-bbaf-0aa8b4000d81", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json new file mode 100644 index 0000000..bb9d425 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json @@ -0,0 +1,14 @@ +{ + "id": "2bf132e0-5d40-4df9-9a11-9106e5333735", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json new file mode 100644 index 0000000..a7ccfd6 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json @@ -0,0 +1,15 @@ +{ + "id": "696e7022-c466-44f6-89c6-8cf93c06a62a", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json new file mode 100644 index 0000000..10c88c2 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json @@ -0,0 +1,15 @@ +{ + "id": "69c80296-36cd-4afc-921a-15369cac5bf0", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json new file mode 100644 index 0000000..95d84ca --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json @@ -0,0 +1,15 @@ +{ + "id": "87fa3f43-5155-42b4-a1ad-174c2595fdaf", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json new file mode 100644 index 0000000..68f36b9 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json @@ -0,0 +1,15 @@ +{ + "id": "8ef215a9-e8d5-43b3-9aa3-cb4333562e36", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json new file mode 100644 index 0000000..4226d48 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json @@ -0,0 +1,15 @@ +{ + "id": "8f32efdc-f6fc-4c27-a3c2-579d109eff60", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json new file mode 100644 index 0000000..2501f4f --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json @@ -0,0 +1,15 @@ +{ + "id": "9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json new file mode 100644 index 0000000..1517375 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json @@ -0,0 +1,15 @@ +{ + "id": "e2150cdc-23ac-4940-a240-6c79c27ab029", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json new file mode 100644 index 0000000..da36c66 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json @@ -0,0 +1,15 @@ +{ + "id": "ef18b06e-4700-4021-9949-ef783cd86be1", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json new file mode 100644 index 0000000..68bfeb3 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json @@ -0,0 +1,15 @@ +{ + "id": "ef18b06e-4700-4021-9949-ef783cd86be8", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json new file mode 100644 index 0000000..4c10f10 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json @@ -0,0 +1,15 @@ +{ + "id": "f1b5ed18-113a-4a98-aae7-d4eba007199c", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "organization_id": null, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/session/068f6bb6-d15f-436d-94f7-b3fd0489c9ef.json b/persistence/sql/migratest/fixtures/session/068f6bb6-d15f-436d-94f7-b3fd0489c9ef.json new file mode 100644 index 0000000..4c8fb26 --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/068f6bb6-d15f-436d-94f7-b3fd0489c9ef.json @@ -0,0 +1,45 @@ +{ + "id": "068f6bb6-d15f-436d-94f7-b3fd0489c9ef", + "active": false, + "expires_at": "2013-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal2", + "authentication_methods": [ + { + "method": "password", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + }, + { + "method": "totp", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [] +} diff --git a/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json new file mode 100644 index 0000000..8d96bfe --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json @@ -0,0 +1,45 @@ +{ + "id": "7458af86-c1d8-401c-978a-8da89133f78b", + "active": true, + "expires_at": "2080-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal2", + "authentication_methods": [ + { + "method": "password", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + }, + { + "method": "totp", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [] +} diff --git a/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f98b.json b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f98b.json new file mode 100644 index 0000000..c033ccc --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f98b.json @@ -0,0 +1,52 @@ +{ + "id": "7458af86-c1d8-401c-978a-8da89133f98b", + "active": false, + "expires_at": "2013-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal2", + "authentication_methods": [ + { + "method": "password", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + }, + { + "method": "totp", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [ + { + "id": "884f556e-eb3a-4b9f-bee3-11763642c6c0", + "ip_address": "54.155.246.232", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "location": "Munich, Germany" + } + ] +} diff --git a/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json new file mode 100644 index 0000000..625530c --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json @@ -0,0 +1,40 @@ +{ + "id": "8571e374-38f2-4f46-8ad3-b9d914e174d3", + "active": false, + "expires_at": "2013-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal1", + "authentication_methods": [ + { + "method": "v0.6_legacy_session", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [] +} diff --git a/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json b/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json new file mode 100644 index 0000000..895bfdf --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json @@ -0,0 +1,40 @@ +{ + "id": "dcde5aaa-f789-4d3d-ae1f-76da8d57e67c", + "active": false, + "expires_at": "2013-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal1", + "authentication_methods": [ + { + "method": "v0.6_legacy_session", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [] +} diff --git a/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json new file mode 100644 index 0000000..1b4474d --- /dev/null +++ b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json @@ -0,0 +1,40 @@ +{ + "id": "f38cdebe-e567-42c9-a562-1bd4dee40998", + "active": false, + "expires_at": "2013-10-07T08:23:19Z", + "authenticated_at": "2013-10-07T08:23:19Z", + "authenticator_assurance_level": "aal1", + "authentication_methods": [ + { + "method": "v0.6_legacy_session", + "aal": "", + "completed_at": "0001-01-01T00:00:00Z" + } + ], + "issued_at": "2013-10-07T08:23:19Z", + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "devices": [] +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json new file mode 100644 index 0000000..c8a4ea7 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json @@ -0,0 +1,65 @@ +{ + "id": "194c5b05-0487-4a11-bcbc-f301c9ff9678", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json new file mode 100644 index 0000000..b27373f --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json @@ -0,0 +1,66 @@ +{ + "id": "19ede218-928c-4e02-ab49-b76e12b34f31", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f32.json b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f32.json new file mode 100644 index 0000000..d8e64f9 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f32.json @@ -0,0 +1,66 @@ +{ + "id": "19ede218-928c-4e02-ab49-b76e12b34f32", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json new file mode 100644 index 0000000..eddfe56 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json @@ -0,0 +1,66 @@ +{ + "id": "21c5f714-3089-49d2-b387-f244d4dd9e00", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json new file mode 100644 index 0000000..be1863a --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json @@ -0,0 +1,38 @@ +{ + "id": "74fd6c53-7651-453e-90b8-2c5adbf911bb", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "5ff66179-c240-4703-b0d8-494592cefff5", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "45e867e9-2745-4f16-8dd4-84334a252b61", + "value": "foo@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json new file mode 100644 index 0000000..e6cc37d --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json @@ -0,0 +1,66 @@ +{ + "id": "77fe4fb3-2d4e-4532-b568-c44b0aece0aa", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json b/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json new file mode 100644 index 0000000..535e060 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json @@ -0,0 +1,66 @@ +{ + "id": "8248bb5d-8ef7-45e3-8e07-9e2003dd5352", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json new file mode 100644 index 0000000..fb1ce29 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json @@ -0,0 +1,66 @@ +{ + "id": "90b4f970-b9ae-42bc-a0a7-73ec750e0aa1", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json new file mode 100644 index 0000000..6eae9a6 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json @@ -0,0 +1,66 @@ +{ + "id": "a79bfcf1-68ae-49de-8b23-4f96921b8341", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json new file mode 100644 index 0000000..eeb540d --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json @@ -0,0 +1,66 @@ +{ + "id": "aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json new file mode 100644 index 0000000..aeb51b1 --- /dev/null +++ b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json @@ -0,0 +1,66 @@ +{ + "id": "cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/settings", + "active": "profile", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "identity": { + "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "foobar@ory.sh" + }, + "verifiable_addresses": [ + { + "id": "b2d59320-8564-4400-a39f-a22a497a23f1", + "value": "foobar+without-code@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "c2427b6d-312b-46d9-9285-536db7ae11fd", + "value": "foobar@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + }, + { + "id": "d4718a67-aec2-418d-8173-6ebc7bde3b86", + "value": "foobar+11345642c6c0@ory.sh", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "recovery_addresses": [ + { + "id": "b8293f1c-010f-45d9-b809-f3fc5365ba80", + "value": "foobar@ory.sh", + "via": "email", + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" + } + ], + "metadata_public": null, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "organization_id": null + }, + "state": "show_form", + "organization_id": null +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/29b2c16e-2955-4faa-bd16-33af098cdf83.json b/persistence/sql/migratest/fixtures/verification_flow/29b2c16e-2955-4faa-bd16-33af098cdf83.json new file mode 100644 index 0000000..b362712 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/29b2c16e-2955-4faa-bd16-33af098cdf83.json @@ -0,0 +1,13 @@ +{ + "id": "29b2c16e-2955-4faa-bd16-33af098cdf83", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "passed_challenge" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/3631e880-ce59-4cbd-a705-0d825eea590d.json b/persistence/sql/migratest/fixtures/verification_flow/3631e880-ce59-4cbd-a705-0d825eea590d.json new file mode 100644 index 0000000..4e23a3c --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/3631e880-ce59-4cbd-a705-0d825eea590d.json @@ -0,0 +1,13 @@ +{ + "id": "3631e880-ce59-4cbd-a705-0d825eea590d", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "passed_challenge" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/42f31e47-65e1-4be9-80ea-e5d8ed64b236.json b/persistence/sql/migratest/fixtures/verification_flow/42f31e47-65e1-4be9-80ea-e5d8ed64b236.json new file mode 100644 index 0000000..adf1580 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/42f31e47-65e1-4be9-80ea-e5d8ed64b236.json @@ -0,0 +1,13 @@ +{ + "id": "42f31e47-65e1-4be9-80ea-e5d8ed64b236", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "passed_challenge" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/5385c962-0295-4575-9b1b-d7eef13c0eda.json b/persistence/sql/migratest/fixtures/verification_flow/5385c962-0295-4575-9b1b-d7eef13c0eda.json new file mode 100644 index 0000000..a0b6ec5 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/5385c962-0295-4575-9b1b-d7eef13c0eda.json @@ -0,0 +1,14 @@ +{ + "id": "5385c962-0295-4575-9b1b-d7eef13c0eda", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration", + "active": "link", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/6aae3159-b880-4cfb-a863-03b114b1371b.json b/persistence/sql/migratest/fixtures/verification_flow/6aae3159-b880-4cfb-a863-03b114b1371b.json new file mode 100644 index 0000000..6b05cb9 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/6aae3159-b880-4cfb-a863-03b114b1371b.json @@ -0,0 +1,13 @@ +{ + "id": "6aae3159-b880-4cfb-a863-03b114b1371b", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d1.json b/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d1.json new file mode 100644 index 0000000..6aac4a8 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d1.json @@ -0,0 +1,13 @@ +{ + "id": "7be6c72c-c868-4b61-a1f0-1130603665d1", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d8.json b/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d8.json new file mode 100644 index 0000000..b741ec3 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/7be6c72c-c868-4b61-a1f0-1130603665d8.json @@ -0,0 +1,13 @@ +{ + "id": "7be6c72c-c868-4b61-a1f0-1130603665d8", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e95119260471.json b/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e95119260471.json new file mode 100644 index 0000000..d7ce014 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e95119260471.json @@ -0,0 +1,13 @@ +{ + "id": "81f74e5d-1fa5-4e1b-a9bf-e95119260471", + "type": "api", + "expires_at": "2022-11-03T08:23:19Z", + "issued_at": "2022-11-03T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e9511926047c.json b/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e9511926047c.json new file mode 100644 index 0000000..4c00781 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/81f74e5d-1fa5-4e1b-a9bf-e9511926047c.json @@ -0,0 +1,13 @@ +{ + "id": "81f74e5d-1fa5-4e1b-a9bf-e9511926047c", + "type": "api", + "expires_at": "2022-11-03T08:23:19Z", + "issued_at": "2022-11-03T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/a8e2b810-a561-4a9e-bbd8-37f649bc26fa.json b/persistence/sql/migratest/fixtures/verification_flow/a8e2b810-a561-4a9e-bbd8-37f649bc26fa.json new file mode 100644 index 0000000..8a4b566 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/a8e2b810-a561-4a9e-bbd8-37f649bc26fa.json @@ -0,0 +1,13 @@ +{ + "id": "a8e2b810-a561-4a9e-bbd8-37f649bc26fa", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "passed_challenge" +} diff --git a/persistence/sql/migratest/fixtures/verification_flow/b37d34c2-4290-4be4-9e3a-c6263ee77082.json b/persistence/sql/migratest/fixtures/verification_flow/b37d34c2-4290-4be4-9e3a-c6263ee77082.json new file mode 100644 index 0000000..a657638 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_flow/b37d34c2-4290-4be4-9e3a-c6263ee77082.json @@ -0,0 +1,13 @@ +{ + "id": "b37d34c2-4290-4be4-9e3a-c6263ee77082", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/verification/email", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "show_form" +} diff --git a/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f0c-43f6-8d26-0062938ae330.json b/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f0c-43f6-8d26-0062938ae330.json new file mode 100644 index 0000000..1a31296 --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f0c-43f6-8d26-0062938ae330.json @@ -0,0 +1,6 @@ +{ + "id": "ee56574d-2f0c-43f6-8d26-0062938ae330", + "verification_address": null, + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f1c-43f6-8d26-0062938ae330.json b/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f1c-43f6-8d26-0062938ae330.json new file mode 100644 index 0000000..93b643a --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_token/ee56574d-2f1c-43f6-8d26-0062938ae330.json @@ -0,0 +1,6 @@ +{ + "id": "ee56574d-2f1c-43f6-8d26-0062938ae330", + "verification_address": null, + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/verification_token/f81fd924-23bb-4cdf-8fa0-56253eff6cc9.json b/persistence/sql/migratest/fixtures/verification_token/f81fd924-23bb-4cdf-8fa0-56253eff6cc9.json new file mode 100644 index 0000000..ed47e0b --- /dev/null +++ b/persistence/sql/migratest/fixtures/verification_token/f81fd924-23bb-4cdf-8fa0-56253eff6cc9.json @@ -0,0 +1,6 @@ +{ + "id": "f81fd924-23bb-4cdf-8fa0-56253eff6cc9", + "verification_address": null, + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go new file mode 100644 index 0000000..9641943 --- /dev/null +++ b/persistence/sql/migratest/migration_test.go @@ -0,0 +1,472 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migratest + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/bradleyjkemp/cupaloy/v2" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "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/persistence/sql" + gomigrations "github.com/ory/kratos/persistence/sql/migrations/go" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/strategy/code" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" + "github.com/ory/x/configx" + "github.com/ory/x/dbal" + "github.com/ory/x/fsx" + "github.com/ory/x/logrusx" + "github.com/ory/x/migratest" + "github.com/ory/x/networkx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlcon/dockertest" +) + +func snapshotFor(paths ...string) *cupaloy.Config { + return cupaloy.New( + cupaloy.CreateNewAutomatically(true), + cupaloy.FailOnUpdate(true), + cupaloy.SnapshotFileExtension(".json"), + cupaloy.SnapshotSubdirectory(filepath.Join(paths...)), + ) +} + +func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id string) { + s := snapshotFor("fixtures", prefix) + actualJSON, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err) + err = s.SnapshotWithName(id, actualJSON) + assert.NoErrorf(t, err, "actual = %s", string(actualJSON)) +} + +func TestMigrations_SQLite(t *testing.T) { + t.Parallel() + sqlite, err := pop.NewConnection(&pop.ConnectionDetails{ + URL: dbal.NewSQLiteTestDatabase(t), + }) + require.NoError(t, err) + require.NoError(t, sqlite.Open()) + + testDatabase(t, "sqlite", sqlite) +} + +func TestMigrations_Postgres(t *testing.T) { + if testing.Short() { + t.Skip("skipping testing in short mode") + } + t.Parallel() + testDatabase(t, "postgres", dockertest.ConnectPop(t, dockertest.RunTestPostgreSQLWithVersion(t, "16"))) +} + +func TestMigrations_Mysql(t *testing.T) { + if testing.Short() { + t.Skip("skipping testing in short mode") + } + t.Parallel() + testDatabase(t, "mysql", dockertest.ConnectPop(t, dockertest.RunTestMySQLWithVersion(t, "8.4"))) +} + +func TestMigrations_Cockroach(t *testing.T) { + if testing.Short() { + t.Skip("skipping testing in short mode") + } + t.Parallel() + testDatabase(t, "cockroach", dockertest.ConnectPop(t, dockertest.RunTestCockroachDBWithVersion(t, "latest-v25.4"))) +} + +func testDatabase(t *testing.T, db string, c *pop.Connection) { + ctx := context.Background() + l := logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)) + + url := c.URL() + // workaround for https://github.com/ory/pop/issues/538 + switch db { + case "mysql": + url = "mysql://" + url + case "sqlite": + url = "sqlite3://" + url + case "cockroach": + url = "cockroach" + strings.TrimPrefix(url, "postgres") + } + if db != "sqlite" { + dbName := "testdb" + strings.ReplaceAll(x.NewUUID().String(), "-", "") + require.NoError(t, c.RawQuery("CREATE DATABASE "+dbName).Exec()) + url = regexp.MustCompile(`/[a-z0-9]+\?`).ReplaceAllString(url, "/"+dbName+"?") + } + + t.Logf("URL: %s", url) + var err error + c, err = pop.NewConnection(&pop.ConnectionDetails{URL: url}) + require.NoError(t, err) + require.NoError(t, c.Open()) + + tm, err := popx.NewMigrationBox( + fsx.Merge(sql.Migrations, networkx.Migrations), + c, l, + popx.WithGoMigrations(gomigrations.All), + popx.WithTestdata(t, os.DirFS("./testdata")), + popx.WithoutGoldenDatabase(), + ) + require.NoError(t, err) + + err = tm.Up(ctx) // for easy breakpointing + // _ = tm.DumpMigrationSchema(ctx) // uncomment to get the current state of the database after migrations have run + if !assert.NoError(t, err) { + assert.NoError(t, tm.DumpMigrationSchema(ctx)) + t.FailNow() + } + + opts := driver.WithConfigOptions( + configx.WithValues(map[string]any{ + config.ViperKeyDSN: url, + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + config.ViperKeyIdentitySchemas: config.Schemas{{ID: "default", URL: "file://stub/default.schema.json"}}, + config.ViperKeySecretsDefault: []string{"secret"}, + }), + configx.SkipValidation(), + ) + + t.Run("suite=fixtures", func(t *testing.T) { + t.Run("case=identity", func(t *testing.T) { + + t.Parallel() + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + ids, _, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, KeySetPagination: []keysetpagination.Option{keysetpagination.WithSize(1000)}}) + require.NoError(t, err) + require.NotEmpty(t, ids) + + var found []string + for y, id := range ids { + found = append(found, id.ID.String()) + actual := &ids[y] + + for _, a := range actual.VerifiableAddresses { + CompareWithFixture(t, a, "identity_verification_address", a.ID.String()) + } + + for _, a := range actual.RecoveryAddresses { + CompareWithFixture(t, a, "identity_recovery_address", a.ID.String()) + } + + CompareWithFixture(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), "identity", id.ID.String()) + } + + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "identity"), found) + }) + + t.Run("case=identity_get", func(t *testing.T) { + + t.Parallel() + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + ids, _, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandNothing, KeySetPagination: []keysetpagination.Option{keysetpagination.WithSize(1000)}}) + require.NoError(t, err) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + actual, err := d.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + found = append(found, actual.ID.String()) + + CompareWithFixture(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), "identity", id.ID.String()) + } + + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "identity"), found) + }) + + t.Run("case=verification_token", func(t *testing.T) { + + t.Parallel() + + var ids []link.VerificationToken + + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + for _, id := range ids { + CompareWithFixture(t, id, "verification_token", id.ID.String()) + } + }) + + t.Run("case=session", func(t *testing.T) { + t.Parallel() + + var ids []session.Session + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.SessionPersister().GetSession(context.Background(), id.ID, session.ExpandEverything) + require.NoErrorf(t, err, "Trying to get session: %s", id.ID) + require.NotEmpty(t, actual.LogoutToken, "check if migrations have generated a logout token for existing sessions") + CompareWithFixture(t, actual, "session", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "session"), found) + }) + + t.Run("case=login", func(t *testing.T) { + + t.Parallel() + + var ids []login.Flow + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.LoginFlowPersister().GetLoginFlow(context.Background(), id.ID) + require.NoError(t, err) + CompareWithFixture(t, actual, "login_flow", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_flow"), found) + }) + + t.Run("case=registration", func(t *testing.T) { + + t.Parallel() + + var ids []registration.Flow + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), id.ID) + require.NoError(t, err) + CompareWithFixture(t, actual, "registration_flow", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_flow"), found) + }) + + t.Run("case=settings_flow", func(t *testing.T) { + + t.Parallel() + + var ids []settings.Flow + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.SettingsFlowPersister().GetSettingsFlow(context.Background(), id.ID) + require.NoError(t, err, id.ID.String()) + CompareWithFixture(t, actual, "settings_flow", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "settings_flow"), found) + }) + + t.Run("case=recovery_flow", func(t *testing.T) { + + t.Parallel() + + var ids []recovery.Flow + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), id.ID) + require.NoError(t, err) + CompareWithFixture(t, actual, "recovery_flow", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_flow"), found) + }) + + t.Run("case=verification_flow", func(t *testing.T) { + + t.Parallel() + + var ids []verification.Flow + require.NoError(t, c.Select("id").All(&ids)) + require.NotEmpty(t, ids) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + actual, err := d.VerificationFlowPersister().GetVerificationFlow(context.Background(), id.ID) + require.NoError(t, err) + CompareWithFixture(t, actual, "verification_flow", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "verification_flow"), found) + }) + + t.Run("case=recovery_token", func(t *testing.T) { + + t.Parallel() + + var ids []link.RecoveryToken + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "recovery_token", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_token"), found) + }) + + t.Run("case=recovery_code", func(t *testing.T) { + + t.Parallel() + + var ids []code.RecoveryCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "recovery_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_code"), found) + }) + + t.Run("case=registration_code", func(t *testing.T) { + + t.Parallel() + + var ids []code.RegistrationCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "registration_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_code"), found) + }) + + t.Run("case=login_code", func(t *testing.T) { + + t.Parallel() + + var ids []code.LoginCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "login_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_code"), found) + }) + }) + + t.Run("suite=constraints", func(t *testing.T) { + t.Cleanup(func() { + // clean up test duplicates - remove identity_credential_identifiers 10985ed1-5b6e-4012-ac10-03d87df65618 - otherwise down migration later fails. + require.NoError(t, c.RawQuery("DELETE FROM identity_credential_identifiers WHERE identifier = '10985ed1-5b6e-4012-ac10-03d87df65618'").Exec()) + }) + + d, err := driver.New( + context.Background(), + os.Stderr, + opts, + ) + require.NoError(t, err) + + sr, err := d.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID("a79bfcf1-68ae-49de-8b23-4f96921b8341")) + require.NoError(t, err) + + require.NoError(t, d.PrivilegedIdentityPool().DeleteIdentity(context.Background(), sr.IdentityID)) + + _, err = d.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID("a79bfcf1-68ae-49de-8b23-4f96921b8341")) + require.Error(t, err) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + t.Run("suite=down", func(t *testing.T) { + err = tm.Down(ctx, -1) // for easy breakpointing + if !assert.NoError(t, err) { + assert.NoError(t, tm.DumpMigrationSchema(ctx)) + t.FailNow() + } + }) +} diff --git a/persistence/sql/migratest/stub/default.schema.json b/persistence/sql/migratest/stub/default.schema.json new file mode 100644 index 0000000..241b41c --- /dev/null +++ b/persistence/sql/migratest/stub/default.schema.json @@ -0,0 +1,30 @@ +{ + "$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/persistence/sql/migratest/testdata/20150100000001_testdata.sql b/persistence/sql/migratest/testdata/20150100000001_testdata.sql new file mode 100644 index 0000000..0b62c9a --- /dev/null +++ b/persistence/sql/migratest/testdata/20150100000001_testdata.sql @@ -0,0 +1 @@ +INSERT INTO networks (id, created_at, updated_at) VALUES ('884f556e-eb3a-4b9f-bee3-11345642c6c0', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20191100000001_testdata.sql b/persistence/sql/migratest/testdata/20191100000001_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000001_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000002_testdata.sql b/persistence/sql/migratest/testdata/20191100000002_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000002_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000003_testdata.sql b/persistence/sql/migratest/testdata/20191100000003_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000003_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000004_testdata.sql b/persistence/sql/migratest/testdata/20191100000004_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000004_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000005_testdata.mysql.sql b/persistence/sql/migratest/testdata/20191100000005_testdata.mysql.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000005_testdata.mysql.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000006_testdata.sql b/persistence/sql/migratest/testdata/20191100000006_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000006_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000007_testdata.sql b/persistence/sql/migratest/testdata/20191100000007_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000007_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000008_testdata.sql b/persistence/sql/migratest/testdata/20191100000008_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000008_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000009_testdata.mysql.sql b/persistence/sql/migratest/testdata/20191100000009_testdata.mysql.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000009_testdata.mysql.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000010_testdata.sql b/persistence/sql/migratest/testdata/20191100000010_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000010_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000011_testdata.sql b/persistence/sql/migratest/testdata/20191100000011_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000011_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20191100000012_testdata.sql b/persistence/sql/migratest/testdata/20191100000012_testdata.sql new file mode 100644 index 0000000..9ff04a3 --- /dev/null +++ b/persistence/sql/migratest/testdata/20191100000012_testdata.sql @@ -0,0 +1,2 @@ +-- no test data up until v0.3.0 + diff --git a/persistence/sql/migratest/testdata/20200317160354_testdata.sql b/persistence/sql/migratest/testdata/20200317160354_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20200317160354_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20200401183443_testdata.sql b/persistence/sql/migratest/testdata/20200401183443_testdata.sql new file mode 100644 index 0000000..81f8fbd --- /dev/null +++ b/persistence/sql/migratest/testdata/20200401183443_testdata.sql @@ -0,0 +1 @@ +-- no test data up until v0.3.0 diff --git a/persistence/sql/migratest/testdata/20200402142539_testdata.sql b/persistence/sql/migratest/testdata/20200402142539_testdata.sql new file mode 100644 index 0000000..6d67a53 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200402142539_testdata.sql @@ -0,0 +1,75 @@ +INSERT INTO identities (id, traits_schema_id, traits, created_at, updated_at) VALUES ('a251ebc2-880c-4f76-a8f3-38e6940eab0e', 'default', '{"email":"foobar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO identities (id, traits_schema_id, traits, created_at, updated_at) VALUES ('5ff66179-c240-4703-b0d8-494592cefff5', 'default', '{"email":"bazbar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO continuity_containers (id, identity_id, name, payload, expires_at, created_at, updated_at) VALUES ('50ba09d3-481b-4060-844a-9541a9cec39c', '5ff66179-c240-4703-b0d8-494592cefff5', 'ory_kratos_settings_profile', '{"traits":{"email":"baz@ory.sh"},"request_id":""}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO courier_messages (id, type, status, body, subject, recipient, created_at, updated_at) VALUES ('98f45a85-0782-49f1-a7b5-8f83d160b4a5', 1, 2, 'Hi, Verify your account by opening the following link: + +http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/swmcFweNFSfvTSTKecmZjO6I8x0hxzZS', 'Please verify your email address', 'foo@ory.sh', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO courier_messages (id, type, status, body, subject, recipient, created_at, updated_at) VALUES ('7a9d6df4-3b60-4cae-996e-d15d78b0fc36', 1, 2, 'Hi, Verify your account by opening the following link: http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/u9ZcBr5HbRTR8f53Qj2Ng3KR8Mv1Zjdb', 'Please verify your email address', 'foobar@ory.sh', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO courier_messages (id, type, status, body, subject, recipient, created_at, updated_at) VALUES ('77fdc5e0-2260-49da-8aae-c36ba255d05b', 1, 2, 'Hi, Verify your account by opening the following link: http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/SQcSX0Jx6IVEDKqAuaLZLNEw00J4vlig', 'Please verify your email address', 'foobar@ory.sh', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO courier_messages (id, type, status, body, subject, recipient, created_at, updated_at) VALUES ('a3ea4c30-0c6e-47b1-99ba-8fa69282e166', 1, 2, 'Hi, Verify your account by opening the following link: http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/AtsREGbtXu0RlIcwv3RPpxHEZNEcq3R9', 'Please verify your email address', 'foo@ory.sh', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_credential_types (id, name) VALUES ('22bff9ae-f5aa-45d7-803b-97ec0b4e7b32', 'password'); +INSERT INTO identity_credential_types (id, name) VALUES ('8071b37b-0d54-4c6f-8234-72cffb4ce784', 'totp'); + +INSERT INTO identity_credentials (id, config, identity_credential_type_id, identity_id, created_at, updated_at) VALUES ('35b60ecf-30f9-42d6-bf5d-47ad41148691', '{"hashed_password":"$argon2id$v=19$m=131072,t=2,p=1$lQFPaKxXqPL56/mU7vRi4w$6aldHyBnURt8sP8+xu41Ng"}', '22bff9ae-f5aa-45d7-803b-97ec0b4e7b32', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO identity_credentials (id, config, identity_credential_type_id, identity_id, created_at, updated_at) VALUES ('74ac2d31-bccb-442f-a792-7b8bb14817f8', '{"hashed_password":"$argon2id$v=19$m=131072,t=2,p=1$lQFPaKxXqPL56/mU7vRi4w$6aldHyBnURt8sP8+xu41Ng"}', '22bff9ae-f5aa-45d7-803b-97ec0b4e7b32', '5ff66179-c240-4703-b0d8-494592cefff5', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_credential_identifiers (id, identifier, identity_credential_id, created_at, updated_at) VALUES ('f63a74f9-12da-439d-a8ce-b0157cd163b1', 'foobar@ory.sh', '35b60ecf-30f9-42d6-bf5d-47ad41148691', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO identity_credential_identifiers (id, identifier, identity_credential_id, created_at, updated_at) VALUES ('1f51d652-8504-4d26-8615-989fd76b7ebf', 'foo@ory.sh', '74ac2d31-bccb-442f-a792-7b8bb14817f8', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_verifiable_addresses (id, code, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) VALUES ('c2427b6d-312b-46d9-9285-536db7ae11fd', 'SQcSX0Jx6IVEDKqAuaLZLNEw00J4vlig', 'pending', 'email', false, 'foobar@ory.sh', null, '2013-10-07 08:23:19', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO identity_verifiable_addresses (id, code, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) VALUES ('45e867e9-2745-4f16-8dd4-84334a252b61', 'AtsREGbtXu0RlIcwv3RPpxHEZNEcq3R9', 'pending', 'email', false, 'foo@ory.sh', null, '2013-10-07 08:23:19', '5ff66179-c240-4703-b0d8-494592cefff5', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('3a9ea34f-0f12-469b-9417-3ae5795a7baa', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false); +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('916ded11-aa64-4a27-b06e-96e221a509d7', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'JdXCIeZGYglwUTjEC5PpKO5LFDEbeVOketqK5hdfZgFX379dVpLcRyRs+lteQJ1PxTMlyN0+btkz+5iVm4miIg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false); +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('99974ce6-388c-4669-a95a-7757ee724020', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'OXDnJU8yP+iXXsf2TduzRYrDLfksjC0vRdU+nN5bdo+YeQt5xGWKbqEX/gggqE1ntM7qavC/qqzaR2B1Pk88Ew==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false); +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('6d387820-f2f4-4f9f-9980-a90d89e7811f', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'Nrq+T0uNsSL8ztjmnbIB2V6NodqifoG+HZ4q6NSAnnWXs1ITwNoEpMqH4Rjwwf/7YIBmSX5NBj2CDHQBNJTU6Q==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false); +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('202c1981-1e25-47f0-8764-75ad506c2bec', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', '7esV9o+i9BJCvpBz3HXWxBPI2802lrGtt3+dBIwBZ/1M4vmqBPVBlHT3qY2xBijmLcUcXuqlNi4o7cPtbBUtYQ==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false); +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) VALUES ('43c99182-bb67-47e1-b564-bb23bd8d4393', 'http://kratos:4433/self-service/browser/flows/login?prompt=login&return_to=http%3A%2F%2F127.0.0.1%3A4455%2F.ory%2Fkratos%2Fpublic%2Fself-service%2Fbrowser%2Fflows%2Fsettings%2Fstrategies%2Fprofile%3Frequest%3D74fd6c53-7651-453e-90b8-2c5adbf911bb', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'dUD3MWqAmTdyoz2xwC9g5Dpv0g1aGUqAHOOsjy+IOh0vfPDqpKSudgZNSq9dqdxft4M0DN411z+5qSCMre5lmg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', true); + +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('9944614a-8f43-421d-aa7d-f051391e463f', 'password', '3a9ea34f-0f12-469b-9417-3ae5795a7baa', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=3a9ea34f-0f12-469b-9417-3ae5795a7baa","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('a2c9f597-6eb2-4ea5-972b-4f4ffa828a6c', 'oidc', '3a9ea34f-0f12-469b-9417-3ae5795a7baa', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/3a9ea34f-0f12-469b-9417-3ae5795a7baa","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('227aa2c4-acf3-489f-a239-7f18d4bc2828', 'password', '916ded11-aa64-4a27-b06e-96e221a509d7', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=916ded11-aa64-4a27-b06e-96e221a509d7","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"JdXCIeZGYglwUTjEC5PpKO5LFDEbeVOketqK5hdfZgFX379dVpLcRyRs+lteQJ1PxTMlyN0+btkz+5iVm4miIg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('bd498938-863f-41de-a60f-69af4220db36', 'oidc', '916ded11-aa64-4a27-b06e-96e221a509d7', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/916ded11-aa64-4a27-b06e-96e221a509d7","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"JdXCIeZGYglwUTjEC5PpKO5LFDEbeVOketqK5hdfZgFX379dVpLcRyRs+lteQJ1PxTMlyN0+btkz+5iVm4miIg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('9cdeaa86-1242-4c62-99b6-575d900d4c47', 'password', '99974ce6-388c-4669-a95a-7757ee724020', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=99974ce6-388c-4669-a95a-7757ee724020","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"OXDnJU8yP+iXXsf2TduzRYrDLfksjC0vRdU+nN5bdo+YeQt5xGWKbqEX/gggqE1ntM7qavC/qqzaR2B1Pk88Ew=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('b370b9bf-b785-4c8a-a013-245adc8db117', 'oidc', '99974ce6-388c-4669-a95a-7757ee724020', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/99974ce6-388c-4669-a95a-7757ee724020","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"OXDnJU8yP+iXXsf2TduzRYrDLfksjC0vRdU+nN5bdo+YeQt5xGWKbqEX/gggqE1ntM7qavC/qqzaR2B1Pk88Ew=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('df9bc6af-5b62-4f38-8bac-618937a67ff8', 'password', '6d387820-f2f4-4f9f-9980-a90d89e7811f', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=6d387820-f2f4-4f9f-9980-a90d89e7811f","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"Nrq+T0uNsSL8ztjmnbIB2V6NodqifoG+HZ4q6NSAnnWXs1ITwNoEpMqH4Rjwwf/7YIBmSX5NBj2CDHQBNJTU6Q=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('9cb49264-492d-49d2-8b19-ed1b7506d5ba', 'oidc', '6d387820-f2f4-4f9f-9980-a90d89e7811f', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/6d387820-f2f4-4f9f-9980-a90d89e7811f","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"Nrq+T0uNsSL8ztjmnbIB2V6NodqifoG+HZ4q6NSAnnWXs1ITwNoEpMqH4Rjwwf/7YIBmSX5NBj2CDHQBNJTU6Q=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('ccc3dbc3-302c-4f55-8f30-891975843ed4', 'password', '202c1981-1e25-47f0-8764-75ad506c2bec', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=202c1981-1e25-47f0-8764-75ad506c2bec","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"7esV9o+i9BJCvpBz3HXWxBPI2802lrGtt3+dBIwBZ/1M4vmqBPVBlHT3qY2xBijmLcUcXuqlNi4o7cPtbBUtYQ=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('49a44013-0650-4393-bad0-d3aaa7396fd6', 'oidc', '202c1981-1e25-47f0-8764-75ad506c2bec', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/202c1981-1e25-47f0-8764-75ad506c2bec","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"7esV9o+i9BJCvpBz3HXWxBPI2802lrGtt3+dBIwBZ/1M4vmqBPVBlHT3qY2xBijmLcUcXuqlNi4o7cPtbBUtYQ=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('6ba34380-19e5-473d-9181-e8f7def63235', 'oidc', '43c99182-bb67-47e1-b564-bb23bd8d4393', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/43c99182-bb67-47e1-b564-bb23bd8d4393","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"dUD3MWqAmTdyoz2xwC9g5Dpv0g1aGUqAHOOsjy+IOh0vfPDqpKSudgZNSq9dqdxft4M0DN411z+5qSCMre5lmg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_login_request_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) VALUES ('32477a6c-b6a5-482f-a218-287dc1208d8e', 'password', '43c99182-bb67-47e1-b564-bb23bd8d4393', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=43c99182-bb67-47e1-b564-bb23bd8d4393","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":"foo@ory.sh"},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"dUD3MWqAmTdyoz2xwC9g5Dpv0g1aGUqAHOOsjy+IOh0vfPDqpKSudgZNSq9dqdxft4M0DN411z+5qSCMre5lmg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) VALUES ('87fa3f43-5155-42b4-a1ad-174c2595fdaf', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) VALUES ('2bf132e0-5d40-4df9-9a11-9106e5333735', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'eyxQ2bNdAWBqzkZpRbzPQDpbDtpdqkkz2b7awuCdg6EJJi2lA4m/Lj7zhPYQb7snESM/I5vtdE6Qn8ixbEtHgg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) VALUES ('05a7f09d-4ef3-41fb-958a-6ad74584b36a', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'R3+y/kB6m+17C3HRkAYQJ+KOIpruSQnbplm+t3JiQd7mdl6iyy0ua01CSC/9de4F3IPlCTJ6jlg5y+BeknYLQg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) VALUES ('22d58184-b97d-44a5-bbaf-0aa8b4000d81', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'vtujrgwX3K+icdz8iHW3WD4VYd+QIGCb1NqjWcF+wj4f0k/yh0BpKZQ45QLlBkl6ABimTEwT5xhLSP2wIWqIog==', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('634a863d-2f86-40f3-aa82-29ac211e2484', 'password', '87fa3f43-5155-42b4-a1ad-174c2595fdaf', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=87fa3f43-5155-42b4-a1ad-174c2595fdaf","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"KeEp3OT+pHSXJpZAP+C1hR3yr4eIjUBDhg9tc/F1WI1b61SgVCoaOsMbVN9qM8HiNoqefk7KfT7PLn8AfaOcrg=="},{"name":"password","type":"password","required":true,"errors":[{"message":"the password does not fulfill the password policy because: the password has been found in data breaches and must no longer be used."}]},{"name":"traits.email","type":"text","value":"foo@ory.sh"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('08ce0a27-50ee-4e9a-aa75-2a842fb6825b', 'oidc', '87fa3f43-5155-42b4-a1ad-174c2595fdaf', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/87fa3f43-5155-42b4-a1ad-174c2595fdaf","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('fd51365b-b954-4a2a-9367-98a37cabf86c', 'password', '2bf132e0-5d40-4df9-9a11-9106e5333735', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=2bf132e0-5d40-4df9-9a11-9106e5333735","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"eyxQ2bNdAWBqzkZpRbzPQDpbDtpdqkkz2b7awuCdg6EJJi2lA4m/Lj7zhPYQb7snESM/I5vtdE6Qn8ixbEtHgg=="},{"name":"password","type":"password","required":true},{"name":"traits.email","type":"email"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('c65e1f49-d458-42a8-940b-8dc956804616', 'oidc', '2bf132e0-5d40-4df9-9a11-9106e5333735', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/2bf132e0-5d40-4df9-9a11-9106e5333735","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"eyxQ2bNdAWBqzkZpRbzPQDpbDtpdqkkz2b7awuCdg6EJJi2lA4m/Lj7zhPYQb7snESM/I5vtdE6Qn8ixbEtHgg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('39f85951-6d5d-43c8-afeb-a93d041f38e4', 'password', '05a7f09d-4ef3-41fb-958a-6ad74584b36a', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=05a7f09d-4ef3-41fb-958a-6ad74584b36a","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"R3+y/kB6m+17C3HRkAYQJ+KOIpruSQnbplm+t3JiQd7mdl6iyy0ua01CSC/9de4F3IPlCTJ6jlg5y+BeknYLQg=="},{"name":"password","type":"password","required":true},{"name":"traits.email","type":"email"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('8555b5d4-7f0c-4353-bddb-0601bc1c7387', 'oidc', '05a7f09d-4ef3-41fb-958a-6ad74584b36a', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/05a7f09d-4ef3-41fb-958a-6ad74584b36a","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"R3+y/kB6m+17C3HRkAYQJ+KOIpruSQnbplm+t3JiQd7mdl6iyy0ua01CSC/9de4F3IPlCTJ6jlg5y+BeknYLQg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('3573b093-4ea1-4f7c-bd90-2bd44275c36f', 'password', '22d58184-b97d-44a5-bbaf-0aa8b4000d81', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=22d58184-b97d-44a5-bbaf-0aa8b4000d81","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"vtujrgwX3K+icdz8iHW3WD4VYd+QIGCb1NqjWcF+wj4f0k/yh0BpKZQ45QLlBkl6ABimTEwT5xhLSP2wIWqIog=="},{"name":"password","type":"password","required":true},{"name":"traits.email","type":"email"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_registration_request_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) VALUES ('c8853b14-a511-4367-a530-b79a514c701b', 'oidc', '22d58184-b97d-44a5-bbaf-0aa8b4000d81', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/auth/22d58184-b97d-44a5-bbaf-0aa8b4000d81","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"vtujrgwX3K+icdz8iHW3WD4VYd+QIGCb1NqjWcF+wj4f0k/yh0BpKZQ45QLlBkl6ABimTEwT5xhLSP2wIWqIog=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) VALUES ('21c5f714-3089-49d2-b387-f244d4dd9e00', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile'); +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) VALUES ('194c5b05-0487-4a11-bcbc-f301c9ff9678', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', true, 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', null); +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) VALUES ('74fd6c53-7651-453e-90b8-2c5adbf911bb', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, '5ff66179-c240-4703-b0d8-494592cefff5', '2013-10-07 08:23:19', '2013-10-07 08:23:19', null); + +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('71896798-3844-4269-bdab-9a9f1f6160f3', 'profile', '21c5f714-3089-49d2-b387-f244d4dd9e00', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('e8cfd338-5112-4311-b2ec-97ebc07fd7e9', 'password', '21c5f714-3089-49d2-b387-f244d4dd9e00', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/password?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"Yohg7gsfztSw5KSEt/7+Tnbfc3QBmFA7zvqPg7DuhdgM8F5HOYmCxT3DDXTfq901/c1lShvHWFsAajdN60amVw=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('61d63a2d-ad74-433a-bcfb-2c93ae7b4d9c', 'oidc', '21c5f714-3089-49d2-b387-f244d4dd9e00', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/settings/connections?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"Yohg7gsfztSw5KSEt/7+Tnbfc3QBmFA7zvqPg7DuhdgM8F5HOYmCxT3DDXTfq901/c1lShvHWFsAajdN60amVw=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('1966b9ba-1fe2-46b9-97f9-e4f86cc8f15d', 'password', '194c5b05-0487-4a11-bcbc-f301c9ff9678', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/password?request=194c5b05-0487-4a11-bcbc-f301c9ff9678","method":"POST","fields":[{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"4apNE3S+VLL1UPu8GhyLOhdnhkzLZxrwTqZ/dx5xf3eP0nO6RigYo3h3UkxySahBnHWQctE4EpCANse5Rdlc+A=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('3095bd65-910b-4110-87fc-893e443df30c', 'oidc', '194c5b05-0487-4a11-bcbc-f301c9ff9678', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/settings/connections?request=194c5b05-0487-4a11-bcbc-f301c9ff9678","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"4apNE3S+VLL1UPu8GhyLOhdnhkzLZxrwTqZ/dx5xf3eP0nO6RigYo3h3UkxySahBnHWQctE4EpCANse5Rdlc+A=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('76da60ae-d629-48ed-92b2-5be892adbef7', 'profile', '194c5b05-0487-4a11-bcbc-f301c9ff9678', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=194c5b05-0487-4a11-bcbc-f301c9ff9678","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"IoLeYNbYJdALHIKvrgK7iYyMHA8r/rhG+6mr3z0nleRM+uDJ5E5pwYY7K1/GV5jyB54KMTGhsCY1ORMRZo+2aw=="},{"name":"traits.email","type":"text","value":"foobar@ory.sh"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('5ce98190-d1e8-4034-a361-7a3e686551eb', 'profile', '74fd6c53-7651-453e-90b8-2c5adbf911bb', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=74fd6c53-7651-453e-90b8-2c5adbf911bb","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"G/2FjcQNeTJHOFrPP0ytFfC/AWeUDDB4DQs6SsMRMjxBwYJWCilOczPWLdGiyhGufVPnZhAgrceoQbZJQXdtuw=="},{"name":"traits.email","type":"text","value":"baz@ory.sh"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('fb401162-9db6-4be4-b44c-01ce724dd817', 'password', '74fd6c53-7651-453e-90b8-2c5adbf911bb', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/password?request=74fd6c53-7651-453e-90b8-2c5adbf911bb","method":"POST","fields":[{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"4G7nFBBjlvSMDzqi3HXROK2ijGojM/2jXmoVHQPAz7i6UuDP3kehtfjhTbxB822DIE5qa6cfYBz7IJkegaaQPw=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_settings_request_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) VALUES ('9068a486-20f2-42f1-b5b8-368aedbb7b89', 'oidc', '74fd6c53-7651-453e-90b8-2c5adbf911bb', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/settings/connections?request=74fd6c53-7651-453e-90b8-2c5adbf911bb","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"4G7nFBBjlvSMDzqi3HXROK2ijGojM/2jXmoVHQPAz7i6UuDP3kehtfjhTbxB822DIE5qa6cfYBz7IJkegaaQPw=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_verification_requests (id, request_url, issued_at, expires_at, form, via, csrf_token, success, created_at, updated_at) VALUES ('a8e2b810-a561-4a9e-bbd8-37f649bc26fa', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'null', 'email', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', true, '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO selfservice_verification_requests (id, request_url, issued_at, expires_at, form, via, csrf_token, success, created_at, updated_at) VALUES ('b37d34c2-4290-4be4-9e3a-c6263ee77082', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/complete?request=b37d34c2-4290-4be4-9e3a-c6263ee77082","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"w+Amwv/g+QE/RaFUuc5/z7qnfFC2fxro9w9kUvoqhyBi6cqedLdMhwkMmKrUvYHthKq7w2pMnWtonTq7Gj7NvA=="},{"name":"to_verify","type":"email","required":true}]}', 'email', 'w+Amwv/g+QE/RaFUuc5/z7qnfFC2fxro9w9kUvoqhyBi6cqedLdMhwkMmKrUvYHthKq7w2pMnWtonTq7Gj7NvA==', false, '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200519101057_testdata.sql b/persistence/sql/migratest/testdata/20200519101057_testdata.sql new file mode 100644 index 0000000..45f7970 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200519101057_testdata.sql @@ -0,0 +1,11 @@ +INSERT INTO identity_recovery_addresses (id, via, value, identity_id, created_at, updated_at) +VALUES ('b8293f1c-010f-45d9-b809-f3fc5365ba80', 'email', 'foobar@ory.sh', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_recovery_requests (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) +VALUES ('13178936-095a-466b-abe0-36d977d3dc18', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_recovery_request_methods (id, method, selfservice_recovery_request_id, config, created_at, updated_at) +VALUES ('921462a6-8af6-4cda-97b4-dc0930ed271b', 'link', '13178936-095a-466b-abe0-36d977d3dc18', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_recovery_tokens (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_request_id, created_at, updated_at) +VALUES ('5529d454-2946-404e-b681-d950f8657fd0', 'd40c167d-a7f2-41a6-86b2-a5483001a010', false, null, 'b8293f1c-010f-45d9-b809-f3fc5365ba80', '13178936-095a-466b-abe0-36d977d3dc18', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200519101058_testdata.mysql.sql b/persistence/sql/migratest/testdata/20200519101058_testdata.mysql.sql new file mode 100644 index 0000000..0711ed9 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200519101058_testdata.mysql.sql @@ -0,0 +1 @@ +-- empty diff --git a/persistence/sql/migratest/testdata/20200601101000_testdata.sql b/persistence/sql/migratest/testdata/20200601101000_testdata.sql new file mode 100644 index 0000000..538f982 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200601101000_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method, messages) VALUES +('a79bfcf1-68ae-49de-8b23-4f96921b8341', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '[]'); diff --git a/persistence/sql/migratest/testdata/20200601101001_testdata.mysql.sql b/persistence/sql/migratest/testdata/20200601101001_testdata.mysql.sql new file mode 100644 index 0000000..0711ed9 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200601101001_testdata.mysql.sql @@ -0,0 +1 @@ +-- empty diff --git a/persistence/sql/migratest/testdata/20200605111551_testdata.sql b/persistence/sql/migratest/testdata/20200605111551_testdata.sql new file mode 100644 index 0000000..4cee785 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200605111551_testdata.sql @@ -0,0 +1,3 @@ +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) VALUES ('56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, '[]'); +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) VALUES ('9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]'); +INSERT INTO selfservice_verification_requests (id, request_url, issued_at, expires_at, form, via, csrf_token, success, created_at, updated_at, messages) VALUES ('29b2c16e-2955-4faa-bd16-33af098cdf83', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'null', 'email', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', true, '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]'); diff --git a/persistence/sql/migratest/testdata/20200607165100_testdata.sql b/persistence/sql/migratest/testdata/20200607165100_testdata.sql new file mode 100644 index 0000000..9fc7ff7 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200607165100_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, messages) VALUES +('77fe4fb3-2d4e-4532-b568-c44b0aece0aa', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '[]'); diff --git a/persistence/sql/migratest/testdata/20200705105359_testdata.sql b/persistence/sql/migratest/testdata/20200705105359_testdata.sql new file mode 100644 index 0000000..7fe4f7b --- /dev/null +++ b/persistence/sql/migratest/testdata/20200705105359_testdata.sql @@ -0,0 +1 @@ +INSERT INTO identities (id, schema_id, traits, created_at, updated_at) VALUES ('d7b9addb-ac15-4bc2-9fa5-562e0bf48755', 'default', '{"email":"d7b9@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200810141652_testdata.sql b/persistence/sql/migratest/testdata/20200810141652_testdata.sql new file mode 100644 index 0000000..dd750f7 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200810141652_testdata.sql @@ -0,0 +1,7 @@ +INSERT INTO selfservice_login_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages, type) VALUES ('47edd3a8-0998-4779-9469-f4b8ee4430df', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, '[]', 'api'); +INSERT INTO selfservice_registration_requests (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages, type) VALUES ('696e7022-c466-44f6-89c6-8cf93c06a62a', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'api'); +INSERT INTO selfservice_verification_requests (id, request_url, issued_at, expires_at, form, via, csrf_token, success, created_at, updated_at, messages, type) VALUES ('3631e880-ce59-4cbd-a705-0d825eea590d', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'null', 'email', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', true, '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'api'); +INSERT INTO selfservice_settings_requests (id, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, messages) VALUES +('cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '[]'); +INSERT INTO selfservice_recovery_requests (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type) +VALUES ('87e871e1-a45f-4ed0-ba4e-a03063c774dc', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api'); diff --git a/persistence/sql/migratest/testdata/20200810161022_testdata.sql b/persistence/sql/migratest/testdata/20200810161022_testdata.sql new file mode 100644 index 0000000..5d76ed9 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200810161022_testdata.sql @@ -0,0 +1,22 @@ +INSERT INTO selfservice_login_flows (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages, type) +VALUES ('b1fac7fb-d016-4a06-a7fe-e4eab2a0429f', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, '[]', 'api'); +INSERT INTO selfservice_login_flow_methods (id, method, selfservice_login_request_id, config, created_at, updated_at) +VALUES ('8ca78d66-772a-4408-804e-f5d3d9fe696e', 'password', 'b1fac7fb-d016-4a06-a7fe-e4eab2a0429f', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=3a9ea34f-0f12-469b-9417-3ae5795a7baa","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_registration_flows (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages, type) +VALUES ('8ef215a9-e8d5-43b3-9aa3-cb4333562e36', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'api'); +INSERT INTO selfservice_registration_flow_methods (id, method, selfservice_registration_request_id, config, created_at, updated_at) +VALUES ('356019d1-900e-4c6d-b3f1-564c86930979', 'password', '8ef215a9-e8d5-43b3-9aa3-cb4333562e36', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=87fa3f43-5155-42b4-a1ad-174c2595fdaf","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"KeEp3OT+pHSXJpZAP+C1hR3yr4eIjUBDhg9tc/F1WI1b61SgVCoaOsMbVN9qM8HiNoqefk7KfT7PLn8AfaOcrg=="},{"name":"password","type":"password","required":true,"errors":[{"message":"the password does not fulfill the password policy because: the password has been found in data breaches and must no longer be used."}]},{"name":"traits.email","type":"text","value":"foo@ory.sh"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_settings_flows (id, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, messages) +VALUES ('90b4f970-b9ae-42bc-a0a7-73ec750e0aa1', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '[]'); +INSERT INTO selfservice_settings_flow_methods (id, method, selfservice_settings_request_id, config, created_at, updated_at) +VALUES ('547e8444-b3b0-4de7-9fdf-b80d6a8de15f', 'profile', '90b4f970-b9ae-42bc-a0a7-73ec750e0aa1', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_verification_flows (id, request_url, issued_at, expires_at, form, via, csrf_token, success, created_at, updated_at, messages, type) +VALUES ('42f31e47-65e1-4be9-80ea-e5d8ed64b236', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'null', 'email', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', true, '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'api'); + +INSERT INTO selfservice_recovery_flows (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type) +VALUES ('0d14427f-e16d-43a5-8695-8278bf85d4eb', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api'); +INSERT INTO selfservice_recovery_flow_methods (id, method, selfservice_recovery_request_id, config, created_at, updated_at) +VALUES ('1a45ce9b-b527-4535-ad31-b808e481d280', 'link', '0d14427f-e16d-43a5-8695-8278bf85d4eb', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200810162450_testdata.sql b/persistence/sql/migratest/testdata/20200810162450_testdata.sql new file mode 100644 index 0000000..88f3c4f --- /dev/null +++ b/persistence/sql/migratest/testdata/20200810162450_testdata.sql @@ -0,0 +1,11 @@ +INSERT INTO selfservice_login_flow_methods (id, method, selfservice_login_flow_id, config, created_at, updated_at) +VALUES ('12aca2ae-248d-448c-a33c-5efb1b165238', 'password', 'b1fac7fb-d016-4a06-a7fe-e4eab2a0429f', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=3a9ea34f-0f12-469b-9417-3ae5795a7baa","method":"POST","fields":[{"name":"identifier","type":"text","required":true,"value":""},{"name":"password","type":"password","required":true},{"name":"csrf_token","type":"hidden","required":true,"value":"fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg=="}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_registration_flow_methods (id, method, selfservice_registration_flow_id, config, created_at, updated_at) +VALUES ('4849e60b-bd37-4e51-9361-3b6c27f7b9f8', 'password', '8ef215a9-e8d5-43b3-9aa3-cb4333562e36', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/registration/strategies/password?request=87fa3f43-5155-42b4-a1ad-174c2595fdaf","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"KeEp3OT+pHSXJpZAP+C1hR3yr4eIjUBDhg9tc/F1WI1b61SgVCoaOsMbVN9qM8HiNoqefk7KfT7PLn8AfaOcrg=="},{"name":"password","type":"password","required":true,"errors":[{"message":"the password does not fulfill the password policy because: the password has been found in data breaches and must no longer be used."}]},{"name":"traits.email","type":"text","value":"foo@ory.sh"}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_settings_flow_methods (id, method, selfservice_settings_flow_id, config, created_at, updated_at) +VALUES ('d3501777-80cb-4fc6-b1ec-04c8d4d5b187', 'profile', '90b4f970-b9ae-42bc-a0a7-73ec750e0aa1', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_recovery_flow_methods (id, method, selfservice_recovery_flow_id, config, created_at, updated_at) +VALUES ('1d53af7f-8379-4ad0-9e00-16a2963bea83', 'link', '0d14427f-e16d-43a5-8695-8278bf85d4eb', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?request=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200812124254_testdata.sql b/persistence/sql/migratest/testdata/20200812124254_testdata.sql new file mode 100644 index 0000000..b832d4c --- /dev/null +++ b/persistence/sql/migratest/testdata/20200812124254_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO sessions (id, issued_at, expires_at, authenticated_at,created_at,updated_at, token, identity_id) +VALUES ('8571e374-38f2-4f46-8ad3-b9d914e174d3','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19', '1001ba7ddd644cb68478e8947e4jfha', '5ff66179-c240-4703-b0d8-494592cefff5') diff --git a/persistence/sql/migratest/testdata/20200812160551_testdata.sql b/persistence/sql/migratest/testdata/20200812160551_testdata.sql new file mode 100644 index 0000000..d0c64a9 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200812160551_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO sessions (id, issued_at, expires_at, authenticated_at,created_at,updated_at, token, identity_id, active) +VALUES ('f38cdebe-e567-42c9-a562-1bd4dee40998','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19', '1001ba7ddd644cb68478e8947e4jfhb', '5ff66179-c240-4703-b0d8-494592cefff5', true) diff --git a/persistence/sql/migratest/testdata/20200830121710_testdata.sql b/persistence/sql/migratest/testdata/20200830121710_testdata.sql new file mode 100644 index 0000000..f16c71a --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830121710_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_recovery_tokens (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) +VALUES ('77ca3f5c-cd39-488b-9f1d-cc7166d14bdc', 'd4c66613-c4cd-47c7-8e26-a70ecc5f5b5f', false, null, 'b8293f1c-010f-45d9-b809-f3fc5365ba80', '13178936-095a-466b-abe0-36d977d3dc18', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200830130642_testdata.sql b/persistence/sql/migratest/testdata/20200830130642_testdata.sql new file mode 100644 index 0000000..190e116 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830130642_testdata.sql @@ -0,0 +1 @@ +-- nothing do to diff --git a/persistence/sql/migratest/testdata/20200830130643_testdata.sql b/persistence/sql/migratest/testdata/20200830130643_testdata.sql new file mode 100644 index 0000000..190e116 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830130643_testdata.sql @@ -0,0 +1 @@ +-- nothing do to diff --git a/persistence/sql/migratest/testdata/20200830130644_testdata.sql b/persistence/sql/migratest/testdata/20200830130644_testdata.sql new file mode 100644 index 0000000..190e116 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830130644_testdata.sql @@ -0,0 +1 @@ +-- nothing do to diff --git a/persistence/sql/migratest/testdata/20200830130645_testdata.sql b/persistence/sql/migratest/testdata/20200830130645_testdata.sql new file mode 100644 index 0000000..190e116 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830130645_testdata.sql @@ -0,0 +1 @@ +-- nothing do to diff --git a/persistence/sql/migratest/testdata/20200830130646_testdata.sql b/persistence/sql/migratest/testdata/20200830130646_testdata.sql new file mode 100644 index 0000000..d32ecc3 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830130646_testdata.sql @@ -0,0 +1,5 @@ +INSERT INTO selfservice_verification_flows (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, created_at, updated_at) +VALUES ('5385c962-0295-4575-9b1b-d7eef13c0eda', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '[]', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) +VALUES ('42021826-ad73-44f1-9365-6e2ba455c883', 'link', '5385c962-0295-4575-9b1b-d7eef13c0eda', '{"action":"http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings/strategies/profile?flow=21c5f714-3089-49d2-b387-f244d4dd9e00","method":"POST","fields":[{"name":"csrf_token","type":"hidden","required":true,"value":"yDwSg0quCmc4kBl7lBqYwGh4W8awrc+TpeWiigZs3iemRCwqeDhGdrW3sIv8T7u742pN+Kryx/NrdRpEXcT9qA=="},{"name":"traits.email","type":"text","value":"foo","errors":[{"message":"validation failed"},{"message":"foo is not valid email"},{"message":"foo is not valid email"}]}]}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200830154602_testdata.sql b/persistence/sql/migratest/testdata/20200830154602_testdata.sql new file mode 100644 index 0000000..a406627 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830154602_testdata.sql @@ -0,0 +1,5 @@ +INSERT INTO identity_verification_tokens (id, token, used, used_at, identity_verifiable_address_id, selfservice_verification_flow_id, created_at, updated_at, expires_at, issued_at) +VALUES ('ee56574d-2f0c-43f6-8d26-0062938ae330', '1001ba7ddd644cb68478e8947e4jfhc', false, null, '45e867e9-2745-4f16-8dd4-84334a252b61', '5385c962-0295-4575-9b1b-d7eef13c0eda', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_verification_tokens (id, token, used, used_at, identity_verifiable_address_id, selfservice_verification_flow_id, created_at, updated_at, expires_at, issued_at) +VALUES ('f81fd924-23bb-4cdf-8fa0-56253eff6cc9', '1001ba7ddd644cb68478e8947e4jfhd', false, null, '45e867e9-2745-4f16-8dd4-84334a252b61', NULL, '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200830172221_testdata.sql b/persistence/sql/migratest/testdata/20200830172221_testdata.sql new file mode 100644 index 0000000..141e685 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200830172221_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_recovery_tokens (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) +VALUES ('1b667e6d-8fda-4194-a765-08185185d7e4', '1001ba7ddd644cb68478e8947e4jfhe', false, null, 'b8293f1c-010f-45d9-b809-f3fc5365ba80', '13178936-095a-466b-abe0-36d977d3dc18', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20200831110752_testdata.sql b/persistence/sql/migratest/testdata/20200831110752_testdata.sql new file mode 100644 index 0000000..645b072 --- /dev/null +++ b/persistence/sql/migratest/testdata/20200831110752_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_verifiable_addresses (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) VALUES +('b2d59320-8564-4400-a39f-a22a497a23f1', 'pending', 'email', false, 'foobar+without-code@ory.sh', null, 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20201201161451_testdata.sql b/persistence/sql/migratest/testdata/20201201161451_testdata.sql new file mode 100644 index 0000000..73be3d0 --- /dev/null +++ b/persistence/sql/migratest/testdata/20201201161451_testdata.sql @@ -0,0 +1,3 @@ +SELECT * +FROM identity_credential_types +WHERE name = 'password' OR name = 'oidc'; diff --git a/persistence/sql/migratest/testdata/20210118113234_testdata.sql b/persistence/sql/migratest/testdata/20210118113234_testdata.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migratest/testdata/20210126114619_testdata.sql b/persistence/sql/migratest/testdata/20210126114619_testdata.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migratest/testdata/20210307130558_testdata.sql b/persistence/sql/migratest/testdata/20210307130558_testdata.sql new file mode 100644 index 0000000..b256c62 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210307130558_testdata.sql @@ -0,0 +1 @@ +INSERT INTO courier_messages (id, type, status, body, subject, recipient, created_at, updated_at) VALUES ('34948489-31dc-454a-ab3b-b2dcc75a787f', 1, 2, 'Hi, Verify your account by opening the following link: http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/AtsREGbtXu0RlIcwv3RPpxHEZNEcq3R9', 'Please verify your email address', 'foo@ory.sh', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20210307130559_testdata.sql b/persistence/sql/migratest/testdata/20210307130559_testdata.sql new file mode 100644 index 0000000..8b2e1c4 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210307130559_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO courier_messages (id, status, type, recipient, body, subject, template_type, template_data, created_at, updated_at) VALUES +('b2d59320-8564-4400-a39f-a22a497a23f1', 1, 1, 'foo@bar.com', 'body', 'subject', 'recovery_invalid', 'binary_data', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20210311102338_testdata.sql b/persistence/sql/migratest/testdata/20210311102338_testdata.sql new file mode 100644 index 0000000..cdbc383 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210311102338_testdata.sql @@ -0,0 +1,14 @@ +INSERT INTO selfservice_login_flows (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui) +VALUES ('0bc96cc9-dda4-4700-9e42-35731f2af91e', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}'); + +INSERT INTO selfservice_registration_flows (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui) +VALUES ('e2150cdc-23ac-4940-a240-6c79c27ab029', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO selfservice_settings_flows (id, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, ui) +VALUES ('aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '{}'); + +INSERT INTO selfservice_verification_flows (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, ui) +VALUES ('6aae3159-b880-4cfb-a863-03b114b1371b', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO selfservice_recovery_flows (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui) +VALUES ('4963f305-e874-4a68-8424-a00bec679e7b', 'http://kratos:4433/self-service/browser/flows/recovery', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); diff --git a/persistence/sql/migratest/testdata/20210410175418_testdata.sql b/persistence/sql/migratest/testdata/20210410175418_testdata.sql new file mode 100644 index 0000000..491ddbb --- /dev/null +++ b/persistence/sql/migratest/testdata/20210410175418_testdata.sql @@ -0,0 +1,24 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui) +VALUES ('d6aa1f23-88c9-4b9b-a850-392f48c7f9e8', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}'); + +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui) +VALUES ('f1b5ed18-113a-4a98-aae7-d4eba007199c', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO selfservice_settings_flows (id, nid, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, ui) +VALUES ('19ede218-928c-4e02-ab49-b76e12b34f31', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '{}'); + +INSERT INTO selfservice_verification_flows (id, nid, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, ui) +VALUES ('7be6c72c-c868-4b61-a1f0-1130603665d8', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO selfservice_recovery_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui) +VALUES ('68fb4010-84a9-4d1e-9f92-2705978ee89e', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/recovery', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'link', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO courier_messages (id, nid, status, type, recipient, body, subject, template_type, template_data, created_at, updated_at) VALUES +('b821adf0-a067-4b3c-9f90-cac496d02a92', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 1, 1, 'foo@bar.com', 'body', 'subject', 'recovery_invalid', 'binary_data', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identity_verifiable_addresses (id, nid, status, via, verified, value, verified_at, identity_id, created_at, updated_at) VALUES +('d4718a67-aec2-418d-8173-6ebc7bde3b86', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'pending', 'email', false, 'foobar+11345642c6c0@ory.sh', null, 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES ('196d8c1e-4f04-40f0-94b3-5ec43996b28a', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', '{"email":"foobar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES ('ed253b2c-48ed-4c58-9b6f-1dc963c30a66', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', '{"email":"bazbar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); + diff --git a/persistence/sql/migratest/testdata/20210504121624_testdata.sql b/persistence/sql/migratest/testdata/20210504121624_testdata.sql new file mode 100644 index 0000000..9298b81 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210504121624_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, state) VALUES ('2ae6a5a7-2983-49e7-a4d8-7740b37c88cb','884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', '{"email":"d7b10@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'active'); +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES ('359963ec-b09b-4ea0-aece-fb4dd95f304a','884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', '{"email":"d7b11@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19'); diff --git a/persistence/sql/migratest/testdata/20210618103120_testdata.sql b/persistence/sql/migratest/testdata/20210618103120_testdata.sql new file mode 100644 index 0000000..f9fc4e4 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210618103120_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO sessions (id, nid, issued_at, expires_at, authenticated_at,created_at,updated_at, token, identity_id, active, logout_token) +VALUES ('dcde5aaa-f789-4d3d-ae1f-76da8d57e67c','884f556e-eb3a-4b9f-bee3-11345642c6c0','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19','2013-10-07 08:23:19', 'eVwBt7UWPweVwBt7UWPw', '5ff66179-c240-4703-b0d8-494592cefff5', true, '123eVwBt7UWPweVwBt7UWPw') diff --git a/persistence/sql/migratest/testdata/20210805112414_testdata.sql b/persistence/sql/migratest/testdata/20210805112414_testdata.sql new file mode 100644 index 0000000..344a7fb --- /dev/null +++ b/persistence/sql/migratest/testdata/20210805112414_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO selfservice_settings_flows (id, nid, request_url, issued_at, expires_at, state, identity_id, created_at, updated_at, active_method, ui, internal_context) +VALUES ('8248bb5d-8ef7-45e3-8e07-9e2003dd5352', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'http://kratos:4433/self-service/browser/flows/settings', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '{}', '{"foo":"bar"}'); diff --git a/persistence/sql/migratest/testdata/20210805122535_testdata.sql b/persistence/sql/migratest/testdata/20210805122535_testdata.sql new file mode 100644 index 0000000..a15eaeb --- /dev/null +++ b/persistence/sql/migratest/testdata/20210805122535_testdata.sql @@ -0,0 +1 @@ +-- nothing to do here because the schema did not change diff --git a/persistence/sql/migratest/testdata/20210810153530_testdata.sql b/persistence/sql/migratest/testdata/20210810153530_testdata.sql new file mode 100644 index 0000000..fdb1e46 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210810153530_testdata.sql @@ -0,0 +1,13 @@ +INSERT INTO sessions (id, nid, issued_at, expires_at, authenticated_at, created_at, updated_at, token, identity_id, + active, logout_token, aal, authentication_methods) +VALUES ('7458af86-c1d8-401c-978a-8da89133f78b', '884f556e-eb3a-4b9f-bee3-11345642c6c0', '2013-10-07 08:23:19', + '2080-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'eVwBt7UAAAAVwBt7UWPw', '5ff66179-c240-4703-b0d8-494592cefff5', true, '123eVwBt7UAAAeVwBt7UWPw', 'aal2', + '[{"method":"password"},{"method":"totp"}]'); + +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, requested_aal) +VALUES ('1fb23c75-b809-42cc-8984-6ca2d0a1192f', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', 'aal2'); diff --git a/persistence/sql/migratest/testdata/20210813150152_testdata.sql b/persistence/sql/migratest/testdata/20210813150152_testdata.sql new file mode 100644 index 0000000..a15eaeb --- /dev/null +++ b/persistence/sql/migratest/testdata/20210813150152_testdata.sql @@ -0,0 +1 @@ +-- nothing to do here because the schema did not change diff --git a/persistence/sql/migratest/testdata/20210816113956_testdata.sql b/persistence/sql/migratest/testdata/20210816113956_testdata.sql new file mode 100644 index 0000000..a15eaeb --- /dev/null +++ b/persistence/sql/migratest/testdata/20210816113956_testdata.sql @@ -0,0 +1 @@ +-- nothing to do here because the schema did not change diff --git a/persistence/sql/migratest/testdata/20210816142650_testdata.sql b/persistence/sql/migratest/testdata/20210816142650_testdata.sql new file mode 100644 index 0000000..1168a2d --- /dev/null +++ b/persistence/sql/migratest/testdata/20210816142650_testdata.sql @@ -0,0 +1,13 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, requested_aal, internal_context) +VALUES ('38caf592-b042-4551-b92f-8d5223c2a4e2', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', 'aal2', '{"foo":"bar"}'); + +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context) +VALUES ('8f32efdc-f6fc-4c27-a3c2-579d109eff60', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration', '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}', '{"foo":"bar"}'); diff --git a/persistence/sql/migratest/testdata/20210817181232_testdata.sql b/persistence/sql/migratest/testdata/20210817181232_testdata.sql new file mode 100644 index 0000000..4776b42 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210817181232_testdata.sql @@ -0,0 +1,7 @@ +INSERT INTO identity_credential_identifiers (id, nid, identifier, identity_credential_id, created_at, updated_at, identity_credential_type_id) +VALUES ('2672f198-4795-437c-8e14-56459b1d941a', '884f556e-eb3a-4b9f-bee3-11345642c6c0','foo1@ory.sh', '35b60ecf-30f9-42d6-bf5d-47ad41148691', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '22bff9ae-f5aa-45d7-803b-97ec0b4e7b32'); + +INSERT INTO identity_credential_identifiers (id, nid, identifier, identity_credential_id, created_at, updated_at, identity_credential_type_id) +VALUES ('10985ed1-5b6e-4012-ac10-03d87df65618', '884f556e-eb3a-4b9f-bee3-11345642c6c0','foo2@ory.sh', '74ac2d31-bccb-442f-a792-7b8bb14817f8', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '6b213fa0-e6ad-46cb-8878-b088d2ce2e3c'); diff --git a/persistence/sql/migratest/testdata/20210829131458_testdata.sql b/persistence/sql/migratest/testdata/20210829131458_testdata.sql new file mode 100644 index 0000000..8601003 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210829131458_testdata.sql @@ -0,0 +1 @@ +-- empty because it is just an SQL insert diff --git a/persistence/sql/migratest/testdata/20210913095309_testdata.sql b/persistence/sql/migratest/testdata/20210913095309_testdata.sql new file mode 100644 index 0000000..c34e690 --- /dev/null +++ b/persistence/sql/migratest/testdata/20210913095309_testdata.sql @@ -0,0 +1,4 @@ +UPDATE identity_recovery_tokens SET identity_id = 'a251ebc2-880c-4f76-a8f3-38e6940eab0e' WHERE id = '5529d454-2946-404e-b681-d950f8657fd0'; +UPDATE identity_recovery_tokens SET identity_id = 'a251ebc2-880c-4f76-a8f3-38e6940eab0e' WHERE id = '77ca3f5c-cd39-488b-9f1d-cc7166d14bdc'; +UPDATE identity_recovery_tokens SET identity_id = 'a251ebc2-880c-4f76-a8f3-38e6940eab0e' WHERE id = '1b667e6d-8fda-4194-a765-08185185d7e4'; + diff --git a/persistence/sql/migratest/testdata/20220118104539_testdata.sql b/persistence/sql/migratest/testdata/20220118104539_testdata.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migratest/testdata/20220301102701_testdata.sql b/persistence/sql/migratest/testdata/20220301102701_testdata.sql new file mode 100644 index 0000000..a6227ee --- /dev/null +++ b/persistence/sql/migratest/testdata/20220301102701_testdata.sql @@ -0,0 +1,6 @@ +INSERT INTO identity_credentials (id, nid, config, identity_credential_type_id, identity_id, created_at, updated_at, + version) +VALUES ('4cefc264-4291-4abc-8f26-cc0217874f14', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + '{"totp_url":"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"}', + '8071b37b-0d54-4c6f-8234-72cffb4ce784', '5ff66179-c240-4703-b0d8-494592cefff5', '2013-10-07 08:23:19', + '2013-10-07 08:23:19', 0); diff --git a/persistence/sql/migratest/testdata/20220420102701_testdata.sql b/persistence/sql/migratest/testdata/20220420102701_testdata.sql new file mode 100644 index 0000000..90faae4 --- /dev/null +++ b/persistence/sql/migratest/testdata/20220420102701_testdata.sql @@ -0,0 +1 @@ +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin) VALUES ('308929d3-41a2-43fe-a33c-75308539d841', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', '{"email":"bazbar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}'); diff --git a/persistence/sql/migratest/testdata/20220607000001_testdata.sql b/persistence/sql/migratest/testdata/20220607000001_testdata.sql new file mode 100644 index 0000000..7093698 --- /dev/null +++ b/persistence/sql/migratest/testdata/20220607000001_testdata.sql @@ -0,0 +1,11 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, requested_aal, internal_context, oauth2_login_challenge) +VALUES ('349c945a-60f8-436a-a301-7a42c92604f9', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login?login_challenge=3caddfd599034bce83ffcae36f42dff7', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'browser', '{}', 'aal2', '{"foo":"bar"}', '3caddfd5-9903-4bce-83ff-cae36f42dff7'); + +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, internal_context, oauth2_login_challenge) +VALUES ('ef18b06e-4700-4021-9949-ef783cd86be8', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge=', '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', '3caddfd5-9903-4bce-83ff-cae36f42dff7'); diff --git a/persistence/sql/migratest/testdata/20220902141902_testdata.sql b/persistence/sql/migratest/testdata/20220902141902_testdata.sql new file mode 100644 index 0000000..50181c4 --- /dev/null +++ b/persistence/sql/migratest/testdata/20220902141902_testdata.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_verification_tokens (id, token, used, used_at, identity_verifiable_address_id, selfservice_verification_flow_id, created_at, updated_at, expires_at, issued_at, nid) +VALUES ('ee56574d-2f1c-43f6-8d26-0062938ae330', '1001ba7ddd644cb68478e8947e4jfhe', false, null, '45e867e9-2745-4f16-8dd4-84334a252b61', '5385c962-0295-4575-9b1b-d7eef13c0eda', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '884f556e-eb3a-4b9f-bee3-11345642c6c0'); diff --git a/persistence/sql/migratest/testdata/20220907132836_testdata.sql b/persistence/sql/migratest/testdata/20220907132836_testdata.sql new file mode 100644 index 0000000..0c3d3ec --- /dev/null +++ b/persistence/sql/migratest/testdata/20220907132836_testdata.sql @@ -0,0 +1,12 @@ +INSERT INTO sessions (id, nid, issued_at, expires_at, authenticated_at, created_at, updated_at, token, identity_id, + active, logout_token, aal, authentication_methods) +VALUES ('7458af86-c1d8-401c-978a-8da89133f98b', '884f556e-eb3a-4b9f-bee3-11345642c6c0', '2013-10-07 08:23:19', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'eVwBt7UAAAAVwBt7XAMw', '5ff66179-c240-4703-b0d8-494592cefff5', true, '123eVwBt7UAAAeVwBt7XAMw', 'aal2', + '[{"method":"password"},{"method":"totp"}]'); + +INSERT INTO session_devices (id, nid, session_id, ip_address, user_agent, location, created_at, updated_at) +VALUES ('884f556e-eb3a-4b9f-bee3-11763642c6c0', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + '7458af86-c1d8-401c-978a-8da89133f98b', '54.155.246.232', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36', + 'Munich, Germany', '2022-08-07 08:23:19', '2022-08-09 08:35:19'); diff --git a/persistence/sql/migratest/testdata/20220929124401_testdata.sql b/persistence/sql/migratest/testdata/20220929124401_testdata.sql new file mode 100644 index 0000000..8a73a98 --- /dev/null +++ b/persistence/sql/migratest/testdata/20220929124401_testdata.sql @@ -0,0 +1,14 @@ +INSERT INTO identity_recovery_codes (id, code, used_at, identity_recovery_address_id, code_type, expires_at, issued_at, selfservice_recovery_flow_id, created_at, updated_at, nid, identity_id) VALUES ( + '8f75f5d9-9cb4-4848-9a73-9344f686f8a6', + '7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', + null, + 'b8293f1c-010f-45d9-b809-f3fc5365ba80', + 1, + '2022-08-18 08:28:18', + '2022-08-18 07:28:18', + '68fb4010-84a9-4d1e-9f92-2705978ee89e', + '2022-08-18 07:28:18', + '2022-08-18 07:28:18', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + '308929d3-41a2-43fe-a33c-75308539d841' +) \ No newline at end of file diff --git a/persistence/sql/migratest/testdata/20221103120601_testdata.sql b/persistence/sql/migratest/testdata/20221103120601_testdata.sql new file mode 100644 index 0000000..ed1a79a --- /dev/null +++ b/persistence/sql/migratest/testdata/20221103120601_testdata.sql @@ -0,0 +1,55 @@ +INSERT INTO + selfservice_verification_flows ( + id, + nid, + request_url, + issued_at, + expires_at, + csrf_token, + created_at, + updated_at, + type, + ui, + submit_count + ) +VALUES + ( + '81f74e5d-1fa5-4e1b-a9bf-e9511926047c', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/verification/email', + '2022-11-03 08:23:19', + '2022-11-03 08:23:19', + '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', + '2022-11-03 08:23:19', + '2022-11-03 08:23:19', + 'api', + '{}', + 0 + ); + +INSERT INTO + identity_verification_codes ( + id, + code_hmac, + used_at, + identity_verifiable_address_id, + expires_at, + issued_at, + selfservice_verification_flow_id, + created_at, + updated_at, + nid + ) +VALUES + ( + '5ab4e72b-332a-48ab-b1e9-3b1ef1ba5b60', + '7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', + null, + '45e867e9-2745-4f16-8dd4-84334a252b61', + '2022-11-03 08:28:18', + '2022-11-03 08:28:18', + '81f74e5d-1fa5-4e1b-a9bf-e9511926047c', + '2022-11-03 08:28:18', + '2022-11-03 08:28:18', + '884f556e-eb3a-4b9f-bee3-11345642c6c0' + ); \ No newline at end of file diff --git a/persistence/sql/migratest/testdata/20221205095201_testdata.sql b/persistence/sql/migratest/testdata/20221205095201_testdata.sql new file mode 100644 index 0000000..fee47fd --- /dev/null +++ b/persistence/sql/migratest/testdata/20221205095201_testdata.sql @@ -0,0 +1,41 @@ +INSERT INTO + courier_messages ( + id, + type, + status, + body, + subject, + recipient, + created_at, + updated_at, + template_type, + nid, + send_count + ) +VALUES + ( + 'd9d4401c-08a1-434c-8ab5-4a7edefde351', + 1, + 2, + 'Hi, Verify your account by opening the following link: http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/verification/email/confirm/u9ZcBr5HbRTR8f53Qj2Ng3KR8Mv1Zjdb', + 'Please verify your email address', + 'foobar@ory.sh', + '2013-10-07 08:23:19', + '2013-10-07 08:23:19', + 'verification_valid', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 4 + ); + +INSERT INTO + courier_message_dispatches +VALUES + ( + 'ea0c9a03-44ce-43a8-8c5d-52f468991cb9', + 'd9d4401c-08a1-434c-8ab5-4a7edefde351', + 'success', + '{}', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + '2013-10-07 08:23:19', + '2013-10-07 08:23:19' + ) diff --git a/persistence/sql/migratest/testdata/20230313141439_testdata.sql b/persistence/sql/migratest/testdata/20230313141439_testdata.sql new file mode 100644 index 0000000..7802292 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230313141439_testdata.sql @@ -0,0 +1,6 @@ +INSERT INTO sessions (id, nid, issued_at, expires_at, authenticated_at, created_at, updated_at, token, identity_id, + active, logout_token, aal, authentication_methods) +VALUES ('068f6bb6-d15f-436d-94f7-b3fd0489c9ef', '884f556e-eb3a-4b9f-bee3-11345642c6c0', '2013-10-07 08:23:19', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'ory_lo_5e5aad0f7a4143452df3d23733a68e3', '5ff66179-c240-4703-b0d8-494592cefff5', true, 'ory_st_5e5aad0f7a4143452df3d23733a68e2', 'aal2', + '[{"method":"password"},{"method":"totp"}]'); diff --git a/persistence/sql/migratest/testdata/20230614205200_testdata.sql b/persistence/sql/migratest/testdata/20230614205200_testdata.sql new file mode 100644 index 0000000..c047c59 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230614205200_testdata.sql @@ -0,0 +1,8 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data) +VALUES ('cccccccc-dda4-4700-9e42-35731f2af91e', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data'); diff --git a/persistence/sql/migratest/testdata/20230705000000_testdata.sql b/persistence/sql/migratest/testdata/20230705000000_testdata.sql new file mode 100644 index 0000000..69ffa56 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230705000000_testdata.sql @@ -0,0 +1,64 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data) +VALUES ('cccccccc-dda4-4700-9e42-35731f2af911', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data'); + +INSERT INTO selfservice_verification_flows (id, + nid, + request_url, + issued_at, + expires_at, + csrf_token, + created_at, + updated_at, + type, + ui, + submit_count) +VALUES ('81f74e5d-1fa5-4e1b-a9bf-e95119260471', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email/self-service/browser/flows/verification/email', + '2022-11-03 08:23:19', + '2022-11-03 08:23:19', + '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', + '2022-11-03 08:23:19', + '2022-11-03 08:23:19', + 'api', + '{}', + 0); + + +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context, oauth2_login_challenge) +VALUES ('ef18b06e-4700-4021-9949-ef783cd86be1', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', + '3caddfd5-9903-4bce-83ff-cae36f42dff7'); + +INSERT INTO selfservice_settings_flows (id, nid, request_url, issued_at, expires_at, state, identity_id, created_at, + updated_at, active_method, ui,internal_context) +VALUES ('19ede218-928c-4e02-ab49-b76e12b34f32', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings/self-service/browser/flows/settings', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'show_form', 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'profile', '{}', '{}'); + +INSERT INTO selfservice_verification_flows (id, nid, request_url, issued_at, expires_at, csrf_token, created_at, + updated_at, type, ui) +VALUES ('7be6c72c-c868-4b61-a1f0-1130603665d1', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/verification/email', '2013-10-07 08:23:19', + '2013-10-07 08:23:19', + '8xoIMa1+UkDqTt+tIHmIEHztQkk0AWk2PJhWWYDmB6dSE+RtJinnxtwH5lNNCnYyQuCF2ugy7rWjCgiwYPJNOw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); + +INSERT INTO selfservice_recovery_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, state, + recovered_identity_id, created_at, updated_at, type, ui) +VALUES ('68fb4010-84a9-4d1e-9f92-2705978ee891', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery/self-service/browser/flows/recovery', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'link', + 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', 'choose_method', + 'a251ebc2-880c-4f76-a8f3-38e6940eab0e', '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'api', '{}'); diff --git a/persistence/sql/migratest/testdata/20230706000000_testdata.sql b/persistence/sql/migratest/testdata/20230706000000_testdata.sql new file mode 100644 index 0000000..162cba6 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230706000000_testdata.sql @@ -0,0 +1,11 @@ +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin, + available_aal) +VALUES ('0149ce5f-76a8-4efe-b2e3-431b8c6cceb6', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', + '{"email":"bazbar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}', + 'aal1'); + +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin, + available_aal) +VALUES ('0149ce5f-76a8-4efe-b2e3-431b8c6cceb7', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', + '{"email":"bazbarbar@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}', + NULL); diff --git a/persistence/sql/migratest/testdata/20230707133700_testdata.sql b/persistence/sql/migratest/testdata/20230707133700_testdata.sql new file mode 100644 index 0000000..bcfc9bc --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133700_testdata.sql @@ -0,0 +1,30 @@ +INSERT INTO selfservice_login_flows (id,nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data, state) +VALUES ('00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data', 'choose_method'); + +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin, + available_aal) +VALUES ('28ff0031-190b-4253-bd15-14308dec013e', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', + '{"email":"bazbarbarfoo@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}', + NULL); + +INSERT INTO identity_login_codes (id, code, address, address_type, used_at, expires_at, issued_at, selfservice_login_flow_id, identity_id, + created_at, updated_at, nid) +VALUES ('bd292366-af32-4ba6-bdf0-11d6d1a217f3', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +'bazbarbarfoo@ory.com', +'email', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', +'28ff0031-190b-4253-bd15-14308dec013e', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migratest/testdata/20230707133701_testdata.sql b/persistence/sql/migratest/testdata/20230707133701_testdata.sql new file mode 100644 index 0000000..8a25631 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133701_testdata.sql @@ -0,0 +1,23 @@ +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context, oauth2_login_challenge, state) +VALUES ('69c80296-36cd-4afc-921a-15369cac5bf0', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', + '3caddfd5-9903-4bce-83ff-cae36f42dff7', 'choose_method'); + +INSERT INTO identity_registration_codes (id, address, address_type, code, used_at, expires_at, issued_at, selfservice_registration_flow_id, + created_at, updated_at, nid) +VALUES ('f1f66a69-ce02-4a12-9591-9e02dda30a0d', +'example@example.com', +'email', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'69c80296-36cd-4afc-921a-15369cac5bf0', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migrations/go/20251105000000000000_identity_id_not_null_fks.go b/persistence/sql/migrations/go/20251105000000000000_identity_id_not_null_fks.go new file mode 100644 index 0000000..4c8151d --- /dev/null +++ b/persistence/sql/migrations/go/20251105000000000000_identity_id_not_null_fks.go @@ -0,0 +1,289 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package gomigrations + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/popx" +) + +var backfillIdentityID = []popx.Migration{ + { + Version: "20251105000000000001", + Path: path(), + Name: "Backfill column identity_id in identity_credential_identifiers", + Direction: "up", + Type: "go", + DBType: "cockroach", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + _, err := c.Store.Exec("CREATE INDEX IF NOT EXISTS ici_identity_id_backfill_storing ON identity_credential_identifiers (identity_id ASC) STORING (identity_credential_id) NOT VISIBLE") + if err != nil { + return errors.WithStack(err) + } + for { + res, err := c.Store.Exec(` + UPDATE + identity_credential_identifiers@ici_identity_id_backfill_storing ici + SET + identity_id = ic.identity_id + FROM + identity_credentials ic + WHERE + ici.identity_credential_id = ic.id + -- AND ici.nid = ic.nid -- not needed because JOIN predicate is on primary key + AND ici.identity_id IS NULL + LIMIT 10000`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in identity_credential_identifiers table\n", n) + } + _, err = c.Store.Exec("DROP INDEX identity_credential_identifiers@ici_identity_id_backfill_storing") + return errors.WithStack(err) + }, + }, + { + Version: "20251105000000000001", + Path: path(), + Name: "Backfill column identity_id in identity_credential_identifiers", + Direction: "up", + Type: "go", + DBType: "postgres", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + for { + res, err := c.Store.Exec(` + WITH to_update AS ( + SELECT ici.id, ic.identity_id + FROM identity_credential_identifiers ici + JOIN identity_credentials ic ON ici.identity_credential_id = ic.id AND ici.nid = ic.nid + WHERE ici.identity_id IS NULL + LIMIT 10000 + ) + UPDATE identity_credential_identifiers ici + SET identity_id = to_update.identity_id + FROM to_update + WHERE ici.id = to_update.id`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in identity_credential_identifiers table\n", n) + } + return nil + }, + }, + { + Version: "20251105000000000001", + Path: path(), + Name: "Backfill column identity_id in identity_credential_identifiers", + Direction: "up", + Type: "go", + DBType: "mysql", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + for { + res, err := c.Store.Exec(` + UPDATE identity_credential_identifiers ici + JOIN ( + SELECT ici2.id + FROM identity_credential_identifiers ici2 + JOIN identity_credentials ic ON ici2.identity_credential_id = ic.id AND ici2.nid = ic.nid + WHERE ici2.identity_id IS NULL + LIMIT 10000 + ) t ON ici.id = t.id + JOIN identity_credentials ic ON ici.identity_credential_id = ic.id + SET ici.identity_id = ic.identity_id`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in identity_credential_identifiers table\n", n) + } + return nil + }, + }, + { + Version: "20251105000000000001", + Path: path(), + Name: "Backfill column identity_id in identity_credential_identifiers (noop)", + Direction: "up", + Type: "go", + DBType: "sqlite3", + Autocommit: false, + Runner: func(m popx.Migration, c *pop.Connection) error { + return nil // nothing, see 20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql + }, + }, + { + Version: "20251105000000000001", + Path: path(), + Name: "Revert backfill column identity_id in identity_credential_identifiers (noop)", + Direction: "down", + Type: "go", + DBType: "all", + Runner: func(m popx.Migration, c *pop.Connection) error { + return nil + }, + }, + { + Version: "20251105000000000002", + Path: path(), + Name: "Backfill column identity_id in session_devices", + Direction: "up", + Type: "go", + DBType: "cockroach", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + for { + res, err := c.Store.Exec(` + UPDATE + session_devices sd + SET + identity_id = s.identity_id + FROM + sessions s + WHERE + sd.session_id = s.id + AND sd.nid = s.nid + AND sd.identity_id IS NULL + LIMIT 10000`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in session_devices table\n", n) + } + return nil + }, + }, + { + Version: "20251105000000000002", + Path: path(), + Name: "Backfill column identity_id in session_devices", + Direction: "up", + Type: "go", + DBType: "postgres", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + for { + res, err := c.Store.Exec(` + WITH to_update AS ( + SELECT sd.id, s.identity_id + FROM session_devices sd + JOIN sessions s ON sd.session_id = s.id AND sd.nid = s.nid + WHERE sd.identity_id IS NULL + LIMIT 10000 + ) + UPDATE session_devices sd + SET identity_id = to_update.identity_id + FROM to_update + WHERE sd.id = to_update.id`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in session_devices table\n", n) + } + return nil + }, + }, + { + Version: "20251105000000000002", + Path: path(), + Name: "Backfill column identity_id in session_devices", + Direction: "up", + Type: "go", + DBType: "mysql", + Autocommit: true, + Runner: func(m popx.Migration, c *pop.Connection) error { + for { + res, err := c.Store.Exec(` + UPDATE session_devices sd + JOIN ( + SELECT sd2.id + FROM session_devices sd2 + JOIN sessions s2 ON sd2.session_id = s2.id AND sd2.nid = s2.nid + WHERE sd2.identity_id IS NULL + LIMIT 10000 + ) t ON sd.id = t.id + JOIN sessions s ON sd.session_id = s.id + SET sd.identity_id = s.identity_id`) + if err != nil { + return errors.WithStack(err) + } + n, err := res.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + if n == 0 { + break + } + fmt.Printf("Backfilled column identity_id for %d rows in session_devices table\n", n) + } + return nil + }, + }, + { + Version: "20251105000000000002", + Path: path(), + Name: "Backfill column identity_id in session_devices (noop)", + Direction: "up", + Type: "go", + DBType: "sqlite3", + Autocommit: false, + Runner: func(m popx.Migration, c *pop.Connection) error { + return nil // nothing, see 20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql + }, + }, + + { + Version: "20251105000000000002", + Path: path(), + Name: "Revert backfill column identity_id in session_devices (noop)", + Direction: "down", + Type: "go", + DBType: "all", + Autocommit: false, + Runner: func(m popx.Migration, c *pop.Connection) error { + return nil + }, + }, +} diff --git a/persistence/sql/migrations/go/gomigrations.go b/persistence/sql/migrations/go/gomigrations.go new file mode 100644 index 0000000..7683a6c --- /dev/null +++ b/persistence/sql/migrations/go/gomigrations.go @@ -0,0 +1,26 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package gomigrations + +import ( + "embed" + "fmt" + "path/filepath" + "runtime" + "slices" +) + +var ( + All = slices.Concat( + backfillIdentityID, + ) + + //go:embed *.go + Src embed.FS +) + +func path() string { + _, file, line, _ := runtime.Caller(1) + return fmt.Sprintf("%s:%d", filepath.Base(file), line) +} diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.down.sql new file mode 100644 index 0000000..30e62aa --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credentials";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credential_types";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identities";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.up.sql new file mode 100644 index 0000000..638b1b7 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.cockroach.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000001_identities.mysql.down.sql new file mode 100644 index 0000000..fcf2432 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_credential_identifiers`; +DROP TABLE `identity_credentials`; +DROP TABLE `identity_credential_types`; +DROP TABLE `identities`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000001_identities.mysql.up.sql new file mode 100644 index 0000000..35d1c2a --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.mysql.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`); +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000001_identities.postgres.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000001_identities.postgres.up.sql new file mode 100644 index 0000000..fec4915 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.postgres.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..bf912cb --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000001_identities.sqlite3.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.down.sql new file mode 100644 index 0000000..14ba150 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.up.sql new file mode 100644 index 0000000..1bca145 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.cockroach.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000002_requests.mysql.down.sql new file mode 100644 index 0000000..8aac48a --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.mysql.down.sql @@ -0,0 +1,5 @@ +DROP TABLE `selfservice_login_request_methods`; +DROP TABLE `selfservice_login_requests`; +DROP TABLE `selfservice_registration_request_methods`; +DROP TABLE `selfservice_registration_requests`; +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000002_requests.mysql.up.sql new file mode 100644 index 0000000..9894e39 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.mysql.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000002_requests.postgres.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.postgres.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000002_requests.postgres.up.sql new file mode 100644 index 0000000..d24b8b0 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.postgres.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..d265527 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000002_requests.sqlite3.up.sql @@ -0,0 +1,50 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.down.sql new file mode 100644 index 0000000..b7ffdf0 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.up.sql new file mode 100644 index 0000000..c2b8ea0 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000003_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.down.sql new file mode 100644 index 0000000..3081431 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.up.sql new file mode 100644 index 0000000..4e6d1a9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000004_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000004_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000004_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000004_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000004_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000005_identities.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000005_identities.mysql.down.sql new file mode 100644 index 0000000..139e50a --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000005_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); diff --git a/persistence/sql/migrations/legacy/20191100000005_identities.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000005_identities.mysql.up.sql new file mode 100644 index 0000000..8069ee9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000005_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.down.sql new file mode 100644 index 0000000..efa6f4e --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.up.sql new file mode 100644 index 0000000..5b10f19 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000006_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000006_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000006_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000006_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000006_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.down.sql new file mode 100644 index 0000000..3b38079 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.up.sql new file mode 100644 index 0000000..434d2ef --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT '';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000007_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000007_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000007_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000007_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.down.sql new file mode 100644 index 0000000..af1b234 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors"; + +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000007_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..ba30655 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_verifiable_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..5e030a2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..2e080c8 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql @@ -0,0 +1,2 @@ +DROP TABLE `selfservice_verification_requests`; +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..5951c67 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..419ec91 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..a12f20a --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000009_verification.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000009_verification.mysql.down.sql new file mode 100644 index 0000000..f8a7e0f --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000009_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); diff --git a/persistence/sql/migrations/legacy/20191100000009_verification.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000009_verification.mysql.up.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000009_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.down.sql new file mode 100644 index 0000000..c42e3bd --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.down.sql @@ -0,0 +1,5 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.up.sql new file mode 100644 index 0000000..e9b8fc9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000010_errors.mysql.down.sql new file mode 100644 index 0000000..525b179 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.mysql.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000010_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000010_errors.postgres.down.sql new file mode 100644 index 0000000..ccf29d7 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.postgres.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000010_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.down.sql new file mode 100644 index 0000000..52ef938 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.down.sql @@ -0,0 +1,13 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.up.sql new file mode 100644 index 0000000..fc59202 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000010_errors.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..53dd25f --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ADD COLUMN "body" text;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "courier_messages" SET "body" = "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000011_courier_body_type.down.sql b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..abd09ec --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages"; +DROP TABLE "courier_messages"; +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..f6ee1d9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..56056e9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..a4db2cc --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/persistence/sql/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..aa1ddb2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..5601793 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..d2e92cd --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON; +DROP TABLE `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..1c15ee7 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..a800a7b --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e5b9297 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..c1e707f --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP TABLE "selfservice_profile_management_request_methods"; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..f8c1bd8 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..11b99bc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..36a86ae --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..2d7a810 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..90b3c60 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..dba65df --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..7d66d48 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..32088d9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_recovery_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..f6c8b50 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..888e504 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_recovery_tokens`; +DROP TABLE `selfservice_recovery_request_methods`; +DROP TABLE `selfservice_recovery_requests`; +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..a935381 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`); +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..5b3fa1c --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..2901619 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,48 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..54c99e1 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); diff --git a/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..7972b34 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.down.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..35028f9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.up.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..127f682 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.down.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.up.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.down.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.up.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..346124a --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200601101001_verification.mysql.down.sql b/persistence/sql/migrations/legacy/20200601101001_verification.mysql.down.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/persistence/sql/migrations/legacy/20200601101001_verification.mysql.up.sql b/persistence/sql/migrations/legacy/20200601101001_verification.mysql.up.sql new file mode 100644 index 0000000..3bf20de --- /dev/null +++ b/persistence/sql/migrations/legacy/20200601101001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.down.sql b/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.down.sql new file mode 100644 index 0000000..80601b5 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.up.sql b/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.up.sql new file mode 100644 index 0000000..a9cfe75 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.mysql.down.sql b/persistence/sql/migrations/legacy/20200605111551_messages.mysql.down.sql new file mode 100644 index 0000000..427a387 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.mysql.up.sql b/persistence/sql/migrations/legacy/20200605111551_messages.mysql.up.sql new file mode 100644 index 0000000..21085a2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.postgres.down.sql b/persistence/sql/migrations/legacy/20200605111551_messages.postgres.down.sql new file mode 100644 index 0000000..7c73915 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.postgres.up.sql b/persistence/sql/migrations/legacy/20200605111551_messages.postgres.up.sql new file mode 100644 index 0000000..4ac597f --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.down.sql new file mode 100644 index 0000000..e1d0c11 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.down.sql @@ -0,0 +1,44 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.up.sql new file mode 100644 index 0000000..cc2a606 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200605111551_messages.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.down.sql b/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.down.sql new file mode 100644 index 0000000..5877dc9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.up.sql b/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.up.sql new file mode 100644 index 0000000..5848a58 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.cockroach.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.mysql.down.sql b/persistence/sql/migrations/legacy/20200607165100_settings.mysql.down.sql new file mode 100644 index 0000000..c43477e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state`; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.mysql.up.sql b/persistence/sql/migrations/legacy/20200607165100_settings.mysql.up.sql new file mode 100644 index 0000000..00b29fa --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.postgres.down.sql b/persistence/sql/migrations/legacy/20200607165100_settings.postgres.down.sql new file mode 100644 index 0000000..eaee899 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.postgres.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.postgres.up.sql b/persistence/sql/migrations/legacy/20200607165100_settings.postgres.up.sql new file mode 100644 index 0000000..4c2ac98 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.postgres.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.down.sql new file mode 100644 index 0000000..7366ef2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.down.sql @@ -0,0 +1,17 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.up.sql new file mode 100644 index 0000000..3892e62 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200607165100_settings.sqlite3.up.sql @@ -0,0 +1,18 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..07a1c56 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..fc35d52 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.down.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.down.sql new file mode 100644 index 0000000..10fcdad --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.up.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.up.sql new file mode 100644 index 0000000..b1edbc9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.down.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.down.sql new file mode 100644 index 0000000..6d503a7 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.up.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.up.sql new file mode 100644 index 0000000..2953d00 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.mysql.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.down.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.down.sql new file mode 100644 index 0000000..e36b97a --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.up.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.up.sql new file mode 100644 index 0000000..74d85bc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.postgres.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..238eea6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql @@ -0,0 +1,82 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at FROM "selfservice_recovery_requests"; + +DROP TABLE "selfservice_recovery_requests"; +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests"; +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..85a72ea --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..7b665f0 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..9b60677 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.down.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.down.sql new file mode 100644 index 0000000..6ba6c69 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests`; +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods`; +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods`; +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests`; +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests`; +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods`; +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests`; +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.up.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.up.sql new file mode 100644 index 0000000..28508f8 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.mysql.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods`; +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows`; +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods`; +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows`; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows`; +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods`; +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows`; +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.down.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.up.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.postgres.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..0ccaced --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..9f7efff --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..9a54733 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..93844b8 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..f9e4897 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..2549800 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql @@ -0,0 +1,8 @@ +DELETE FROM sessions;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "sessions" SET "token" = "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" DROP COLUMN "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "sessions_token_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.down.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.up.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.up.sql new file mode 100644 index 0000000..fed24fb --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.mysql.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions; +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32); +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32); +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`); +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.down.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.up.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.up.sql new file mode 100644 index 0000000..a819c47 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.postgres.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..75b3650 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +DROP INDEX IF EXISTS "sessions_token_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..a9f847a --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +DELETE FROM sessions; +ALTER TABLE "sessions" ADD COLUMN "token" TEXT; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..f51c199 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..cc90f31 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9e7bfcb --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,19 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..180633b --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..b254b6e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..25bc849 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_verification_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..c325dcb --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..fd97b4c --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON; +DROP TABLE `selfservice_verification_flow_methods`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state`; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..adbf65d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_verification_flow_methods"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..27a7bfa --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,34 @@ +DROP TABLE "selfservice_verification_flow_methods"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.down.sql b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..34053ed --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.down.sql b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..2dfbfde --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e8761c6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6677fbd --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.down.sql b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..fdccf04 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.down.sql b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..496f1eb --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..3aa5b8e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..db1391d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..374a2cf --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..12d59e2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..6050119 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`); +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..a171839 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..9250c46 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..9a992a2 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql @@ -0,0 +1,10 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..adb3e1d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..696bde3 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at`; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e5f6e9d --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..38e7b8b --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at"; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..79c2e87 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ea864bb --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,63 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..7ad4a61 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,23 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..932fd71 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1,15 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..24a61df --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..9cb6530 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32); +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME; +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..91dafe3 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1,4 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses`; +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..6425e54 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..840985e --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1,4 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX "identity_verifiable_addresses_code_idx"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f79f141 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,48 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME; +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..1279dc6 --- /dev/null +++ b/persistence/sql/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,43 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/persistence/sql/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.down.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.up.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.cockroach.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.down.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.down.sql new file mode 100644 index 0000000..beb6b14 --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `networks`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.up.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.up.sql new file mode 100644 index 0000000..0ba5bfc --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.mysql.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE `networks` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.down.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.up.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.postgres.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.down.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.up.sql b/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.up.sql new file mode 100644 index 0000000..f808e33 --- /dev/null +++ b/persistence/sql/migrations/sql/20150100000001000000_networks.sqlite3.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "networks" ( +"id" TEXT PRIMARY KEY, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.up.sql new file mode 100644 index 0000000..c8f210b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.cockroach.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.down.sql new file mode 100644 index 0000000..ae2d9ec --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identities`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.up.sql new file mode 100644 index 0000000..e3e9f95 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.mysql.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.up.sql new file mode 100644 index 0000000..605753a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.postgres.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.up.sql new file mode 100644 index 0000000..e175950 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000000_identities.sqlite3.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.down.sql new file mode 100644 index 0000000..445682f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.up.sql new file mode 100644 index 0000000..8c5916c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.cockroach.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.down.sql new file mode 100644 index 0000000..11feb1e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_types`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.up.sql new file mode 100644 index 0000000..af3947e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.down.sql new file mode 100644 index 0000000..445682f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.up.sql new file mode 100644 index 0000000..8c5916c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.postgres.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..445682f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..1a6b439 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000001_identities.sqlite3.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.down.sql new file mode 100644 index 0000000..caec556 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.up.sql new file mode 100644 index 0000000..45a7346 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.down.sql new file mode 100644 index 0000000..7a4140e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credentials`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.up.sql new file mode 100644 index 0000000..e637276 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.down.sql new file mode 100644 index 0000000..caec556 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.up.sql new file mode 100644 index 0000000..45a7346 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.down.sql new file mode 100644 index 0000000..caec556 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.up.sql new file mode 100644 index 0000000..45a7346 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000002_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.down.sql new file mode 100644 index 0000000..523de38 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.up.sql new file mode 100644 index 0000000..f9c95b4 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.down.sql new file mode 100644 index 0000000..c4423ce --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.up.sql new file mode 100644 index 0000000..d1d7259 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.down.sql new file mode 100644 index 0000000..523de38 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.up.sql new file mode 100644 index 0000000..2968680 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.down.sql new file mode 100644 index 0000000..523de38 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.up.sql new file mode 100644 index 0000000..055e545 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000003_identities.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.cockroach.up.sql new file mode 100644 index 0000000..4bcc671 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000004_identities.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.mysql.up.sql new file mode 100644 index 0000000..c588047 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000004_identities.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.postgres.up.sql new file mode 100644 index 0000000..7a58e1d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000004_identities.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000004_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000004_identities.sqlite3.up.sql new file mode 100644 index 0000000..e99de42 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000004_identities.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.cockroach.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000005_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.mysql.up.sql new file mode 100644 index 0000000..759def9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000005_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.postgres.down.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.postgres.up.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.postgres.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000005_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000001000005_identities.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000001000005_identities.sqlite3.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000001000005_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.up.sql new file mode 100644 index 0000000..0df62f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.down.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.down.sql new file mode 100644 index 0000000..77db2f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.up.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.up.sql new file mode 100644 index 0000000..d7071b2 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.down.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.up.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.up.sql new file mode 100644 index 0000000..0df62f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.up.sql new file mode 100644 index 0000000..ff50a80 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000000_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.down.sql new file mode 100644 index 0000000..2dea4fd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.up.sql new file mode 100644 index 0000000..d6b8f6e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.down.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.down.sql new file mode 100644 index 0000000..63abd4b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.up.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.up.sql new file mode 100644 index 0000000..0970e9d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.down.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.down.sql new file mode 100644 index 0000000..2dea4fd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.up.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.up.sql new file mode 100644 index 0000000..89e32e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.down.sql new file mode 100644 index 0000000..2dea4fd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.up.sql new file mode 100644 index 0000000..0247aef --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000001_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.down.sql new file mode 100644 index 0000000..f721215 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.up.sql new file mode 100644 index 0000000..440cd00 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.down.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.down.sql new file mode 100644 index 0000000..b192494 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.up.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.up.sql new file mode 100644 index 0000000..811b186 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.down.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.down.sql new file mode 100644 index 0000000..f721215 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.up.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.up.sql new file mode 100644 index 0000000..440cd00 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..f721215 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..19cb55a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000002_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.down.sql new file mode 100644 index 0000000..360cfa9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.up.sql new file mode 100644 index 0000000..1d3b464 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.down.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.down.sql new file mode 100644 index 0000000..494551e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.up.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.up.sql new file mode 100644 index 0000000..75fc4aa --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.down.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.down.sql new file mode 100644 index 0000000..360cfa9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.up.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.up.sql new file mode 100644 index 0000000..fbfe3a0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.down.sql new file mode 100644 index 0000000..360cfa9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.up.sql new file mode 100644 index 0000000..1418fd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000003_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.down.sql new file mode 100644 index 0000000..3fa2ae0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.up.sql new file mode 100644 index 0000000..d073080 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.down.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.down.sql new file mode 100644 index 0000000..f295d62 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.up.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.up.sql new file mode 100644 index 0000000..1fc559a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.down.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.down.sql new file mode 100644 index 0000000..3fa2ae0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.up.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.up.sql new file mode 100644 index 0000000..7530465 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.down.sql new file mode 100644 index 0000000..3fa2ae0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.up.sql new file mode 100644 index 0000000..48fa320 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000002000004_requests.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.up.sql new file mode 100644 index 0000000..9cf7fc2 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.down.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.up.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.down.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.up.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000003000000_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.up.sql new file mode 100644 index 0000000..a920e94 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.down.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.up.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.down.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.up.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000004000000_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000005000000_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000005000000_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000005000000_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000005000000_identities.mysql.up.sql new file mode 100644 index 0000000..4e227d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000005000000_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000005000001_identities.mysql.down.sql b/persistence/sql/migrations/sql/20191100000005000001_identities.mysql.down.sql new file mode 100644 index 0000000..48aed3b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000005000001_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000005000001_identities.mysql.up.sql b/persistence/sql/migrations/sql/20191100000005000001_identities.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.down.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.up.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.down.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.up.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000006000000_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.down.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.up.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.down.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.up.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000000_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000001_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000007000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..62218cd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000001_errors.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000001_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000007000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000007000002_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000007000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..cd90e4b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000002_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000007000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000007000003_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000007000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..03de169 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000007000003_errors.sqlite3.down.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000007000003_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000007000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..fa63a66 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..10d3639 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..8d1613d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..a3d979c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..a313e56 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000000_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..68853cd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..b1f6f0b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..12a661d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..68853cd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..68853cd --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000001_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..788e152 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000002_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..e5a9e36 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000003_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..27e4055 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000004_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..e843d51 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..a3dcda2 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.postgres.down.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.postgres.up.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..86d22fb --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..c4c063f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000008000005_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000009000000_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000009000000_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000009000000_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000009000000_verification.mysql.up.sql new file mode 100644 index 0000000..97d4ea6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000009000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000009000001_verification.mysql.down.sql b/persistence/sql/migrations/sql/20191100000009000001_verification.mysql.down.sql new file mode 100644 index 0000000..ae7e38c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000009000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000009000001_verification.mysql.up.sql b/persistence/sql/migrations/sql/20191100000009000001_verification.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.down.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.up.sql new file mode 100644 index 0000000..b3e412f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.down.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.down.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.up.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.down.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.down.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.up.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..c73b605 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000000_errors.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.down.sql new file mode 100644 index 0000000..8e611b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.up.sql new file mode 100644 index 0000000..c0ea09d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.mysql.down.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.mysql.down.sql new file mode 100644 index 0000000..b29a763 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.mysql.up.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.postgres.down.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.postgres.down.sql new file mode 100644 index 0000000..b29a763 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.postgres.up.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..40dcbfa --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000001_errors.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.down.sql new file mode 100644 index 0000000..c0ea09d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.up.sql new file mode 100644 index 0000000..8e611b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000002_errors.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..40dcbfa --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000002_errors.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.down.sql new file mode 100644 index 0000000..b3e412f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.up.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000003_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..c73b605 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000003_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000004_errors.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000010000004_errors.cockroach.down.sql new file mode 100644 index 0000000..b29a763 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000004_errors.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000010000004_errors.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000010000004_errors.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000010000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..b29a763 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000010000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000010000004_errors.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000010000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..f85fd78 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.mysql.down.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.mysql.up.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.postgres.down.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.postgres.up.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..46c5669 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000000_courier_body_type.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..fde7b16 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "body" text; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..49390f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000001_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..d936b42 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "courier_messages" SET "body" = "_body_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000002_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..025fa5c --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000003_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000011000004_courier_body_type.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000011000004_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000011000004_courier_body_type.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000011000004_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..228eae4 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000011000004_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.down.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.up.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.down.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.up.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.down.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.up.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..fd575c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000000_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000001_login_request_forced.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000012000001_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..19264fe --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000001_login_request_forced.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000001_login_request_forced.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000012000001_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000012000002_login_request_forced.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000012000002_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..ff5cb9a --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000002_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000002_login_request_forced.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000012000002_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20191100000012000003_login_request_forced.sqlite3.down.sql b/persistence/sql/migrations/sql/20191100000012000003_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..8a55b0d --- /dev/null +++ b/persistence/sql/migrations/sql/20191100000012000003_login_request_forced.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20191100000012000003_login_request_forced.sqlite3.up.sql b/persistence/sql/migrations/sql/20191100000012000003_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.down.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.up.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..7a1ebac --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..0e22af6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..a705732 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..9b6f5d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..196ecd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000000_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.down.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..f6233c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.up.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..96a425f --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..8284683 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..c4b4909 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..f6233c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..96a425f --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..c6d6419 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..4ea0ebd --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000001_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.down.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e0aeb11 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.up.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..64f30ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..e425b0d --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..64f30ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..16314ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..64f30ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..1489f62 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful, form) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful, form FROM "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..64f30ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000002_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.cockroach.down.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.cockroach.up.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..2ef80b8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..adbcdb4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..2ef80b8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..5b001b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +"form" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..bee98b0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000003_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.mysql.down.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..e17a1f5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.mysql.up.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.postgres.down.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..d076eb8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.postgres.up.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f6233c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..dcfaab6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000004_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..c6d6419 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000005_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000006_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000007_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000007_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..ac628b7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000007_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful, form) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful, form FROM "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000007_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000007_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000008_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000008_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..9273baf --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000008_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +"form" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000008_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000008_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000009_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000009_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..2ef80b8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000009_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000009_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000009_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200317160354000010_create_profile_request_forms.sqlite3.down.sql b/persistence/sql/migrations/sql/20200317160354000010_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..82f6995 --- /dev/null +++ b/persistence/sql/migrations/sql/20200317160354000010_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200317160354000010_create_profile_request_forms.sqlite3.up.sql b/persistence/sql/migrations/sql/20200317160354000010_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.down.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.up.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..cf9678d --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.down.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.up.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.down.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.up.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.down.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.up.sql b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/persistence/sql/migrations/sql/20200401183443000000_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.down.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.up.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..5733aca --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.down.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..c0c0ace --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.up.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..5c98c98 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.down.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.up.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..5733aca --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.down.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.up.sql b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..5733aca --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000000_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.down.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..02ba553 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.up.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..ef5a5ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.down.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..f959196 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.up.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..e1a5d5d --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.down.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..02ba553 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.up.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..ef5a5ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.down.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..02ba553 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.up.sql b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..ef5a5ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000001_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.down.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..570dc3f --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.up.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.down.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..4f97f55 --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.up.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..29362dd --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.down.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..570dc3f --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.up.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.down.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..570dc3f --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.up.sql b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/persistence/sql/migrations/sql/20200402142539000002_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..938344a --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..d79504e --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..13dcec2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..8b0c8e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..44665c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000000_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..96a05cd --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..ace942a --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..1fe6468 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..96a05cd --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..96a05cd --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000001_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..d4245d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..8c04682 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..b33e881 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..d4245d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..d4245d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000002_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..992b3f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..f5d669a --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_tokens`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..42814bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..4f26fe1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..90a5167 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000003_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..cf185b2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..61a59db --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..bf2c248 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..c0918c5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000004_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..e430dee --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..1317da0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..6fda20c --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..082adfa --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000005_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..1ede5de --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..4cb2443 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..1ede5de --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..1ede5de --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000006_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..0fb0af8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.sqlite3.down.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.sqlite3.up.sql b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101057000007_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101058000000_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101058000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200519101058000000_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101058000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..bdb89e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101058000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101058000001_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20200519101058000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..15ef490 --- /dev/null +++ b/persistence/sql/migrations/sql/20200519101058000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200519101058000001_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20200519101058000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.down.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.up.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..d1e8b79 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.down.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.up.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.down.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.up.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000001_create_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200601101000000001_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..68ce272 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000001_create_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000001_create_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200601101000000001_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200601101000000002_create_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200601101000000002_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..68eead3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000002_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000002_create_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200601101000000002_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200601101000000003_create_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200601101000000003_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..7844f07 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101000000003_create_messages.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101000000003_create_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200601101000000003_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200601101001000000_verification.mysql.down.sql b/persistence/sql/migrations/sql/20200601101001000000_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200601101001000000_verification.mysql.up.sql b/persistence/sql/migrations/sql/20200601101001000000_verification.mysql.up.sql new file mode 100644 index 0000000..1b46f2c --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101001000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101001000001_verification.mysql.down.sql b/persistence/sql/migrations/sql/20200601101001000001_verification.mysql.down.sql new file mode 100644 index 0000000..97d4ea6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200601101001000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200601101001000001_verification.mysql.up.sql b/persistence/sql/migrations/sql/20200601101001000001_verification.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.down.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.up.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.up.sql new file mode 100644 index 0000000..e207c1c --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.down.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.down.sql new file mode 100644 index 0000000..81f1b3b --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.up.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.up.sql new file mode 100644 index 0000000..0306ce5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.down.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.up.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.up.sql new file mode 100644 index 0000000..95d15ae --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.down.sql new file mode 100644 index 0000000..0ff2399 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.up.sql new file mode 100644 index 0000000..2f5bee8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000000_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.down.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.down.sql new file mode 100644 index 0000000..602c87d --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.up.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.up.sql new file mode 100644 index 0000000..dbd61c5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.down.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.down.sql new file mode 100644 index 0000000..1b1e34c --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.up.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.up.sql new file mode 100644 index 0000000..a83cda2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.down.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.down.sql new file mode 100644 index 0000000..602c87d --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.up.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.up.sql new file mode 100644 index 0000000..5dcf383 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.down.sql new file mode 100644 index 0000000..19344bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.up.sql new file mode 100644 index 0000000..b3bc29f --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000001_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.down.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.down.sql new file mode 100644 index 0000000..8d8dc7f --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.up.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.up.sql new file mode 100644 index 0000000..3d9176a --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.down.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.down.sql new file mode 100644 index 0000000..d8ee29d --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.up.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.up.sql new file mode 100644 index 0000000..d67d0f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.down.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.down.sql new file mode 100644 index 0000000..8d8dc7f --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.up.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.up.sql new file mode 100644 index 0000000..41236ec --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.down.sql new file mode 100644 index 0000000..eb1e72a --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.up.sql new file mode 100644 index 0000000..a99388b --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000002_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000003_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000003_messages.sqlite3.down.sql new file mode 100644 index 0000000..52d0d24 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000003_messages.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000003_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000003_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000004_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000004_messages.sqlite3.down.sql new file mode 100644 index 0000000..fd575c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000004_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000004_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000004_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000005_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000005_messages.sqlite3.down.sql new file mode 100644 index 0000000..19264fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000005_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000005_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000005_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000006_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000006_messages.sqlite3.down.sql new file mode 100644 index 0000000..294dbb3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000006_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000006_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000006_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000007_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000007_messages.sqlite3.down.sql new file mode 100644 index 0000000..bb6b486 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000007_messages.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000007_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000007_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000008_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000008_messages.sqlite3.down.sql new file mode 100644 index 0000000..f8c6563 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000008_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000008_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000008_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000009_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000009_messages.sqlite3.down.sql new file mode 100644 index 0000000..1effd84 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000009_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000009_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000009_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000010_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000010_messages.sqlite3.down.sql new file mode 100644 index 0000000..857cfa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000010_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, nid, form, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, nid, form, via, success FROM "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000010_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000010_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000011_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000011_messages.sqlite3.down.sql new file mode 100644 index 0000000..18aa050 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000011_messages.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_requests_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000011_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000011_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000012_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000012_messages.sqlite3.down.sql new file mode 100644 index 0000000..05a9816 --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000012_messages.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"form" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000012_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000012_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200605111551000013_messages.sqlite3.down.sql b/persistence/sql/migrations/sql/20200605111551000013_messages.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20200605111551000013_messages.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200605111551000013_messages.sqlite3.up.sql b/persistence/sql/migrations/sql/20200605111551000013_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.down.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.up.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.up.sql new file mode 100644 index 0000000..ff4bd62 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.down.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.down.sql new file mode 100644 index 0000000..8455290 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.up.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.up.sql new file mode 100644 index 0000000..c476011 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.down.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.up.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.up.sql new file mode 100644 index 0000000..ff4bd62 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.down.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.up.sql b/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.up.sql new file mode 100644 index 0000000..6fa03cb --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000000_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.down.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.down.sql new file mode 100644 index 0000000..6124b76 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.up.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.down.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.down.sql new file mode 100644 index 0000000..ae0511e --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.up.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.up.sql new file mode 100644 index 0000000..b922439 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.down.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.down.sql new file mode 100644 index 0000000..6124b76 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.up.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.down.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.down.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.up.sql b/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.up.sql new file mode 100644 index 0000000..0cfec2e --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000001_settings.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.down.sql b/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.down.sql new file mode 100644 index 0000000..68ce272 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.up.sql b/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.up.sql new file mode 100644 index 0000000..98b68a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000002_settings.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.down.sql b/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.down.sql new file mode 100644 index 0000000..098428e --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.up.sql b/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.up.sql new file mode 100644 index 0000000..68ce272 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000003_settings.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.down.sql b/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.down.sql new file mode 100644 index 0000000..32320a4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.up.sql b/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.up.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/persistence/sql/migrations/sql/20200607165100000004_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.down.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.up.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.down.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.up.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.down.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.up.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.down.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.up.sql b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/persistence/sql/migrations/sql/20200705105359000000_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.down.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.up.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.up.sql new file mode 100644 index 0000000..6e1701c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.down.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.down.sql new file mode 100644 index 0000000..5fae17d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.up.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.up.sql new file mode 100644 index 0000000..1e9e7eb --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.down.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.up.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.up.sql new file mode 100644 index 0000000..6e1701c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..f8c6563 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..ae1964c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000000_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.down.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.down.sql new file mode 100644 index 0000000..590f243 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.up.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.up.sql new file mode 100644 index 0000000..1334f1d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.down.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.down.sql new file mode 100644 index 0000000..f69c6ed --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.up.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.up.sql new file mode 100644 index 0000000..47d7694 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.down.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.down.sql new file mode 100644 index 0000000..590f243 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.up.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.up.sql new file mode 100644 index 0000000..1334f1d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..1effd84 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..1369bd4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000001_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.down.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.down.sql new file mode 100644 index 0000000..2522a4b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.up.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.up.sql new file mode 100644 index 0000000..c57ac0e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.down.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.down.sql new file mode 100644 index 0000000..a587259 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.up.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.up.sql new file mode 100644 index 0000000..03b037e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.down.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.down.sql new file mode 100644 index 0000000..2522a4b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.up.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.up.sql new file mode 100644 index 0000000..c57ac0e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..a9cb2d4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, nid, messages, form, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, nid, messages, form, via, success FROM "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..c196cd8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000002_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.down.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.down.sql new file mode 100644 index 0000000..a33b4ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.up.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.up.sql new file mode 100644 index 0000000..2764a33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.down.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.down.sql new file mode 100644 index 0000000..7793552 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.up.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.up.sql new file mode 100644 index 0000000..8ecb10b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.down.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.down.sql new file mode 100644 index 0000000..a33b4ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.up.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.up.sql new file mode 100644 index 0000000..2764a33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..18aa050 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_requests_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..2301125 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000003_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.down.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.down.sql new file mode 100644 index 0000000..5720680 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.up.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.down.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.down.sql new file mode 100644 index 0000000..231ee3f --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.up.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.up.sql new file mode 100644 index 0000000..c1282d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.down.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.down.sql new file mode 100644 index 0000000..5720680 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.up.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..4b8c831 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"messages" TEXT, +"form" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..3bf96ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000004_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000005_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000005_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000005_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000005_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000005_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000006_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000006_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..66f1b02 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000006_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000006_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000006_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000007_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000007_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..1207ce7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000007_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000007_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000007_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000008_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000008_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..60a301a --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000008_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, nid, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, nid, messages FROM "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000008_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000008_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000009_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000009_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..9e04136 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000009_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_recovery_flows_nid_idx" ON "_selfservice_recovery_requests_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000009_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000009_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000010_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000010_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..039cb52 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000010_flow_type.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"messages" TEXT, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000010_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000010_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000011_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000011_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..853ffd4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000011_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_recovery_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000011_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000011_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000012_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000012_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000012_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000012_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000012_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000013_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000013_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..68ce272 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000013_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000013_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000013_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000014_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000014_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..d90086e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000014_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, messages FROM "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000014_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000014_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000015_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000015_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..49335c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000015_flow_type.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000015_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000015_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000016_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000016_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..0ff2399 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000016_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000016_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000016_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000017_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000017_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..19344bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000017_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000017_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000017_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000018_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000018_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..b99f6f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000018_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000018_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000018_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000019_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000019_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..eaea68e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000019_flow_type.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000019_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000019_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000020_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000020_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..fd575c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000020_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000020_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000020_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000021_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000021_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..19264fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000021_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000021_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000021_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000022_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000022_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..52763b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000022_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000022_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000022_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810141652000023_flow_type.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810141652000023_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..53e879d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810141652000023_flow_type.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810141652000023_flow_type.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810141652000023_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..383c06f --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.down.sql new file mode 100644 index 0000000..dd9aedc --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.up.sql new file mode 100644 index 0000000..c6ceb5e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.up.sql new file mode 100644 index 0000000..383c06f --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..383c06f --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000000_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..34774ad --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..27b08ee --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.down.sql new file mode 100644 index 0000000..2ff5c8a --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.up.sql new file mode 100644 index 0000000..ef5b278 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.down.sql new file mode 100644 index 0000000..34774ad --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.up.sql new file mode 100644 index 0000000..27b08ee --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..34774ad --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..27b08ee --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000001_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..33c6e26 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..2210f98 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.down.sql new file mode 100644 index 0000000..9649f7f --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.up.sql new file mode 100644 index 0000000..c886772 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.down.sql new file mode 100644 index 0000000..33c6e26 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.up.sql new file mode 100644 index 0000000..2210f98 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..33c6e26 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..2210f98 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000002_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..c9a46a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..c90d56c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.down.sql new file mode 100644 index 0000000..5576ddd --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.up.sql new file mode 100644 index 0000000..ce5d00a --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.down.sql new file mode 100644 index 0000000..c9a46a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.up.sql new file mode 100644 index 0000000..c90d56c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..c9a46a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..c90d56c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000003_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..451e178 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..7fe0ee4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.down.sql new file mode 100644 index 0000000..dc916c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.up.sql new file mode 100644 index 0000000..70a94f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.down.sql new file mode 100644 index 0000000..451e178 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.up.sql new file mode 100644 index 0000000..7fe0ee4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..451e178 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..7fe0ee4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000004_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..f088f83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..0647561 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.down.sql new file mode 100644 index 0000000..60da736 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.up.sql new file mode 100644 index 0000000..72d7cbe --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.down.sql new file mode 100644 index 0000000..f088f83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.up.sql new file mode 100644 index 0000000..0647561 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..f088f83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..0647561 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000005_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..6f37525 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..3575de0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.down.sql new file mode 100644 index 0000000..a147115 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.up.sql new file mode 100644 index 0000000..e051834 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.down.sql new file mode 100644 index 0000000..6f37525 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.up.sql new file mode 100644 index 0000000..3575de0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..6f37525 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..3575de0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000006_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..03cf7f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..e121887 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.down.sql new file mode 100644 index 0000000..c4bf221 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.up.sql new file mode 100644 index 0000000..58aa473 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.down.sql new file mode 100644 index 0000000..03cf7f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.up.sql new file mode 100644 index 0000000..e121887 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..03cf7f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..e121887 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000007_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..f9ff5d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.down.sql new file mode 100644 index 0000000..3ff21dc --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.up.sql new file mode 100644 index 0000000..dc3fdb9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.down.sql new file mode 100644 index 0000000..f9ff5d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..f9ff5d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810161022000008_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..8bf38e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..dcc2ab2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..c8cb769 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..8bf38e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..8bf38e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000000_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..58f60ce --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..65616ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..730614b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..be876b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..58f60ce --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..65616ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..58f60ce --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..65616ef --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000001_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..c0805d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..74c4c2b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..c3992af --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..33ff3b5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..c0805d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..74c4c2b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..c0805d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..74c4c2b --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000002_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.down.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..4c880bd --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.up.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.down.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..edc2c1d --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.up.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..78ea09e --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.down.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..4c880bd --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.up.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.down.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..4c880bd --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.up.sql b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/persistence/sql/migrations/sql/20200810162450000003_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..fca0469 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.down.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.up.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.up.sql new file mode 100644 index 0000000..fca0469 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.down.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.up.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.up.sql new file mode 100644 index 0000000..fca0469 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..fca0469 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000000_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..0875f13 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.mysql.down.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.mysql.up.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.mysql.up.sql new file mode 100644 index 0000000..7a58bc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.postgres.down.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.postgres.up.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.postgres.up.sql new file mode 100644 index 0000000..0875f13 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..56b4a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..20b49e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000001_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..f5eaf3b --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.mysql.down.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.mysql.up.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.mysql.up.sql new file mode 100644 index 0000000..ec8d7cc --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.postgres.down.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.postgres.up.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.postgres.up.sql new file mode 100644 index 0000000..a814d18 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..5751651 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, nid) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, nid FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..989c809 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000002_add_session_token.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..0875f13 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.mysql.down.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.mysql.up.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.mysql.up.sql new file mode 100644 index 0000000..1f9c7b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.postgres.down.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.postgres.up.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.postgres.up.sql new file mode 100644 index 0000000..ac3176c --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..02044c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000003_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..948b0e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "token" = "_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.mysql.down.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.mysql.up.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.mysql.up.sql new file mode 100644 index 0000000..b4c20a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.postgres.down.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.postgres.up.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.postgres.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..01f0eb4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000004_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000005_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000005_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..5c0f615 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000005_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000006_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000006_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..ac3176c --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..ac3176c --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000006_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000007_add_session_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812124254000007_add_session_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/persistence/sql/migrations/sql/20200812124254000007_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.down.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.up.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.down.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.up.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.down.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.up.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000000_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000001_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000001_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..56b4a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000001_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000001_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000001_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000002_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000002_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..93c6e2b --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000002_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, nid) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, nid FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000002_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000002_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000003_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000003_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000003_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000003_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000003_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000004_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000004_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000004_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000004_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000004_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000005_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000005_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000005_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000005_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000005_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000006_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000006_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9b46bce --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000006_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000006_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000006_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000007_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000007_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000007_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000007_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000007_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000008_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000008_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000008_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000008_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000008_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200812160551000009_add_session_revoke.sqlite3.down.sql b/persistence/sql/migrations/sql/20200812160551000009_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20200812160551000009_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200812160551000009_add_session_revoke.sqlite3.up.sql b/persistence/sql/migrations/sql/20200812160551000009_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/persistence/sql/migrations/sql/20200830121710000000_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..970590a --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..11368e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..b12c9a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..11368e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..427e85b --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..563224c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..399cc5a --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..563224c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..87f57fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..38193bb --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..87f57fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e56d984 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..d8cbffd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, nid, messages, form) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, nid, messages, form FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..fd9fb31 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..7e4aaa0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..1866679 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..f19552f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..4ae2a88 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..4ae2a88 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..a3a9d99 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"nid" char(36), +"messages" TEXT, +"form" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..f634e9d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..6b37106 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000008_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000008_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000008_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000009_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000009_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000009_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000010_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e916d99 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000010_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, nid, messages, form) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, nid, messages, form FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000010_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000011_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000011_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..f19552f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000011_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000011_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000011_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000012_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000012_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..a69bc75 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000012_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"nid" char(36), +"messages" TEXT, +"form" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000012_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000012_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000013_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000013_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000013_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000013_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000013_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000014_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000014_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000014_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000014_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000014_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000015_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000015_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000015_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000015_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000015_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000016_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000016_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..524b49f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000016_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000016_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000016_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000017_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000017_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..a699868 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000017_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, nid, messages, form) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, nid, messages, form FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000017_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000017_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000018_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000018_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..f19552f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000018_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000018_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000018_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000019_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000019_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..0c065d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000019_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT, +"nid" char(36), +"messages" TEXT, +"form" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000019_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000019_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000020_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000020_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000020_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000020_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000020_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000021_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000021_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..4ae2a88 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000021_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000021_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000021_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130642000022_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130642000022_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..74329b9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130642000022_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130642000022_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130642000022_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130643000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..3ff645f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..15bdaf9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..69bf22f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..71113f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..621021c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..2568649 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130644000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130645000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..854db25 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..f2b3a00 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..854db25 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1c7781b --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..0e4894c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..892a6de --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..0e4894c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..c6c1828 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.cockroach.down.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.cockroach.up.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.mysql.down.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.mysql.up.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ae68024 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.postgres.down.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.postgres.up.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000002_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000003_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000003_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000003_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000004_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000004_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..9162ec4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000004_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000005_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000005_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..b13ba1a --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000005_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000006_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000006_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000006_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000007_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000007_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000007_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000008_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000008_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..f2a5266 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000008_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000009_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000009_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6556741 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000009_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000010_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000010_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000010_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830130646000011_add_verification_methods.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830130646000011_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830130646000011_add_verification_methods.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830130646000011_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830130646000011_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..394e51f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..886aae2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..3c561f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..a5ca578 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000000_add_verification_token.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..11b8494 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..0ae8b3b --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..11b8494 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..11b8494 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000001_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..3346749 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..2ac4d75 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..3346749 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..3346749 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000002_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..712309f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..bab0563 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..712309f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..712309f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000003_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.cockroach.down.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.cockroach.up.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.mysql.down.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.mysql.up.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..e134442 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.postgres.down.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.postgres.up.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830154602000004_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..4f90d79 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.down.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..80ec024 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.up.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..a0d4dbf --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.down.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.up.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..4f90d79 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..d125704 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000000_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..3e9112c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..1abf516 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.down.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..46e2207 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.up.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..c979077 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.down.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..3e9112c --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.up.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..1abf516 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..40f1908 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..21bfbd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000001_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..c15f380 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..686b39d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.down.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..c8fe30d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.up.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..0fe490d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.down.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..a18a256 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.up.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..a4605d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..808ce7f --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000002_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..a71a930 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..bb32d2e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.mysql.down.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..8ef683d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.mysql.up.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.postgres.down.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..8ef683d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.postgres.up.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000003_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..4d88bcd --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..fb31270 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..026a832 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000004_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..78fb09a --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..78fb09a --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000005_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..fb31270 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..a71a930 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..32d3cc8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000006_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..bb32d2e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..c15f380 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..f69ebe2 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000007_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..686b39d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000008_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.cockroach.down.sql b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..8ef683d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.cockroach.up.sql b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000009_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000010_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000010_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000010_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000010_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000010_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000011_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000011_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..40f1908 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000011_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000011_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000011_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000012_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000012_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..607683e --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000012_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000012_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000012_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000013_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000013_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000013_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000013_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000013_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000014_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000014_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000014_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000014_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000014_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000015_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000015_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000015_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000015_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000015_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000016_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000016_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..fad5737 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000016_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000016_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000016_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000017_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000017_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000017_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000017_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000017_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000018_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000018_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000018_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000018_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000018_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000019_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000019_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000019_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000019_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000019_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000020_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000020_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000020_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000020_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000020_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000021_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000021_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000021_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000021_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000021_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000022_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000022_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..c367dde --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000022_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000022_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000022_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000023_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000023_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000023_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000023_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000023_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000024_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000024_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000024_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000024_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000024_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000025_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000025_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000025_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000025_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000025_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000026_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000026_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..b985645 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000026_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000026_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000026_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000027_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000027_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000027_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000027_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000027_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000028_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000028_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000028_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000028_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000028_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000029_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000029_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000029_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000029_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000029_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200830172221000030_recovery_token_expires.sqlite3.down.sql b/persistence/sql/migrations/sql/20200830172221000030_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..8ef683d --- /dev/null +++ b/persistence/sql/migrations/sql/20200830172221000030_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200830172221000030_recovery_token_expires.sqlite3.up.sql b/persistence/sql/migrations/sql/20200830172221000030_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..ffacc3d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..788e152 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..acfd2d1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..833cac5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..ffacc3d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..d87692c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..12a661d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..a0b1d13 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..08188b7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..96f1581 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..d87692c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..2db92fe --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..fb0b2ca --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..3cc35f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..29b8ce6 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..18b9221 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..fb0b2ca --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..665e5e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..e89d1f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e58903e --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..b1010bc --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..a6c793b --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..373854c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..373854c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..65407ca --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..7b2bae4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..e9021d4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..516f5f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL; diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..32b44b4 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..2a281d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..12885bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..d224958 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..1022ffb --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..a6c793b --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..12885bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..2a281d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..105e698 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..4b8fc83 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..5ade1f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..306e79f --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..20e22aa --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..09a7ea8 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..0e18f68 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..5ade1f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..168a506 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..373854c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..119c801 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..65407ca --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..efadb66 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..a6c793b --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..2a281d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..12885bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..5ade1f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..12885bf --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..2a281d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..306e79f --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..cf33908 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..0e18f68 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..373854c --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..558122d --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6680d00 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql b/persistence/sql/migrations/sql/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..3169861 --- /dev/null +++ b/persistence/sql/migrations/sql/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql b/persistence/sql/migrations/sql/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.down.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.up.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..b678c7d --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.down.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.up.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..b678c7d --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.down.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.up.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..b678c7d --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.down.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.up.sql b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..b678c7d --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000000_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.cockroach.down.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.cockroach.up.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.mysql.down.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.mysql.up.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.postgres.down.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.postgres.up.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.sqlite3.down.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.sqlite3.up.sql b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/persistence/sql/migrations/sql/20201201161451000001_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.down.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.down.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.up.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.up.sql new file mode 100644 index 0000000..8b96e84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "courier_messages" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.down.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.down.sql new file mode 100644 index 0000000..351d9a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `courier_messages_status_idx` ON `courier_messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.up.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.up.sql new file mode 100644 index 0000000..ee94b65 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `courier_messages_status_idx` ON `courier_messages` (`status`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.down.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.down.sql new file mode 100644 index 0000000..66ed612 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.up.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.up.sql new file mode 100644 index 0000000..8b96e84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "courier_messages" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.down.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.up.sql new file mode 100644 index 0000000..8b96e84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130558000000_courier_status_index.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "courier_messages" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.down.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.down.sql new file mode 100644 index 0000000..bb776c0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "template_data"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.up.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.up.sql new file mode 100644 index 0000000..b7fc4bf --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_type" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.down.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.down.sql new file mode 100644 index 0000000..df853a4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` DROP COLUMN `template_data`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.up.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.up.sql new file mode 100644 index 0000000..2683396 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` ADD COLUMN `template_type` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.down.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.down.sql new file mode 100644 index 0000000..bb776c0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "template_data"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.up.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.up.sql new file mode 100644 index 0000000..b7fc4bf --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_type" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..b7acceb --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000000_courier_message_template.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_type" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.down.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.down.sql new file mode 100644 index 0000000..a808538 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "template_type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.up.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.up.sql new file mode 100644 index 0000000..b83b390 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_data" BYTES; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.down.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.down.sql new file mode 100644 index 0000000..aeb2008 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` DROP COLUMN `template_type`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.up.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.up.sql new file mode 100644 index 0000000..e4d07f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` ADD COLUMN `template_data` BLOB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.down.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.down.sql new file mode 100644 index 0000000..a808538 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "template_type"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.up.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.up.sql new file mode 100644 index 0000000..684b47f --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_data" bytea; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..eee18d2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..19e7968 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000001_courier_message_template.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "template_data" BLOB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000002_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000002_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..49390f7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000002_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000002_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000002_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000003_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000003_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..2c09bfd --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000003_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "_courier_messages_tmp" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000003_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000003_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000004_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000004_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..46c5669 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000004_courier_message_template.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000004_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000004_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000005_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000005_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000005_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000005_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000005_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000006_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000006_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000006_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000006_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000006_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000007_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000007_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..eee18d2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000007_courier_message_template.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000007_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000007_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000008_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000008_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..c6c32d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000008_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at, template_data) SELECT id, type, status, body, subject, recipient, created_at, updated_at, template_data FROM "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000008_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000008_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000009_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000009_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..2c09bfd --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000009_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "_courier_messages_tmp" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000009_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000009_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000010_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000010_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..ffc4662 --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000010_courier_message_template.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"template_data" BLOB +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000010_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000010_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210307130559000011_courier_message_template.sqlite3.down.sql b/persistence/sql/migrations/sql/20210307130559000011_courier_message_template.sqlite3.down.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210307130559000011_courier_message_template.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210307130559000011_courier_message_template.sqlite3.up.sql b/persistence/sql/migrations/sql/20210307130559000011_courier_message_template.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..a7d435f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..cf20d7e --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..288424c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..b86bfd8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..8927f4a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..cf20d7e --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..111d939 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..cf20d7e --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000000_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..334c6c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..bb05725 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..2acd74f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `ui`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..6dec693 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..334c6c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..bb05725 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ddea97d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000001_form_refactoring.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e060348 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_verification_flow_methods_selfservice_verification_flow_methods_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..6634b8b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..2b06973 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flow_methods` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..432b4eb --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..31945f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..b44983f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "ui" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..203c0c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000002_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e245612 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..a168ccc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..33291fe --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..a168ccc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..a9c899b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..a168ccc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..32a56c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, nid) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, nid FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..7524b95 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000003_form_refactoring.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..31a75ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..23feaba --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME COLUMN "ui" TO "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..88d0deb --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` DROP COLUMN `ui`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..35d9295 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` MODIFY `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..31a75ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..1e08e17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ALTER COLUMN "ui" TYPE jsonb, ALTER COLUMN "ui" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..f19552f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "_selfservice_verification_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000004_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..64cd517 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_recovery_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_flow_methods_selfservice_recovery_flow_methods_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..6634b8b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..b85e455 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_recovery_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_recovery_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_flow_id`) REFERENCES `selfservice_recovery_flow_methods` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..6f4f54a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..75a2f3d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_recovery_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..2b7d305 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..d182a04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..28bf235 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000005_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "ui" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..a41692f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..a0bbd8c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_login_flows" SET "ui" = "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..612bf60 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..68baec8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..5efe411 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..c3c322b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..96b610c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_verification_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..a168ccc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000006_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..84ba08a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..a4869cc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e9efa7d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` DROP COLUMN `ui`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..dee3d77 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` ADD COLUMN `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..84ba08a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..ec4dc18 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "ui" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..5d3da71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flow_methods (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..1da510c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000007_form_refactoring.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..6afc7b0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_settings_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_settings_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_settings_flow_methods_selfservice_settings_flow_methods_id_fk" FOREIGN KEY ("selfservice_settings_flow_id") REFERENCES "selfservice_settings_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..2b7d305 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..f8f4dd1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_settings_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_settings_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_settings_flow_id`) REFERENCES `selfservice_settings_flow_methods` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..dbbfcbf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..7d73599 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_settings_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_settings_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_settings_flow_id") REFERENCES "selfservice_settings_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..dbbfcbf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..aec9d79 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..811d7de --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000008_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..4563448 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..c3c322b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..059a6f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..49b2f41 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` MODIFY `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..0c9bab2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..790c06f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ALTER COLUMN "ui" TYPE jsonb, ALTER COLUMN "ui" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..5e05593 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_flows_tmp" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..7b62a8e --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000009_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..d82e123 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..c2dcc38 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..5efde64 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` DROP COLUMN `ui`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..58addfb --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_settings_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..d82e123 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..966bafd --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..8e89f28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000010_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..7fdf1a7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_flow_methods_selfservice_registration_flow_methods_id_fk" FOREIGN KEY ("selfservice_registration_flow_id") REFERENCES "selfservice_registration_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..dbbfcbf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..9ac48f6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_registration_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_flow_id`) REFERENCES `selfservice_registration_flow_methods` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..c39f52c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..01789b2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_flow_id") REFERENCES "selfservice_registration_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..c04adac --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..ea467e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, nid FROM "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..2b7d305 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000011_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..b52e366 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..0397eb9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME COLUMN "ui" TO "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..d5547f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..c18fd57 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` ADD COLUMN `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..b4a70de --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..a77124d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "ui" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..ab0c7e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_recovery_flows_nid_idx" ON "_selfservice_recovery_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..706e6ed --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000012_form_refactoring.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..915bf15 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..c2dcc38 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..84aad92 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `ui`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..bd1c278 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..915bf15 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "ui"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..bd1c278 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..5c67c36 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_recovery_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"nid" char(36), +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..44450da --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000013_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..754eb8c --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_flow_methods_selfservice_login_flow_methods_id_fk" FOREIGN KEY ("selfservice_login_flow_id") REFERENCES "selfservice_login_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..01619d7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_registration_flows" SET "ui" = "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..6a5d1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_login_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_flow_id`) REFERENCES `selfservice_login_flow_methods` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..a5e1fd3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` MODIFY `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..db2a469 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_flow_id") REFERENCES "selfservice_login_flow_methods" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..479e11a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ALTER COLUMN "ui" TYPE jsonb, ALTER COLUMN "ui" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..853ffd4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_recovery_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..bcd63c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000014_form_refactoring.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..256c031 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..08cfd4b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..35915dc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..eeecd32 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_recovery_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flow_methods (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000015_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..966bafd --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..24d4408 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..b2b6e51 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e43b3f4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..772cf91 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000016_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "ui" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..c04adac --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..33b4126 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` ADD COLUMN `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..8a8a45d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "ui" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..dbbfcbf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000017_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..d85c611 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..90a7ca8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..90a7ca8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..b67a469 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..e681fe2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000018_form_refactoring.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..bd1c278 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..358d36a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` MODIFY `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..3a11020 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ALTER COLUMN "ui" TYPE jsonb, ALTER COLUMN "ui" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..455f8eb --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..836456b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000019_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..68d9a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME COLUMN "ui" TO "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..e56d984 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_flow_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..bfc74a9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..d8b935d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000020_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..d85c611 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..917597f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..8241e73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..948a876 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_settings_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_settings_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_settings_flow_id) REFERENCES selfservice_settings_flow_methods (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000021_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..f7fe33f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_settings_flows" SET "ui" = "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..6b697ed --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..4fbba6e --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "ui" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..b1117de --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..966bafd --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000022_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..a38788b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..dc27917 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..dc27917 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..375dac5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000023_form_refactoring.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..35915dc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.mysql.down.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.mysql.up.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.mysql.up.sql new file mode 100644 index 0000000..f86e8f8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` MODIFY `ui` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.postgres.down.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.postgres.up.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.postgres.up.sql new file mode 100644 index 0000000..6e652da --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "ui" TYPE jsonb, ALTER COLUMN "ui" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..bcd63c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..455f8eb --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000024_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..b2b6e51 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..44450da --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..b67a469 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000025_form_refactoring.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..645b70f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..36594c3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000026_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..90a7ca8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..7c36056 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_registration_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_flow_id) REFERENCES selfservice_registration_flow_methods (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..fc79faf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000027_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "ui" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..8eaaa23 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME COLUMN "ui" TO "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..c1bf72f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..bd1c278 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000028_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..645b70f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..74b49e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000029_form_refactoring.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..ba33b18 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_recovery_flows" SET "ui" = "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..7524b95 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..ff2a1c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000030_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..2adc0e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..203c0c1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..b76f8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000031_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..8d6929b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000032_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..8241e73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..f048002 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_login_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_flow_id) REFERENCES selfservice_login_flow_methods (id) ON DELETE cascade +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..35915dc --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000033_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..9e218d5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..665ad28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000034_form_refactoring.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_recovery_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..dc27917 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..26fda62 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000035_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type FROM "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..41c618b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME COLUMN "ui" TO "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..8e89f28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000036_form_refactoring.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..9e218d5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "ui" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..5e05593 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000037_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_flows_tmp" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..38dc56f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_verification_flows" SET "ui" = "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..d94452d --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000038_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "ui" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.cockroach.down.sql b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.cockroach.up.sql b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.cockroach.up.sql new file mode 100644 index 0000000..f4a09fe --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "_ui_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..90a7ca8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000039_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000040_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000040_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000040_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000040_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..d6be19a --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000040_form_refactoring.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_recovery_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000041_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000041_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000041_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000041_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..fd1ad16 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000041_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui FROM "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000042_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000042_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000042_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000042_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..06c842b --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000042_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000043_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000043_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000043_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000043_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..5e05593 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000043_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_flows_tmp" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000044_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000044_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000044_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000044_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..6103960 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000044_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000045_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000045_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000045_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000045_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..09608df --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000045_form_refactoring.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000046_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000046_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000046_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000046_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..9a6d6f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000046_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000047_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000047_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000047_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000047_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..d2e3fdd --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000047_form_refactoring.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000048_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000048_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000048_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000048_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000048_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000049_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000049_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000049_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000049_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..559cdaf --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000049_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "ui" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000050_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000050_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000050_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000050_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..dc27917 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000050_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET ui='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000051_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000051_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000051_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000051_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..b8355d9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000051_form_refactoring.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT, +"ui" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000052_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000052_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000052_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000052_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..e4963e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000052_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, ui) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, ui FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000053_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000053_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000053_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000053_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..524b49f --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000053_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210311102338000054_form_refactoring.sqlite3.down.sql b/persistence/sql/migrations/sql/20210311102338000054_form_refactoring.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210311102338000054_form_refactoring.sqlite3.up.sql b/persistence/sql/migrations/sql/20210311102338000054_form_refactoring.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20210311102338000054_form_refactoring.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.down.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.up.sql new file mode 100644 index 0000000..0c44bce --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000000_network.mysql.down.sql new file mode 100644 index 0000000..27e4055 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.mysql.down.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000000_network.mysql.up.sql new file mode 100644 index 0000000..201aac4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000000_network.postgres.down.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.postgres.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000000_network.postgres.up.sql new file mode 100644 index 0000000..0c44bce --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.down.sql new file mode 100644 index 0000000..517300c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.up.sql new file mode 100644 index 0000000..a46ffc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000000_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.down.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.up.sql new file mode 100644 index 0000000..e4d4497 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD CONSTRAINT "selfservice_login_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000001_network.mysql.down.sql new file mode 100644 index 0000000..e5a9e36 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000001_network.mysql.up.sql new file mode 100644 index 0000000..356bd4c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD CONSTRAINT `selfservice_login_flows_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000001_network.postgres.down.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000001_network.postgres.up.sql new file mode 100644 index 0000000..e4d4497 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD CONSTRAINT "selfservice_login_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.down.sql new file mode 100644 index 0000000..87ce296 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.up.sql new file mode 100644 index 0000000..36ed7a4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000001_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_login_flows DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.down.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.up.sql new file mode 100644 index 0000000..f69073e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000002_network.mysql.down.sql new file mode 100644 index 0000000..b33e881 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.mysql.down.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000002_network.mysql.up.sql new file mode 100644 index 0000000..f69073e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000002_network.postgres.down.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.postgres.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000002_network.postgres.up.sql new file mode 100644 index 0000000..f69073e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.down.sql new file mode 100644 index 0000000..16faaa1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.up.sql new file mode 100644 index 0000000..db249f6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000002_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_login_flows ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.down.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.up.sql new file mode 100644 index 0000000..5e01871 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP CONSTRAINT "selfservice_login_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000003_network.mysql.down.sql new file mode 100644 index 0000000..1fe6468 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000003_network.mysql.up.sql new file mode 100644 index 0000000..cdf1b44 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000003_network.postgres.down.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000003_network.postgres.up.sql new file mode 100644 index 0000000..adcc4ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.down.sql new file mode 100644 index 0000000..7fb5e7c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.up.sql new file mode 100644 index 0000000..f69073e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000003_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.down.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.up.sql new file mode 100644 index 0000000..e474cb9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000004_network.mysql.down.sql new file mode 100644 index 0000000..759def9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000004_network.mysql.up.sql new file mode 100644 index 0000000..398b48c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_login_flows_nid_idx` ON `selfservice_login_flows` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000004_network.postgres.down.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000004_network.postgres.up.sql new file mode 100644 index 0000000..6fce62c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "selfservice_login_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.down.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.up.sql new file mode 100644 index 0000000..4fa58e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000004_network.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.down.sql new file mode 100644 index 0000000..a63304f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.up.sql new file mode 100644 index 0000000..0c44bce --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000005_network.mysql.down.sql new file mode 100644 index 0000000..d7e33b0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000005_network.mysql.up.sql new file mode 100644 index 0000000..ffde820 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000005_network.postgres.down.sql new file mode 100644 index 0000000..a63304f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000005_network.postgres.up.sql new file mode 100644 index 0000000..1212bba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.down.sql new file mode 100644 index 0000000..4c12302 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.up.sql new file mode 100644 index 0000000..f7a306d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000005_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.down.sql new file mode 100644 index 0000000..b4bac71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.up.sql new file mode 100644 index 0000000..5cb8fc8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_login_flows" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000006_network.mysql.down.sql new file mode 100644 index 0000000..00c56ec --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credential_identifiers_identifier_nid_uq_idx` ON `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000006_network.mysql.up.sql new file mode 100644 index 0000000..bc55ddc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` ADD CONSTRAINT `selfservice_registration_flows_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000006_network.postgres.down.sql new file mode 100644 index 0000000..d1ef620 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000006_network.postgres.up.sql new file mode 100644 index 0000000..7eb5b6c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD CONSTRAINT "selfservice_registration_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.down.sql new file mode 100644 index 0000000..c31ee07 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.up.sql new file mode 100644 index 0000000..7b62a8e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000006_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.down.sql new file mode 100644 index 0000000..2c736d4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.up.sql new file mode 100644 index 0000000..6fe3c21 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000007_network.mysql.down.sql new file mode 100644 index 0000000..3750f5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000007_network.mysql.up.sql new file mode 100644 index 0000000..acb192f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000007_network.postgres.down.sql new file mode 100644 index 0000000..2c736d4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000007_network.postgres.up.sql new file mode 100644 index 0000000..acb192f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.down.sql new file mode 100644 index 0000000..7c00980 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credential_identifiers_tmp" (id, identifier, identity_credential_id, created_at, updated_at) SELECT id, identifier, identity_credential_id, created_at, updated_at FROM "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.up.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000007_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.down.sql new file mode 100644 index 0000000..a4d25a5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.up.sql new file mode 100644 index 0000000..e4d4497 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD CONSTRAINT "selfservice_login_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000008_network.mysql.down.sql new file mode 100644 index 0000000..350192c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credentials` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000008_network.mysql.up.sql new file mode 100644 index 0000000..ad2cda3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000008_network.postgres.down.sql new file mode 100644 index 0000000..a4d25a5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000008_network.postgres.up.sql new file mode 100644 index 0000000..e71d5aa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.down.sql new file mode 100644 index 0000000..f36854e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.down.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.up.sql new file mode 100644 index 0000000..6fce62c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000008_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "selfservice_login_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.down.sql new file mode 100644 index 0000000..3c7fe28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.up.sql new file mode 100644 index 0000000..6fce62c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "selfservice_login_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000009_network.mysql.down.sql new file mode 100644 index 0000000..a0189b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000009_network.mysql.up.sql new file mode 100644 index 0000000..508ceb3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_registration_flows_nid_idx` ON `selfservice_registration_flows` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000009_network.postgres.down.sql new file mode 100644 index 0000000..3c7fe28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000009_network.postgres.up.sql new file mode 100644 index 0000000..50bee5e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_registration_flows_nid_idx" ON "selfservice_registration_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.down.sql new file mode 100644 index 0000000..b4bac71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.up.sql new file mode 100644 index 0000000..6b09a69 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000009_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.down.sql new file mode 100644 index 0000000..3c2268a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.up.sql new file mode 100644 index 0000000..1212bba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000010_network.mysql.down.sql new file mode 100644 index 0000000..b4aa473 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000010_network.mysql.up.sql new file mode 100644 index 0000000..7ae9172 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000010_network.postgres.down.sql new file mode 100644 index 0000000..3c2268a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000010_network.postgres.up.sql new file mode 100644 index 0000000..a8f8523 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.down.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.up.sql new file mode 100644 index 0000000..fc44ba6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000010_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_registration_flows DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.down.sql new file mode 100644 index 0000000..0b116c5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.up.sql new file mode 100644 index 0000000..7eb5b6c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD CONSTRAINT "selfservice_registration_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000011_network.mysql.down.sql new file mode 100644 index 0000000..8400483 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000011_network.mysql.up.sql new file mode 100644 index 0000000..c4dbfc6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` ADD CONSTRAINT `selfservice_settings_flows_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000011_network.postgres.down.sql new file mode 100644 index 0000000..0b116c5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000011_network.postgres.up.sql new file mode 100644 index 0000000..cad60c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD CONSTRAINT "selfservice_settings_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.down.sql new file mode 100644 index 0000000..0e18f68 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.up.sql new file mode 100644 index 0000000..e6120d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000011_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_registration_flows ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.down.sql new file mode 100644 index 0000000..bf79597 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.up.sql new file mode 100644 index 0000000..acb192f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000012_network.mysql.down.sql new file mode 100644 index 0000000..f03d0c0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `continuity_containers` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000012_network.mysql.up.sql new file mode 100644 index 0000000..c972661 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000012_network.postgres.down.sql new file mode 100644 index 0000000..bf79597 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000012_network.postgres.up.sql new file mode 100644 index 0000000..c972661 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.down.sql new file mode 100644 index 0000000..cf33908 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.up.sql new file mode 100644 index 0000000..acb192f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000012_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.down.sql new file mode 100644 index 0000000..fb952ec --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.up.sql new file mode 100644 index 0000000..74a26ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP CONSTRAINT "selfservice_registration_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000013_network.mysql.down.sql new file mode 100644 index 0000000..afc80b5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000013_network.mysql.up.sql new file mode 100644 index 0000000..1256b51 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000013_network.postgres.down.sql new file mode 100644 index 0000000..fb952ec --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000013_network.postgres.up.sql new file mode 100644 index 0000000..0266f63 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.down.sql new file mode 100644 index 0000000..efadb66 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.up.sql new file mode 100644 index 0000000..6db09ef --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000013_network.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.down.sql new file mode 100644 index 0000000..8941a21 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.up.sql new file mode 100644 index 0000000..132a8dd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000014_network.mysql.down.sql new file mode 100644 index 0000000..17c8bbd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000014_network.mysql.up.sql new file mode 100644 index 0000000..73dd5b7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_settings_flows_nid_idx` ON `selfservice_settings_flows` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000014_network.postgres.down.sql new file mode 100644 index 0000000..8941a21 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000014_network.postgres.up.sql new file mode 100644 index 0000000..6dacc54 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_settings_flows_nid_idx" ON "selfservice_settings_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.down.sql new file mode 100644 index 0000000..05e6c76 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credentials_tmp" RENAME TO "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.up.sql new file mode 100644 index 0000000..743a30c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000014_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.down.sql new file mode 100644 index 0000000..038747c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.up.sql new file mode 100644 index 0000000..1212bba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000015_network.mysql.down.sql new file mode 100644 index 0000000..0f08587 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `nid`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000015_network.mysql.up.sql new file mode 100644 index 0000000..c69f788 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000015_network.postgres.down.sql new file mode 100644 index 0000000..038747c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "nid"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000015_network.postgres.up.sql new file mode 100644 index 0000000..e85df78 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.down.sql new file mode 100644 index 0000000..02f6172 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.up.sql new file mode 100644 index 0000000..d8b935d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000015_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.down.sql new file mode 100644 index 0000000..0966d17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.up.sql new file mode 100644 index 0000000..dd27ac3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_registration_flows" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000016_network.mysql.down.sql new file mode 100644 index 0000000..38a0567 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000016_network.mysql.up.sql new file mode 100644 index 0000000..21d297a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD CONSTRAINT `selfservice_errors_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000016_network.postgres.down.sql new file mode 100644 index 0000000..8afb020 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000016_network.postgres.up.sql new file mode 100644 index 0000000..4c1b36a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD CONSTRAINT "selfservice_errors_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.down.sql new file mode 100644 index 0000000..c31d03b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credentials_tmp" (id, config, identity_credential_type_id, identity_id, created_at, updated_at) SELECT id, config, identity_credential_type_id, identity_id, created_at, updated_at FROM "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.up.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000016_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.down.sql new file mode 100644 index 0000000..2a972f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.up.sql new file mode 100644 index 0000000..afe5be4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000017_network.mysql.down.sql new file mode 100644 index 0000000..d78266e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000017_network.mysql.up.sql new file mode 100644 index 0000000..25de05d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000017_network.postgres.down.sql new file mode 100644 index 0000000..ba5fb94 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000017_network.postgres.up.sql new file mode 100644 index 0000000..25de05d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.down.sql new file mode 100644 index 0000000..7187693 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_identity_credentials_tmp" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.up.sql new file mode 100644 index 0000000..50bee5e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000017_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_registration_flows_nid_idx" ON "selfservice_registration_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.down.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.up.sql new file mode 100644 index 0000000..7eb5b6c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD CONSTRAINT "selfservice_registration_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000018_network.mysql.down.sql new file mode 100644 index 0000000..f448837 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000018_network.mysql.up.sql new file mode 100644 index 0000000..6d30fcc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000018_network.postgres.down.sql new file mode 100644 index 0000000..2ba2965 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000018_network.postgres.up.sql new file mode 100644 index 0000000..6a924f8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.down.sql new file mode 100644 index 0000000..09c5e50 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identities_tmp" RENAME TO "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.up.sql new file mode 100644 index 0000000..bf2dea8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000018_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.down.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.up.sql new file mode 100644 index 0000000..50bee5e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_registration_flows_nid_idx" ON "selfservice_registration_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000019_network.mysql.down.sql new file mode 100644 index 0000000..14b457a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000019_network.mysql.up.sql new file mode 100644 index 0000000..7daf44b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_errors_nid_idx` ON `selfservice_errors` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000019_network.postgres.down.sql new file mode 100644 index 0000000..cf7935e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000019_network.postgres.up.sql new file mode 100644 index 0000000..a310b30 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_errors_nid_idx" ON "selfservice_errors" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.down.sql new file mode 100644 index 0000000..10597b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.up.sql new file mode 100644 index 0000000..ce8b125 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000019_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_settings_flows DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.down.sql new file mode 100644 index 0000000..be327ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.up.sql new file mode 100644 index 0000000..a8f8523 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000020_network.mysql.down.sql new file mode 100644 index 0000000..f9d0675 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credential_identifiers_nid_idx` ON `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000020_network.mysql.up.sql new file mode 100644 index 0000000..5ab0ac2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `continuity_containers` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000020_network.postgres.down.sql new file mode 100644 index 0000000..1009993 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000020_network.postgres.up.sql new file mode 100644 index 0000000..4254cdc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.down.sql new file mode 100644 index 0000000..13f6ed5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at) SELECT id, schema_id, traits, created_at, updated_at FROM "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.up.sql new file mode 100644 index 0000000..38f09dd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000020_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_settings_flows ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.down.sql new file mode 100644 index 0000000..23f9534 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.up.sql new file mode 100644 index 0000000..cad60c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD CONSTRAINT "selfservice_settings_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000021_network.mysql.down.sql new file mode 100644 index 0000000..8310c22 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_recovery_addresses_nid_idx` ON `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000021_network.mysql.up.sql new file mode 100644 index 0000000..6d347bc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `continuity_containers` ADD CONSTRAINT `continuity_containers_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000021_network.postgres.down.sql new file mode 100644 index 0000000..fe96005 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000021_network.postgres.up.sql new file mode 100644 index 0000000..61761fc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD CONSTRAINT "continuity_containers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.down.sql new file mode 100644 index 0000000..186afad --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.down.sql @@ -0,0 +1,7 @@ +CREATE TABLE "_identities_tmp" ( +"id" TEXT PRIMARY KEY, +"schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.up.sql new file mode 100644 index 0000000..c972661 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000021_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.down.sql new file mode 100644 index 0000000..68bca5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.up.sql new file mode 100644 index 0000000..c972661 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000022_network.mysql.down.sql new file mode 100644 index 0000000..e0575be --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_nid_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000022_network.mysql.up.sql new file mode 100644 index 0000000..31c9140 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE continuity_containers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000022_network.postgres.down.sql new file mode 100644 index 0000000..65668c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000022_network.postgres.up.sql new file mode 100644 index 0000000..31c9140 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE continuity_containers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.up.sql new file mode 100644 index 0000000..38f7330 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000022_network.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.down.sql new file mode 100644 index 0000000..fe89b8d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credentials_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.up.sql new file mode 100644 index 0000000..ec7258a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP CONSTRAINT "selfservice_settings_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000023_network.mysql.down.sql new file mode 100644 index 0000000..6a5298d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credentials_nid_idx` ON `identity_credentials`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000023_network.mysql.up.sql new file mode 100644 index 0000000..24e64ad --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `continuity_containers` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000023_network.postgres.down.sql new file mode 100644 index 0000000..e984cd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credentials_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000023_network.postgres.up.sql new file mode 100644 index 0000000..a053da3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.down.sql new file mode 100644 index 0000000..62218cd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.up.sql new file mode 100644 index 0000000..7327300 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000023_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.down.sql new file mode 100644 index 0000000..295533e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identities_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.up.sql new file mode 100644 index 0000000..532954f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000024_network.mysql.down.sql new file mode 100644 index 0000000..a319706 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identities_nid_idx` ON `identities`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000024_network.mysql.up.sql new file mode 100644 index 0000000..aa77daf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `continuity_containers_nid_idx` ON `continuity_containers` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000024_network.postgres.down.sql new file mode 100644 index 0000000..904504c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identities_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000024_network.postgres.up.sql new file mode 100644 index 0000000..640b95d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "continuity_containers_nid_idx" ON "continuity_containers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.down.sql new file mode 100644 index 0000000..40dcbfa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.up.sql new file mode 100644 index 0000000..b76f8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000024_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.down.sql new file mode 100644 index 0000000..13d0673 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_errors_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.up.sql new file mode 100644 index 0000000..a8f8523 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000025_network.mysql.down.sql new file mode 100644 index 0000000..fd2c476 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `selfservice_errors_nid_idx` ON `selfservice_errors`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000025_network.mysql.up.sql new file mode 100644 index 0000000..4d3da75 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000025_network.postgres.down.sql new file mode 100644 index 0000000..1baaecf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "selfservice_errors_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000025_network.postgres.up.sql new file mode 100644 index 0000000..5d2f5e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.down.sql new file mode 100644 index 0000000..c73b605 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.up.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000025_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.down.sql new file mode 100644 index 0000000..2d1a6c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.up.sql new file mode 100644 index 0000000..c2bc195 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_settings_flows" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000026_network.mysql.down.sql new file mode 100644 index 0000000..9a9d288 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `courier_messages_nid_idx` ON `courier_messages`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000026_network.mysql.up.sql new file mode 100644 index 0000000..b16c883 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` ADD CONSTRAINT `courier_messages_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000026_network.postgres.down.sql new file mode 100644 index 0000000..995f298 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "courier_messages_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000026_network.postgres.up.sql new file mode 100644 index 0000000..b640d84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD CONSTRAINT "courier_messages_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.down.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.up.sql new file mode 100644 index 0000000..6dacc54 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000026_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_settings_flows_nid_idx" ON "selfservice_settings_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.down.sql new file mode 100644 index 0000000..cfdfdea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "continuity_containers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.up.sql new file mode 100644 index 0000000..5902759 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000027_network.mysql.down.sql new file mode 100644 index 0000000..7462ad6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `continuity_containers_nid_idx` ON `continuity_containers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000027_network.mysql.up.sql new file mode 100644 index 0000000..06bc814 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE courier_messages SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000027_network.postgres.down.sql new file mode 100644 index 0000000..1f8ae37 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "continuity_containers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000027_network.postgres.up.sql new file mode 100644 index 0000000..06bc814 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE courier_messages SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.down.sql new file mode 100644 index 0000000..eee18d2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.up.sql new file mode 100644 index 0000000..58a5d44 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000027_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.down.sql new file mode 100644 index 0000000..fb35cc6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_settings_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.up.sql new file mode 100644 index 0000000..cad60c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD CONSTRAINT "selfservice_settings_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000028_network.mysql.down.sql new file mode 100644 index 0000000..6ca9dd2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `selfservice_settings_flows_nid_idx` ON `selfservice_settings_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000028_network.mysql.up.sql new file mode 100644 index 0000000..f639537 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000028_network.postgres.down.sql new file mode 100644 index 0000000..9244b84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "selfservice_settings_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000028_network.postgres.up.sql new file mode 100644 index 0000000..4dca5f0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.down.sql new file mode 100644 index 0000000..2287178 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at, template_type, template_data) SELECT id, type, status, body, subject, recipient, created_at, updated_at, template_type, template_data FROM "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.up.sql new file mode 100644 index 0000000..2106355 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000028_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_errors DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.down.sql new file mode 100644 index 0000000..44c4132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_registration_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.up.sql new file mode 100644 index 0000000..6dacc54 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_settings_flows_nid_idx" ON "selfservice_settings_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000029_network.mysql.down.sql new file mode 100644 index 0000000..f97f26c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `selfservice_registration_flows_nid_idx` ON `selfservice_registration_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000029_network.mysql.up.sql new file mode 100644 index 0000000..d0710fa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `courier_messages_nid_idx` ON `courier_messages` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000029_network.postgres.down.sql new file mode 100644 index 0000000..8483ad6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "selfservice_registration_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000029_network.postgres.up.sql new file mode 100644 index 0000000..93c586c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_nid_idx" ON "courier_messages" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.down.sql new file mode 100644 index 0000000..2c09bfd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "_courier_messages_tmp" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.up.sql new file mode 100644 index 0000000..e959d2d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000029_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_errors ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.down.sql new file mode 100644 index 0000000..33d0fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.up.sql new file mode 100644 index 0000000..e85df78 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000030_network.mysql.down.sql new file mode 100644 index 0000000..18cf6ae --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `selfservice_login_flows_nid_idx` ON `selfservice_login_flows`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000030_network.mysql.up.sql new file mode 100644 index 0000000..fcdeacd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000030_network.postgres.down.sql new file mode 100644 index 0000000..3848392 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000030_network.postgres.up.sql new file mode 100644 index 0000000..3d072a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.down.sql new file mode 100644 index 0000000..ba9f463 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"template_type" TEXT NOT NULL DEFAULT '', +"template_data" BLOB +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.up.sql new file mode 100644 index 0000000..25de05d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000030_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.down.sql new file mode 100644 index 0000000..a387444 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP CONSTRAINT "identity_credential_identifiers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.up.sql new file mode 100644 index 0000000..4c1b36a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD CONSTRAINT "selfservice_errors_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000031_network.mysql.down.sql new file mode 100644 index 0000000..d76e429 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` DROP FOREIGN KEY `identity_credential_identifiers_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000031_network.mysql.up.sql new file mode 100644 index 0000000..aa55fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` ADD CONSTRAINT `identities_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000031_network.postgres.down.sql new file mode 100644 index 0000000..a387444 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP CONSTRAINT "identity_credential_identifiers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000031_network.postgres.up.sql new file mode 100644 index 0000000..20e0943 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD CONSTRAINT "identities_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.down.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.up.sql new file mode 100644 index 0000000..9d1b69e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000031_network.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '', +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.down.sql new file mode 100644 index 0000000..65fa394 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" DROP CONSTRAINT "identity_recovery_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.up.sql new file mode 100644 index 0000000..25de05d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000032_network.mysql.down.sql new file mode 100644 index 0000000..3bda605 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_addresses` DROP FOREIGN KEY `identity_recovery_addresses_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000032_network.mysql.up.sql new file mode 100644 index 0000000..54dba29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identities SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000032_network.postgres.down.sql new file mode 100644 index 0000000..65fa394 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" DROP CONSTRAINT "identity_recovery_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000032_network.postgres.up.sql new file mode 100644 index 0000000..54dba29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identities SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.down.sql new file mode 100644 index 0000000..f16e190 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_continuity_containers_tmp" RENAME TO "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.up.sql new file mode 100644 index 0000000..3e3de8a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000032_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token, nid) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token, nid FROM "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.down.sql new file mode 100644 index 0000000..ec1bbe4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP CONSTRAINT "identity_verifiable_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.up.sql new file mode 100644 index 0000000..c03d732 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP CONSTRAINT "selfservice_errors_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000033_network.mysql.down.sql new file mode 100644 index 0000000..ed8dbb1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP FOREIGN KEY `identity_verifiable_addresses_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000033_network.mysql.up.sql new file mode 100644 index 0000000..bd7f5b4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000033_network.postgres.down.sql new file mode 100644 index 0000000..ec1bbe4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP CONSTRAINT "identity_verifiable_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000033_network.postgres.up.sql new file mode 100644 index 0000000..502a0bc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.down.sql new file mode 100644 index 0000000..db387d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.up.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000033_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.down.sql new file mode 100644 index 0000000..744dee6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP CONSTRAINT "identity_credentials_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.up.sql new file mode 100644 index 0000000..b9e8747 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000034_network.mysql.down.sql new file mode 100644 index 0000000..5b4121a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credentials` DROP FOREIGN KEY `identity_credentials_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000034_network.mysql.up.sql new file mode 100644 index 0000000..2022979 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identities_nid_idx` ON `identities` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000034_network.postgres.down.sql new file mode 100644 index 0000000..744dee6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP CONSTRAINT "identity_credentials_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000034_network.postgres.up.sql new file mode 100644 index 0000000..000b979 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identities_nid_idx" ON "identities" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.down.sql new file mode 100644 index 0000000..4b8308a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_continuity_containers_tmp" (id, identity_id, name, payload, expires_at, created_at, updated_at) SELECT id, identity_id, name, payload, expires_at, created_at, updated_at FROM "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.up.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000034_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.down.sql new file mode 100644 index 0000000..bf94002 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP CONSTRAINT "identities_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.up.sql new file mode 100644 index 0000000..e85df78 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000035_network.mysql.down.sql new file mode 100644 index 0000000..e049df9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` DROP FOREIGN KEY `identities_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000035_network.mysql.up.sql new file mode 100644 index 0000000..d9c2bd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credentials` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000035_network.postgres.down.sql new file mode 100644 index 0000000..bf94002 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP CONSTRAINT "identities_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000035_network.postgres.up.sql new file mode 100644 index 0000000..7a5e15b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.down.sql new file mode 100644 index 0000000..251dd38 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_continuity_containers_tmp" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.up.sql new file mode 100644 index 0000000..a310b30 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000035_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_errors_nid_idx" ON "selfservice_errors" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.down.sql new file mode 100644 index 0000000..c03d732 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP CONSTRAINT "selfservice_errors_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.up.sql new file mode 100644 index 0000000..fd4014a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000036_network.mysql.down.sql new file mode 100644 index 0000000..fade2fe --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP FOREIGN KEY `selfservice_errors_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000036_network.mysql.up.sql new file mode 100644 index 0000000..718be66 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credentials` ADD CONSTRAINT `identity_credentials_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000036_network.postgres.down.sql new file mode 100644 index 0000000..c03d732 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP CONSTRAINT "selfservice_errors_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000036_network.postgres.up.sql new file mode 100644 index 0000000..0fa3653 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD CONSTRAINT "identity_credentials_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.down.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.up.sql new file mode 100644 index 0000000..61f693d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000036_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.down.sql new file mode 100644 index 0000000..dd1e98b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP CONSTRAINT "courier_messages_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.up.sql new file mode 100644 index 0000000..0b02a3b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000037_network.mysql.down.sql new file mode 100644 index 0000000..aa9a569 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` DROP FOREIGN KEY `courier_messages_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000037_network.mysql.up.sql new file mode 100644 index 0000000..3584be5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_credentials SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000037_network.postgres.down.sql new file mode 100644 index 0000000..dd1e98b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP CONSTRAINT "courier_messages_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000037_network.postgres.up.sql new file mode 100644 index 0000000..3584be5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_credentials SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.down.sql new file mode 100644 index 0000000..b67a469 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.up.sql new file mode 100644 index 0000000..0999555 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000037_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE continuity_containers DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.down.sql new file mode 100644 index 0000000..9899e17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP CONSTRAINT "continuity_containers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.up.sql new file mode 100644 index 0000000..4c1b36a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD CONSTRAINT "selfservice_errors_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000038_network.mysql.down.sql new file mode 100644 index 0000000..5c2f02c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `continuity_containers` DROP FOREIGN KEY `continuity_containers_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000038_network.mysql.up.sql new file mode 100644 index 0000000..7cc3e80 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credentials` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000038_network.postgres.down.sql new file mode 100644 index 0000000..9899e17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP CONSTRAINT "continuity_containers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000038_network.postgres.up.sql new file mode 100644 index 0000000..901347f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.down.sql new file mode 100644 index 0000000..ff2a1c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.up.sql new file mode 100644 index 0000000..7e032e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000038_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE continuity_containers ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.down.sql new file mode 100644 index 0000000..ec7258a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP CONSTRAINT "selfservice_settings_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.up.sql new file mode 100644 index 0000000..a310b30 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_errors_nid_idx" ON "selfservice_errors" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000039_network.mysql.down.sql new file mode 100644 index 0000000..c5937c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` DROP FOREIGN KEY `selfservice_settings_flows_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000039_network.mysql.up.sql new file mode 100644 index 0000000..351fd5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_credentials_nid_idx` ON `identity_credentials` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000039_network.postgres.down.sql new file mode 100644 index 0000000..ec7258a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP CONSTRAINT "selfservice_settings_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000039_network.postgres.up.sql new file mode 100644 index 0000000..e083ff2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_idx" ON "identity_credentials" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.down.sql new file mode 100644 index 0000000..fe9406e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.up.sql new file mode 100644 index 0000000..31c9140 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000039_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE continuity_containers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.down.sql new file mode 100644 index 0000000..74a26ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP CONSTRAINT "selfservice_registration_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.up.sql new file mode 100644 index 0000000..4254cdc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000040_network.mysql.down.sql new file mode 100644 index 0000000..baca4d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` DROP FOREIGN KEY `selfservice_registration_flows_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000040_network.mysql.up.sql new file mode 100644 index 0000000..0b45612 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000040_network.postgres.down.sql new file mode 100644 index 0000000..74a26ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP CONSTRAINT "selfservice_registration_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000040_network.postgres.up.sql new file mode 100644 index 0000000..a6a0fa0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.down.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.up.sql new file mode 100644 index 0000000..a672610 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000040_network.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_continuity_containers_tmp" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.down.sql new file mode 100644 index 0000000..5e01871 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP CONSTRAINT "selfservice_login_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.up.sql new file mode 100644 index 0000000..61761fc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD CONSTRAINT "continuity_containers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000041_network.mysql.down.sql new file mode 100644 index 0000000..363c5ad --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP FOREIGN KEY `selfservice_login_flows_nid_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000041_network.mysql.up.sql new file mode 100644 index 0000000..e080343 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD CONSTRAINT `identity_credential_identifiers_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000041_network.postgres.down.sql new file mode 100644 index 0000000..5e01871 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP CONSTRAINT "selfservice_login_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000041_network.postgres.up.sql new file mode 100644 index 0000000..ae13f34 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.down.sql new file mode 100644 index 0000000..bcd63c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.up.sql new file mode 100644 index 0000000..98a900f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000041_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_continuity_containers_tmp" (id, identity_id, name, payload, expires_at, created_at, updated_at, nid) SELECT id, identity_id, name, payload, expires_at, created_at, updated_at, nid FROM "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000042_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000042_network.cockroach.up.sql new file mode 100644 index 0000000..31c9140 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000042_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE continuity_containers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000042_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000042_network.mysql.up.sql new file mode 100644 index 0000000..696ed22 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000042_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000042_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000042_network.postgres.up.sql new file mode 100644 index 0000000..696ed22 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000042_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.down.sql new file mode 100644 index 0000000..836456b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.up.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000042_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000043_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000043_network.cockroach.up.sql new file mode 100644 index 0000000..9899e17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000043_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP CONSTRAINT "continuity_containers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000043_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000043_network.mysql.up.sql new file mode 100644 index 0000000..13568f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000043_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000043_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000043_network.postgres.up.sql new file mode 100644 index 0000000..09930ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000043_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.down.sql new file mode 100644 index 0000000..4489a3e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.up.sql new file mode 100644 index 0000000..f16e190 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000043_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_continuity_containers_tmp" RENAME TO "continuity_containers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000044_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000044_network.cockroach.up.sql new file mode 100644 index 0000000..6fe1460 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000044_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000044_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000044_network.mysql.up.sql new file mode 100644 index 0000000..6ed27ad --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000044_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_credential_identifiers_nid_idx` ON `identity_credential_identifiers` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000044_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000044_network.postgres.up.sql new file mode 100644 index 0000000..f0ef73c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000044_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_idx" ON "identity_credential_identifiers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.down.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.up.sql new file mode 100644 index 0000000..640b95d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000044_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "continuity_containers_nid_idx" ON "continuity_containers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000045_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000045_network.cockroach.up.sql new file mode 100644 index 0000000..4254cdc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000045_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000045_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000045_network.mysql.up.sql new file mode 100644 index 0000000..b4a6104 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000045_network.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000045_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000045_network.postgres.up.sql new file mode 100644 index 0000000..dc424da --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000045_network.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_identifier_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.down.sql new file mode 100644 index 0000000..7524b95 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.up.sql new file mode 100644 index 0000000..26921e1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000045_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000046_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000046_network.cockroach.up.sql new file mode 100644 index 0000000..fa705c0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000046_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "continuity_containers" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000046_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000046_network.mysql.up.sql new file mode 100644 index 0000000..933660a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000046_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_nid_uq_idx` ON `identity_credential_identifiers` (`nid`, `identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000046_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000046_network.postgres.up.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000046_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.down.sql new file mode 100644 index 0000000..811d7de --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.up.sql new file mode 100644 index 0000000..cea5a78 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000046_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000047_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000047_network.cockroach.up.sql new file mode 100644 index 0000000..65c5e57 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000047_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000047_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000047_network.mysql.up.sql new file mode 100644 index 0000000..a63d76c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000047_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000047_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000047_network.postgres.up.sql new file mode 100644 index 0000000..6b6f687 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000047_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.down.sql new file mode 100644 index 0000000..94387aa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.up.sql new file mode 100644 index 0000000..ecaf894 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000047_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000048_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000048_network.cockroach.up.sql new file mode 100644 index 0000000..61761fc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000048_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "continuity_containers" ADD CONSTRAINT "continuity_containers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000048_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000048_network.mysql.up.sql new file mode 100644 index 0000000..aa59c40 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000048_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` ADD CONSTRAINT `selfservice_recovery_flows_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000048_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000048_network.postgres.up.sql new file mode 100644 index 0000000..a50e851 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000048_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD CONSTRAINT "selfservice_recovery_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.down.sql new file mode 100644 index 0000000..0966d17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.up.sql new file mode 100644 index 0000000..06bc814 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000048_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE courier_messages SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000049_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000049_network.cockroach.up.sql new file mode 100644 index 0000000..640b95d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000049_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "continuity_containers_nid_idx" ON "continuity_containers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000049_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000049_network.mysql.up.sql new file mode 100644 index 0000000..6b00ee0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000049_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000049_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000049_network.postgres.up.sql new file mode 100644 index 0000000..6b00ee0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000049_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.down.sql new file mode 100644 index 0000000..2a972f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.up.sql new file mode 100644 index 0000000..98535ee --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000049_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_status_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000050_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000050_network.cockroach.up.sql new file mode 100644 index 0000000..5d2f5e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000050_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000050_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000050_network.mysql.up.sql new file mode 100644 index 0000000..42b0957 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000050_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000050_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000050_network.postgres.up.sql new file mode 100644 index 0000000..bd3718d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000050_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.down.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.up.sql new file mode 100644 index 0000000..d799146 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000050_network.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"template_type" TEXT NOT NULL DEFAULT '', +"template_data" BLOB, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000051_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000051_network.cockroach.up.sql new file mode 100644 index 0000000..b640d84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000051_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD CONSTRAINT "courier_messages_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000051_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000051_network.mysql.up.sql new file mode 100644 index 0000000..448243c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000051_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_recovery_flows_nid_idx` ON `selfservice_recovery_flows` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000051_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000051_network.postgres.up.sql new file mode 100644 index 0000000..23a4322 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000051_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_recovery_flows_nid_idx" ON "selfservice_recovery_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.down.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.up.sql new file mode 100644 index 0000000..2c09bfd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000051_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_status_idx" ON "_courier_messages_tmp" (status); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000052_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000052_network.cockroach.up.sql new file mode 100644 index 0000000..06bc814 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000052_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE courier_messages SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000052_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000052_network.mysql.up.sql new file mode 100644 index 0000000..382ba61 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000052_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_addresses` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000052_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000052_network.postgres.up.sql new file mode 100644 index 0000000..675828b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000052_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.down.sql new file mode 100644 index 0000000..be327ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.up.sql new file mode 100644 index 0000000..f444ab3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000052_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at, template_type, template_data, nid) SELECT id, type, status, body, subject, recipient, created_at, updated_at, template_type, template_data, nid FROM "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000053_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000053_network.cockroach.up.sql new file mode 100644 index 0000000..dd1e98b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000053_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP CONSTRAINT "courier_messages_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000053_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000053_network.mysql.up.sql new file mode 100644 index 0000000..8d66c36 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000053_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_addresses` ADD CONSTRAINT `identity_recovery_addresses_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000053_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000053_network.postgres.up.sql new file mode 100644 index 0000000..cfc8c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000053_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD CONSTRAINT "identity_recovery_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.down.sql new file mode 100644 index 0000000..23f9534 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.up.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000053_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000054_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000054_network.cockroach.up.sql new file mode 100644 index 0000000..3840f04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000054_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000054_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000054_network.mysql.up.sql new file mode 100644 index 0000000..cc0d8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000054_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000054_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000054_network.postgres.up.sql new file mode 100644 index 0000000..cc0d8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000054_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.down.sql new file mode 100644 index 0000000..68bca5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.up.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000054_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000055_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000055_network.cockroach.up.sql new file mode 100644 index 0000000..5d2f5e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000055_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000055_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000055_network.mysql.up.sql new file mode 100644 index 0000000..a81bcf4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000055_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_addresses` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000055_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000055_network.postgres.up.sql new file mode 100644 index 0000000..dd6c07e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000055_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.down.sql new file mode 100644 index 0000000..fe89b8d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credentials_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.up.sql new file mode 100644 index 0000000..93c586c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000055_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_nid_idx" ON "courier_messages" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000056_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000056_network.cockroach.up.sql new file mode 100644 index 0000000..698071d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000056_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "courier_messages" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000056_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000056_network.mysql.up.sql new file mode 100644 index 0000000..8c45855 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000056_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_nid_idx` ON `identity_recovery_addresses` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000056_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000056_network.postgres.up.sql new file mode 100644 index 0000000..93a69e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000056_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_idx" ON "identity_recovery_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.down.sql new file mode 100644 index 0000000..295533e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identities_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.up.sql new file mode 100644 index 0000000..e5e5a0e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000056_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000057_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000057_network.cockroach.up.sql new file mode 100644 index 0000000..93924d1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000057_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000057_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000057_network.mysql.up.sql new file mode 100644 index 0000000..d78266e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000057_network.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000057_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000057_network.postgres.up.sql new file mode 100644 index 0000000..ba5fb94 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000057_network.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.down.sql new file mode 100644 index 0000000..13d0673 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_errors_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.up.sql new file mode 100644 index 0000000..88749a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000057_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identities DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000058_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000058_network.cockroach.up.sql new file mode 100644 index 0000000..b640d84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000058_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD CONSTRAINT "courier_messages_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000058_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000058_network.mysql.up.sql new file mode 100644 index 0000000..38a0567 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000058_network.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000058_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000058_network.postgres.up.sql new file mode 100644 index 0000000..8afb020 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000058_network.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.down.sql new file mode 100644 index 0000000..2d1a6c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "courier_messages_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.up.sql new file mode 100644 index 0000000..9a34414 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000058_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identities ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000059_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000059_network.cockroach.up.sql new file mode 100644 index 0000000..93c586c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000059_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "courier_messages_nid_idx" ON "courier_messages" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000059_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000059_network.mysql.up.sql new file mode 100644 index 0000000..9356574 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000059_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`nid`, `via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000059_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000059_network.postgres.up.sql new file mode 100644 index 0000000..e12f410 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000059_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.down.sql new file mode 100644 index 0000000..cfdfdea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "continuity_containers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.up.sql new file mode 100644 index 0000000..54dba29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000059_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identities SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000060_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000060_network.cockroach.up.sql new file mode 100644 index 0000000..3d072a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000060_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000060_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000060_network.mysql.up.sql new file mode 100644 index 0000000..ed32ff2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000060_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`nid`, `via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000060_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000060_network.postgres.up.sql new file mode 100644 index 0000000..5bf3cea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000060_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.down.sql new file mode 100644 index 0000000..fb35cc6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_settings_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.up.sql new file mode 100644 index 0000000..82c6236 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000060_network.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_identities_tmp" ( +"id" TEXT PRIMARY KEY, +"schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000061_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000061_network.cockroach.up.sql new file mode 100644 index 0000000..20e0943 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000061_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD CONSTRAINT "identities_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000061_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000061_network.mysql.up.sql new file mode 100644 index 0000000..fc88451 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000061_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000061_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000061_network.postgres.up.sql new file mode 100644 index 0000000..13aef79 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000061_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.down.sql new file mode 100644 index 0000000..44c4132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_registration_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.up.sql new file mode 100644 index 0000000..bd5056d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000061_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid) SELECT id, schema_id, traits, created_at, updated_at, nid FROM "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000062_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000062_network.cockroach.up.sql new file mode 100644 index 0000000..54dba29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000062_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identities SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000062_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000062_network.mysql.up.sql new file mode 100644 index 0000000..c0f99e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000062_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD CONSTRAINT `identity_recovery_tokens_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000062_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000062_network.postgres.up.sql new file mode 100644 index 0000000..6877222 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000062_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.down.sql new file mode 100644 index 0000000..33d0fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.up.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000062_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000063_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000063_network.cockroach.up.sql new file mode 100644 index 0000000..bf94002 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000063_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP CONSTRAINT "identities_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000063_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000063_network.mysql.up.sql new file mode 100644 index 0000000..dff2340 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000063_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000063_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000063_network.postgres.up.sql new file mode 100644 index 0000000..dff2340 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000063_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000063_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000063_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000063_network.sqlite3.up.sql new file mode 100644 index 0000000..09c5e50 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000063_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identities_tmp" RENAME TO "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000064_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000064_network.cockroach.up.sql new file mode 100644 index 0000000..dfb74e5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000064_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000064_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000064_network.mysql.up.sql new file mode 100644 index 0000000..1221f76 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000064_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000064_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000064_network.postgres.up.sql new file mode 100644 index 0000000..ecefd08 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000064_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000064_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000064_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000064_network.sqlite3.up.sql new file mode 100644 index 0000000..000b979 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000064_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identities_nid_idx" ON "identities" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000065_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000065_network.cockroach.up.sql new file mode 100644 index 0000000..3d072a6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000065_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000065_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000065_network.mysql.up.sql new file mode 100644 index 0000000..c2683c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000065_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_tokens_nid_idx` ON `identity_recovery_tokens` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000065_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000065_network.postgres.up.sql new file mode 100644 index 0000000..f164f31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000065_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "identity_recovery_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000065_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000065_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000065_network.sqlite3.up.sql new file mode 100644 index 0000000..42f1b3d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000065_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000066_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000066_network.cockroach.up.sql new file mode 100644 index 0000000..827ba35 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000066_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identities" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000066_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000066_network.mysql.up.sql new file mode 100644 index 0000000..465659b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000066_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000066_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000066_network.postgres.up.sql new file mode 100644 index 0000000..47b2b75 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000066_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000066_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000066_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000066_network.sqlite3.up.sql new file mode 100644 index 0000000..afaa0bc --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000066_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000067_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000067_network.cockroach.up.sql new file mode 100644 index 0000000..bba739e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000067_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000067_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000067_network.mysql.up.sql new file mode 100644 index 0000000..69cc0c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000067_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD CONSTRAINT `selfservice_verification_flows_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000067_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000067_network.postgres.up.sql new file mode 100644 index 0000000..c93db42 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000067_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD CONSTRAINT "selfservice_verification_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000067_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000067_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000067_network.sqlite3.up.sql new file mode 100644 index 0000000..7e4fb8d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000067_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000068_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000068_network.cockroach.up.sql new file mode 100644 index 0000000..20e0943 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000068_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD CONSTRAINT "identities_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000068_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000068_network.mysql.up.sql new file mode 100644 index 0000000..03928ba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000068_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000068_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000068_network.postgres.up.sql new file mode 100644 index 0000000..03928ba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000068_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000068_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000068_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000068_network.sqlite3.up.sql new file mode 100644 index 0000000..3584be5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000068_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_credentials SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000069_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000069_network.cockroach.up.sql new file mode 100644 index 0000000..000b979 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000069_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identities_nid_idx" ON "identities" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000069_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000069_network.mysql.up.sql new file mode 100644 index 0000000..2fa3ee8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000069_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000069_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000069_network.postgres.up.sql new file mode 100644 index 0000000..fb068cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000069_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000069_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000069_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000069_network.sqlite3.up.sql new file mode 100644 index 0000000..d881db1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000069_network.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_identity_credentials_tmp" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000070_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000070_network.cockroach.up.sql new file mode 100644 index 0000000..7a5e15b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000070_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000070_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000070_network.mysql.up.sql new file mode 100644 index 0000000..add3eb5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000070_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `selfservice_verification_flows_nid_idx` ON `selfservice_verification_flows` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000070_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000070_network.postgres.up.sql new file mode 100644 index 0000000..6328921 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000070_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "selfservice_verification_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000070_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000070_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000070_network.sqlite3.up.sql new file mode 100644 index 0000000..c50fb1c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000070_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credentials_tmp" (id, config, identity_credential_type_id, identity_id, created_at, updated_at, nid) SELECT id, config, identity_credential_type_id, identity_id, created_at, updated_at, nid FROM "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000071_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000071_network.cockroach.up.sql new file mode 100644 index 0000000..0fa3653 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000071_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD CONSTRAINT "identity_credentials_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000071_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000071_network.mysql.up.sql new file mode 100644 index 0000000..2318a75 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000071_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000071_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000071_network.postgres.up.sql new file mode 100644 index 0000000..8d465d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000071_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000071_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000071_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000071_network.sqlite3.up.sql new file mode 100644 index 0000000..caec556 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000071_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000072_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000072_network.cockroach.up.sql new file mode 100644 index 0000000..3584be5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000072_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_credentials SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000072_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000072_network.mysql.up.sql new file mode 100644 index 0000000..27f564a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000072_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD CONSTRAINT `identity_verifiable_addresses_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000072_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000072_network.postgres.up.sql new file mode 100644 index 0000000..f388f85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000072_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD CONSTRAINT "identity_verifiable_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000072_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000072_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000072_network.sqlite3.up.sql new file mode 100644 index 0000000..05e6c76 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000072_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credentials_tmp" RENAME TO "identity_credentials"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000073_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000073_network.cockroach.up.sql new file mode 100644 index 0000000..744dee6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000073_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP CONSTRAINT "identity_credentials_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000073_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000073_network.mysql.up.sql new file mode 100644 index 0000000..a835535 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000073_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000073_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000073_network.postgres.up.sql new file mode 100644 index 0000000..a835535 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000073_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000073_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000073_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000073_network.sqlite3.up.sql new file mode 100644 index 0000000..e083ff2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000073_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_idx" ON "identity_credentials" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000074_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000074_network.cockroach.up.sql new file mode 100644 index 0000000..608966e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000074_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000074_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000074_network.mysql.up.sql new file mode 100644 index 0000000..0a89219 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000074_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000074_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000074_network.postgres.up.sql new file mode 100644 index 0000000..a60f47f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000074_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000074_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000074_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000074_network.sqlite3.up.sql new file mode 100644 index 0000000..da753f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000074_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000075_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000075_network.cockroach.up.sql new file mode 100644 index 0000000..7a5e15b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000075_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000075_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000075_network.mysql.up.sql new file mode 100644 index 0000000..49c847b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000075_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_nid_idx` ON `identity_verifiable_addresses` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000075_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000075_network.postgres.up.sql new file mode 100644 index 0000000..a0f33ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000075_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_idx" ON "identity_verifiable_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000075_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000075_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000075_network.sqlite3.up.sql new file mode 100644 index 0000000..fbfd6df --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000075_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000076_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000076_network.cockroach.up.sql new file mode 100644 index 0000000..9f554c8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000076_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_credentials" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000076_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000076_network.mysql.up.sql new file mode 100644 index 0000000..14b457a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000076_network.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000076_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000076_network.postgres.up.sql new file mode 100644 index 0000000..cf7935e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000076_network.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000076_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000076_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000076_network.sqlite3.up.sql new file mode 100644 index 0000000..1c8545e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000076_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000077_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000077_network.cockroach.up.sql new file mode 100644 index 0000000..08a713f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000077_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000077_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000077_network.mysql.up.sql new file mode 100644 index 0000000..f448837 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000077_network.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000077_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000077_network.postgres.up.sql new file mode 100644 index 0000000..2ba2965 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000077_network.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000077_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000077_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000077_network.sqlite3.up.sql new file mode 100644 index 0000000..696ed22 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000077_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000078_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000078_network.cockroach.up.sql new file mode 100644 index 0000000..0fa3653 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000078_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credentials" ADD CONSTRAINT "identity_credentials_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000078_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000078_network.mysql.up.sql new file mode 100644 index 0000000..de087cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000078_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`nid`, `via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000078_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000078_network.postgres.up.sql new file mode 100644 index 0000000..c2c8cdb --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000078_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000078_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000078_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000078_network.sqlite3.up.sql new file mode 100644 index 0000000..b61b631 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000078_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000079_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000079_network.cockroach.up.sql new file mode 100644 index 0000000..e083ff2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000079_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_idx" ON "identity_credentials" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000079_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000079_network.mysql.up.sql new file mode 100644 index 0000000..9a70ce0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000079_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`nid`, `via`, `value`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000079_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000079_network.postgres.up.sql new file mode 100644 index 0000000..5e9dc85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000079_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000079_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000079_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000079_network.sqlite3.up.sql new file mode 100644 index 0000000..dc27ecf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000079_network.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000080_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000080_network.cockroach.up.sql new file mode 100644 index 0000000..a6a0fa0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000080_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000080_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000080_network.mysql.up.sql new file mode 100644 index 0000000..81fd41e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000080_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verification_tokens` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000080_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000080_network.postgres.up.sql new file mode 100644 index 0000000..a018a2f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000080_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000080_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000080_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000080_network.sqlite3.up.sql new file mode 100644 index 0000000..f246891 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000080_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "_identity_credential_identifiers_tmp" (identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000081_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000081_network.cockroach.up.sql new file mode 100644 index 0000000..ae13f34 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000081_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000081_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000081_network.mysql.up.sql new file mode 100644 index 0000000..df87510 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000081_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verification_tokens` ADD CONSTRAINT `identity_verification_tokens_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000081_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000081_network.postgres.up.sql new file mode 100644 index 0000000..72b9cfa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000081_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD CONSTRAINT "identity_verification_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000081_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000081_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000081_network.sqlite3.up.sql new file mode 100644 index 0000000..00bfb99 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000081_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credential_identifiers_tmp" (id, identifier, identity_credential_id, created_at, updated_at, nid) SELECT id, identifier, identity_credential_id, created_at, updated_at, nid FROM "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000082_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000082_network.cockroach.up.sql new file mode 100644 index 0000000..696ed22 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000082_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000082_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000082_network.mysql.up.sql new file mode 100644 index 0000000..29977f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000082_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_verification_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000082_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000082_network.postgres.up.sql new file mode 100644 index 0000000..29977f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000082_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_verification_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000082_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000082_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000082_network.sqlite3.up.sql new file mode 100644 index 0000000..523de38 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000082_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000083_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000083_network.cockroach.up.sql new file mode 100644 index 0000000..a387444 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000083_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP CONSTRAINT "identity_credential_identifiers_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000083_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000083_network.mysql.up.sql new file mode 100644 index 0000000..8718a70 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000083_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verification_tokens` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000083_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000083_network.postgres.up.sql new file mode 100644 index 0000000..a55133b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000083_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000083_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000083_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000083_network.sqlite3.up.sql new file mode 100644 index 0000000..4c12302 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000083_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000084_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000084_network.cockroach.up.sql new file mode 100644 index 0000000..78f778a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000084_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000084_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000084_network.mysql.up.sql new file mode 100644 index 0000000..ec98f0c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000084_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_nid_idx` ON `identity_verification_tokens` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000084_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000084_network.postgres.up.sql new file mode 100644 index 0000000..12eaae1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000084_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_nid_idx" ON "identity_verification_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000084_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000084_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000084_network.sqlite3.up.sql new file mode 100644 index 0000000..f0ef73c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000084_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_idx" ON "identity_credential_identifiers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000085_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000085_network.cockroach.up.sql new file mode 100644 index 0000000..a6a0fa0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000085_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000085_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000085_network.mysql.up.sql new file mode 100644 index 0000000..8809e86 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000085_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000085_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000085_network.postgres.up.sql new file mode 100644 index 0000000..15dbf2c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000085_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000085_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000085_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000085_network.sqlite3.up.sql new file mode 100644 index 0000000..b61b631 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000085_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000086_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000086_network.cockroach.up.sql new file mode 100644 index 0000000..2b64217 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000086_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_credential_identifiers" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000086_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000086_network.mysql.up.sql new file mode 100644 index 0000000..c019a72 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000086_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD CONSTRAINT `sessions_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000086_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000086_network.postgres.up.sql new file mode 100644 index 0000000..aa89dc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000086_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000086_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000086_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000086_network.sqlite3.up.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000086_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000087_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000087_network.cockroach.up.sql new file mode 100644 index 0000000..8a375cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000087_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000087_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000087_network.mysql.up.sql new file mode 100644 index 0000000..755182f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000087_network.mysql.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000087_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000087_network.postgres.up.sql new file mode 100644 index 0000000..755182f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000087_network.postgres.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000087_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000087_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000087_network.sqlite3.up.sql new file mode 100644 index 0000000..3c423c2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000087_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000088_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000088_network.cockroach.up.sql new file mode 100644 index 0000000..ae13f34 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000088_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000088_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000088_network.mysql.up.sql new file mode 100644 index 0000000..705c6b7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000088_network.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `nid` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000088_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000088_network.postgres.up.sql new file mode 100644 index 0000000..acb4860 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000088_network.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "nid" TYPE UUID, ALTER COLUMN "nid" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000088_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000088_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000088_network.sqlite3.up.sql new file mode 100644 index 0000000..560cb28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000088_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_recovery_flows DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000089_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000089_network.cockroach.up.sql new file mode 100644 index 0000000..f0ef73c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000089_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_idx" ON "identity_credential_identifiers" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.mysql.down.sql b/persistence/sql/migrations/sql/20210410175418000089_network.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.mysql.up.sql b/persistence/sql/migrations/sql/20210410175418000089_network.mysql.up.sql new file mode 100644 index 0000000..0056ef3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000089_network.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `sessions_nid_idx` ON `sessions` (`id`, `nid`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.postgres.down.sql b/persistence/sql/migrations/sql/20210410175418000089_network.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.postgres.up.sql b/persistence/sql/migrations/sql/20210410175418000089_network.postgres.up.sql new file mode 100644 index 0000000..8dbe054 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000089_network.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "sessions" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000089_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000089_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000089_network.sqlite3.up.sql new file mode 100644 index 0000000..1a9289a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000089_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_recovery_flows ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000090_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000090_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000090_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000090_network.cockroach.up.sql new file mode 100644 index 0000000..b61b631 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000090_network.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000090_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000090_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000090_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000090_network.sqlite3.up.sql new file mode 100644 index 0000000..6b00ee0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000090_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000091_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000091_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000091_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000091_network.cockroach.up.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000091_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000091_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000091_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000091_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000091_network.sqlite3.up.sql new file mode 100644 index 0000000..9f0adc3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000091_network.sqlite3.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_recovery_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000092_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000092_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000092_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000092_network.cockroach.up.sql new file mode 100644 index 0000000..6b6f687 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000092_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000092_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000092_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000092_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000092_network.sqlite3.up.sql new file mode 100644 index 0000000..5967bc0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000092_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at, type, ui, nid FROM "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000093_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000093_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000093_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000093_network.cockroach.up.sql new file mode 100644 index 0000000..a50e851 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000093_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD CONSTRAINT "selfservice_recovery_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000093_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000093_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000093_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000093_network.sqlite3.up.sql new file mode 100644 index 0000000..06c842b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000093_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000094_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000094_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000094_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000094_network.cockroach.up.sql new file mode 100644 index 0000000..6b00ee0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000094_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_recovery_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000094_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000094_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000094_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000094_network.sqlite3.up.sql new file mode 100644 index 0000000..5e05593 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000094_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_flows_tmp" RENAME TO "selfservice_recovery_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000095_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000095_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000095_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000095_network.cockroach.up.sql new file mode 100644 index 0000000..f37e7c2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000095_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP CONSTRAINT "selfservice_recovery_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000095_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000095_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000095_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000095_network.sqlite3.up.sql new file mode 100644 index 0000000..23a4322 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000095_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_recovery_flows_nid_idx" ON "selfservice_recovery_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000096_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000096_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000096_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000096_network.cockroach.up.sql new file mode 100644 index 0000000..b7eb009 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000096_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000096_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000096_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000096_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000096_network.sqlite3.up.sql new file mode 100644 index 0000000..7755798 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000096_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000097_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000097_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000097_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000097_network.cockroach.up.sql new file mode 100644 index 0000000..6b6f687 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000097_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000097_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000097_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000097_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000097_network.sqlite3.up.sql new file mode 100644 index 0000000..e8c2648 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000097_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000098_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000098_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000098_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000098_network.cockroach.up.sql new file mode 100644 index 0000000..6198832 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000098_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_recovery_flows" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000098_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000098_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000098_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000098_network.sqlite3.up.sql new file mode 100644 index 0000000..8484f28 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000098_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000099_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000099_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000099_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000099_network.cockroach.up.sql new file mode 100644 index 0000000..bc42a82 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000099_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000099_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000099_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000099_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000099_network.sqlite3.up.sql new file mode 100644 index 0000000..cc0d8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000099_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000100_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000100_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000100_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000100_network.cockroach.up.sql new file mode 100644 index 0000000..a50e851 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000100_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" ADD CONSTRAINT "selfservice_recovery_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000100_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000100_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000100_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000100_network.sqlite3.up.sql new file mode 100644 index 0000000..0966d17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000100_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000101_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000101_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000101_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000101_network.cockroach.up.sql new file mode 100644 index 0000000..23a4322 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000101_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_recovery_flows_nid_idx" ON "selfservice_recovery_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000101_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000101_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000101_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000101_network.sqlite3.up.sql new file mode 100644 index 0000000..2a972f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000101_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000102_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000102_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000102_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000102_network.cockroach.up.sql new file mode 100644 index 0000000..675828b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000102_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000102_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000102_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000102_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000102_network.sqlite3.up.sql new file mode 100644 index 0000000..6225d47 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000102_network.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_identity_recovery_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000103_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000103_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000103_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000103_network.cockroach.up.sql new file mode 100644 index 0000000..cfc8c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000103_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD CONSTRAINT "identity_recovery_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000103_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000103_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000103_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000103_network.sqlite3.up.sql new file mode 100644 index 0000000..e503571 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000103_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "_identity_recovery_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000104_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000104_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000104_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000104_network.cockroach.up.sql new file mode 100644 index 0000000..cc0d8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000104_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000104_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000104_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000104_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000104_network.sqlite3.up.sql new file mode 100644 index 0000000..3aa1a87 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000104_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "_identity_recovery_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000105_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000105_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000105_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000105_network.cockroach.up.sql new file mode 100644 index 0000000..65fa394 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000105_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" DROP CONSTRAINT "identity_recovery_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000105_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000105_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000105_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000105_network.sqlite3.up.sql new file mode 100644 index 0000000..9dd9091 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000105_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_addresses_tmp" (id, via, value, identity_id, created_at, updated_at, nid) SELECT id, via, value, identity_id, created_at, updated_at, nid FROM "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000106_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000106_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000106_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000106_network.cockroach.up.sql new file mode 100644 index 0000000..5c37ef8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000106_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000106_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000106_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000106_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000106_network.sqlite3.up.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000106_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000107_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000107_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000107_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000107_network.cockroach.up.sql new file mode 100644 index 0000000..675828b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000107_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000107_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000107_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000107_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000107_network.sqlite3.up.sql new file mode 100644 index 0000000..40c64f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000107_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_addresses_tmp" RENAME TO "identity_recovery_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000108_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000108_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000108_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000108_network.cockroach.up.sql new file mode 100644 index 0000000..a757f0c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000108_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_addresses" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000108_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000108_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000108_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000108_network.sqlite3.up.sql new file mode 100644 index 0000000..93a69e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000108_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_idx" ON "identity_recovery_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000109_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000109_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000109_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000109_network.cockroach.up.sql new file mode 100644 index 0000000..81b5d9c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000109_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000109_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000109_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000109_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000109_network.sqlite3.up.sql new file mode 100644 index 0000000..2a972f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000109_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000110_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000110_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000110_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000110_network.cockroach.up.sql new file mode 100644 index 0000000..cfc8c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000110_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_addresses" ADD CONSTRAINT "identity_recovery_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000110_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000110_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000110_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000110_network.sqlite3.up.sql new file mode 100644 index 0000000..0966d17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000110_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000111_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000111_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000111_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000111_network.cockroach.up.sql new file mode 100644 index 0000000..93a69e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000111_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_idx" ON "identity_recovery_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000111_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000111_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000111_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000111_network.sqlite3.up.sql new file mode 100644 index 0000000..e12f410 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000111_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000112_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000112_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000112_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000112_network.cockroach.up.sql new file mode 100644 index 0000000..2a972f2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000112_network.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000112_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000112_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000112_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000112_network.sqlite3.up.sql new file mode 100644 index 0000000..5bf3cea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000112_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000113_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000113_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000113_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000113_network.cockroach.up.sql new file mode 100644 index 0000000..0966d17 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000113_network.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000113_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000113_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000113_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000113_network.sqlite3.up.sql new file mode 100644 index 0000000..f9f5d82 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000113_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000114_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000114_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000114_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000114_network.cockroach.up.sql new file mode 100644 index 0000000..e12f410 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000114_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000114_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000114_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000114_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000114_network.sqlite3.up.sql new file mode 100644 index 0000000..0fcf6d1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000114_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000115_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000115_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000115_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000115_network.cockroach.up.sql new file mode 100644 index 0000000..5bf3cea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000115_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000115_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000115_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000115_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000115_network.sqlite3.up.sql new file mode 100644 index 0000000..94112df --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000115_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000116_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000116_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000116_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000116_network.cockroach.up.sql new file mode 100644 index 0000000..13aef79 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000116_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000116_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000116_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000116_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000116_network.sqlite3.up.sql new file mode 100644 index 0000000..dff2340 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000116_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000117_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000117_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000117_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000117_network.cockroach.up.sql new file mode 100644 index 0000000..6877222 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000117_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000117_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000117_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000117_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000117_network.sqlite3.up.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000117_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000118_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000118_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000118_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000118_network.cockroach.up.sql new file mode 100644 index 0000000..dff2340 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000118_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000118_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000118_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000118_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000118_network.sqlite3.up.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000118_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000119_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000119_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000119_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000119_network.cockroach.up.sql new file mode 100644 index 0000000..3dba992 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000119_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000119_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000119_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000119_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000119_network.sqlite3.up.sql new file mode 100644 index 0000000..9a18fbd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000119_network.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000120_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000120_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000120_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000120_network.cockroach.up.sql new file mode 100644 index 0000000..42bebdd --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000120_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000120_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000120_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000120_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000120_network.sqlite3.up.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000120_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000121_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000121_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000121_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000121_network.cockroach.up.sql new file mode 100644 index 0000000..13aef79 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000121_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000121_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000121_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000121_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000121_network.sqlite3.up.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000121_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000122_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000122_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000122_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000122_network.cockroach.up.sql new file mode 100644 index 0000000..90dc4e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000122_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000122_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000122_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000122_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000122_network.sqlite3.up.sql new file mode 100644 index 0000000..c367dde --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000122_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000123_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000123_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000123_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000123_network.cockroach.up.sql new file mode 100644 index 0000000..7bb1334 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000123_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000123_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000123_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000123_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000123_network.sqlite3.up.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000123_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000124_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000124_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000124_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000124_network.cockroach.up.sql new file mode 100644 index 0000000..6877222 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000124_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000124_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000124_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000124_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000124_network.sqlite3.up.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000124_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000125_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000125_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000125_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000125_network.cockroach.up.sql new file mode 100644 index 0000000..f164f31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000125_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "identity_recovery_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000125_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000125_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000125_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000125_network.sqlite3.up.sql new file mode 100644 index 0000000..f164f31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000125_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "identity_recovery_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000126_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000126_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000126_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000126_network.cockroach.up.sql new file mode 100644 index 0000000..47b2b75 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000126_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000126_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000126_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000126_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000126_network.sqlite3.up.sql new file mode 100644 index 0000000..08addc3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000126_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000127_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000127_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000127_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000127_network.cockroach.up.sql new file mode 100644 index 0000000..c93db42 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000127_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD CONSTRAINT "selfservice_verification_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000127_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000127_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000127_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000127_network.sqlite3.up.sql new file mode 100644 index 0000000..8cdea86 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000127_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000128_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000128_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000128_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000128_network.cockroach.up.sql new file mode 100644 index 0000000..03928ba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000128_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000128_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000128_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000128_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000128_network.sqlite3.up.sql new file mode 100644 index 0000000..0aa3481 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000128_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000129_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000129_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000129_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000129_network.cockroach.up.sql new file mode 100644 index 0000000..1e16184 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000129_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP CONSTRAINT "selfservice_verification_flows_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000129_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000129_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000129_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000129_network.sqlite3.up.sql new file mode 100644 index 0000000..03928ba --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000129_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000130_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000130_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000130_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000130_network.cockroach.up.sql new file mode 100644 index 0000000..9364fa5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000130_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000130_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000130_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000130_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000130_network.sqlite3.up.sql new file mode 100644 index 0000000..0f9c957 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000130_network.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT, +"ui" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000131_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000131_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000131_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000131_network.cockroach.up.sql new file mode 100644 index 0000000..47b2b75 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000131_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000131_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000131_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000131_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000131_network.sqlite3.up.sql new file mode 100644 index 0000000..b25d3f5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000131_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, ui, nid) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, type, state, active_method, ui, nid FROM "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000132_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000132_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000132_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000132_network.cockroach.up.sql new file mode 100644 index 0000000..f71edef --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000132_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_verification_flows" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000132_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000132_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000132_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000132_network.sqlite3.up.sql new file mode 100644 index 0000000..524b49f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000132_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000133_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000133_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000133_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000133_network.cockroach.up.sql new file mode 100644 index 0000000..8a57910 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000133_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000133_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000133_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000133_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000133_network.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000133_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000134_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000134_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000134_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000134_network.cockroach.up.sql new file mode 100644 index 0000000..c93db42 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000134_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD CONSTRAINT "selfservice_verification_flows_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000134_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000134_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000134_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000134_network.sqlite3.up.sql new file mode 100644 index 0000000..6328921 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000134_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "selfservice_verification_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000135_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000135_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000135_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000135_network.cockroach.up.sql new file mode 100644 index 0000000..6328921 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000135_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_verification_flows_nid_idx" ON "selfservice_verification_flows" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000135_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000135_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000135_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000135_network.sqlite3.up.sql new file mode 100644 index 0000000..f204be9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000135_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000136_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000136_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000136_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000136_network.cockroach.up.sql new file mode 100644 index 0000000..8d465d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000136_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000136_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000136_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000136_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000136_network.sqlite3.up.sql new file mode 100644 index 0000000..434516a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000136_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000137_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000137_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000137_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000137_network.cockroach.up.sql new file mode 100644 index 0000000..f388f85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000137_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD CONSTRAINT "identity_verifiable_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000137_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000137_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000137_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000137_network.sqlite3.up.sql new file mode 100644 index 0000000..386ad4f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000137_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000138_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000138_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000138_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000138_network.cockroach.up.sql new file mode 100644 index 0000000..a835535 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000138_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000138_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000138_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000138_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000138_network.sqlite3.up.sql new file mode 100644 index 0000000..a835535 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000138_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000139_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000139_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000139_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000139_network.cockroach.up.sql new file mode 100644 index 0000000..ec1bbe4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000139_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP CONSTRAINT "identity_verifiable_addresses_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000139_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000139_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000139_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000139_network.sqlite3.up.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000139_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000140_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000140_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000140_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000140_network.cockroach.up.sql new file mode 100644 index 0000000..bb80668 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000140_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000140_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000140_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000140_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000140_network.sqlite3.up.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000140_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000141_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000141_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000141_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000141_network.cockroach.up.sql new file mode 100644 index 0000000..8d465d3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000141_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000141_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000141_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000141_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000141_network.sqlite3.up.sql new file mode 100644 index 0000000..123f894 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000141_network.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000142_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000142_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000142_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000142_network.cockroach.up.sql new file mode 100644 index 0000000..1bddba9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000142_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000142_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000142_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000142_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000142_network.sqlite3.up.sql new file mode 100644 index 0000000..2a281d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000142_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000143_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000143_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000143_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000143_network.cockroach.up.sql new file mode 100644 index 0000000..73a00ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000143_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000143_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000143_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000143_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000143_network.sqlite3.up.sql new file mode 100644 index 0000000..12885bf --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000143_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000144_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000144_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000144_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000144_network.cockroach.up.sql new file mode 100644 index 0000000..f388f85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000144_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD CONSTRAINT "identity_verifiable_addresses_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000144_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000144_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000144_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000144_network.sqlite3.up.sql new file mode 100644 index 0000000..ab072b2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000144_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, nid) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, nid FROM "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000145_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000145_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000145_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000145_network.cockroach.up.sql new file mode 100644 index 0000000..a0f33ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000145_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_idx" ON "identity_verifiable_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000145_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000145_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000145_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000145_network.sqlite3.up.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000145_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000146_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000146_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000146_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000146_network.cockroach.up.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000146_network.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000146_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000146_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000146_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000146_network.sqlite3.up.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000146_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000147_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000147_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000147_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000147_network.cockroach.up.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000147_network.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000147_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000147_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000147_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000147_network.sqlite3.up.sql new file mode 100644 index 0000000..a0f33ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000147_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_idx" ON "identity_verifiable_addresses" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000148_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000148_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000148_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000148_network.cockroach.up.sql new file mode 100644 index 0000000..c2c8cdb --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000148_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000148_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000148_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000148_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000148_network.sqlite3.up.sql new file mode 100644 index 0000000..a1f61d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000148_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000149_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000149_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000149_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000149_network.cockroach.up.sql new file mode 100644 index 0000000..5e9dc85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000149_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000149_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000149_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000149_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000149_network.sqlite3.up.sql new file mode 100644 index 0000000..7adfc4d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000149_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000150_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000150_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000150_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000150_network.cockroach.up.sql new file mode 100644 index 0000000..a018a2f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000150_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000150_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000150_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000150_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000150_network.sqlite3.up.sql new file mode 100644 index 0000000..c2c8cdb --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000150_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000151_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000151_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000151_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000151_network.cockroach.up.sql new file mode 100644 index 0000000..72b9cfa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000151_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD CONSTRAINT "identity_verification_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000151_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000151_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000151_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000151_network.sqlite3.up.sql new file mode 100644 index 0000000..5e9dc85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000151_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (nid, via, value); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000152_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000152_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000152_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000152_network.cockroach.up.sql new file mode 100644 index 0000000..29977f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000152_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_verification_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000152_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000152_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000152_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000152_network.sqlite3.up.sql new file mode 100644 index 0000000..c94fc4f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000152_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000153_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000153_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000153_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000153_network.cockroach.up.sql new file mode 100644 index 0000000..2488cc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000153_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" DROP CONSTRAINT "identity_verification_tokens_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000153_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000153_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000153_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000153_network.sqlite3.up.sql new file mode 100644 index 0000000..6eee836 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000153_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verification_tokens DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000154_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000154_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000154_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000154_network.cockroach.up.sql new file mode 100644 index 0000000..bdbdbe6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000154_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000154_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000154_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000154_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000154_network.sqlite3.up.sql new file mode 100644 index 0000000..a90c69f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000154_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verification_tokens ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000155_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000155_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000155_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000155_network.cockroach.up.sql new file mode 100644 index 0000000..a018a2f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000155_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000155_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000155_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000155_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000155_network.sqlite3.up.sql new file mode 100644 index 0000000..29977f1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000155_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_verification_tokens SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000156_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000156_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000156_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000156_network.cockroach.up.sql new file mode 100644 index 0000000..1285e0d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000156_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_verification_tokens" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000156_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000156_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000156_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000156_network.sqlite3.up.sql new file mode 100644 index 0000000..8f19346 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000156_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verification_tokens_verification_flow_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000157_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000157_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000157_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000157_network.cockroach.up.sql new file mode 100644 index 0000000..a9c956b --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000157_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000157_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000157_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000157_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000157_network.sqlite3.up.sql new file mode 100644 index 0000000..3d8d628 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000157_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verification_tokens_verifiable_address_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000158_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000158_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000158_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000158_network.cockroach.up.sql new file mode 100644 index 0000000..72b9cfa --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000158_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verification_tokens" ADD CONSTRAINT "identity_verification_tokens_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000158_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000158_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000158_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000158_network.sqlite3.up.sql new file mode 100644 index 0000000..561dd99 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000158_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verification_tokens_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000159_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000159_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000159_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000159_network.cockroach.up.sql new file mode 100644 index 0000000..12eaae1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000159_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_nid_idx" ON "identity_verification_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000159_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000159_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000159_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000159_network.sqlite3.up.sql new file mode 100644 index 0000000..753bf5e --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000159_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verification_tokens_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000160_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000160_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000160_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000160_network.cockroach.up.sql new file mode 100644 index 0000000..15dbf2c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000160_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000160_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000160_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000160_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000160_network.sqlite3.up.sql new file mode 100644 index 0000000..d4a2b6f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000160_network.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_identity_verification_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000161_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000161_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000161_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000161_network.cockroach.up.sql new file mode 100644 index 0000000..aa89dc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000161_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000161_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000161_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000161_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000161_network.sqlite3.up.sql new file mode 100644 index 0000000..0e06f56 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000161_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "_identity_verification_tokens_tmp" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000162_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000162_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000162_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000162_network.cockroach.up.sql new file mode 100644 index 0000000..755182f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000162_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000162_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000162_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000162_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000162_network.sqlite3.up.sql new file mode 100644 index 0000000..4630d96 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000162_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "_identity_verification_tokens_tmp" (identity_verifiable_address_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000163_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000163_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000163_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000163_network.cockroach.up.sql new file mode 100644 index 0000000..85bce5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000163_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP CONSTRAINT "sessions_nid_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000163_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000163_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000163_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000163_network.sqlite3.up.sql new file mode 100644 index 0000000..2ffc146 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000163_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "_identity_verification_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000164_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000164_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000164_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000164_network.cockroach.up.sql new file mode 100644 index 0000000..0daf9ad --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000164_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "nid" TO "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000164_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000164_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000164_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000164_network.sqlite3.up.sql new file mode 100644 index 0000000..0e663c5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000164_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "_identity_verification_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000165_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000165_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000165_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000165_network.cockroach.up.sql new file mode 100644 index 0000000..15dbf2c --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000165_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "nid" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000165_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000165_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000165_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000165_network.sqlite3.up.sql new file mode 100644 index 0000000..6981f59 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000165_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verification_tokens_tmp" (id, token, used, used_at, expires_at, issued_at, identity_verifiable_address_id, selfservice_verification_flow_id, created_at, updated_at, nid) SELECT id, token, used, used_at, expires_at, issued_at, identity_verifiable_address_id, selfservice_verification_flow_id, created_at, updated_at, nid FROM "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000166_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000166_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000166_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000166_network.cockroach.up.sql new file mode 100644 index 0000000..53e984f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000166_network.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "nid" = "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000166_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000166_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000166_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000166_network.sqlite3.up.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000166_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000167_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000167_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000167_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000167_network.cockroach.up.sql new file mode 100644 index 0000000..e38f034 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000167_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_nid_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000167_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000167_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000167_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000167_network.sqlite3.up.sql new file mode 100644 index 0000000..0022f6d --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000167_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verification_tokens_tmp" RENAME TO "identity_verification_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000168_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000168_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000168_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000168_network.cockroach.up.sql new file mode 100644 index 0000000..aa89dc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000168_network.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_nid_fk_idx" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000168_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000168_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000168_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000168_network.sqlite3.up.sql new file mode 100644 index 0000000..12eaae1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000168_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_nid_idx" ON "identity_verification_tokens" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000169_network.cockroach.down.sql b/persistence/sql/migrations/sql/20210410175418000169_network.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000169_network.cockroach.up.sql b/persistence/sql/migrations/sql/20210410175418000169_network.cockroach.up.sql new file mode 100644 index 0000000..8dbe054 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000169_network.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "sessions" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000169_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000169_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000169_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000169_network.sqlite3.up.sql new file mode 100644 index 0000000..ae605c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000169_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "nid" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000170_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000170_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000170_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000170_network.sqlite3.up.sql new file mode 100644 index 0000000..681db05 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000170_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE sessions DROP COLUMN nid; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000171_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000171_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000171_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000171_network.sqlite3.up.sql new file mode 100644 index 0000000..f80db30 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000171_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE sessions ADD COLUMN nid CHAR(36) NULL REFERENCES networks(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000172_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000172_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000172_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000172_network.sqlite3.up.sql new file mode 100644 index 0000000..755182f --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000172_network.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET nid = (SELECT id FROM networks LIMIT 1); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000173_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000173_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000173_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000173_network.sqlite3.up.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000173_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000174_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000174_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000174_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000174_network.sqlite3.up.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000174_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000175_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000175_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000175_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000175_network.sqlite3.up.sql new file mode 100644 index 0000000..66b3ae2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000175_network.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000176_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000176_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000176_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000176_network.sqlite3.up.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000176_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000177_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000177_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000177_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000177_network.sqlite3.up.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000177_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000178_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000178_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000178_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000178_network.sqlite3.up.sql new file mode 100644 index 0000000..9860c0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000178_network.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000179_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000179_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000179_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000179_network.sqlite3.up.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000179_network.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000180_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000180_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000180_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000180_network.sqlite3.up.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000180_network.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210410175418000181_network.sqlite3.down.sql b/persistence/sql/migrations/sql/20210410175418000181_network.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210410175418000181_network.sqlite3.up.sql b/persistence/sql/migrations/sql/20210410175418000181_network.sqlite3.up.sql new file mode 100644 index 0000000..8dbe054 --- /dev/null +++ b/persistence/sql/migrations/sql/20210410175418000181_network.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "sessions" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql new file mode 100644 index 0000000..f75038c --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "state_changed_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql new file mode 100644 index 0000000..6915848 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'active'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql new file mode 100644 index 0000000..d3e77b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` DROP COLUMN `state_changed_at`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql new file mode 100644 index 0000000..931022d --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'active'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql new file mode 100644 index 0000000..f75038c --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "state_changed_at"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql new file mode 100644 index 0000000..6915848 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'active'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..09c5e50 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identities_tmp" RENAME TO "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..4eb6d51 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'active'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql new file mode 100644 index 0000000..e49529c --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql new file mode 100644 index 0000000..31d1e71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql new file mode 100644 index 0000000..8e13fbe --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` DROP COLUMN `state`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql new file mode 100644 index 0000000..ba1d26c --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` ADD COLUMN `state_changed_at` DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql new file mode 100644 index 0000000..e49529c --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" DROP COLUMN "state"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql new file mode 100644 index 0000000..31d1e71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..10597b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..7b0b440 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" ADD COLUMN "state_changed_at" DATETIME; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..bd5056d --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid) SELECT id, schema_id, traits, created_at, updated_at, nid FROM "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..044042e --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..82c6236 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_identities_tmp" ( +"id" TEXT PRIMARY KEY, +"schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..295533e --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identities_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..09c5e50 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identities_tmp" RENAME TO "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..10597b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..1710145 --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid, state_changed_at) SELECT id, schema_id, traits, created_at, updated_at, nid, state_changed_at FROM "identities"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..044042e --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..0a7fdbe --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_identities_tmp" ( +"id" TEXT PRIMARY KEY, +"schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"state_changed_at" DATETIME +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql new file mode 100644 index 0000000..295533e --- /dev/null +++ b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identities_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.down.sql new file mode 100644 index 0000000..34d8132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "logout_token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.up.sql new file mode 100644 index 0000000..38fadf2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "logout_token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.down.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.down.sql new file mode 100644 index 0000000..dd7865f --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `logout_token`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.up.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.up.sql new file mode 100644 index 0000000..3538140 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `logout_token` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.down.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.down.sql new file mode 100644 index 0000000..34d8132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "logout_token"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.up.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.up.sql new file mode 100644 index 0000000..38fadf2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "logout_token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..a05b7ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000000_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "logout_token" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.cockroach.up.sql new file mode 100644 index 0000000..9f4ddd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000001_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET logout_token = token; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.mysql.down.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.mysql.up.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.mysql.up.sql new file mode 100644 index 0000000..9f4ddd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000001_logout_token.mysql.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET logout_token = token; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.postgres.down.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.postgres.up.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.postgres.up.sql new file mode 100644 index 0000000..9f4ddd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000001_logout_token.postgres.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET logout_token = token; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..56b4a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..9f4ddd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000001_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET logout_token = token; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.cockroach.up.sql new file mode 100644 index 0000000..a485b77 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000002_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "logout_token" TO "_logout_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.mysql.down.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.mysql.up.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.mysql.up.sql new file mode 100644 index 0000000..9182ea1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000002_logout_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `logout_token` VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.postgres.down.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.postgres.up.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.postgres.up.sql new file mode 100644 index 0000000..697e4d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000002_logout_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "logout_token" TYPE VARCHAR (32), ALTER COLUMN "logout_token" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..9860c0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000002_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.cockroach.up.sql new file mode 100644 index 0000000..38fadf2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000003_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "logout_token" VARCHAR (32); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.mysql.down.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.mysql.up.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.mysql.up.sql new file mode 100644 index 0000000..badf0e8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000003_logout_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `sessions_logout_token_uq_idx` ON `sessions` (`logout_token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.postgres.down.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.postgres.up.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.postgres.up.sql new file mode 100644 index 0000000..2918b1e --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000003_logout_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000003_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.cockroach.up.sql new file mode 100644 index 0000000..56c409b --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000004_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "logout_token" = "_logout_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.mysql.down.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.mysql.up.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.mysql.up.sql new file mode 100644 index 0000000..ecf4549 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000004_logout_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `sessions_logout_token_idx` ON `sessions` (`logout_token`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.postgres.down.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.postgres.up.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.postgres.up.sql new file mode 100644 index 0000000..04e5e44 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000004_logout_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000004_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000005_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000005_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000005_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000005_logout_token.cockroach.up.sql new file mode 100644 index 0000000..fa2bf69 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000005_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_logout_token_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..f3d1eae --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000005_logout_token.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +"logout_token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000006_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000006_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000006_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000006_logout_token.cockroach.up.sql new file mode 100644 index 0000000..2918b1e --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000006_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..0bba8e7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000006_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000007_logout_token.cockroach.down.sql b/persistence/sql/migrations/sql/20210618103120000007_logout_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000007_logout_token.cockroach.up.sql b/persistence/sql/migrations/sql/20210618103120000007_logout_token.cockroach.up.sql new file mode 100644 index 0000000..04e5e44 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000007_logout_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000007_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000008_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..914bfa3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000009_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..f0384e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000010_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..4b4cc12 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000011_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000012_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000012_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000012_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000012_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..2918b1e --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000012_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210618103120000013_logout_token.sqlite3.down.sql b/persistence/sql/migrations/sql/20210618103120000013_logout_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210618103120000013_logout_token.sqlite3.up.sql b/persistence/sql/migrations/sql/20210618103120000013_logout_token.sqlite3.up.sql new file mode 100644 index 0000000..04e5e44 --- /dev/null +++ b/persistence/sql/migrations/sql/20210618103120000013_logout_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "sessions" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..6d19c19 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..901d07e --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.down.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.down.sql new file mode 100644 index 0000000..64f9c3c --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` DROP COLUMN `internal_context`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.up.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.up.sql new file mode 100644 index 0000000..a5aafb8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` ADD COLUMN `internal_context` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.down.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.down.sql new file mode 100644 index 0000000..6d19c19 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.up.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.up.sql new file mode 100644 index 0000000..71c4dd0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "internal_context" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..6779322 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000000_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "internal_context" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..88a767d --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.mysql.down.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.mysql.up.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.mysql.up.sql new file mode 100644 index 0000000..88a767d --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.postgres.down.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.postgres.up.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.postgres.up.sql new file mode 100644 index 0000000..88a767d --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..b67a469 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..88a767d --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000001_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_settings_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..f2f4b88 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME COLUMN "internal_context" TO "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.mysql.down.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.mysql.up.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.mysql.up.sql new file mode 100644 index 0000000..2e171f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` MODIFY `internal_context` JSON NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.postgres.down.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.postgres.up.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.postgres.up.sql new file mode 100644 index 0000000..379e958 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ALTER COLUMN "internal_context" TYPE jsonb, ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..7327300 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..fb35cc6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000002_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_settings_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..901d07e --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..79acc29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_settings_flows_nid_idx" ON "_selfservice_settings_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..77558e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000003_settings_flow_context.sqlite3.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +"internal_context" TEXT NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..789981d --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_settings_flows" SET "internal_context" = "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..28bdcff --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_settings_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..79acc29 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000004_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_settings_flows_nid_idx" ON "_selfservice_settings_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..fad02ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..fb35cc6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_settings_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..551aee4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000005_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_flows_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid, internal_context) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, state, type, ui, nid, internal_context FROM "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.cockroach.up.sql new file mode 100644 index 0000000..bf0fad6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" DROP COLUMN "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..b76f8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000006_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805112414000007_settings_flow_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805112414000007_settings_flow_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210805112414000007_settings_flow_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805112414000007_settings_flow_context.sqlite3.up.sql new file mode 100644 index 0000000..f4db776 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805112414000007_settings_flow_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_flows_tmp" RENAME TO "selfservice_settings_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.down.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.down.sql new file mode 100644 index 0000000..60474c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'totp'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.up.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.up.sql new file mode 100644 index 0000000..9d37f5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '5e29b036-aa47-457f-9fe6-aa8b854a752b', 'totp' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'totp'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.down.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.down.sql new file mode 100644 index 0000000..60474c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'totp'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.up.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.up.sql new file mode 100644 index 0000000..9d37f5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '5e29b036-aa47-457f-9fe6-aa8b854a752b', 'totp' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'totp'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.down.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.down.sql new file mode 100644 index 0000000..60474c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'totp'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.up.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.up.sql new file mode 100644 index 0000000..9d37f5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '5e29b036-aa47-457f-9fe6-aa8b854a752b', 'totp' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'totp'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.down.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.down.sql new file mode 100644 index 0000000..60474c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'totp'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.up.sql b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.up.sql new file mode 100644 index 0000000..9d37f5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210805122535000000_credential_types_totp.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '5e29b036-aa47-457f-9fe6-aa8b854a752b', 'totp' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'totp'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.down.sql new file mode 100644 index 0000000..605064f --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "requested_aal"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.up.sql new file mode 100644 index 0000000..61aa5d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "aal" VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.down.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.down.sql new file mode 100644 index 0000000..fba0893 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `requested_aal`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.up.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.up.sql new file mode 100644 index 0000000..9857751 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `aal` VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.down.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.down.sql new file mode 100644 index 0000000..605064f --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "requested_aal"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.up.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.up.sql new file mode 100644 index 0000000..61aa5d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "aal" VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.down.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.up.sql new file mode 100644 index 0000000..86f7fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000000_aal.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "aal" TEXT NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.down.sql new file mode 100644 index 0000000..426467e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "aal"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.up.sql new file mode 100644 index 0000000..b8be1eb --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "authentication_methods" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.down.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.down.sql new file mode 100644 index 0000000..60b3eea --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `aal`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.up.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.up.sql new file mode 100644 index 0000000..6477fe6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `authentication_methods` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.down.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.down.sql new file mode 100644 index 0000000..426467e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "aal"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.up.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.up.sql new file mode 100644 index 0000000..6d42a55 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "authentication_methods" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.down.sql new file mode 100644 index 0000000..7524b95 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.up.sql new file mode 100644 index 0000000..f18ed89 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000001_aal.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "authentication_methods" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.down.sql new file mode 100644 index 0000000..410f624 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "authentication_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.up.sql new file mode 100644 index 0000000..f55e050 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.down.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.down.sql new file mode 100644 index 0000000..4c82058 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `authentication_methods`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.up.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.up.sql new file mode 100644 index 0000000..f55e050 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.mysql.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.down.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.down.sql new file mode 100644 index 0000000..410f624 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "authentication_methods"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.up.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.up.sql new file mode 100644 index 0000000..f55e050 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.postgres.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.down.sql new file mode 100644 index 0000000..f7a306d --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.up.sql new file mode 100644 index 0000000..f55e050 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000002_aal.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.cockroach.up.sql new file mode 100644 index 0000000..beafed7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000003_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "authentication_methods" TO "_authentication_methods_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.mysql.down.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.mysql.up.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.mysql.up.sql new file mode 100644 index 0000000..843aac9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000003_aal.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `authentication_methods` JSON NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.postgres.down.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.postgres.up.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.postgres.up.sql new file mode 100644 index 0000000..b176b66 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000003_aal.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "authentication_methods" TYPE jsonb, ALTER COLUMN "authentication_methods" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.down.sql new file mode 100644 index 0000000..417cce4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "_selfservice_login_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.up.sql new file mode 100644 index 0000000..4b4cc12 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000003_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.cockroach.up.sql new file mode 100644 index 0000000..b8be1eb --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000004_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "authentication_methods" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.mysql.down.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.mysql.up.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.mysql.up.sql new file mode 100644 index 0000000..4350a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000004_aal.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `requested_aal` VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.postgres.down.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.postgres.up.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.postgres.up.sql new file mode 100644 index 0000000..66b6f58 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000004_aal.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "requested_aal" VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.down.sql new file mode 100644 index 0000000..dd6709e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.up.sql new file mode 100644 index 0000000..f0384e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000004_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000005_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000005_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000005_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000005_aal.cockroach.up.sql new file mode 100644 index 0000000..20e4f77 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000005_aal.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "authentication_methods" = "_authentication_methods_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.down.sql new file mode 100644 index 0000000..33d0fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.up.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000005_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000006_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000006_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000006_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000006_aal.cockroach.up.sql new file mode 100644 index 0000000..e492ca5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000006_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "authentication_methods" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.up.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000006_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000007_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000007_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000007_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000007_aal.cockroach.up.sql new file mode 100644 index 0000000..383b8c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000007_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_authentication_methods_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.down.sql new file mode 100644 index 0000000..56b4a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.up.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000007_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000008_aal.cockroach.down.sql b/persistence/sql/migrations/sql/20210810153530000008_aal.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000008_aal.cockroach.up.sql b/persistence/sql/migrations/sql/20210810153530000008_aal.cockroach.up.sql new file mode 100644 index 0000000..66b6f58 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000008_aal.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "requested_aal" VARCHAR (4) NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.down.sql new file mode 100644 index 0000000..914bfa3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.up.sql new file mode 100644 index 0000000..26b5b01 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000008_aal.sqlite3.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +"logout_token" TEXT, +"aal" TEXT NOT NULL DEFAULT 'aal1', +"authentication_methods" TEXT NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.down.sql new file mode 100644 index 0000000..ac4e679 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.up.sql new file mode 100644 index 0000000..ac4e679 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000009_aal.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.down.sql new file mode 100644 index 0000000..9bc1d9e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.up.sql new file mode 100644 index 0000000..9bc1d9e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000010_aal.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.down.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.up.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000011_aal.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.down.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.up.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000012_aal.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.down.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.up.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000013_aal.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.down.sql new file mode 100644 index 0000000..57be4ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +"logout_token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.up.sql new file mode 100644 index 0000000..95f7f7d --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000014_aal.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token, aal, authentication_methods) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token, aal, authentication_methods FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.down.sql new file mode 100644 index 0000000..4b4cc12 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.up.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000015_aal.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.down.sql new file mode 100644 index 0000000..f0384e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.up.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000016_aal.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.down.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.up.sql new file mode 100644 index 0000000..ce93eaa --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000017_aal.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "requested_aal" TEXT NOT NULL DEFAULT 'aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000018_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000018_aal.sqlite3.down.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000018_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000018_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000018_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000019_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000019_aal.sqlite3.down.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000019_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000019_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000019_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000020_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000020_aal.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000020_aal.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000020_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000020_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000021_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000021_aal.sqlite3.down.sql new file mode 100644 index 0000000..56b4a31 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000021_aal.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000021_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000021_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000022_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000022_aal.sqlite3.down.sql new file mode 100644 index 0000000..e8f54c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000022_aal.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token, aal) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token, active, nid, logout_token, aal FROM "sessions"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000022_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000022_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000023_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000023_aal.sqlite3.down.sql new file mode 100644 index 0000000..ac4e679 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000023_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_logout_token_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000023_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000023_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000024_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000024_aal.sqlite3.down.sql new file mode 100644 index 0000000..9bc1d9e --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000024_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_logout_token_uq_idx" ON "_sessions_tmp" (logout_token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000024_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000024_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000025_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000025_aal.sqlite3.down.sql new file mode 100644 index 0000000..c7d1a27 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000025_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000025_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000025_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000026_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000026_aal.sqlite3.down.sql new file mode 100644 index 0000000..be0b1f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000026_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000026_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000026_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000027_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000027_aal.sqlite3.down.sql new file mode 100644 index 0000000..f9abd73 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000027_aal.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_nid_idx" ON "_sessions_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000027_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000027_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000028_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000028_aal.sqlite3.down.sql new file mode 100644 index 0000000..4674e18 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000028_aal.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +"active" NUMERIC DEFAULT 'false', +"nid" char(36), +"logout_token" TEXT, +"aal" TEXT NOT NULL DEFAULT 'aal1', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000028_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000028_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000029_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000029_aal.sqlite3.down.sql new file mode 100644 index 0000000..4b4cc12 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000029_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000029_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000029_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000030_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000030_aal.sqlite3.down.sql new file mode 100644 index 0000000..f0384e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000030_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_logout_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000030_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000030_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000031_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000031_aal.sqlite3.down.sql new file mode 100644 index 0000000..8111de7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000031_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000031_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000031_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000032_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000032_aal.sqlite3.down.sql new file mode 100644 index 0000000..481ab0a --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000032_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000032_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000032_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210810153530000033_aal.sqlite3.down.sql b/persistence/sql/migrations/sql/20210810153530000033_aal.sqlite3.down.sql new file mode 100644 index 0000000..caef0de --- /dev/null +++ b/persistence/sql/migrations/sql/20210810153530000033_aal.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210810153530000033_aal.sqlite3.up.sql b/persistence/sql/migrations/sql/20210810153530000033_aal.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.down.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.down.sql new file mode 100644 index 0000000..18c7e85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'lookup_secret'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.up.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.up.sql new file mode 100644 index 0000000..4cab89a --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '567a0730-7f48-4dd7-a13d-df87a51c245f', 'lookup_secret' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'lookup_secret'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.down.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.down.sql new file mode 100644 index 0000000..18c7e85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'lookup_secret'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.up.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.up.sql new file mode 100644 index 0000000..4cab89a --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '567a0730-7f48-4dd7-a13d-df87a51c245f', 'lookup_secret' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'lookup_secret'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.down.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.down.sql new file mode 100644 index 0000000..18c7e85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'lookup_secret'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.up.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.up.sql new file mode 100644 index 0000000..4cab89a --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '567a0730-7f48-4dd7-a13d-df87a51c245f', 'lookup_secret' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'lookup_secret'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.down.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.down.sql new file mode 100644 index 0000000..18c7e85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'lookup_secret'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.up.sql b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.up.sql new file mode 100644 index 0000000..4cab89a --- /dev/null +++ b/persistence/sql/migrations/sql/20210813150152000000_credential_types_lookup.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '567a0730-7f48-4dd7-a13d-df87a51c245f', 'lookup_secret' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'lookup_secret'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.down.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.down.sql new file mode 100644 index 0000000..bae0510 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'webauthn'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.up.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.up.sql new file mode 100644 index 0000000..291c470 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6b213fa0-e6ad-46cb-8878-b088d2ce2e3c', 'webauthn' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'webauthn'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.down.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.down.sql new file mode 100644 index 0000000..bae0510 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'webauthn'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.up.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.up.sql new file mode 100644 index 0000000..291c470 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6b213fa0-e6ad-46cb-8878-b088d2ce2e3c', 'webauthn' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'webauthn'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.down.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.down.sql new file mode 100644 index 0000000..bae0510 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'webauthn'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.up.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.up.sql new file mode 100644 index 0000000..291c470 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6b213fa0-e6ad-46cb-8878-b088d2ce2e3c', 'webauthn' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'webauthn'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.down.sql new file mode 100644 index 0000000..bae0510 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'webauthn'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.up.sql new file mode 100644 index 0000000..291c470 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816113956000000_webauthn.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6b213fa0-e6ad-46cb-8878-b088d2ce2e3c', 'webauthn' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'webauthn'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..7d21dce --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..f9df566 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..aa48ec0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` DROP COLUMN `internal_context`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..97c68bb --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `internal_context` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..7d21dce --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..2152452 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "internal_context" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..bae7310 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000000_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "internal_context" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..f0a8f98 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..18d851a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..c48c2f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `internal_context`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..18d851a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..f0a8f98 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "internal_context"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..18d851a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..bcd63c4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..18d851a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000001_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_login_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..4072ee7 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME COLUMN "internal_context" TO "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..43e395a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` MODIFY `internal_context` JSON NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..0dd7bc8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ALTER COLUMN "internal_context" TYPE jsonb, ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..743a30c --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..33d0fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000002_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..f9df566 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..b492ac9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` ADD COLUMN `internal_context` JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..f2de3b8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "internal_context" jsonb; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..ea7d0bf --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_registration_flows_nid_idx" ON "_selfservice_registration_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..2826dd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000003_flow_internal_context.sqlite3.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +"requested_aal" TEXT NOT NULL DEFAULT 'aal1', +"internal_context" TEXT NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..3d82da8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_login_flows" SET "internal_context" = "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..aa9060c --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..aa9060c --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..8168fa5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36) +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..417cce4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000004_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "_selfservice_login_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..c130266 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.mysql.down.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.mysql.up.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.mysql.up.sql new file mode 100644 index 0000000..550504a --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` MODIFY `internal_context` JSON NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.postgres.down.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.postgres.up.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.postgres.up.sql new file mode 100644 index 0000000..351ffcf --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ALTER COLUMN "internal_context" TYPE jsonb, ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..44c4132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_registration_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..f9444fa --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000005_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid, requested_aal, internal_context) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid, requested_aal, internal_context FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..b31869d --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..7b62a8e --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000006_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..e8bd651 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..7524b95 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..ee6e078 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000007_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_flows_tmp" RENAME TO "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..aa9060c --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..3ec24b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid, requested_aal) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, type, ui, nid, requested_aal FROM "selfservice_login_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..090b7b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000008_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "internal_context" TEXT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..74606ad --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME COLUMN "internal_context" TO "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..417cce4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_login_flows_nid_idx" ON "_selfservice_login_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..aa9060c --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000009_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_registration_flows SET internal_context='{}'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..e8bd651 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "internal_context" json; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..2ae4a01 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_login_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +"requested_aal" TEXT NOT NULL DEFAULT 'aal1' +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..44c4132 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000010_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_registration_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..7f02019 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_registration_flows" SET "internal_context" = "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..33d0fe5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "selfservice_login_flows_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..f3315a9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000011_flow_internal_context.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_registration_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"type" TEXT NOT NULL DEFAULT 'browser', +"ui" TEXT, +"nid" char(36), +"internal_context" TEXT NOT NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..24cdced --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" ALTER COLUMN "internal_context" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..ea7d0bf --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000012_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "selfservice_registration_flows_nid_idx" ON "_selfservice_registration_flows_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.cockroach.down.sql b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.cockroach.up.sql b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.cockroach.up.sql new file mode 100644 index 0000000..bf6be85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "_internal_context_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..077b7c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000013_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_flows_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid, internal_context) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, type, ui, nid, internal_context FROM "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000014_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000014_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000014_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000014_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..d8b935d --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000014_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210816142650000015_flow_internal_context.sqlite3.down.sql b/persistence/sql/migrations/sql/20210816142650000015_flow_internal_context.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210816142650000015_flow_internal_context.sqlite3.up.sql b/persistence/sql/migrations/sql/20210816142650000015_flow_internal_context.sqlite3.up.sql new file mode 100644 index 0000000..ef61789 --- /dev/null +++ b/persistence/sql/migrations/sql/20210816142650000015_flow_internal_context.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_flows_tmp" RENAME TO "selfservice_registration_flows"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..b4bac71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..933660a --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_nid_uq_idx` ON `identity_credential_identifiers` (`nid`, `identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..a9eea55 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers DROP FOREIGN KEY identity_credential_identifiers_nid_fk_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..d1ef620 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..ebb42cf --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_uq_idx" ON "identity_credential_identifiers" (nid, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..b4bac71 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000000_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..188214b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "identity_credential_type_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..1cab0e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "identity_credential_type_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..a2a0863 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` DROP COLUMN `identity_credential_type_id`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..00c56ec --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credential_identifiers_identifier_nid_uq_idx` ON `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..188214b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "identity_credential_type_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..1cab0e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "identity_credential_type_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..4c12302 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..2cf8867 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000001_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "identity_credential_type_id" char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..f041994 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_type_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..2da79ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_type_id_fk_idx" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..e080343 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD CONSTRAINT `identity_credential_identifiers_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..e080343 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD CONSTRAINT `identity_credential_identifiers_nid_fk_idx` FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..2dac340 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..2da79ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_type_id_fk_idx" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..c31ee07 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..7f096e0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000002_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers DROP COLUMN identity_credential_type_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..4c0856b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET identity_credential_type_id = (SELECT ict.id FROM identity_credential_types as ict JOIN identity_credentials AS ic ON (ic.identity_credential_type_id = ict.id) WHERE ic.id = identity_credential_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..ed1679e --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX `identity_credential_identifiers_identifier_nid_type_uq_idx` ON `identity_credential_identifiers`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..29e9e5c --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD COLUMN `identity_credential_type_id` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..4c0856b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET identity_credential_type_id = (SELECT ict.id FROM identity_credential_types as ict JOIN identity_credentials AS ic ON (ic.identity_credential_type_id = ict.id) WHERE ic.id = identity_credential_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..00bfb99 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credential_identifiers_tmp" (id, identifier, identity_credential_id, created_at, updated_at, nid) SELECT id, identifier, identity_credential_id, created_at, updated_at, nid FROM "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..7d1d965 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000003_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers ADD COLUMN identity_credential_type_id CHAR(36) NULL REFERENCES identity_credential_types(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..95d8c2f --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP CONSTRAINT "identity_credential_identifiers_type_id_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..8f5313a --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers DROP FOREIGN KEY identity_credential_identifiers_type_id_fk_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..1ec228e --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` ADD CONSTRAINT `identity_credential_identifiers_type_id_fk_idx` FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..4f843d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ALTER COLUMN "identity_credential_type_id" TYPE UUID, ALTER COLUMN "identity_credential_type_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..fb7dd42 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_idx" ON "_identity_credential_identifiers_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..4c0856b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000004_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET identity_credential_type_id = (SELECT ict.id FROM identity_credential_types as ict JOIN identity_credentials AS ic ON (ic.identity_credential_type_id = ict.id) WHERE ic.id = identity_credential_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..3b17e84 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" RENAME COLUMN "identity_credential_type_id" TO "_identity_credential_type_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..a9eea55 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers DROP FOREIGN KEY identity_credential_identifiers_nid_fk_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..4c0856b --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_credential_identifiers SET identity_credential_type_id = (SELECT ict.id FROM identity_credential_types as ict JOIN identity_credentials AS ic ON (ic.identity_credential_type_id = ict.id) WHERE ic.id = identity_credential_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.postgres.down.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.postgres.up.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.postgres.up.sql new file mode 100644 index 0000000..de3b6e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx" ON "identity_credential_identifiers" (nid, identity_credential_type_id, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..dc27ecf --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..be327ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000005_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..1cab0e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD COLUMN "identity_credential_type_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..1322e7a --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_credential_identifiers` MODIFY `identity_credential_type_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..be327ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..2da65d9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000006_unique_credentials.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"identity_credential_type_id" char(36) NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..58af9f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_credential_identifiers" SET "identity_credential_type_id" = "_identity_credential_type_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.mysql.up.sql new file mode 100644 index 0000000..a9701a8 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_nid_type_uq_idx` ON `identity_credential_identifiers` (`nid`, `identity_credential_type_id`, `identifier`); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..f041994 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_identifier_nid_type_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..fb7dd42 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000007_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_idx" ON "_identity_credential_identifiers_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..fb8ba1c --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ALTER COLUMN "identity_credential_type_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..aa45cb4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000008_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_credential_identifiers_tmp" (id, identifier, identity_credential_id, created_at, updated_at, nid, identity_credential_type_id) SELECT id, identifier, identity_credential_id, created_at, updated_at, nid, identity_credential_type_id FROM "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..5fb3a2a --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" DROP COLUMN "_identity_credential_type_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..523de38 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000009_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..2da79ce --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_credential_identifiers" ADD CONSTRAINT "identity_credential_identifiers_type_id_fk_idx" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..4c12302 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000010_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.cockroach.down.sql b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.cockroach.up.sql b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.cockroach.up.sql new file mode 100644 index 0000000..de3b6e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx" ON "identity_credential_identifiers" (nid, identity_credential_type_id, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.sqlite3.down.sql b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.sqlite3.up.sql b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.sqlite3.up.sql new file mode 100644 index 0000000..de3b6e6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210817181232000011_unique_credentials.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx" ON "identity_credential_identifiers" (nid, identity_credential_type_id, identifier); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.down.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.down.sql new file mode 100644 index 0000000..61c557d --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]' WHERE authentication_methods='[{"method":"v0.6_legacy_session"}]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.up.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.up.sql new file mode 100644 index 0000000..9c62b85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[{"method":"v0.6_legacy_session"}]' WHERE authentication_methods='[]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.down.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.down.sql new file mode 100644 index 0000000..61c557d --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.down.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]' WHERE authentication_methods='[{"method":"v0.6_legacy_session"}]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.up.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.up.sql new file mode 100644 index 0000000..adb529e --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.mysql.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[{"method":"v0.6_legacy_session"}]' WHERE JSON_LENGTH(authentication_methods)=0 AND aal='aal1'; diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.down.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.down.sql new file mode 100644 index 0000000..61c557d --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.down.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]' WHERE authentication_methods='[{"method":"v0.6_legacy_session"}]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.up.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.up.sql new file mode 100644 index 0000000..9c62b85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.postgres.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[{"method":"v0.6_legacy_session"}]' WHERE authentication_methods='[]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.down.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.down.sql new file mode 100644 index 0000000..61c557d --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[]' WHERE authentication_methods='[{"method":"v0.6_legacy_session"}]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.up.sql b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.up.sql new file mode 100644 index 0000000..9c62b85 --- /dev/null +++ b/persistence/sql/migrations/sql/20210829131458000000_session_aal_legacy.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE sessions SET authentication_methods='[{"method":"v0.6_legacy_session"}]' WHERE authentication_methods='[]' AND aal='aal1'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..cedce35 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "identity_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..575a9ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.down.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.down.sql new file mode 100644 index 0000000..81cbf64 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `identity_id`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.up.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.up.sql new file mode 100644 index 0000000..462174b --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `identity_recovery_address_id` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.down.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.down.sql new file mode 100644 index 0000000..cedce35 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "identity_id"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.up.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.up.sql new file mode 100644 index 0000000..df4509b --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "identity_recovery_address_id" TYPE UUID, ALTER COLUMN "identity_recovery_address_id" DROP NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000000_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..574e76e --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_identity_id_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..f87c725 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "identity_recovery_address_id" TO "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.down.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.down.sql new file mode 100644 index 0000000..d54e7d2 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP FOREIGN KEY `identity_recovery_tokens_identity_id_fk_idx`; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.up.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.up.sql new file mode 100644 index 0000000..ece7998 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `identity_id` char(36); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.down.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.down.sql new file mode 100644 index 0000000..574e76e --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_identity_id_fk_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.up.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.up.sql new file mode 100644 index 0000000..5cfffe9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "identity_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..40f1908 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000001_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..062bbc0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..87e9e3f --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "identity_recovery_address_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.down.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.down.sql new file mode 100644 index 0000000..46ed801 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `identity_recovery_address_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.up.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.up.sql new file mode 100644 index 0000000..48d951b --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.mysql.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET identity_id=(SELECT identity_id FROM identity_recovery_addresses WHERE id=identity_recovery_address_id) WHERE identity_id = '' OR identity_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.down.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.down.sql new file mode 100644 index 0000000..5fa2db4 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "identity_recovery_address_id" TYPE UUID, ALTER COLUMN "identity_recovery_address_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.up.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.up.sql new file mode 100644 index 0000000..20ba484 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.postgres.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET identity_id=(SELECT identity_id FROM identity_recovery_addresses WHERE id=identity_recovery_address_id) WHERE identity_id = '00000000-0000-0000-0000-000000000000' OR identity_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..c367dde --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000002_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..1acf8f5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..87706a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "identity_recovery_address_id" = "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.down.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.down.sql new file mode 100644 index 0000000..5e215d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE identity_recovery_address_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.up.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.up.sql new file mode 100644 index 0000000..c3a24ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `identity_id` char(36) NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.down.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.down.sql new file mode 100644 index 0000000..5e215d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE identity_recovery_address_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.up.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.up.sql new file mode 100644 index 0000000..de2bef9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "identity_id" TYPE UUID, ALTER COLUMN "identity_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..2adb379 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000003_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36), +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..ac2e738 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "identity_recovery_address_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..1acf8f5 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.mysql.down.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.mysql.up.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.mysql.up.sql new file mode 100644 index 0000000..e641159 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD CONSTRAINT `identity_recovery_tokens_identity_id_fk_idx` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.postgres.down.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.postgres.up.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.postgres.up.sql new file mode 100644 index 0000000..45a3a3a --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_identity_id_fk_idx" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000004_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..87706a1 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "identity_recovery_address_id" = "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..062bbc0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000005_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..87e9e3f --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "identity_recovery_address_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..5cfffe9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "identity_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..9a18fbd --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000006_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..f87c725 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "identity_recovery_address_id" TO "_identity_recovery_address_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..20ba484 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET identity_id=(SELECT identity_id FROM identity_recovery_addresses WHERE id=identity_recovery_address_id) WHERE identity_id = '00000000-0000-0000-0000-000000000000' OR identity_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..c367dde --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000007_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..575a9ea --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..35ed38e --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "identity_id" TO "_identity_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000008_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..5e215d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE identity_recovery_address_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..5cfffe9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "identity_id" UUID; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000009_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..1f600ca --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "identity_id" = "_identity_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..0e3e64d --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000010_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens ADD COLUMN identity_id CHAR(36) NULL REFERENCES identities(id) ON DELETE CASCADE ON UPDATE RESTRICT; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..998578b --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "identity_id" SET NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..7cda028 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..48d951b --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000011_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE identity_recovery_tokens SET identity_id=(SELECT identity_id FROM identity_recovery_addresses WHERE id=identity_recovery_address_id) WHERE identity_id = '' OR identity_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..7fa1e1c --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_identity_id_tmp"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..8789bba --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid, identity_id) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at, nid, identity_id FROM "identity_recovery_tokens"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..9835ff0 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000012_identity_recovery_tokens.sqlite3.up.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE identity_recovery_address_id IS NULL AND identity_id = ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.cockroach.down.sql b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.cockroach.up.sql b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.cockroach.up.sql new file mode 100644 index 0000000..45a3a3a --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_identity_id_fk_idx" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..95955ab --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_tokens_nid_idx" ON "_identity_recovery_tokens_tmp" (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000013_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000014_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000014_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..d266323 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000014_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000014_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000014_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000015_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000015_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..5da22e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000015_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000015_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000015_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000016_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000016_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..cb88582 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000016_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1,17 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"nid" char(36), +"identity_id" CHAR(36), +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE +); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000016_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000016_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000017_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000017_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..bd7b122 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000017_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_tokens_nid_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000017_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000017_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000018_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000018_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..fc7fe83 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000018_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000018_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000018_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000019_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000019_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..03159a3 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000019_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000019_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000019_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20210913095309000020_identity_recovery_tokens.sqlite3.down.sql b/persistence/sql/migrations/sql/20210913095309000020_identity_recovery_tokens.sqlite3.down.sql new file mode 100644 index 0000000..5e215d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20210913095309000020_identity_recovery_tokens.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE identity_recovery_address_id IS NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20210913095309000020_identity_recovery_tokens.sqlite3.up.sql b/persistence/sql/migrations/sql/20210913095309000020_identity_recovery_tokens.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.down.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.down.sql new file mode 100644 index 0000000..5ecab22 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.up.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.up.sql new file mode 100644 index 0000000..3b52eda --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_identity_id_idx" ON "identity_credentials" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.down.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.down.sql new file mode 100644 index 0000000..4bb7cb3 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.up.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.up.sql new file mode 100644 index 0000000..3b52eda --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_identity_id_idx" ON "identity_credentials" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.down.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.down.sql new file mode 100644 index 0000000..5ecab22 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.up.sql b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.up.sql new file mode 100644 index 0000000..3b52eda --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000000_identity_fk_indexes.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credentials_nid_identity_id_idx" ON "identity_credentials" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.down.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.down.sql new file mode 100644 index 0000000..09dc31b --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.up.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.up.sql new file mode 100644 index 0000000..643f2d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_identity_credential_id_idx" ON "identity_credential_identifiers" (identity_credential_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.down.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.down.sql new file mode 100644 index 0000000..00902ae --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_recovery_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.up.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.up.sql new file mode 100644 index 0000000..643f2d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_identity_credential_id_idx" ON "identity_credential_identifiers" (identity_credential_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.down.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.down.sql new file mode 100644 index 0000000..09dc31b --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.up.sql b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.up.sql new file mode 100644 index 0000000..643f2d8 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000001_identity_fk_indexes.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_credential_identifiers_nid_identity_credential_id_idx" ON "identity_credential_identifiers" (identity_credential_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.down.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.down.sql new file mode 100644 index 0000000..241057d --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_identity_credential_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.up.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.up.sql new file mode 100644 index 0000000..e26ce6a --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_identity_id_idx" ON "identity_recovery_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.down.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.down.sql new file mode 100644 index 0000000..8fa00c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credential_identifiers_nid_identity_credential_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.up.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.up.sql new file mode 100644 index 0000000..e26ce6a --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_identity_id_idx" ON "identity_recovery_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.down.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.down.sql new file mode 100644 index 0000000..241057d --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credential_identifiers_nid_identity_credential_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.up.sql b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.up.sql new file mode 100644 index 0000000..e26ce6a --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000002_identity_fk_indexes.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_nid_identity_id_idx" ON "identity_recovery_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.down.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.down.sql new file mode 100644 index 0000000..9ff1691 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credentials_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.up.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.up.sql new file mode 100644 index 0000000..5852ba0 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_identity_id_idx" ON "identity_verifiable_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.down.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.down.sql new file mode 100644 index 0000000..adf0de8 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX "identity_credentials_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.up.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.up.sql new file mode 100644 index 0000000..5852ba0 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_identity_id_idx" ON "identity_verifiable_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.down.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.down.sql new file mode 100644 index 0000000..9ff1691 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_credentials_nid_identity_id_idx"; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.up.sql b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.up.sql new file mode 100644 index 0000000..5852ba0 --- /dev/null +++ b/persistence/sql/migrations/sql/20220118104539000003_identity_fk_indexes.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_nid_identity_id_idx" ON "identity_verifiable_addresses" (identity_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.cockroach.down.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.cockroach.up.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.cockroach.up.sql new file mode 100644 index 0000000..bf64250 --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials ADD version INT NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.mysql.down.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.mysql.up.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.mysql.up.sql new file mode 100644 index 0000000..bf64250 --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials ADD version INT NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.postgres.down.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.postgres.up.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.postgres.up.sql new file mode 100644 index 0000000..bf64250 --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials ADD version INT NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.sqlite3.down.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.sqlite3.up.sql b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.sqlite3.up.sql new file mode 100644 index 0000000..bf64250 --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000000_identity_credentials_version.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials ADD version INT NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.cockroach.down.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.cockroach.down.sql new file mode 100644 index 0000000..a117f3d --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials DROP COLUMN version; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.cockroach.up.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.mysql.down.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.mysql.down.sql new file mode 100644 index 0000000..a117f3d --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials DROP COLUMN version; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.mysql.up.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.postgres.down.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.postgres.down.sql new file mode 100644 index 0000000..a117f3d --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials DROP COLUMN version; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.postgres.up.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.sqlite3.down.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.sqlite3.down.sql new file mode 100644 index 0000000..a117f3d --- /dev/null +++ b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credentials DROP COLUMN version; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.sqlite3.up.sql b/persistence/sql/migrations/sql/20220301102701000001_identity_credentials_version.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.down.sql b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.down.sql new file mode 100644 index 0000000..19c17d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE identities DROP COLUMN metadata_public; +ALTER TABLE identities DROP COLUMN metadata_admin; diff --git a/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.mysql.up.sql b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.mysql.up.sql new file mode 100644 index 0000000..e50ac1b --- /dev/null +++ b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identities ADD metadata_public JSON NULL; +ALTER TABLE identities ADD metadata_admin JSON NULL; diff --git a/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.up.sql b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.up.sql new file mode 100644 index 0000000..880a363 --- /dev/null +++ b/persistence/sql/migrations/sql/20220420102701000000_identity_metadata.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identities ADD metadata_public jsonb NULL; +ALTER TABLE identities ADD metadata_admin jsonb NULL; diff --git a/persistence/sql/migrations/sql/20220512102703000000_missing_indices.down.sql b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.down.sql new file mode 100644 index 0000000..3c2c6de --- /dev/null +++ b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.down.sql @@ -0,0 +1,64 @@ +CREATE INDEX sessions_nid_idx ON sessions (id, nid); +CREATE INDEX sessions_token_idx ON sessions (token); +CREATE INDEX sessions_logout_token_idx ON sessions (logout_token); + +CREATE INDEX identities_nid_idx ON identities (id, nid); + +CREATE INDEX continuity_containers_nid_idx ON continuity_containers (id, nid); + +CREATE INDEX courier_messages_nid_idx ON courier_messages (id, nid); + +CREATE INDEX identity_credential_identifiers_nid_idx ON identity_credential_identifiers (id, nid); + +CREATE INDEX identity_credentials_nid_idx ON identity_credentials (id, nid); + +CREATE INDEX identity_recovery_addresses_nid_idx ON identity_recovery_addresses (id, nid); + +CREATE INDEX identity_recovery_tokens_nid_idx ON identity_recovery_tokens (id, nid); +CREATE INDEX identity_recovery_addresses_code_idx ON identity_recovery_tokens (token); + +CREATE INDEX identity_verifiable_addresses_nid_idx ON identity_verifiable_addresses (id, nid); + +CREATE INDEX identity_verification_tokens_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_token_idx ON identity_verification_tokens (token); + +CREATE INDEX selfservice_login_flows_nid_idx ON selfservice_login_flows (id,nid); +CREATE INDEX selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows (id,nid); +CREATE INDEX selfservice_registration_flows_nid_idx ON selfservice_registration_flows (id,nid); +CREATE INDEX selfservice_settings_flows_nid_idx ON selfservice_settings_flows (id,nid); +CREATE INDEX selfservice_verification_flows_nid_idx ON selfservice_verification_flows (id,nid); + +DROP INDEX sessions_identity_id_nid_idx; +DROP INDEX sessions_nid_id_identity_id_idx; +DROP INDEX sessions_id_nid_idx; +DROP INDEX sessions_token_nid_idx; + +DROP INDEX identities_id_nid_idx; +DROP INDEX identities_nid_id_idx; +DROP INDEX continuity_containers_nid_id_idx; +DROP INDEX continuity_containers_id_nid_idx; +DROP INDEX courier_messages_nid_id_idx; +DROP INDEX courier_messages_id_nid_idx; +DROP INDEX identity_credential_identifiers_nid_id_idx; +DROP INDEX identity_credential_identifiers_id_nid_idx; +DROP INDEX identity_credentials_nid_id_idx; +DROP INDEX identity_credentials_id_nid_idx; +DROP INDEX identity_recovery_addresses_nid_id_idx; +DROP INDEX identity_recovery_addresses_id_nid_idx; +DROP INDEX identity_recovery_tokens_nid_id_idx; +DROP INDEX identity_recovery_tokens_id_nid_idx; +DROP INDEX identity_recovery_tokens_selfservice_recovery_flow_id_idx; +DROP INDEX identity_recovery_tokens_identity_recovery_address_id_idx; +DROP INDEX identity_verification_tokens_nid_id_idx; +DROP INDEX identity_verification_tokens_id_nid_idx; +DROP INDEX identity_verification_tokens_token_nid_used_idx; +DROP INDEX selfservice_login_flows_nid_id_idx; +DROP INDEX selfservice_login_flows_id_nid_idx; +DROP INDEX selfservice_recovery_flows_nid_id_idx; +DROP INDEX selfservice_recovery_flows_id_nid_idx; +DROP INDEX selfservice_registration_flows_nid_id_idx; +DROP INDEX selfservice_registration_flows_id_nid_idx; +DROP INDEX selfservice_settings_flows_nid_id_idx; +DROP INDEX selfservice_settings_flows_id_nid_idx; +DROP INDEX selfservice_verification_flows_nid_id_idx; +DROP INDEX selfservice_verification_flows_id_nid_idx; diff --git a/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.down.sql b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.down.sql new file mode 100644 index 0000000..a869c0d --- /dev/null +++ b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.down.sql @@ -0,0 +1,90 @@ +-- This file has a couple more indexes added which MySQL needs for its FK constraints. Other +-- databases generate those indices automatically. +CREATE INDEX sessions_nid_idx ON sessions (id, nid); +CREATE INDEX sessions_nid_mysqlfk_idx ON sessions (nid); + +CREATE INDEX sessions_token_idx ON sessions (token); +CREATE INDEX sessions_mysql_identity_id_idx ON sessions (identity_id); +CREATE INDEX sessions_logout_token_idx ON sessions (logout_token); + +CREATE INDEX identities_nid_idx ON identities (id, nid); +CREATE INDEX identities_nid_mysqlfk_idx ON identities (nid); + +CREATE INDEX continuity_containers_nid_idx ON continuity_containers (id, nid); +CREATE INDEX continuity_containers_mysqlfk_idx ON continuity_containers (nid); + +CREATE INDEX courier_messages_nid_idx ON courier_messages (id, nid); +CREATE INDEX courier_messages_mysqlfk_idx ON courier_messages (nid); + +CREATE INDEX identity_credential_identifiers_nid_idx ON identity_credential_identifiers (id, nid); +CREATE INDEX identity_credential_identifiers_mysqlfk_idx ON identity_credential_identifiers (nid); + +CREATE INDEX identity_credentials_nid_idx ON identity_credentials (id, nid); +CREATE INDEX identity_credentials_mysqlfk_idx ON identity_credentials (nid); + +CREATE INDEX identity_recovery_addresses_nid_idx ON identity_recovery_addresses (id, nid); +CREATE INDEX identity_recovery_addresses_nid_mysqlfk_idx ON identity_recovery_addresses (nid); + +CREATE INDEX identity_recovery_tokens_nid_idx ON identity_recovery_tokens (id, nid); +CREATE INDEX identity_recovery_tokens_nid_mysqlfk_idx ON identity_recovery_tokens (nid); +CREATE INDEX identity_recovery_addresses_code_idx ON identity_recovery_tokens (token); +CREATE INDEX identity_recovery_tokens_srf_id_mysqlfk_idx ON identity_recovery_tokens (selfservice_recovery_flow_id); +CREATE INDEX identity_recovery_tokens_ira_id_mysqlfk_idx ON identity_recovery_tokens (identity_recovery_address_id); + +CREATE INDEX identity_verifiable_addresses_nid_idx ON identity_verifiable_addresses (id, nid); +CREATE INDEX identity_verifiable_addresses_nid_mysqlfk_idx ON identity_verifiable_addresses (nid); + +CREATE INDEX identity_verification_tokens_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_nid_mysqlfk_idx ON identity_verification_tokens (nid); +CREATE INDEX identity_verification_tokens_token_idx ON identity_verification_tokens (token); + +CREATE INDEX selfservice_login_flows_nid_idx ON selfservice_login_flows (id, nid); +CREATE INDEX selfservice_login_flows_nid_mysqlfk_idx ON selfservice_login_flows (nid); + +CREATE INDEX selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows (id, nid); +CREATE INDEX selfservice_recovery_flows_nid_mysqlfk_idx ON selfservice_recovery_flows (nid); + +CREATE INDEX selfservice_registration_flows_nid_idx ON selfservice_registration_flows (id, nid); +CREATE INDEX selfservice_registration_flows_nid_mysqlfk_idx ON selfservice_registration_flows (nid); + +CREATE INDEX selfservice_settings_flows_nid_idx ON selfservice_settings_flows (id, nid); +CREATE INDEX selfservice_settings_flows_nid_mysqlfk_idx ON selfservice_settings_flows (nid); + +CREATE INDEX selfservice_verification_flows_nid_idx ON selfservice_verification_flows (id, nid); +CREATE INDEX selfservice_verification_flows_nid_mysqlfk_idx ON selfservice_verification_flows (nid); + + +DROP INDEX sessions_nid_id_identity_id_idx ON sessions; +DROP INDEX sessions_id_nid_idx ON sessions; +DROP INDEX sessions_token_nid_idx ON sessions; + +DROP INDEX sessions_identity_id_nid_idx ON sessions; +DROP INDEX identities_id_nid_idx ON identities; +DROP INDEX identities_nid_id_idx ON identities; +DROP INDEX continuity_containers_nid_id_idx ON continuity_containers; +DROP INDEX continuity_containers_id_nid_idx ON continuity_containers; +DROP INDEX courier_messages_nid_id_idx ON courier_messages; +DROP INDEX courier_messages_id_nid_idx ON courier_messages; +DROP INDEX identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers; +DROP INDEX identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers; +DROP INDEX identity_credentials_nid_id_idx ON identity_credentials; +DROP INDEX identity_credentials_id_nid_idx ON identity_credentials; +DROP INDEX identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses; +DROP INDEX identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses; +DROP INDEX identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_selfservice_recovery_flow_id_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_identity_recovery_address_id_idx ON identity_recovery_tokens; +DROP INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens; +DROP INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens; +DROP INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens; +DROP INDEX selfservice_login_flows_nid_id_idx ON selfservice_login_flows; +DROP INDEX selfservice_login_flows_id_nid_idx ON selfservice_login_flows; +DROP INDEX selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows; +DROP INDEX selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows; +DROP INDEX selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows; +DROP INDEX selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows; +DROP INDEX selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows; +DROP INDEX selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows; +DROP INDEX selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows; +DROP INDEX selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows; diff --git a/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.up.sql b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.up.sql new file mode 100644 index 0000000..3cd3bef --- /dev/null +++ b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.mysql.up.sql @@ -0,0 +1,70 @@ +CREATE INDEX sessions_identity_id_nid_idx ON sessions (identity_id, nid); + +CREATE INDEX identities_id_nid_idx ON identities (id, nid); +CREATE INDEX identities_nid_id_idx ON identities (nid, id); +DROP INDEX identities_nid_idx ON identities; + +CREATE INDEX continuity_containers_nid_id_idx ON continuity_containers (nid, id); +CREATE INDEX continuity_containers_id_nid_idx ON continuity_containers (id, nid); +DROP INDEX continuity_containers_nid_idx ON continuity_containers; + +CREATE INDEX courier_messages_nid_id_idx ON courier_messages (nid, id); +CREATE INDEX courier_messages_id_nid_idx ON courier_messages (id, nid); +DROP INDEX courier_messages_nid_idx ON courier_messages; + +CREATE INDEX identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers (nid, id); +CREATE INDEX identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers (id, nid); +DROP INDEX identity_credential_identifiers_nid_idx ON identity_credential_identifiers; + +CREATE INDEX identity_credentials_nid_id_idx ON identity_credentials (nid, id); +CREATE INDEX identity_credentials_id_nid_idx ON identity_credentials (id, nid); +DROP INDEX identity_credentials_nid_idx ON identity_credentials; + +CREATE INDEX identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses (nid, id); +CREATE INDEX identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses (id, nid); +DROP INDEX identity_recovery_addresses_nid_idx ON identity_recovery_addresses; + +CREATE INDEX identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens (nid, id); +CREATE INDEX identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens (id, nid); +CREATE INDEX identity_recovery_tokens_selfservice_recovery_flow_id_idx ON identity_recovery_tokens (selfservice_recovery_flow_id); +CREATE INDEX identity_recovery_tokens_identity_recovery_address_id_idx ON identity_recovery_tokens (identity_recovery_address_id); +CREATE INDEX identity_recovery_tokens_token_nid_used_idx ON identity_recovery_tokens (nid, token, used); +DROP INDEX identity_recovery_tokens_nid_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_addresses_code_idx ON identity_recovery_tokens; + +CREATE INDEX identity_verifiable_addresses_nid_id_idx ON identity_verifiable_addresses (nid, id); +CREATE INDEX identity_verifiable_addresses_id_nid_idx ON identity_verifiable_addresses (id, nid); +DROP INDEX identity_verifiable_addresses_nid_idx ON identity_verifiable_addresses; + +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens (nid, token, used); +DROP INDEX identity_verification_tokens_nid_idx ON identity_verification_tokens; +DROP INDEX identity_verification_tokens_token_idx ON identity_verification_tokens; + +CREATE INDEX selfservice_login_flows_nid_id_idx ON selfservice_login_flows (nid, id); +CREATE INDEX selfservice_login_flows_id_nid_idx ON selfservice_login_flows (id, nid); +DROP INDEX selfservice_login_flows_nid_idx ON selfservice_login_flows; + +CREATE INDEX selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows (nid, id); +CREATE INDEX selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows (id, nid); +DROP INDEX selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows; + +CREATE INDEX selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows (nid, id); +CREATE INDEX selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows (id, nid); +DROP INDEX selfservice_registration_flows_nid_idx ON selfservice_registration_flows; + +CREATE INDEX selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows (nid, id); +CREATE INDEX selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows (id, nid); +DROP INDEX selfservice_settings_flows_nid_idx ON selfservice_settings_flows; + +CREATE INDEX selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows (nid, id); +CREATE INDEX selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows (id, nid); +DROP INDEX selfservice_verification_flows_nid_idx ON selfservice_verification_flows; + +CREATE INDEX sessions_nid_id_identity_id_idx ON sessions (nid, identity_id, id); +CREATE INDEX sessions_id_nid_idx ON sessions (id, nid); +CREATE INDEX sessions_token_nid_idx ON sessions (nid, token); +DROP INDEX sessions_nid_idx ON sessions; +DROP INDEX sessions_token_idx ON sessions; +DROP INDEX sessions_logout_token_idx ON sessions; diff --git a/persistence/sql/migrations/sql/20220512102703000000_missing_indices.up.sql b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.up.sql new file mode 100644 index 0000000..1c7268c --- /dev/null +++ b/persistence/sql/migrations/sql/20220512102703000000_missing_indices.up.sql @@ -0,0 +1,71 @@ +CREATE INDEX sessions_identity_id_nid_idx ON sessions (identity_id, nid); + +CREATE INDEX identities_id_nid_idx ON identities (id, nid); +CREATE INDEX identities_nid_id_idx ON identities (nid, id); +DROP INDEX identities_nid_idx; + +CREATE INDEX continuity_containers_nid_id_idx ON continuity_containers (nid, id); +CREATE INDEX continuity_containers_id_nid_idx ON continuity_containers (id, nid); +DROP INDEX continuity_containers_nid_idx; + +CREATE INDEX courier_messages_nid_id_idx ON courier_messages (nid, id); +CREATE INDEX courier_messages_id_nid_idx ON courier_messages (id, nid); +DROP INDEX courier_messages_nid_idx; + +CREATE INDEX identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers (nid, id); +CREATE INDEX identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers (id, nid); +DROP INDEX identity_credential_identifiers_nid_idx; + +CREATE INDEX identity_credentials_nid_id_idx ON identity_credentials (nid, id); +CREATE INDEX identity_credentials_id_nid_idx ON identity_credentials (id, nid); +DROP INDEX identity_credentials_nid_idx; + +CREATE INDEX identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses (nid, id); +CREATE INDEX identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses (id, nid); +DROP INDEX identity_recovery_addresses_nid_idx; + +CREATE INDEX identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens (nid, id); +CREATE INDEX identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens (id, nid); +CREATE INDEX identity_recovery_tokens_selfservice_recovery_flow_id_idx ON identity_recovery_tokens (selfservice_recovery_flow_id); +CREATE INDEX identity_recovery_tokens_identity_recovery_address_id_idx ON identity_recovery_tokens (identity_recovery_address_id); +CREATE INDEX identity_recovery_tokens_token_nid_used_idx ON identity_recovery_tokens (nid, token, used); +DROP INDEX identity_recovery_addresses_code_idx; +DROP INDEX identity_recovery_tokens_nid_idx; + +CREATE INDEX identity_verifiable_addresses_nid_id_idx ON identity_verifiable_addresses (nid, id); +CREATE INDEX identity_verifiable_addresses_id_nid_idx ON identity_verifiable_addresses (id, nid); +DROP INDEX identity_verifiable_addresses_nid_idx; + +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens (nid, token, used); +DROP INDEX identity_verification_tokens_nid_idx; +DROP INDEX identity_verification_tokens_token_idx; + +CREATE INDEX selfservice_login_flows_nid_id_idx ON selfservice_login_flows (nid, id); +CREATE INDEX selfservice_login_flows_id_nid_idx ON selfservice_login_flows (id, nid); +DROP INDEX selfservice_login_flows_nid_idx; + +CREATE INDEX selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows (nid, id); +CREATE INDEX selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows (id, nid); +DROP INDEX selfservice_recovery_flows_nid_idx; + +CREATE INDEX selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows (nid, id); +CREATE INDEX selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows (id, nid); +DROP INDEX selfservice_registration_flows_nid_idx; + +CREATE INDEX selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows (nid, id); +CREATE INDEX selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows (id, nid); +DROP INDEX selfservice_settings_flows_nid_idx; + +CREATE INDEX selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows (nid, id); +CREATE INDEX selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows (id, nid); +DROP INDEX selfservice_verification_flows_nid_idx; + +CREATE INDEX sessions_nid_id_identity_id_idx ON sessions (nid, identity_id, id); +CREATE INDEX sessions_id_nid_idx ON sessions (id, nid); +CREATE INDEX sessions_token_nid_idx ON sessions (nid, token); + +DROP INDEX sessions_nid_idx; +DROP INDEX sessions_token_idx; +DROP INDEX sessions_logout_token_idx; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.down.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.down.sql new file mode 100644 index 0000000..0bf9f82 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "oauth2_login_challenge"; +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "oauth2_login_challenge"; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.up.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.up.sql new file mode 100644 index 0000000..8a61077 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.cockroach.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "oauth2_login_challenge" UUID NULL; +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "oauth2_login_challenge" UUID NULL; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.down.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.down.sql new file mode 100644 index 0000000..c537f46 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_login_flows` DROP COLUMN `oauth2_login_challenge`; +ALTER TABLE `selfservice_registration_flows` DROP COLUMN `oauth2_login_challenge`; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.up.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.up.sql new file mode 100644 index 0000000..e4e7907 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_login_flows` ADD COLUMN `oauth2_login_challenge` CHAR(36) NULL; +ALTER TABLE `selfservice_registration_flows` ADD COLUMN `oauth2_login_challenge` CHAR(36) NULL; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.down.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.down.sql new file mode 100644 index 0000000..0bf9f82 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "oauth2_login_challenge"; +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "oauth2_login_challenge"; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.up.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.up.sql new file mode 100644 index 0000000..8a61077 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.postgres.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "oauth2_login_challenge" UUID NULL; +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "oauth2_login_challenge" UUID NULL; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.down.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.down.sql new file mode 100644 index 0000000..0bf9f82 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" DROP COLUMN "oauth2_login_challenge"; +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "oauth2_login_challenge"; diff --git a/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.up.sql b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.up.sql new file mode 100644 index 0000000..be22767 --- /dev/null +++ b/persistence/sql/migrations/sql/20220607000001000000_hydra_login_challenge.sqlite3.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_login_flows" ADD COLUMN "oauth2_login_challenge" CHAR(36) NULL; +ALTER TABLE "selfservice_registration_flows" ADD COLUMN "oauth2_login_challenge" CHAR(36) NULL; diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.cockroach.down.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.cockroach.up.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.cockroach.up.sql new file mode 100644 index 0000000..8fc16ef --- /dev/null +++ b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.cockroach.up.sql @@ -0,0 +1,2 @@ +UPDATE identity_recovery_addresses SET value = LOWER(value) WHERE TRUE; +UPDATE identity_verifiable_addresses SET value = LOWER(value) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.mysql.down.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.mysql.up.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.mysql.up.sql new file mode 100644 index 0000000..8fc16ef --- /dev/null +++ b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.mysql.up.sql @@ -0,0 +1,2 @@ +UPDATE identity_recovery_addresses SET value = LOWER(value) WHERE TRUE; +UPDATE identity_verifiable_addresses SET value = LOWER(value) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.postgres.down.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.postgres.up.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.postgres.up.sql new file mode 100644 index 0000000..8fc16ef --- /dev/null +++ b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.postgres.up.sql @@ -0,0 +1,2 @@ +UPDATE identity_recovery_addresses SET value = LOWER(value) WHERE TRUE; +UPDATE identity_verifiable_addresses SET value = LOWER(value) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.sqlite3.down.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.sqlite3.up.sql b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.sqlite3.up.sql new file mode 100644 index 0000000..8fc16ef --- /dev/null +++ b/persistence/sql/migrations/sql/20220610155809000000_identity_address_casing.sqlite3.up.sql @@ -0,0 +1,2 @@ +UPDATE identity_recovery_addresses SET value = LOWER(value) WHERE TRUE; +UPDATE identity_verifiable_addresses SET value = LOWER(value) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.down.sql b/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.down.sql new file mode 100644 index 0000000..bc90e6b --- /dev/null +++ b/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.down.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages DROP COLUMN send_count; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.up.sql b/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.up.sql new file mode 100644 index 0000000..983fcd5 --- /dev/null +++ b/persistence/sql/migrations/sql/20220802103909000000_courier_send_count.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE courier_messages +ADD send_count INT NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.down.sql b/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.down.sql new file mode 100644 index 0000000..e57722f --- /dev/null +++ b/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_recovery_tokens +DROP token_type; diff --git a/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.up.sql b/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.up.sql new file mode 100644 index 0000000..b4fc9ee --- /dev/null +++ b/persistence/sql/migrations/sql/20220824165300000000_add_flow_type_to_identity_recovery_tokens.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_recovery_tokens +ADD token_type int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20220824165300000001_populate_flow_type_in_recovery_tokens.down.sql b/persistence/sql/migrations/sql/20220824165300000001_populate_flow_type_in_recovery_tokens.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220824165300000001_populate_flow_type_in_recovery_tokens.up.sql b/persistence/sql/migrations/sql/20220824165300000001_populate_flow_type_in_recovery_tokens.up.sql new file mode 100644 index 0000000..311af2a --- /dev/null +++ b/persistence/sql/migrations/sql/20220824165300000001_populate_flow_type_in_recovery_tokens.up.sql @@ -0,0 +1,7 @@ +UPDATE identity_recovery_tokens +SET token_type = 1 +WHERE selfservice_recovery_flow_id IS NULL; + +UPDATE identity_recovery_tokens +SET token_type = 2 +WHERE selfservice_recovery_flow_id IS NOT NULL; diff --git a/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.down.sql b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.sqlite3.down.sql b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.sqlite3.up.sql b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.sqlite3.up.sql new file mode 100644 index 0000000..590dfca --- /dev/null +++ b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.sqlite3.up.sql @@ -0,0 +1 @@ +-- SQLITE does not support Check constraints in all cases diff --git a/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.up.sql b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.up.sql new file mode 100644 index 0000000..00b652b --- /dev/null +++ b/persistence/sql/migrations/sql/20220824165300000002_add_flow_type_check_constraint.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_recovery_tokens +ADD CONSTRAINT identity_recovery_tokens_token_type_ck CHECK (token_type = 1 OR token_type = 2); diff --git a/persistence/sql/migrations/sql/20220825134336000000_delete_verification_token_without_flow_id.down.sql b/persistence/sql/migrations/sql/20220825134336000000_delete_verification_token_without_flow_id.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20220825134336000000_delete_verification_token_without_flow_id.up.sql b/persistence/sql/migrations/sql/20220825134336000000_delete_verification_token_without_flow_id.up.sql new file mode 100644 index 0000000..4e7eba1 --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000000_delete_verification_token_without_flow_id.up.sql @@ -0,0 +1,2 @@ +DELETE FROM identity_verification_tokens +WHERE selfservice_verification_flow_id IS NULL; diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.down.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.down.sql new file mode 100644 index 0000000..5e755e4 --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE identity_verification_tokens +ALTER selfservice_verification_flow_id DROP NOT NULL; + +DROP INDEX identity_verification_tokens_token_nid_used_flow_id_idx; +CREATE INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens (nid, token, used); diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.down.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.down.sql new file mode 100644 index 0000000..726a40c --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE identity_verification_tokens +MODIFY selfservice_verification_flow_id CHAR(36) NULL; + +DROP INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens; +CREATE INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens (nid, token, used); diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.up.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.up.sql new file mode 100644 index 0000000..53d22be --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.mysql.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE identity_verification_tokens +MODIFY selfservice_verification_flow_id CHAR(36) NOT NULL; + +DROP INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens; +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid, token, used, selfservice_verification_flow_id); diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.down.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.down.sql new file mode 100644 index 0000000..325516f --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.down.sql @@ -0,0 +1,37 @@ +ALTER TABLE identity_verification_tokens +RENAME TO identity_verification_tokens_; + +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); + +DROP INDEX identity_verification_tokens_id_nid_idx; +DROP INDEX identity_verification_tokens_nid_id_idx; +DROP INDEX identity_verification_tokens_token_nid_used_flow_id_idx; +DROP INDEX identity_verification_tokens_token_uq_idx; +DROP INDEX identity_verification_tokens_verifiable_address_id_idx; +DROP INDEX identity_verification_tokens_verification_flow_id_idx; + +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_token_nid_used_idx ON identity_verification_tokens (nid, token, used); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); + +INSERT INTO identity_verification_tokens +SELECT * FROM identity_verification_tokens_; + +DROP TABLE identity_verification_tokens_; diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.up.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.up.sql new file mode 100644 index 0000000..b417f6e --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.sqlite3.up.sql @@ -0,0 +1,37 @@ +ALTER TABLE identity_verification_tokens +RENAME TO identity_verification_tokens_; + +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); + +DROP INDEX identity_verification_tokens_id_nid_idx; +DROP INDEX identity_verification_tokens_nid_id_idx; +DROP INDEX identity_verification_tokens_token_nid_used_idx; +DROP INDEX identity_verification_tokens_token_uq_idx; +DROP INDEX identity_verification_tokens_verifiable_address_id_idx; +DROP INDEX identity_verification_tokens_verification_flow_id_idx; + +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid, token, used, selfservice_verification_flow_id); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); + +INSERT INTO identity_verification_tokens +SELECT * FROM identity_verification_tokens_; + +DROP TABLE identity_verification_tokens_; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.up.sql b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.up.sql new file mode 100644 index 0000000..3d4173c --- /dev/null +++ b/persistence/sql/migrations/sql/20220825134336000001_not_null_constraint_verification_token_flow_id.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE identity_verification_tokens +ALTER selfservice_verification_flow_id SET NOT NULL; + +DROP INDEX identity_verification_tokens_token_nid_used_idx; +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid, token, used, selfservice_verification_flow_id); diff --git a/persistence/sql/migrations/sql/20220901123209000000_recovery_code.down.sql b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.down.sql new file mode 100644 index 0000000..f01e222 --- /dev/null +++ b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.down.sql @@ -0,0 +1,5 @@ +DROP TABLE identity_recovery_codes; + +ALTER TABLE selfservice_recovery_flows DROP submit_count; + +ALTER TABLE selfservice_recovery_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.down.sql b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.down.sql new file mode 100644 index 0000000..f01e222 --- /dev/null +++ b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.down.sql @@ -0,0 +1,5 @@ +DROP TABLE identity_recovery_codes; + +ALTER TABLE selfservice_recovery_flows DROP submit_count; + +ALTER TABLE selfservice_recovery_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.up.sql b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.up.sql new file mode 100644 index 0000000..d94117b --- /dev/null +++ b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.mysql.up.sql @@ -0,0 +1,37 @@ +CREATE TABLE identity_recovery_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + identity_recovery_address_id CHAR(36), + code_type int NOT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_recovery_flow_id CHAR(36), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + identity_id CHAR(36) NOT NULL, + CONSTRAINT identity_recovery_codes_identity_recovery_addresses_id_fk + FOREIGN KEY (identity_recovery_address_id) + REFERENCES identity_recovery_addresses (id) + ON DELETE cascade, + CONSTRAINT identity_recovery_codes_selfservice_recovery_flows_id_fk + FOREIGN KEY (selfservice_recovery_flow_id) + REFERENCES selfservice_recovery_flows (id) + ON DELETE cascade, + CONSTRAINT identity_recovery_tokens_identity_id_fk + FOREIGN KEY (identity_id) + REFERENCES identities (id) + ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_recovery_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_recovery_codes_nid_flow_id_idx ON identity_recovery_codes (nid, selfservice_recovery_flow_id); +CREATE INDEX identity_recovery_codes_id_nid_idx ON identity_recovery_codes (id, nid); + +ALTER TABLE selfservice_recovery_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_recovery_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20220901123209000000_recovery_code.up.sql b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.up.sql new file mode 100644 index 0000000..4964d0a --- /dev/null +++ b/persistence/sql/migrations/sql/20220901123209000000_recovery_code.up.sql @@ -0,0 +1,37 @@ +CREATE TABLE identity_recovery_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + identity_recovery_address_id UUID, + code_type INT NOT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_recovery_flow_id UUID NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + nid UUID NOT NULL, + identity_id UUID NOT NULL, + CONSTRAINT identity_recovery_codes_identity_recovery_addresses_id_fk + FOREIGN KEY (identity_recovery_address_id) + REFERENCES identity_recovery_addresses (id) + ON DELETE cascade, + CONSTRAINT identity_recovery_codes_selfservice_recovery_flows_id_fk + FOREIGN KEY (selfservice_recovery_flow_id) + REFERENCES selfservice_recovery_flows (id) + ON DELETE cascade, + CONSTRAINT identity_recovery_codes_identity_id_fk + FOREIGN KEY (identity_id) + REFERENCES identities (id) + ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_recovery_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_recovery_codes_nid_flow_id_idx ON identity_recovery_codes (nid, selfservice_recovery_flow_id); +CREATE INDEX identity_recovery_codes_id_nid_idx ON identity_recovery_codes (id, nid); + +ALTER TABLE selfservice_recovery_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_recovery_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.down.sql b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.down.sql new file mode 100644 index 0000000..350a545 --- /dev/null +++ b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.down.sql @@ -0,0 +1 @@ +DROP TABLE "session_devices"; diff --git a/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.down.sql b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.down.sql new file mode 100644 index 0000000..10b4309 --- /dev/null +++ b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE session_devices; diff --git a/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.up.sql b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.up.sql new file mode 100644 index 0000000..ce15c6f --- /dev/null +++ b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.mysql.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE `session_devices` +( + `id` char(36) NOT NULL, + PRIMARY KEY (`id`), + `ip_address` VARCHAR(50) DEFAULT '', + `user_agent` VARCHAR(512) DEFAULT '', + `location` VARCHAR(512) DEFAULT '', + `session_id` char(36) NOT NULL, + `nid` char(36) NOT NULL, + `created_at` DATETIME NOT NULL, + `updated_at` DATETIME NOT NULL, + FOREIGN KEY (`session_id`) REFERENCES `sessions` (`id`) ON DELETE cascade, + FOREIGN KEY (`nid`) REFERENCES `networks` (`id`) ON DELETE cascade, + CONSTRAINT unique_session_device UNIQUE (nid, session_id, ip_address, user_agent) +) ENGINE = InnoDB; +CREATE INDEX `session_devices_id_nid_idx` ON `session_devices` (`id`, `nid`); +CREATE INDEX `session_devices_session_id_nid_idx` ON `session_devices` (`session_id`, `nid`); diff --git a/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.up.sql b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.up.sql new file mode 100644 index 0000000..7f69848 --- /dev/null +++ b/persistence/sql/migrations/sql/20220907132836000000_add_session_devices_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "session_devices" +( + "id" UUID PRIMARY KEY NOT NULL, + "ip_address" VARCHAR(50) DEFAULT '', + "user_agent" VARCHAR(512) DEFAULT '', + "location" VARCHAR(512) DEFAULT '', + "nid" UUID NOT NULL, + "session_id" UUID NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "session_metadata_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "sessions" ("id") ON DELETE cascade, + CONSTRAINT "session_metadata_nid_fk" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON DELETE cascade, + CONSTRAINT unique_session_device UNIQUE (nid, session_id, ip_address, user_agent) +); +CREATE INDEX "session_devices_id_nid_idx" ON "session_devices" (id, nid); +CREATE INDEX "session_devices_session_id_nid_idx" ON "session_devices" (session_id, nid); diff --git a/persistence/sql/migrations/sql/20221024182336000000_verification_code.down.sql b/persistence/sql/migrations/sql/20221024182336000000_verification_code.down.sql new file mode 100644 index 0000000..c3744b3 --- /dev/null +++ b/persistence/sql/migrations/sql/20221024182336000000_verification_code.down.sql @@ -0,0 +1,4 @@ +DROP TABLE identity_verification_codes; + +ALTER TABLE + selfservice_verification_flows DROP COLUMN submit_count; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221024182336000000_verification_code.mysql.up.sql b/persistence/sql/migrations/sql/20221024182336000000_verification_code.mysql.up.sql new file mode 100644 index 0000000..e89d343 --- /dev/null +++ b/persistence/sql/migrations/sql/20221024182336000000_verification_code.mysql.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE identity_verification_codes ( + id CHAR(36) NOT NULL PRIMARY KEY, + code_hmac VARCHAR (64) NOT NULL, + -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + identity_verifiable_address_id CHAR(36), + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_verification_flow_id CHAR(36) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_verification_codes_identity_verifiable_addresses_id_fk FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, + CONSTRAINT identity_verification_codes_selfservice_verification_flows_id_fk FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade, + CONSTRAINT identity_verification_codes_networks_id_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +ALTER TABLE + selfservice_verification_flows +ADD + COLUMN submit_count INT NOT NULL DEFAULT 0; + +CREATE INDEX identity_verification_codes_nid_flow_id_idx ON identity_verification_codes (nid, selfservice_verification_flow_id); + +CREATE INDEX identity_verification_codes_id_nid_idx ON identity_verification_codes (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221024182336000000_verification_code.up.sql b/persistence/sql/migrations/sql/20221024182336000000_verification_code.up.sql new file mode 100644 index 0000000..0f86ca4 --- /dev/null +++ b/persistence/sql/migrations/sql/20221024182336000000_verification_code.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE identity_verification_codes ( + id UUID NOT NULL PRIMARY KEY, + code_hmac VARCHAR (64) NOT NULL, + -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + identity_verifiable_address_id UUID, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_verification_flow_id UUID NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + nid UUID NOT NULL, + CONSTRAINT identity_verification_codes_identity_verifiable_addresses_id_fk FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, + CONSTRAINT identity_verification_codes_selfservice_verification_flows_id_fk FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade, + CONSTRAINT identity_verification_codes_networks_id_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +ALTER TABLE + selfservice_verification_flows +ADD + COLUMN submit_count INT NOT NULL DEFAULT 0; + +CREATE INDEX identity_verification_codes_nid_flow_id_idx ON identity_verification_codes (nid, selfservice_verification_flow_id); + +CREATE INDEX identity_verification_codes_id_nid_idx ON identity_verification_codes (id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.down.sql b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.down.sql new file mode 100644 index 0000000..2154ad8 --- /dev/null +++ b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE courier_message_dispatches; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.mysql.up.sql b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.mysql.up.sql new file mode 100644 index 0000000..20f9fbc --- /dev/null +++ b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE courier_message_dispatches ( + id CHAR(36) PRIMARY KEY, + message_id CHAR(36) NOT NULL, + status VARCHAR(7) NOT NULL, + error JSON, + nid CHAR(36) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT courier_message_dispatches_message_id_fk FOREIGN KEY (message_id) REFERENCES courier_messages (id) ON DELETE cascade, + CONSTRAINT courier_message_dispatches_nid_fk FOREIGN KEY (nid) REFERENCES networks (id) ON DELETE cascade +); + +CREATE INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id, message_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.up.sql b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.up.sql new file mode 100644 index 0000000..27c2113 --- /dev/null +++ b/persistence/sql/migrations/sql/20221205092803000000_add_courier_send_attempts_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE courier_message_dispatches ( + id UUID PRIMARY KEY, + message_id UUID NOT NULL, + status VARCHAR(7) NOT NULL, + error JSON, + nid UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT courier_message_dispatches_message_id_fk FOREIGN KEY (message_id) REFERENCES courier_messages (id) ON DELETE cascade, + CONSTRAINT courier_message_dispatches_nid_fk FOREIGN KEY (nid) REFERENCES networks (id) ON DELETE cascade +); + +CREATE INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id, message_id, nid); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.down.sql b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.down.sql new file mode 100644 index 0000000..cfd645a --- /dev/null +++ b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.down.sql @@ -0,0 +1,13 @@ +DROP INDEX IF EXISTS "identity_recovery_codes_identity_id_nid_idx"; + +DROP INDEX IF EXISTS "identity_verification_codes_verifiable_address_nid_idx"; + +DROP INDEX IF EXISTS "selfservice_settings_flows_identity_id_nid_idx"; + +DROP INDEX IF EXISTS "continuity_containers_identity_id_nid_idx"; + +DROP INDEX IF EXISTS "selfservice_recovery_flows_recovered_identity_id_nid_idx"; + +DROP INDEX IF EXISTS "identity_recovery_tokens_identity_id_nid_idx"; + +DROP INDEX IF EXISTS "identity_recovery_codes_identity_recovery_address_id_nid_idx"; diff --git a/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.down.sql b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.down.sql new file mode 100644 index 0000000..d826bf8 --- /dev/null +++ b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.down.sql @@ -0,0 +1,36 @@ +-- MySQL requires indexes on foreign keys and referenced keys so that foreign key checks can be fast and not require a table scan. +-- In the referencing table, there must be an index where the foreign key columns are listed as the first columns in the same order. +-- Such an index is created on the referencing table automatically if it does not exist. This index might be silently dropped later +-- if you create another index that can be used to enforce the foreign key constraint. + +-- from https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html + +-- -> The indexes in question already existed. We have to create new ones that are just the foreign key to restore the previous state. + +ALTER TABLE identity_recovery_codes ADD INDEX (identity_id); + +DROP INDEX identity_recovery_codes_identity_id_nid_idx ON identity_recovery_codes; + +ALTER TABLE identity_verification_codes ADD INDEX (identity_verifiable_address_id); + +DROP INDEX identity_verification_codes_verifiable_address_nid_idx ON identity_verification_codes; + +ALTER TABLE selfservice_settings_flows ADD INDEX (identity_id); + +DROP INDEX selfservice_settings_flows_identity_id_nid_idx ON selfservice_settings_flows; + +ALTER TABLE continuity_containers ADD INDEX (identity_id); + +DROP INDEX continuity_containers_identity_id_nid_idx ON continuity_containers; + +ALTER TABLE selfservice_recovery_flows ADD INDEX (recovered_identity_id); + +DROP INDEX selfservice_recovery_flows_recovered_identity_id_nid_idx ON selfservice_recovery_flows; + +ALTER TABLE identity_recovery_tokens ADD INDEX (identity_id); + +DROP INDEX identity_recovery_tokens_identity_id_nid_idx ON identity_recovery_tokens; + +ALTER TABLE identity_recovery_codes ADD INDEX (identity_recovery_address_id); + +DROP INDEX identity_recovery_codes_identity_recovery_address_id_nid_idx ON identity_recovery_codes; diff --git a/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.up.sql b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.up.sql new file mode 100644 index 0000000..6340ae6 --- /dev/null +++ b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.mysql.up.sql @@ -0,0 +1,22 @@ +-- MySQL requires indexes on foreign keys and referenced keys so that foreign key checks can be fast and not require a table scan. +-- In the referencing table, there must be an index where the foreign key columns are listed as the first columns in the same order. +-- Such an index is created on the referencing table automatically if it does not exist. This index might be silently dropped later +-- if you create another index that can be used to enforce the foreign key constraint. + +-- from https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html + +-- -> We create new indexes to be consistent with the other databases. However, dropping those will be a bit different. + +CREATE INDEX identity_recovery_codes_identity_id_nid_idx ON identity_recovery_codes (identity_id, nid); + +CREATE INDEX identity_verification_codes_verifiable_address_nid_idx ON identity_verification_codes (identity_verifiable_address_id, nid); + +CREATE INDEX selfservice_settings_flows_identity_id_nid_idx ON selfservice_settings_flows (identity_id, nid); + +CREATE INDEX continuity_containers_identity_id_nid_idx ON continuity_containers (identity_id, nid); + +CREATE INDEX selfservice_recovery_flows_recovered_identity_id_nid_idx ON selfservice_recovery_flows (recovered_identity_id, nid); + +CREATE INDEX identity_recovery_tokens_identity_id_nid_idx ON identity_recovery_tokens (identity_id, nid); + +CREATE INDEX identity_recovery_codes_identity_recovery_address_id_nid_idx ON identity_recovery_codes (identity_recovery_address_id, nid); diff --git a/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.up.sql b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.up.sql new file mode 100644 index 0000000..1adf507 --- /dev/null +++ b/persistence/sql/migrations/sql/20221214101328000000_identity_delete_indices.up.sql @@ -0,0 +1,13 @@ +CREATE INDEX IF NOT EXISTS "identity_recovery_codes_identity_id_nid_idx" ON "identity_recovery_codes" (identity_id, nid); + +CREATE INDEX IF NOT EXISTS "identity_verification_codes_verifiable_address_nid_idx" ON "identity_verification_codes" (identity_verifiable_address_id, nid); + +CREATE INDEX IF NOT EXISTS "selfservice_settings_flows_identity_id_nid_idx" ON "selfservice_settings_flows" (identity_id, nid); + +CREATE INDEX IF NOT EXISTS "continuity_containers_identity_id_nid_idx" ON "continuity_containers" (identity_id, nid); + +CREATE INDEX IF NOT EXISTS "selfservice_recovery_flows_recovered_identity_id_nid_idx" ON "selfservice_recovery_flows" (recovered_identity_id, nid); + +CREATE INDEX IF NOT EXISTS "identity_recovery_tokens_identity_id_nid_idx" ON "identity_recovery_tokens" (identity_id, nid); + +CREATE INDEX IF NOT EXISTS "identity_recovery_codes_identity_recovery_address_id_nid_idx" ON "identity_recovery_codes" (identity_recovery_address_id, nid); diff --git a/persistence/sql/migrations/sql/20221220124639000000_errors_index.down.sql b/persistence/sql/migrations/sql/20221220124639000000_errors_index.down.sql new file mode 100644 index 0000000..706fa24 --- /dev/null +++ b/persistence/sql/migrations/sql/20221220124639000000_errors_index.down.sql @@ -0,0 +1,3 @@ +CREATE INDEX selfservice_errors_nid_idx ON selfservice_errors (id, nid); + +DROP INDEX selfservice_errors_errors_nid_id_idx; diff --git a/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.down.sql b/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.down.sql new file mode 100644 index 0000000..d07b359 --- /dev/null +++ b/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.down.sql @@ -0,0 +1,6 @@ +CREATE INDEX selfservice_errors_nid_idx ON selfservice_errors (id, nid); + +-- needed for foreign key constraint, was there before implicitly +CREATE INDEX selfservice_errors_nid_only_idx ON selfservice_errors (nid); + +DROP INDEX selfservice_errors_errors_nid_id_idx ON selfservice_errors; diff --git a/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.up.sql b/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.up.sql new file mode 100644 index 0000000..30cbda7 --- /dev/null +++ b/persistence/sql/migrations/sql/20221220124639000000_errors_index.mysql.up.sql @@ -0,0 +1,4 @@ +CREATE INDEX selfservice_errors_errors_nid_id_idx ON selfservice_errors (nid, id); + +-- This index is not needed anymore, because the primary ID index together with the new index cover all queries. +DROP INDEX selfservice_errors_nid_idx ON selfservice_errors; diff --git a/persistence/sql/migrations/sql/20221220124639000000_errors_index.up.sql b/persistence/sql/migrations/sql/20221220124639000000_errors_index.up.sql new file mode 100644 index 0000000..0720225 --- /dev/null +++ b/persistence/sql/migrations/sql/20221220124639000000_errors_index.up.sql @@ -0,0 +1,4 @@ +CREATE INDEX selfservice_errors_errors_nid_id_idx ON selfservice_errors (nid, id); + +-- This index is not needed anymore, because the primary ID index together with the new index cover all queries. +DROP INDEX selfservice_errors_nid_idx; diff --git a/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.down.sql b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.down.sql new file mode 100644 index 0000000..93fb3ea --- /dev/null +++ b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.down.sql @@ -0,0 +1,5 @@ +DROP INDEX courier_messages_nid_created_at_id_idx; + +DROP INDEX courier_messages_nid_status_created_at_id_idx; + +DROP INDEX courier_messages_nid_recipient_created_at_id_idx; diff --git a/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.mysql.down.sql b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.mysql.down.sql new file mode 100644 index 0000000..04721fa --- /dev/null +++ b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.mysql.down.sql @@ -0,0 +1,5 @@ +DROP INDEX courier_messages_nid_created_at_id_idx ON courier_messages; + +DROP INDEX courier_messages_nid_status_created_at_id_idx ON courier_messages; + +DROP INDEX courier_messages_nid_recipient_created_at_id_idx ON courier_messages; diff --git a/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.up.sql b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.up.sql new file mode 100644 index 0000000..41a2a04 --- /dev/null +++ b/persistence/sql/migrations/sql/20230104193739000000_courier_list_index.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX courier_messages_nid_created_at_id_idx ON courier_messages (nid, created_at DESC); + +CREATE INDEX courier_messages_nid_status_created_at_id_idx ON courier_messages (nid, status, created_at DESC); + +CREATE INDEX courier_messages_nid_recipient_created_at_id_idx ON courier_messages (nid, recipient, created_at DESC); diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.down.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.down.sql new file mode 100644 index 0000000..6f61198 --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX IF NOT EXISTS unique_session_device ON session_devices (nid, session_id, ip_address, user_agent); diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.up.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.up.sql new file mode 100644 index 0000000..8c94c46 --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS session_devices@unique_session_device CASCADE; diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.down.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.down.sql new file mode 100644 index 0000000..6f61198 --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX IF NOT EXISTS unique_session_device ON session_devices (nid, session_id, ip_address, user_agent); diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.down.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.down.sql new file mode 100644 index 0000000..9ad9614 --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX unique_session_device ON session_devices (nid, session_id, ip_address, user_agent); diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.up.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.up.sql new file mode 100644 index 0000000..569acdd --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE session_devices DROP FOREIGN KEY session_devices_ibfk_2; +ALTER TABLE session_devices DROP INDEX unique_session_device; +ALTER TABLE session_devices ADD CONSTRAINT session_devices_ibfk_2 FOREIGN KEY (nid) REFERENCES networks(id) ON DELETE CASCADE; diff --git a/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.up.sql b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.up.sql new file mode 100644 index 0000000..2a326bb --- /dev/null +++ b/persistence/sql/migrations/sql/20230216142104000000_session_devices_index_drop.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS session_devices.unique_session_device; diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.cockroach.down.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.cockroach.down.sql new file mode 100644 index 0000000..8d8a4d0 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.cockroach.down.sql @@ -0,0 +1,5 @@ +-- Downsizing is not yet supported in CockroachDB. Since this migration has no real-world impact on the application, we can safely +-- not execute it. +-- +-- ALTER TABLE sessions ALTER COLUMN token TYPE varchar(32); +-- ALTER TABLE sessions ALTER COLUMN logout_token TYPE varchar(32); diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.down.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.down.sql new file mode 100644 index 0000000..809c038 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE sessions ALTER COLUMN token TYPE varchar(32); +ALTER TABLE sessions ALTER COLUMN logout_token TYPE varchar(32); diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.down.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.down.sql new file mode 100644 index 0000000..6cd7e10 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE sessions MODIFY COLUMN token varchar(32) NULL; +ALTER TABLE sessions MODIFY COLUMN logout_token varchar(32) NULL; diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.up.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.up.sql new file mode 100644 index 0000000..2adcb7c --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE sessions MODIFY COLUMN token varchar(39) NULL; +ALTER TABLE sessions MODIFY COLUMN logout_token varchar(39) NULL; diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.down.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.down.sql new file mode 100644 index 0000000..967a418 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.down.sql @@ -0,0 +1,29 @@ +DROP INDEX sessions_token_uq_idx; +DROP INDEX sessions_logout_token_uq_idx; +DROP INDEX sessions_token_nid_idx; + +ALTER TABLE sessions RENAME COLUMN token TO old_token; +ALTER TABLE sessions RENAME COLUMN logout_token TO old_logout_token; + +ALTER TABLE sessions + ADD COLUMN token varchar(32) NULL; +ALTER TABLE sessions + ADD COLUMN logout_token varchar(32) NULL; + +UPDATE sessions +SET token = old_token +WHERE true; + +UPDATE sessions +SET logout_token = old_logout_token +WHERE true; + +ALTER TABLE sessions + DROP COLUMN old_token; + +ALTER TABLE sessions + DROP COLUMN old_logout_token; + +CREATE UNIQUE INDEX sessions_token_uq_idx ON sessions (logout_token); +CREATE UNIQUE INDEX sessions_logout_token_uq_idx ON sessions (token); +CREATE INDEX sessions_token_nid_idx ON sessions (nid, token); diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.up.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.up.sql new file mode 100644 index 0000000..92b88cc --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.sqlite3.up.sql @@ -0,0 +1,27 @@ +DROP INDEX sessions_token_uq_idx; +DROP INDEX sessions_logout_token_uq_idx; +DROP INDEX sessions_token_nid_idx; + +ALTER TABLE sessions RENAME COLUMN token TO old_token; +ALTER TABLE sessions RENAME COLUMN logout_token TO old_logout_token; +ALTER TABLE sessions + ADD COLUMN token varchar(39) NULL; +ALTER TABLE sessions + ADD COLUMN logout_token varchar(39) NULL; + +UPDATE sessions +SET token = old_token +WHERE true; + +UPDATE sessions +SET logout_token = old_logout_token +WHERE true; + +ALTER TABLE sessions + DROP COLUMN old_token; +ALTER TABLE sessions + DROP COLUMN old_logout_token; + +CREATE UNIQUE INDEX sessions_token_uq_idx ON sessions (logout_token); +CREATE UNIQUE INDEX sessions_logout_token_uq_idx ON sessions (token); +CREATE INDEX sessions_token_nid_idx ON sessions (nid, token); diff --git a/persistence/sql/migrations/sql/20230313141439000000_session_token_length.up.sql b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.up.sql new file mode 100644 index 0000000..c78bf9b --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000000_session_token_length.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE sessions ALTER COLUMN token TYPE varchar(39); +ALTER TABLE sessions ALTER COLUMN logout_token TYPE varchar(39); diff --git a/persistence/sql/migrations/sql/20230313141439000001_session_token_length.down.sql b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.down.sql new file mode 100644 index 0000000..db28543 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.down.sql @@ -0,0 +1,2 @@ +UPDATE sessions SET token = LEFT(token, 32) WHERE TRUE; +UPDATE sessions SET logout_token = LEFT(logout_token, 32) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20230313141439000001_session_token_length.sqlite3.down.sql b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.sqlite3.down.sql new file mode 100644 index 0000000..d0707dd --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.sqlite3.down.sql @@ -0,0 +1,2 @@ +UPDATE sessions SET token = substr(token, 0, 32) WHERE TRUE; +UPDATE sessions SET logout_token = substr(logout_token, 0, 32) WHERE TRUE; diff --git a/persistence/sql/migrations/sql/20230313141439000001_session_token_length.up.sql b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.up.sql new file mode 100644 index 0000000..4ba2805 --- /dev/null +++ b/persistence/sql/migrations/sql/20230313141439000001_session_token_length.up.sql @@ -0,0 +1 @@ +-- diff --git a/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.down.sql b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.down.sql new file mode 100644 index 0000000..1041896 --- /dev/null +++ b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_i_ici_idx; diff --git a/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.mysql.down.sql b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.mysql.down.sql new file mode 100644 index 0000000..ac444a9 --- /dev/null +++ b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_i_ici_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.up.sql b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.up.sql new file mode 100644 index 0000000..a2f1f74 --- /dev/null +++ b/persistence/sql/migrations/sql/20230322144139000001_missing_login_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX identity_credential_identifiers_nid_i_ici_idx ON identity_credential_identifiers (nid, identifier, identity_credential_id); diff --git a/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.down.sql b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.down.sql new file mode 100644 index 0000000..25dfb10 --- /dev/null +++ b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.down.sql @@ -0,0 +1 @@ +DROP TABLE session_token_exchanges; diff --git a/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.down.sql b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.down.sql new file mode 100644 index 0000000..25dfb10 --- /dev/null +++ b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE session_token_exchanges; diff --git a/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.up.sql b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.up.sql new file mode 100644 index 0000000..7a11347 --- /dev/null +++ b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.mysql.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE session_token_exchanges ( + id CHAR(36) NOT NULL PRIMARY KEY, + nid CHAR(36) NOT NULL, + flow_id CHAR(36) NOT NULL, + session_id CHAR(36) DEFAULT NULL, + init_code VARCHAR(64) NOT NULL, + return_to_code VARCHAR(64) NOT NULL, + + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Relevant query: +-- SELECT * from session_token_exchanges +-- WHERE nid = ? AND code = ? AND code <> '' AND session_id IS NOT NULL +CREATE INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code, nid); + +-- Relevant query: +-- UPDATE session_token_exchanges SET session_id = ? WHERE flow_id = ? AND nid = ? +CREATE INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id, nid); diff --git a/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.up.sql b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.up.sql new file mode 100644 index 0000000..d43f02d --- /dev/null +++ b/persistence/sql/migrations/sql/20230405000000000001_create_session_token_exchanges.up.sql @@ -0,0 +1,21 @@ +CREATE TABLE session_token_exchanges ( + "id" UUID NOT NULL PRIMARY KEY, + "nid" UUID NOT NULL, + "flow_id" UUID NOT NULL, + "session_id" UUID DEFAULT NULL, + "init_code" VARCHAR(64) NOT NULL, + "return_to_code" VARCHAR(64) NOT NULL, + + + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); + +-- Relevant query: +-- SELECT * from session_token_exchanges +-- WHERE nid = ? AND code = ? AND code <> '' AND session_id IS NOT NULL +CREATE INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code, nid); + +-- Relevant query: +-- UPDATE session_token_exchanges SET session_id = ? WHERE flow_id = ? AND nid = ? +CREATE INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id, nid); diff --git a/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.down.sql b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.down.sql new file mode 100644 index 0000000..e4207a2 --- /dev/null +++ b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.down.sql @@ -0,0 +1,5 @@ +-- ALTER TABLE selfservice_login_flows ADD COLUMN oauth2_login_challenge UUID NULL; +-- ALTER TABLE selfservice_registration_flows ADD COLUMN oauth2_login_challenge UUID NULL; + +ALTER TABLE selfservice_login_flows DROP COLUMN oauth2_login_challenge_data; +ALTER TABLE selfservice_registration_flows DROP COLUMN oauth2_login_challenge_data; diff --git a/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.mysql.down.sql b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.mysql.down.sql new file mode 100644 index 0000000..d3d8b4f --- /dev/null +++ b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.mysql.down.sql @@ -0,0 +1,5 @@ +-- ALTER TABLE selfservice_login_flows ADD COLUMN oauth2_login_challenge CHAR(36) NULL; +-- ALTER TABLE selfservice_registration_flows ADD COLUMN oauth2_login_challenge CHAR(36) NULL; + +ALTER TABLE selfservice_login_flows DROP COLUMN oauth2_login_challenge_data; +ALTER TABLE selfservice_registration_flows DROP COLUMN oauth2_login_challenge_data; diff --git a/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.sqlite3.down.sql b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.sqlite3.down.sql new file mode 100644 index 0000000..1a251ee --- /dev/null +++ b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.sqlite3.down.sql @@ -0,0 +1,5 @@ +-- ALTER TABLE `selfservice_login_flows` ADD COLUMN `oauth2_login_challenge` CHAR(36) NULL; +-- ALTER TABLE `selfservice_registration_flows` ADD COLUMN `oauth2_login_challenge` CHAR(36) NULL; + +ALTER TABLE "selfservice_login_flows" DROP COLUMN "oauth2_login_challenge_data"; +ALTER TABLE "selfservice_registration_flows" DROP COLUMN "oauth2_login_challenge_data"; diff --git a/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.up.sql b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.up.sql new file mode 100644 index 0000000..70785b7 --- /dev/null +++ b/persistence/sql/migrations/sql/20230614000001000000_hydra_login_challenge_format.up.sql @@ -0,0 +1,6 @@ +-- Drop columns in a later migration. +-- ALTER TABLE selfservice_login_flows DROP COLUMN oauth2_login_challenge; +-- ALTER TABLE selfservice_registration_flows DROP COLUMN oauth2_login_challenge; + +ALTER TABLE selfservice_login_flows ADD COLUMN oauth2_login_challenge_data TEXT NULL; +ALTER TABLE selfservice_registration_flows ADD COLUMN oauth2_login_challenge_data TEXT NULL; diff --git a/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.down.sql b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.down.sql new file mode 100644 index 0000000..b8e5be3 --- /dev/null +++ b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.down.sql @@ -0,0 +1,4 @@ +CREATE INDEX sessions_identity_id_nid_idx + ON sessions (identity_id, nid); + +DROP INDEX sessions_identity_id_nid_sorted_idx; diff --git a/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.down.sql b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.down.sql new file mode 100644 index 0000000..cfed7ce --- /dev/null +++ b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.down.sql @@ -0,0 +1,5 @@ +CREATE INDEX sessions_identity_id_nid_idx + ON sessions (identity_id, nid); + +DROP INDEX sessions_identity_id_nid_sorted_idx + ON sessions; diff --git a/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.up.sql b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.up.sql new file mode 100644 index 0000000..2f4f328 --- /dev/null +++ b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX sessions_identity_id_nid_sorted_idx + ON sessions (identity_id, nid, authenticated_at DESC); + +DROP INDEX sessions_identity_id_nid_idx + ON sessions; diff --git a/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.up.sql b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.up.sql new file mode 100644 index 0000000..9d75971 --- /dev/null +++ b/persistence/sql/migrations/sql/20230619000000000001_sessions_add_sorted_indices.up.sql @@ -0,0 +1,4 @@ +CREATE INDEX sessions_identity_id_nid_sorted_idx + ON sessions (identity_id, nid, authenticated_at DESC); + +DROP INDEX sessions_identity_id_nid_idx; diff --git a/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.down.sql b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.down.sql new file mode 100644 index 0000000..e95cb5d --- /dev/null +++ b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.down.sql @@ -0,0 +1 @@ +-- nothing diff --git a/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.down.sql b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.down.sql new file mode 100644 index 0000000..10bbfbe --- /dev/null +++ b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.down.sql @@ -0,0 +1 @@ +-- nothing: needed for foreign key anyways diff --git a/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.up.sql b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.up.sql new file mode 100644 index 0000000..8841960 --- /dev/null +++ b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX identity_credential_identifiers_nid_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id, nid); diff --git a/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.up.sql b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.up.sql new file mode 100644 index 0000000..d89b40b --- /dev/null +++ b/persistence/sql/migrations/sql/20230626000000000001_identity_credential_identifiers_nid_identity_credential_id_idx.up.sql @@ -0,0 +1 @@ +-- index already exists diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql new file mode 100644 index 0000000..ddd3c7b --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_registration_flows DROP COLUMN state; +ALTER table selfservice_login_flows DROP COLUMN state; diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql new file mode 100644 index 0000000..26f4d06 --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_login_flows ADD state VARCHAR(255) NULL; +ALTER table selfservice_registration_flows ADD state VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.cockroach.down.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.cockroach.down.sql new file mode 100644 index 0000000..34e58af --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.cockroach.down.sql @@ -0,0 +1,5 @@ +UPDATE selfservice_login_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_recovery_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_registration_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_settings_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_verification_flows SET request_url = substr(request_url, 1, 1024); diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.down.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.down.sql new file mode 100644 index 0000000..75a516d --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.down.sql @@ -0,0 +1,11 @@ +UPDATE selfservice_login_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_recovery_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_registration_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_settings_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_verification_flows SET request_url = substr(request_url, 1, 1024); + +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/20230705000000000001_cookie_flow_request_url.mysql.down.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.mysql.down.sql new file mode 100644 index 0000000..cc09770 --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.mysql.down.sql @@ -0,0 +1,11 @@ +UPDATE selfservice_login_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_recovery_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_registration_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_settings_flows SET request_url = substr(request_url, 1, 1024); +UPDATE selfservice_verification_flows SET request_url = substr(request_url, 1, 1024); + +ALTER TABLE selfservice_login_flows MODIFY request_url VARCHAR(1024); +ALTER TABLE selfservice_recovery_flows MODIFY request_url VARCHAR(1024); +ALTER TABLE selfservice_registration_flows MODIFY request_url VARCHAR(1024); +ALTER TABLE selfservice_settings_flows MODIFY request_url VARCHAR(1024); +ALTER TABLE selfservice_verification_flows MODIFY request_url VARCHAR(1024); diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.mysql.up.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.mysql.up.sql new file mode 100644 index 0000000..ca77f87 --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.mysql.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE selfservice_login_flows MODIFY request_url TEXT; +ALTER TABLE selfservice_recovery_flows MODIFY request_url TEXT; +ALTER TABLE selfservice_registration_flows MODIFY request_url TEXT; +ALTER TABLE selfservice_settings_flows MODIFY request_url TEXT; +ALTER TABLE selfservice_verification_flows MODIFY request_url TEXT; diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.down.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.down.sql new file mode 100644 index 0000000..a667b2b --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.down.sql @@ -0,0 +1,42 @@ +ALTER TABLE selfservice_login_flows + ADD COLUMN request_url_new VARCHAR(1024) NOT NULL DEFAULT ''; +ALTER TABLE selfservice_recovery_flows + ADD COLUMN request_url_new VARCHAR(1024) NOT NULL DEFAULT ''; +ALTER TABLE selfservice_registration_flows + ADD COLUMN request_url_new VARCHAR(1024) NOT NULL DEFAULT ''; +ALTER TABLE selfservice_settings_flows + ADD COLUMN request_url_new VARCHAR(1024) NOT NULL DEFAULT ''; +ALTER TABLE selfservice_verification_flows + ADD COLUMN request_url_new VARCHAR(1024) NOT NULL DEFAULT ''; + +UPDATE selfservice_login_flows +SET request_url_new = substr(request_url, 1, 1024); +UPDATE selfservice_recovery_flows +SET request_url_new = substr(request_url, 1, 1024); +UPDATE selfservice_registration_flows +SET request_url_new = substr(request_url, 1, 1024); +UPDATE selfservice_settings_flows +SET request_url_new = substr(request_url, 1, 1024); +UPDATE selfservice_verification_flows +SET request_url_new = substr(request_url, 1, 1024); + +ALTER TABLE selfservice_login_flows + DROP COLUMN request_url; +ALTER TABLE selfservice_login_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_recovery_flows + DROP COLUMN request_url; +ALTER TABLE selfservice_recovery_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_registration_flows + DROP COLUMN request_url; +ALTER TABLE selfservice_registration_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_settings_flows + DROP COLUMN request_url; +ALTER TABLE selfservice_settings_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_verification_flows + DROP COLUMN request_url; +ALTER TABLE selfservice_verification_flows RENAME COLUMN request_url_new TO request_url; + diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.up.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.up.sql new file mode 100644 index 0000000..89d6581 --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.sqlite.up.sql @@ -0,0 +1,27 @@ +ALTER TABLE selfservice_login_flows ADD COLUMN request_url_new TEXT NOT NULL DEFAULT ''; +ALTER TABLE selfservice_recovery_flows ADD COLUMN request_url_new TEXT NOT NULL DEFAULT ''; +ALTER TABLE selfservice_registration_flows ADD COLUMN request_url_new TEXT NOT NULL DEFAULT ''; +ALTER TABLE selfservice_settings_flows ADD COLUMN request_url_new TEXT NOT NULL DEFAULT ''; +ALTER TABLE selfservice_verification_flows ADD COLUMN request_url_new TEXT NOT NULL DEFAULT ''; + +UPDATE selfservice_login_flows SET request_url_new = request_url; +UPDATE selfservice_recovery_flows SET request_url_new = request_url; +UPDATE selfservice_registration_flows SET request_url_new = request_url; +UPDATE selfservice_settings_flows SET request_url_new = request_url; +UPDATE selfservice_verification_flows SET request_url_new = request_url; + +ALTER TABLE selfservice_login_flows DROP COLUMN request_url; +ALTER TABLE selfservice_login_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_recovery_flows DROP COLUMN request_url; +ALTER TABLE selfservice_recovery_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_registration_flows DROP COLUMN request_url; +ALTER TABLE selfservice_registration_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_settings_flows DROP COLUMN request_url; +ALTER TABLE selfservice_settings_flows RENAME COLUMN request_url_new TO request_url; + +ALTER TABLE selfservice_verification_flows DROP COLUMN request_url; +ALTER TABLE selfservice_verification_flows RENAME COLUMN request_url_new TO request_url; + diff --git a/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.up.sql b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.up.sql new file mode 100644 index 0000000..64a8dd8 --- /dev/null +++ b/persistence/sql/migrations/sql/20230705000000000001_cookie_flow_request_url.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE selfservice_login_flows ALTER COLUMN request_url TYPE TEXT; +ALTER TABLE selfservice_recovery_flows ALTER COLUMN request_url TYPE TEXT; +ALTER TABLE selfservice_registration_flows ALTER COLUMN request_url TYPE TEXT; +ALTER TABLE selfservice_settings_flows ALTER COLUMN request_url TYPE TEXT; +ALTER TABLE selfservice_verification_flows ALTER COLUMN request_url TYPE TEXT; diff --git a/persistence/sql/migrations/sql/20230706000000000001_available_aal.down.sql b/persistence/sql/migrations/sql/20230706000000000001_available_aal.down.sql new file mode 100644 index 0000000..8de3375 --- /dev/null +++ b/persistence/sql/migrations/sql/20230706000000000001_available_aal.down.sql @@ -0,0 +1 @@ +ALTER TABLE identities DROP COLUMN available_aal; diff --git a/persistence/sql/migrations/sql/20230706000000000001_available_aal.up.sql b/persistence/sql/migrations/sql/20230706000000000001_available_aal.up.sql new file mode 100644 index 0000000..1eb506e --- /dev/null +++ b/persistence/sql/migrations/sql/20230706000000000001_available_aal.up.sql @@ -0,0 +1 @@ +ALTER TABLE identities ADD COLUMN available_aal VARCHAR(4) NULL; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql new file mode 100644 index 0000000..3738073 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql @@ -0,0 +1,3 @@ +DROP TABLE identity_login_codes; + +ALTER TABLE selfservice_login_flows DROP submit_count; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql new file mode 100644 index 0000000..9ddc6c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE identity_login_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id CHAR(36), + identity_id CHAR(36) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql new file mode 100644 index 0000000..7df0e9b --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE identity_login_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id UUID NOT NULL, + identity_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql new file mode 100644 index 0000000..d4211e9 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql @@ -0,0 +1,3 @@ +DROP TABLE identity_registration_codes; + +ALTER TABLE selfservice_registration_flows DROP submit_count; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql new file mode 100644 index 0000000..36f049d --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql @@ -0,0 +1,27 @@ +CREATE TABLE identity_registration_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id CHAR(36), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql new file mode 100644 index 0000000..0dc3a98 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql @@ -0,0 +1,27 @@ +CREATE TABLE identity_registration_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql new file mode 100644 index 0000000..84f10f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql new file mode 100644 index 0000000..47e0cf0 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.down.sql b/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.down.sql new file mode 100644 index 0000000..3aa1dba --- /dev/null +++ b/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.down.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows DROP COLUMN oauth2_login_challenge; diff --git a/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.up.sql b/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.up.sql new file mode 100644 index 0000000..70209b6 --- /dev/null +++ b/persistence/sql/migrations/sql/20230811000000000001_verification_add_oauth2_login_challenge.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN oauth2_login_challenge TEXT NULL; diff --git a/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.down.sql b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.down.sql new file mode 100644 index 0000000..d8fa088 --- /dev/null +++ b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_verification_flows DROP COLUMN session_id; + diff --git a/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.mysql.up.sql b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.mysql.up.sql new file mode 100644 index 0000000..db94be8 --- /dev/null +++ b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN session_id VARCHAR(36); diff --git a/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.up.sql b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.up.sql new file mode 100644 index 0000000..af54e4e --- /dev/null +++ b/persistence/sql/migrations/sql/20230818000000000001_verification_add_oauth2_login_challenge_identity_id.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN session_id UUID; diff --git a/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.down.sql b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.down.sql new file mode 100644 index 0000000..1a1437a --- /dev/null +++ b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_verification_flows DROP COLUMN identity_id; +ALTER TABLE selfservice_verification_flows DROP COLUMN authentication_methods; diff --git a/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.mysql.up.sql b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.mysql.up.sql new file mode 100644 index 0000000..31614eb --- /dev/null +++ b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN identity_id VARCHAR(36); +ALTER TABLE selfservice_verification_flows ADD COLUMN authentication_methods JSON; diff --git a/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.sqlite.up.sql b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.sqlite.up.sql new file mode 100644 index 0000000..ab661e1 --- /dev/null +++ b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.sqlite.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN identity_id VARCHAR(36); +ALTER TABLE selfservice_verification_flows ADD COLUMN authentication_methods TEXT; diff --git a/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.up.sql b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.up.sql new file mode 100644 index 0000000..42b75ec --- /dev/null +++ b/persistence/sql/migrations/sql/20230823000000000001_verification_add_oauth2_login_challenge_params.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_verification_flows ADD COLUMN identity_id UUID; +ALTER TABLE selfservice_verification_flows ADD COLUMN authentication_methods JSON; diff --git a/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.down.sql b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.down.sql new file mode 100644 index 0000000..d9e1092 --- /dev/null +++ b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.down.sql @@ -0,0 +1,3 @@ +alter table selfservice_login_flows drop column organization_id; +alter table selfservice_registration_flows drop column organization_id; +alter table identities drop column organization_id; diff --git a/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.mysql.up.sql b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.mysql.up.sql new file mode 100644 index 0000000..55974d6 --- /dev/null +++ b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.mysql.up.sql @@ -0,0 +1,3 @@ +alter table selfservice_login_flows add column organization_id char(36) null; +alter table selfservice_registration_flows add column organization_id char(36) null; +alter table identities add column organization_id char(36) null; diff --git a/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.up.sql b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.up.sql new file mode 100644 index 0000000..8f2be2c --- /dev/null +++ b/persistence/sql/migrations/sql/20230907085000000000_add_organization_id.up.sql @@ -0,0 +1,3 @@ +alter table selfservice_login_flows add column organization_id uuid null; +alter table selfservice_registration_flows add column organization_id uuid null; +alter table identities add column organization_id uuid null; diff --git a/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.down.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.down.sql new file mode 100644 index 0000000..d1c8cdf --- /dev/null +++ b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.up.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.up.sql new file mode 100644 index 0000000..f692e99 --- /dev/null +++ b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.cockroach.up.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/20230920171028000000_identity_search_index.down.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.down.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.down.sql new file mode 100644 index 0000000..159cb60 --- /dev/null +++ b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.up.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.up.sql new file mode 100644 index 0000000..70d519f --- /dev/null +++ b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.postgres.up.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/20230920171028000000_identity_search_index.up.sql b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.up.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/persistence/sql/migrations/sql/20230920171028000000_identity_search_index.up.sql @@ -0,0 +1 @@ + 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 0000000..656e0e5 --- /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 0000000..7d5e3f0 --- /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/20231130094628000000_courier_message_channel.down.sql b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.down.sql new file mode 100644 index 0000000..a43df50 --- /dev/null +++ b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE + courier_messages DROP column channel; diff --git a/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql new file mode 100644 index 0000000..a4075c0 --- /dev/null +++ b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE + courier_messages +ADD + column channel VARCHAR(32) NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.down.sql b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.down.sql new file mode 100644 index 0000000..5f937e3 --- /dev/null +++ b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.down.sql @@ -0,0 +1 @@ +DROP INDEX sessions_nid_created_at_id_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.mysql.down.sql b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.mysql.down.sql new file mode 100644 index 0000000..50d8926 --- /dev/null +++ b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX sessions_nid_created_at_id_idx ON sessions; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.up.sql b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.up.sql new file mode 100644 index 0000000..c2b2f9e --- /dev/null +++ b/persistence/sql/migrations/sql/20240119094628000000_sessions_created_at_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX sessions_nid_created_at_id_idx ON sessions (nid, created_at DESC, id ASC); \ 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 0000000..c6e7f9d --- /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 0000000..14c295e --- /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 0000000..e69de29 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 0000000..e69de29 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 0000000..352396f --- /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 0000000..7e45f3f --- /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 0000000..c277f4d --- /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 0000000..46aa3a5 --- /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 0000000..1daec47 --- /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 0000000..73d7df8 --- /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 0000000..838a770 --- /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 0000000..c1e3580 --- /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 0000000..61741fc --- /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 0000000..6c3e7e4 --- /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 0000000..e69de29 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 0000000..70d519f --- /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 0000000..159cb60 --- /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 0000000..e69de29 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 0000000..f692e99 --- /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 0000000..d1c8cdf --- /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 0000000..e69de29 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 0000000..e69de29 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 0000000..dd8f3d4 --- /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 0000000..a953dd4 --- /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 0000000..e69de29 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 0000000..e69de29 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 0000000..14c295e --- /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 0000000..91e0c2a --- /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 0000000..e69de29 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 0000000..e69de29 diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql new file mode 100644 index 0000000..cca461b --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql @@ -0,0 +1 @@ +DROP INDEX identities_nid_organization_id_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql new file mode 100644 index 0000000..c2fbff7 --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identities_nid_organization_id_idx ON identities; diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql new file mode 100644 index 0000000..10a84cb --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX identities_nid_organization_id_idx ON identities (organization_id); diff --git a/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.down.sql b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.down.sql new file mode 100644 index 0000000..2c71ddb --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.down.sql @@ -0,0 +1,6 @@ +CREATE INDEX IF NOT EXISTS identity_credentials_id_nid_idx ON identity_credentials (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_credentials_nid_id_idx ON identity_credentials (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_credentials_nid_identity_id_idx ON identity_credentials (identity_id ASC, nid ASC); + +DROP INDEX IF EXISTS identity_credentials_identity_id_idx; +DROP INDEX IF EXISTS identity_credentials_nid_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.down.sql b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.down.sql new file mode 100644 index 0000000..b863042 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.down.sql @@ -0,0 +1,4 @@ +CREATE INDEX identity_credentials_id_nid_idx ON identity_credentials (id ASC, nid ASC); +CREATE INDEX identity_credentials_nid_id_idx ON identity_credentials (nid ASC, id ASC); + +DROP INDEX identity_credentials_nid_idx ON identity_credentials; diff --git a/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.up.sql b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.up.sql new file mode 100644 index 0000000..d53879b --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX identity_credentials_nid_idx ON identity_credentials (nid ASC); + +DROP INDEX identity_credentials_id_nid_idx ON identity_credentials; +DROP INDEX identity_credentials_nid_id_idx ON identity_credentials; + diff --git a/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.up.sql b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.up.sql new file mode 100644 index 0000000..c360d9a --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000001_drop_unused_indices_identity_credentials.up.sql @@ -0,0 +1,6 @@ +CREATE INDEX IF NOT EXISTS identity_credentials_identity_id_idx ON identity_credentials (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_credentials_nid_idx ON identity_credentials (nid ASC); + +DROP INDEX IF EXISTS identity_credentials_id_nid_idx; +DROP INDEX IF EXISTS identity_credentials_nid_id_idx; +DROP INDEX IF EXISTS identity_credentials_nid_identity_id_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.down.sql b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.down.sql new file mode 100644 index 0000000..3514f4b --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.down.sql @@ -0,0 +1,9 @@ +CREATE INDEX IF NOT EXISTS sessions_nid_id_identity_id_idx ON sessions(nid ASC, identity_id ASC, id ASC); +CREATE INDEX IF NOT EXISTS sessions_id_nid_idx ON sessions(id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS sessions_token_nid_idx ON sessions(nid ASC, token ASC); +CREATE INDEX IF NOT EXISTS sessions_identity_id_nid_sorted_idx ON sessions(identity_id ASC, nid ASC, authenticated_at DESC); +CREATE INDEX IF NOT EXISTS sessions_nid_created_at_id_idx ON sessions(nid ASC, created_at DESC, id ASC); + +DROP INDEX IF EXISTS sessions_list_idx; +DROP INDEX IF EXISTS sessions_list_active_idx; +DROP INDEX IF EXISTS sessions_list_identity_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.down.sql b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.down.sql new file mode 100644 index 0000000..357de26 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.down.sql @@ -0,0 +1,9 @@ +CREATE INDEX sessions_nid_id_identity_id_idx ON sessions(nid ASC, identity_id ASC, id ASC); +CREATE INDEX sessions_id_nid_idx ON sessions(id ASC, nid ASC); +CREATE INDEX sessions_token_nid_idx ON sessions(nid ASC, token ASC); +CREATE INDEX sessions_identity_id_nid_sorted_idx ON sessions(identity_id ASC, nid ASC, authenticated_at DESC); +CREATE INDEX sessions_nid_created_at_id_idx ON sessions(nid ASC, created_at DESC, id ASC); + +DROP INDEX sessions_list_idx ON sessions; +DROP INDEX sessions_list_active_idx ON sessions; +DROP INDEX sessions_list_identity_idx ON sessions; diff --git a/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.up.sql b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.up.sql new file mode 100644 index 0000000..fecfd83 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE INDEX sessions_list_idx ON sessions (nid ASC, created_at DESC, id ASC); +CREATE INDEX sessions_list_active_idx ON sessions (nid ASC, expires_at ASC, active ASC, created_at DESC, id ASC); +CREATE INDEX sessions_list_identity_idx ON sessions (identity_id ASC, nid ASC, created_at DESC); + +DROP INDEX sessions_nid_id_identity_id_idx ON sessions; +DROP INDEX sessions_id_nid_idx ON sessions; +DROP INDEX sessions_token_nid_idx ON sessions; +DROP INDEX sessions_identity_id_nid_sorted_idx ON sessions; +DROP INDEX sessions_nid_created_at_id_idx ON sessions; diff --git a/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.up.sql b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.up.sql new file mode 100644 index 0000000..cd51229 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000002_drop_unused_indices_sessions.up.sql @@ -0,0 +1,9 @@ +CREATE INDEX IF NOT EXISTS sessions_list_idx ON sessions (nid ASC, created_at DESC, id ASC); +CREATE INDEX IF NOT EXISTS sessions_list_active_idx ON sessions (nid ASC, expires_at ASC, active ASC, created_at DESC, id ASC); +CREATE INDEX IF NOT EXISTS sessions_list_identity_idx ON sessions (identity_id ASC, nid ASC, created_at DESC); + +DROP INDEX IF EXISTS sessions_nid_id_identity_id_idx; +DROP INDEX IF EXISTS sessions_id_nid_idx; +DROP INDEX IF EXISTS sessions_token_nid_idx; +DROP INDEX IF EXISTS sessions_identity_id_nid_sorted_idx; +DROP INDEX IF EXISTS sessions_nid_created_at_id_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.down.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.down.sql new file mode 100644 index 0000000..696d002 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.down.sql @@ -0,0 +1,11 @@ +-- THIS IS COCKROACH ONLY +ALTER INDEX identity_credential_identifiers_identifier_nid_type_uq_idx RENAME TO identity_credential_identifiers_identifier_nid_type_uq_idx_deleteme; +CREATE UNIQUE INDEX IF NOT EXISTS identity_credential_identifiers_identifier_nid_type_uq_idx ON identity_credential_identifiers(nid ASC, identity_credential_type_id ASC, identifier ASC); +DROP INDEX IF EXISTS identity_credential_identifiers_identifier_nid_type_uq_idx_deleteme; +-- + +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC, nid ASC); + +DROP INDEX IF EXISTS identity_credential_identifiers_identity_credential_id_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.up.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.up.sql new file mode 100644 index 0000000..b03363c --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.cockroach.up.sql @@ -0,0 +1,11 @@ +-- THIS IS COCKROACH ONLY +ALTER INDEX identity_credential_identifiers_identifier_nid_type_uq_idx RENAME TO identity_credential_identifiers_identifier_nid_type_uq_idx_deleteme; +CREATE UNIQUE INDEX IF NOT EXISTS identity_credential_identifiers_identifier_nid_type_uq_idx ON identity_credential_identifiers (nid ASC, identity_credential_type_id ASC, identifier ASC) STORING (identity_credential_id); +DROP INDEX IF EXISTS identity_credential_identifiers_identifier_nid_type_uq_idx_deleteme; +-- + +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC); + +DROP INDEX IF EXISTS identity_credential_identifiers_nid_id_idx; +DROP INDEX IF EXISTS identity_credential_identifiers_id_nid_idx; +DROP INDEX IF EXISTS identity_credential_identifiers_nid_identity_credential_id_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.down.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.down.sql new file mode 100644 index 0000000..dd899bc --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.down.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC, nid ASC); + +DROP INDEX IF EXISTS identity_credential_identifiers_identity_credential_id_idx; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.down.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.down.sql new file mode 100644 index 0000000..53fcfae --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.down.sql @@ -0,0 +1,5 @@ +CREATE INDEX identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers (nid ASC, id ASC); +CREATE INDEX identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers (id ASC, nid ASC); +CREATE INDEX identity_credential_identifiers_nid_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC, nid ASC); + +DROP INDEX identity_credential_identifiers_identity_credential_id_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.up.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.up.sql new file mode 100644 index 0000000..9cbfb64 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX identity_credential_identifiers_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC); + +DROP INDEX identity_credential_identifiers_nid_id_idx ON identity_credential_identifiers; +DROP INDEX identity_credential_identifiers_id_nid_idx ON identity_credential_identifiers; +DROP INDEX identity_credential_identifiers_nid_identity_credential_id_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.up.sql b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.up.sql new file mode 100644 index 0000000..9841ea6 --- /dev/null +++ b/persistence/sql/migrations/sql/20241023142500000003_drop_unused_indices_credential_identifiers.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_identity_credential_id_idx ON identity_credential_identifiers (identity_credential_id ASC); + +DROP INDEX IF EXISTS identity_credential_identifiers_nid_id_idx; +DROP INDEX IF EXISTS identity_credential_identifiers_id_nid_idx; +DROP INDEX IF EXISTS identity_credential_identifiers_nid_identity_credential_id_idx; diff --git a/persistence/sql/migrations/sql/20241029102200000001_self_service.down.sql b/persistence/sql/migrations/sql/20241029102200000001_self_service.down.sql new file mode 100644 index 0000000..9ca6f56 --- /dev/null +++ b/persistence/sql/migrations/sql/20241029102200000001_self_service.down.sql @@ -0,0 +1,26 @@ +CREATE INDEX IF NOT EXISTS selfservice_login_flows_nid_id_idx ON selfservice_login_flows (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS selfservice_login_flows_id_nid_idx ON selfservice_login_flows (id ASC, nid ASC); +DROP INDEX IF EXISTS selfservice_login_flows_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_errors_errors_nid_id_idx ON selfservice_errors (nid ASC, id ASC); +DROP INDEX IF EXISTS selfservice_errors_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS selfservice_recovery_flows_recovered_identity_id_nid_idx ON selfservice_recovery_flows (recovered_identity_id ASC, nid ASC); +DROP INDEX IF EXISTS selfservice_recovery_flows_nid_idx; +DROP INDEX IF EXISTS selfservice_recovery_flows_recovered_identity_id_idx; + +CREATE INDEX IF NOT EXISTS selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows (id ASC, nid ASC); +DROP INDEX IF EXISTS selfservice_registration_flows_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS selfservice_settings_flows_identity_id_nid_idx ON selfservice_settings_flows (identity_id ASC, nid ASC); +DROP INDEX IF EXISTS selfservice_settings_flows_nid_idx; +DROP INDEX IF EXISTS selfservice_settings_flows_identity_id_idx; + +CREATE INDEX IF NOT EXISTS selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows (id ASC, nid ASC); +DROP INDEX IF EXISTS selfservice_verification_flows_nid_idx; diff --git a/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.down.sql b/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.down.sql new file mode 100644 index 0000000..0d669e2 --- /dev/null +++ b/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.down.sql @@ -0,0 +1,26 @@ +CREATE INDEX selfservice_login_flows_nid_id_idx ON selfservice_login_flows (nid ASC, id ASC); +CREATE INDEX selfservice_login_flows_id_nid_idx ON selfservice_login_flows (id ASC, nid ASC); +DROP INDEX selfservice_login_flows_nid_idx ON selfservice_login_flows; + +CREATE INDEX selfservice_errors_errors_nid_id_idx ON selfservice_errors (nid ASC, id ASC); +DROP INDEX selfservice_errors_nid_idx ON selfservice_errors; + +CREATE INDEX selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows (nid ASC, id ASC); +CREATE INDEX selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows (id ASC, nid ASC); +CREATE INDEX selfservice_recovery_flows_recovered_identity_id_nid_idx ON selfservice_recovery_flows (recovered_identity_id ASC, nid ASC); +DROP INDEX selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows; +DROP INDEX selfservice_recovery_flows_recovered_identity_id_idx ON selfservice_recovery_flows; + +CREATE INDEX selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows (nid ASC, id ASC); +CREATE INDEX selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows (id ASC, nid ASC); +DROP INDEX selfservice_registration_flows_nid_idx ON selfservice_registration_flows; + +CREATE INDEX selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows (nid ASC, id ASC); +CREATE INDEX selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows (id ASC, nid ASC); +CREATE INDEX selfservice_settings_flows_identity_id_nid_idx ON selfservice_settings_flows (identity_id ASC, nid ASC); +DROP INDEX selfservice_settings_flows_nid_idx ON selfservice_settings_flows; +DROP INDEX selfservice_settings_flows_identity_id_idx ON selfservice_settings_flows; + +CREATE INDEX selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows (nid ASC, id ASC); +CREATE INDEX selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows (id ASC, nid ASC); +DROP INDEX selfservice_verification_flows_nid_idx ON selfservice_verification_flows; diff --git a/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.up.sql b/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.up.sql new file mode 100644 index 0000000..678658c --- /dev/null +++ b/persistence/sql/migrations/sql/20241029102200000001_self_service.mysql.up.sql @@ -0,0 +1,26 @@ +CREATE INDEX selfservice_login_flows_nid_idx ON selfservice_login_flows (nid ASC); +DROP INDEX selfservice_login_flows_nid_id_idx ON selfservice_login_flows; +DROP INDEX selfservice_login_flows_id_nid_idx ON selfservice_login_flows; + +CREATE INDEX selfservice_errors_nid_idx ON selfservice_errors (nid ASC); +DROP INDEX selfservice_errors_errors_nid_id_idx ON selfservice_errors; + +CREATE INDEX selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows (nid ASC); +CREATE INDEX selfservice_recovery_flows_recovered_identity_id_idx ON selfservice_recovery_flows (recovered_identity_id ASC); +DROP INDEX selfservice_recovery_flows_nid_id_idx ON selfservice_recovery_flows; +DROP INDEX selfservice_recovery_flows_id_nid_idx ON selfservice_recovery_flows; +DROP INDEX selfservice_recovery_flows_recovered_identity_id_nid_idx ON selfservice_recovery_flows; + +CREATE INDEX selfservice_registration_flows_nid_idx ON selfservice_registration_flows (nid ASC); +DROP INDEX selfservice_registration_flows_nid_id_idx ON selfservice_registration_flows; +DROP INDEX selfservice_registration_flows_id_nid_idx ON selfservice_registration_flows; + +CREATE INDEX selfservice_settings_flows_nid_idx ON selfservice_settings_flows (nid ASC); +CREATE INDEX selfservice_settings_flows_identity_id_idx ON selfservice_settings_flows (identity_id ASC); +DROP INDEX selfservice_settings_flows_nid_id_idx ON selfservice_settings_flows; +DROP INDEX selfservice_settings_flows_id_nid_idx ON selfservice_settings_flows; +DROP INDEX selfservice_settings_flows_identity_id_nid_idx ON selfservice_settings_flows; + +CREATE INDEX selfservice_verification_flows_nid_idx ON selfservice_verification_flows (nid ASC); +DROP INDEX selfservice_verification_flows_nid_id_idx ON selfservice_verification_flows; +DROP INDEX selfservice_verification_flows_id_nid_idx ON selfservice_verification_flows; diff --git a/persistence/sql/migrations/sql/20241029102200000001_self_service.up.sql b/persistence/sql/migrations/sql/20241029102200000001_self_service.up.sql new file mode 100644 index 0000000..e40ce8d --- /dev/null +++ b/persistence/sql/migrations/sql/20241029102200000001_self_service.up.sql @@ -0,0 +1,26 @@ +CREATE INDEX IF NOT EXISTS selfservice_login_flows_nid_idx ON selfservice_login_flows (nid ASC); +DROP INDEX IF EXISTS selfservice_login_flows_nid_id_idx; +DROP INDEX IF EXISTS selfservice_login_flows_id_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_errors_nid_idx ON selfservice_errors (nid ASC); +DROP INDEX IF EXISTS selfservice_errors_errors_nid_id_idx; + +CREATE INDEX IF NOT EXISTS selfservice_recovery_flows_recovered_identity_id_idx ON selfservice_recovery_flows (recovered_identity_id ASC); +CREATE INDEX IF NOT EXISTS selfservice_recovery_flows_nid_idx ON selfservice_recovery_flows (nid ASC); +DROP INDEX IF EXISTS selfservice_recovery_flows_nid_id_idx; +DROP INDEX IF EXISTS selfservice_recovery_flows_id_nid_idx; +DROP INDEX IF EXISTS selfservice_recovery_flows_recovered_identity_id_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_registration_flows_nid_idx ON selfservice_registration_flows (nid ASC); +DROP INDEX IF EXISTS selfservice_registration_flows_nid_id_idx; +DROP INDEX IF EXISTS selfservice_registration_flows_id_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_settings_flows_nid_idx ON selfservice_settings_flows (nid ASC); +CREATE INDEX IF NOT EXISTS selfservice_settings_flows_identity_id_idx ON selfservice_settings_flows (identity_id ASC); +DROP INDEX IF EXISTS selfservice_settings_flows_nid_id_idx; +DROP INDEX IF EXISTS selfservice_settings_flows_id_nid_idx; +DROP INDEX IF EXISTS selfservice_settings_flows_identity_id_nid_idx; + +CREATE INDEX IF NOT EXISTS selfservice_verification_flows_nid_idx ON selfservice_verification_flows (nid ASC); +DROP INDEX IF EXISTS selfservice_verification_flows_nid_id_idx; +DROP INDEX IF EXISTS selfservice_verification_flows_id_nid_idx; diff --git a/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.down.sql b/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.down.sql new file mode 100644 index 0000000..eea887f --- /dev/null +++ b/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.down.sql @@ -0,0 +1,13 @@ +CREATE INDEX IF NOT EXISTS identities_id_nid_idx ON identities (id ASC, nid ASC); + +CREATE INDEX IF NOT EXISTS identity_recovery_addresses_status_via_idx ON identity_recovery_addresses (nid ASC, via ASC, value ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_addresses_nid_identity_id_idx ON identity_recovery_addresses (identity_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses (id ASC, nid ASC); +DROP INDEX IF EXISTS identity_recovery_addresses_identity_id_idx; + +CREATE INDEX IF NOT EXISTS identity_verifiable_addresses_status_via_idx ON identity_verifiable_addresses (nid ASC, via ASC, value ASC); +CREATE INDEX IF NOT EXISTS identity_verifiable_addresses_nid_identity_id_idx ON identity_verifiable_addresses (identity_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_verifiable_addresses_nid_id_idx ON identity_verifiable_addresses (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_verifiable_addresses_id_nid_idx ON identity_verifiable_addresses (id ASC, nid ASC); +DROP INDEX IF EXISTS identity_verifiable_addresses_identity_id_idx; diff --git a/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.up.sql b/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.up.sql new file mode 100644 index 0000000..faea351 --- /dev/null +++ b/persistence/sql/migrations/sql/20241029153900000001_identities.autocommit.up.sql @@ -0,0 +1,13 @@ +DROP INDEX IF EXISTS identities_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_recovery_addresses_identity_id_idx ON identity_recovery_addresses(identity_id ASC); +DROP INDEX IF EXISTS identity_recovery_addresses_status_via_idx; +DROP INDEX IF EXISTS identity_recovery_addresses_nid_identity_id_idx; +DROP INDEX IF EXISTS identity_recovery_addresses_nid_id_idx; +DROP INDEX IF EXISTS identity_recovery_addresses_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_verifiable_addresses_identity_id_idx ON identity_verifiable_addresses (identity_id ASC); +DROP INDEX IF EXISTS identity_verifiable_addresses_status_via_idx; +DROP INDEX IF EXISTS identity_verifiable_addresses_nid_identity_id_idx; +DROP INDEX IF EXISTS identity_verifiable_addresses_nid_id_idx; +DROP INDEX IF EXISTS identity_verifiable_addresses_id_nid_idx; diff --git a/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.down.sql b/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.down.sql new file mode 100644 index 0000000..83a531c --- /dev/null +++ b/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.down.sql @@ -0,0 +1,17 @@ +CREATE INDEX identities_id_nid_idx ON identities (id ASC, nid ASC); + +CREATE INDEX identity_recovery_addresses_status_via_idx ON identity_recovery_addresses (nid ASC, via ASC, value ASC); +-- While this index did not exist in the past, it is needed in MySQL for foreign key relations. We accept +-- that this index is "unaccounted" for if we execute down and then up migrations on MySQL. +CREATE INDEX identity_recovery_addresses_identity_id_fk_idx ON identity_recovery_addresses (identity_id ASC); +CREATE INDEX identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses (nid ASC, id ASC); +CREATE INDEX identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses (id ASC, nid ASC); +DROP INDEX identity_recovery_addresses_identity_id_idx ON identity_recovery_addresses; + +CREATE INDEX identity_verifiable_addresses_status_via_idx ON identity_verifiable_addresses (nid ASC, via ASC, value ASC); +-- While this index did not exist in the past, it is needed in MySQL for foreign key relations. We accept +-- that this index is "unaccounted" for if we execute down and then up migrations on MySQL. +CREATE INDEX identity_verifiable_addresses_identity_id_fk_idx ON identity_verifiable_addresses (identity_id ASC); +CREATE INDEX identity_verifiable_addresses_nid_id_idx ON identity_verifiable_addresses (nid ASC, id ASC); +CREATE INDEX identity_verifiable_addresses_id_nid_idx ON identity_verifiable_addresses (id ASC, nid ASC); +DROP INDEX identity_verifiable_addresses_identity_id_idx ON identity_verifiable_addresses; diff --git a/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.up.sql b/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.up.sql new file mode 100644 index 0000000..8d2ce70 --- /dev/null +++ b/persistence/sql/migrations/sql/20241029153900000001_identities.mysql.up.sql @@ -0,0 +1,13 @@ +DROP INDEX identities_id_nid_idx ON identities; + +CREATE INDEX identity_recovery_addresses_identity_id_idx ON identity_recovery_addresses (identity_id ASC); +DROP INDEX identity_recovery_addresses_status_via_idx ON identity_recovery_addresses; +-- DROP INDEX identity_recovery_addresses_nid_identity_id_idx ON identity_recovery_addresses; +DROP INDEX identity_recovery_addresses_nid_id_idx ON identity_recovery_addresses; +DROP INDEX identity_recovery_addresses_id_nid_idx ON identity_recovery_addresses; + +CREATE INDEX identity_verifiable_addresses_identity_id_idx ON identity_verifiable_addresses (identity_id ASC); +DROP INDEX identity_verifiable_addresses_status_via_idx ON identity_verifiable_addresses; +-- DROP INDEX identity_verifiable_addresses_nid_identity_id_idx ON identity_verifiable_addresses; +DROP INDEX identity_verifiable_addresses_nid_id_idx ON identity_verifiable_addresses; +DROP INDEX identity_verifiable_addresses_id_nid_idx ON identity_verifiable_addresses; diff --git a/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.down.sql b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.down.sql new file mode 100644 index 0000000..487f44f --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.down.sql @@ -0,0 +1,56 @@ +CREATE INDEX IF NOT EXISTS session_devices_id_nid_idx ON session_devices (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS session_devices_session_id_nid_idx ON session_devices (session_id ASC, nid ASC); +DROP INDEX IF EXISTS session_devices_nid_idx; +DROP INDEX IF EXISTS session_devices_session_id_idx; + +CREATE INDEX IF NOT EXISTS session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code ASC, nid ASC); +CREATE INDEX IF NOT EXISTS session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id ASC, nid ASC); +DROP INDEX IF EXISTS session_token_exchanges_flow_id_nid_init_code_idx; +DROP INDEX IF EXISTS session_token_exchanges_nid_init_code_idx; + +CREATE INDEX IF NOT EXISTS courier_messages_status_idx ON courier_messages (status ASC); +CREATE INDEX IF NOT EXISTS courier_messages_nid_id_idx ON courier_messages (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS courier_messages_id_nid_idx ON courier_messages (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS courier_messages_nid_created_at_id_idx ON courier_messages (nid ASC, created_at DESC); +DROP INDEX IF EXISTS courier_messages_status_id_idx; +DROP INDEX IF EXISTS courier_messages_nid_id_created_at_idx; + +CREATE INDEX IF NOT EXISTS continuity_containers_nid_id_idx ON continuity_containers (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS continuity_containers_id_nid_idx ON continuity_containers (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS continuity_containers_identity_id_nid_idx ON continuity_containers (identity_id ASC, nid ASC); +DROP INDEX IF EXISTS continuity_containers_identity_id_idx; +DROP INDEX IF EXISTS continuity_containers_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_verification_codes_nid_flow_id_idx ON identity_verification_codes (nid ASC, selfservice_verification_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_id_nid_idx ON identity_verification_codes (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_verifiable_address_nid_idx ON identity_verification_codes (identity_verifiable_address_id ASC, nid ASC); +DROP INDEX IF EXISTS identity_verification_codes_identity_verifiable_address_id_idx; +DROP INDEX IF EXISTS identity_verification_codes_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid ASC, token ASC, used ASC, selfservice_verification_flow_id ASC); +DROP INDEX IF EXISTS identity_verification_tokens_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid ASC, selfservice_registration_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_registration_codes_id_nid_idx ON identity_registration_codes (id ASC, nid ASC); +DROP INDEX IF EXISTS identity_registration_codes_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens (nid ASC, id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_token_nid_used_idx ON identity_recovery_tokens (nid ASC, token ASC, used ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_identity_id_nid_idx ON identity_recovery_tokens (identity_id ASC, nid ASC); +DROP INDEX IF EXISTS identity_recovery_tokens_identity_id_idx; +DROP INDEX IF EXISTS identity_recovery_tokens_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_recovery_codes_nid_flow_id_idx ON identity_recovery_codes (nid ASC, selfservice_recovery_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_id_nid_idx ON identity_recovery_codes (id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_identity_id_nid_idx ON identity_recovery_codes (identity_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_identity_recovery_address_id_nid_idx ON identity_recovery_codes (identity_recovery_address_id ASC, nid ASC); +DROP INDEX IF EXISTS identity_recovery_codes_identity_recovery_address_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes_identity_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid ASC, selfservice_login_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_login_codes_id_nid_idx ON identity_login_codes (id ASC, nid ASC); +DROP INDEX IF EXISTS identity_login_codes_nid_idx; diff --git a/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.up.sql b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.up.sql new file mode 100644 index 0000000..1e9ffae --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.autocommit.up.sql @@ -0,0 +1,56 @@ +CREATE INDEX IF NOT EXISTS session_devices_nid_idx ON session_devices (nid ASC); +CREATE INDEX IF NOT EXISTS session_devices_session_id_idx ON session_devices (session_id ASC); +DROP INDEX IF EXISTS session_devices_id_nid_idx; +DROP INDEX IF EXISTS session_devices_session_id_nid_idx; + +CREATE INDEX IF NOT EXISTS session_token_exchanges_flow_id_nid_init_code_idx ON session_token_exchanges (flow_id ASC, nid ASC, init_code ASC); +CREATE INDEX IF NOT EXISTS session_token_exchanges_nid_init_code_idx ON session_token_exchanges (nid ASC, init_code ASC); +DROP INDEX IF EXISTS session_token_exchanges_nid_code_idx; +DROP INDEX IF EXISTS session_token_exchanges_nid_flow_id_idx; + +CREATE INDEX IF NOT EXISTS courier_messages_status_id_idx ON courier_messages (status ASC, id ASC); +CREATE INDEX IF NOT EXISTS courier_messages_nid_id_created_at_idx ON courier_messages (nid ASC, id ASC, created_at DESC); +DROP INDEX IF EXISTS courier_messages_status_idx; +DROP INDEX IF EXISTS courier_messages_nid_id_idx; +DROP INDEX IF EXISTS courier_messages_id_nid_idx; +DROP INDEX IF EXISTS courier_messages_nid_created_at_id_idx; + +CREATE INDEX IF NOT EXISTS continuity_containers_identity_id_idx ON continuity_containers (identity_id ASC); +CREATE INDEX IF NOT EXISTS continuity_containers_nid_idx ON continuity_containers (nid ASC); +DROP INDEX IF EXISTS continuity_containers_nid_id_idx; +DROP INDEX IF EXISTS continuity_containers_id_nid_idx; +DROP INDEX IF EXISTS continuity_containers_identity_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_verification_codes_identity_verifiable_address_id_idx ON identity_verification_codes (identity_verifiable_address_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_nid_idx ON identity_verification_codes (nid ASC); +DROP INDEX IF EXISTS identity_verification_codes_nid_flow_id_idx; +DROP INDEX IF EXISTS identity_verification_codes_id_nid_idx; +DROP INDEX IF EXISTS identity_verification_codes_verifiable_address_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_verification_tokens_nid_idx ON identity_verification_tokens (nid ASC); +DROP INDEX IF EXISTS identity_verification_tokens_nid_id_idx; +DROP INDEX IF EXISTS identity_verification_tokens_id_nid_idx; +DROP INDEX IF EXISTS identity_verification_tokens_token_nid_used_flow_id_idx; + +CREATE INDEX IF NOT EXISTS identity_registration_codes_nid_idx ON identity_registration_codes (nid ASC); +DROP INDEX IF EXISTS identity_registration_codes_nid_flow_id_idx; +DROP INDEX IF EXISTS identity_registration_codes_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_identity_id_idx ON identity_recovery_tokens (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_tokens_nid_idx ON identity_recovery_tokens (nid ASC); +DROP INDEX IF EXISTS identity_recovery_tokens_nid_id_idx; +DROP INDEX IF EXISTS identity_recovery_tokens_id_nid_idx; +DROP INDEX IF EXISTS identity_recovery_tokens_token_nid_used_idx; +DROP INDEX IF EXISTS identity_recovery_tokens_identity_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_recovery_codes_identity_recovery_address_id_idx ON identity_recovery_codes (identity_recovery_address_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_identity_id_idx ON identity_recovery_codes (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_nid_idx ON identity_recovery_codes (nid ASC); +DROP INDEX IF EXISTS identity_recovery_codes_nid_flow_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes_id_nid_idx; +DROP INDEX IF EXISTS identity_recovery_codes_identity_id_nid_idx; +DROP INDEX IF EXISTS identity_recovery_codes_identity_recovery_address_id_nid_idx; + +CREATE INDEX IF NOT EXISTS identity_login_codes_nid_idx ON identity_login_codes (nid ASC); +DROP INDEX IF EXISTS identity_login_codes_nid_flow_id_idx; +DROP INDEX IF EXISTS identity_login_codes_id_nid_idx; diff --git a/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.down.sql b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.down.sql new file mode 100644 index 0000000..61d7636 --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.down.sql @@ -0,0 +1,56 @@ +CREATE INDEX session_devices_id_nid_idx ON session_devices (nid ASC, id ASC); -- the original index is id, nid - but then we can't drop session_devices_nid_idx +CREATE INDEX session_devices_session_id_nid_idx ON session_devices (session_id ASC, nid ASC); +DROP INDEX session_devices_nid_idx ON session_devices; +DROP INDEX session_devices_session_id_idx ON session_devices; + +CREATE INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code ASC, nid ASC); +CREATE INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id ASC, nid ASC); +DROP INDEX session_token_exchanges_flow_id_nid_init_code_idx ON session_token_exchanges; +DROP INDEX session_token_exchanges_nid_init_code_idx ON session_token_exchanges; + +CREATE INDEX courier_messages_status_idx ON courier_messages (status ASC); +CREATE INDEX courier_messages_nid_id_idx ON courier_messages (nid ASC, id ASC); +CREATE INDEX courier_messages_id_nid_idx ON courier_messages (id ASC, nid ASC); +CREATE INDEX courier_messages_nid_created_at_id_idx ON courier_messages (nid ASC, created_at DESC); +DROP INDEX courier_messages_status_id_idx ON courier_messages; +DROP INDEX courier_messages_nid_id_created_at_idx ON courier_messages; + +CREATE INDEX continuity_containers_nid_id_idx ON continuity_containers (nid ASC, id ASC); +CREATE INDEX continuity_containers_id_nid_idx ON continuity_containers (id ASC, nid ASC); +CREATE INDEX continuity_containers_identity_id_nid_idx ON continuity_containers (identity_id ASC, nid ASC); +DROP INDEX continuity_containers_identity_id_idx ON continuity_containers; +DROP INDEX continuity_containers_nid_idx ON continuity_containers; + +CREATE INDEX identity_verification_codes_nid_flow_id_idx ON identity_verification_codes (nid ASC, selfservice_verification_flow_id ASC); +CREATE INDEX identity_verification_codes_id_nid_idx ON identity_verification_codes (id ASC, nid ASC); +CREATE INDEX identity_verification_codes_verifiable_address_nid_idx ON identity_verification_codes (identity_verifiable_address_id ASC, nid ASC); +DROP INDEX identity_verification_codes_verifiable_address_idx ON identity_verification_codes; +DROP INDEX identity_verification_codes_nid_idx ON identity_verification_codes; + +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid ASC, id ASC); +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id ASC, nid ASC); +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid ASC, token ASC, used ASC, selfservice_verification_flow_id ASC); +DROP INDEX identity_verification_tokens_nid_idx ON identity_verification_tokens; + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid ASC, selfservice_registration_flow_id ASC); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id ASC, nid ASC); +DROP INDEX identity_registration_codes_nid_idx ON identity_registration_codes; + +CREATE INDEX identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens (nid ASC, id ASC); +CREATE INDEX identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens (id ASC, nid ASC); +CREATE INDEX identity_recovery_tokens_token_nid_used_idx ON identity_recovery_tokens (nid ASC, token ASC, used ASC); +CREATE INDEX identity_recovery_tokens_identity_id_nid_idx ON identity_recovery_tokens (identity_id ASC, nid ASC); +DROP INDEX identity_recovery_tokens_identity_id_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_nid_idx ON identity_recovery_tokens; + +CREATE INDEX identity_recovery_codes_nid_flow_id_idx ON identity_recovery_codes (nid ASC, selfservice_recovery_flow_id ASC); +CREATE INDEX identity_recovery_codes_id_nid_idx ON identity_recovery_codes (id ASC, nid ASC); +CREATE INDEX identity_recovery_codes_identity_id_nid_idx ON identity_recovery_codes (identity_id ASC, nid ASC); +CREATE INDEX identity_recovery_codes_identity_recovery_address_id_nid_idx ON identity_recovery_codes (identity_recovery_address_id ASC, nid ASC); +DROP INDEX identity_recovery_codes_address_id_idx ON identity_recovery_codes; +DROP INDEX identity_recovery_codes_identity_id_idx ON identity_recovery_codes; +DROP INDEX identity_recovery_codes_nid_idx ON identity_recovery_codes; + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid ASC, selfservice_login_flow_id ASC); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id ASC, nid ASC); +DROP INDEX identity_login_codes_nid_idx ON identity_login_codes; diff --git a/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.up.sql b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.up.sql new file mode 100644 index 0000000..1f6a636 --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000001_remaining_unused_indices.mysql.up.sql @@ -0,0 +1,56 @@ +CREATE INDEX session_devices_nid_idx ON session_devices (nid ASC); +CREATE INDEX session_devices_session_id_idx ON session_devices (session_id ASC); +DROP INDEX session_devices_id_nid_idx ON session_devices; +DROP INDEX session_devices_session_id_nid_idx ON session_devices; + +CREATE INDEX session_token_exchanges_flow_id_nid_init_code_idx ON session_token_exchanges (flow_id ASC, nid ASC, init_code ASC); +CREATE INDEX session_token_exchanges_nid_init_code_idx ON session_token_exchanges (nid ASC, init_code ASC); +DROP INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges; +DROP INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges; + +CREATE INDEX courier_messages_status_id_idx ON courier_messages (status ASC, id ASC); +CREATE INDEX courier_messages_nid_id_created_at_idx ON courier_messages (nid ASC, id ASC, created_at DESC); +DROP INDEX courier_messages_status_idx ON courier_messages; +DROP INDEX courier_messages_nid_id_idx ON courier_messages; +DROP INDEX courier_messages_id_nid_idx ON courier_messages; +DROP INDEX courier_messages_nid_created_at_id_idx ON courier_messages; + +CREATE INDEX continuity_containers_identity_id_idx ON continuity_containers (identity_id ASC); +CREATE INDEX continuity_containers_nid_idx ON continuity_containers (nid ASC); +DROP INDEX continuity_containers_nid_id_idx ON continuity_containers; +DROP INDEX continuity_containers_id_nid_idx ON continuity_containers; +DROP INDEX continuity_containers_identity_id_nid_idx ON continuity_containers; + +CREATE INDEX identity_verification_codes_verifiable_address_idx ON identity_verification_codes (identity_verifiable_address_id ASC); +CREATE INDEX identity_verification_codes_nid_idx ON identity_verification_codes (nid ASC); +DROP INDEX identity_verification_codes_nid_flow_id_idx ON identity_verification_codes; +DROP INDEX identity_verification_codes_id_nid_idx ON identity_verification_codes; +DROP INDEX identity_verification_codes_verifiable_address_nid_idx ON identity_verification_codes; + +CREATE INDEX identity_verification_tokens_nid_idx ON identity_verification_tokens (nid ASC); +DROP INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens; +DROP INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens; +DROP INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens; + +CREATE INDEX identity_registration_codes_nid_idx ON identity_registration_codes (nid ASC); +DROP INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes; +DROP INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes; + +CREATE INDEX identity_recovery_tokens_identity_id_idx ON identity_recovery_tokens (identity_id ASC); +CREATE INDEX identity_recovery_tokens_nid_idx ON identity_recovery_tokens (nid ASC); +DROP INDEX identity_recovery_tokens_nid_id_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_id_nid_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_token_nid_used_idx ON identity_recovery_tokens; +DROP INDEX identity_recovery_tokens_identity_id_nid_idx ON identity_recovery_tokens; + +CREATE INDEX identity_recovery_codes_address_id_idx ON identity_recovery_codes (identity_recovery_address_id ASC); +CREATE INDEX identity_recovery_codes_identity_id_idx ON identity_recovery_codes (identity_id ASC); +CREATE INDEX identity_recovery_codes_nid_idx ON identity_recovery_codes (nid ASC); +DROP INDEX identity_recovery_codes_nid_flow_id_idx ON identity_recovery_codes; +DROP INDEX identity_recovery_codes_id_nid_idx ON identity_recovery_codes; +DROP INDEX identity_recovery_codes_identity_id_nid_idx ON identity_recovery_codes; +DROP INDEX identity_recovery_codes_identity_recovery_address_id_nid_idx ON identity_recovery_codes; + +CREATE INDEX identity_login_codes_nid_idx ON identity_login_codes (nid ASC); +DROP INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes; +DROP INDEX identity_login_codes_id_nid_idx ON identity_login_codes; diff --git a/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.down.sql b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.down.sql new file mode 100644 index 0000000..b31030e --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.down.sql @@ -0,0 +1 @@ +ALTER TABLE session_token_exchanges DROP CONSTRAINT session_token_exchanges_nid_fk; diff --git a/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.up.sql b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.up.sql new file mode 100644 index 0000000..1180d43 --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.autocommit.up.sql @@ -0,0 +1 @@ +ALTER TABLE session_token_exchanges ADD CONSTRAINT session_token_exchanges_nid_fk FOREIGN KEY (nid) REFERENCES networks (id) ON DELETE CASCADE; diff --git a/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.down.sql b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.down.sql new file mode 100644 index 0000000..f0d12fd --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.down.sql @@ -0,0 +1,27 @@ +-- Step 1: Create a temporary table without the nid column and foreign key constraint +CREATE TABLE session_token_exchanges_temp +( + id TEXT NOT NULL, + flow_id TEXT NOT NULL, + session_id TEXT, + init_code VARCHAR(64) NOT NULL, + return_to_code VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY (id) +); + +-- Step 2: Copy data from the original table to the temporary table (excluding the nid column) +INSERT INTO session_token_exchanges_temp (id, flow_id, session_id, init_code, return_to_code, created_at, updated_at) +SELECT id, flow_id, session_id, init_code, return_to_code, created_at, updated_at +FROM session_token_exchanges; + +-- Step 3: Drop the original table +DROP TABLE session_token_exchanges; + +-- Step 4: Rename the temporary table to the original table name +ALTER TABLE session_token_exchanges_temp RENAME TO session_token_exchanges; + +-- Step 5: Recreate indexes as needed (excluding nid) +CREATE INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code); +CREATE INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id); diff --git a/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.up.sql b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.up.sql new file mode 100644 index 0000000..210f7f9 --- /dev/null +++ b/persistence/sql/migrations/sql/20241031094100000002_foreign_key.sqlite.up.sql @@ -0,0 +1,37 @@ +-- Step 1: Create a temporary table with the new column and foreign key constraint +CREATE TABLE session_token_exchanges_temp +( + id TEXT NOT NULL, + nid TEXT NOT NULL, + flow_id TEXT NOT NULL, + session_id TEXT, + init_code VARCHAR(64) NOT NULL, + return_to_code VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (nid) REFERENCES networks (id) ON DELETE CASCADE +); + +-- Step 2: Copy data from the original table to the temporary table +INSERT INTO session_token_exchanges_temp (id, nid, flow_id, session_id, init_code, return_to_code, created_at, + updated_at) +SELECT id, + nid, + flow_id, + session_id, + init_code, + return_to_code, + created_at, + updated_at +FROM session_token_exchanges; + +-- Step 3: Drop the original table +DROP TABLE session_token_exchanges; + +-- Step 4: Rename the temporary table to the original table name +ALTER TABLE session_token_exchanges_temp RENAME TO session_token_exchanges; + +-- Step 5: Recreate indexes as needed +CREATE INDEX session_token_exchanges_nid_code_idx ON session_token_exchanges (init_code, nid); +CREATE INDEX session_token_exchanges_nid_flow_id_idx ON session_token_exchanges (flow_id, nid); diff --git a/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.down.sql b/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.down.sql new file mode 100644 index 0000000..a989898 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identity_credential_identifiers_nid_ici_i_idx; diff --git a/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.up.sql b/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.up.sql new file mode 100644 index 0000000..57c2306 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000001_identities.autocommit.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_ici_i_idx + ON identity_credential_identifiers (nid ASC, identity_credential_id ASC, identifier ASC); diff --git a/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.down.sql b/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.down.sql new file mode 100644 index 0000000..8592523 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_ici_i_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.up.sql b/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.up.sql new file mode 100644 index 0000000..a39eab6 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000001_identities.mysql.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX identity_credential_identifiers_nid_ici_i_idx + ON identity_credential_identifiers (nid ASC, identity_credential_id ASC, identifier ASC); diff --git a/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.down.sql b/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.down.sql new file mode 100644 index 0000000..d6b7145 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.down.sql @@ -0,0 +1,2 @@ + +DROP INDEX IF EXISTS identity_credential_identifiers_ici_nid_i_idx; diff --git a/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.up.sql b/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.up.sql new file mode 100644 index 0000000..75f0854 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000002_identities.autocommit.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_ici_nid_i_idx + ON identity_credential_identifiers (identity_credential_id ASC, nid ASC, identifier ASC); diff --git a/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.down.sql b/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.down.sql new file mode 100644 index 0000000..1eef66d --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_ici_nid_i_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.up.sql b/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.up.sql new file mode 100644 index 0000000..bd4b293 --- /dev/null +++ b/persistence/sql/migrations/sql/20241106142200000002_identities.mysql.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX identity_credential_identifiers_ici_nid_i_idx + ON identity_credential_identifiers (identity_credential_id ASC, nid ASC, identifier ASC); diff --git a/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.down.sql b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.down.sql new file mode 100644 index 0000000..12d9fec --- /dev/null +++ b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.down.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_ici_i_idx + ON identity_credential_identifiers (nid ASC, identity_credential_id ASC, identifier ASC); + +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_identity_credential_id_idx + ON identity_credential_identifiers (identity_credential_id ASC); diff --git a/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.up.sql b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.up.sql new file mode 100644 index 0000000..f672d72 --- /dev/null +++ b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.autocommit.up.sql @@ -0,0 +1,5 @@ +-- This index is replaced by identity_credential_identifiers_ici_nid_i_idx (included in the previous OEL release) +DROP INDEX IF EXISTS identity_credential_identifiers_nid_ici_i_idx; + +-- This index is replaced by identity_credential_identifiers_ici_nid_i_idx (included in the previous OEL release) +DROP INDEX IF EXISTS identity_credential_identifiers_identity_credential_id_idx; diff --git a/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.down.sql b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.down.sql new file mode 100644 index 0000000..6f1e2d2 --- /dev/null +++ b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.down.sql @@ -0,0 +1,5 @@ +CREATE INDEX identity_credential_identifiers_nid_ici_i_idx + ON identity_credential_identifiers (nid ASC, identity_credential_id ASC, identifier ASC); + +CREATE INDEX identity_credential_identifiers_identity_credential_id_idx + ON identity_credential_identifiers (identity_credential_id ASC); diff --git a/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.up.sql b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.up.sql new file mode 100644 index 0000000..28f9cbd --- /dev/null +++ b/persistence/sql/migrations/sql/20241108105000000001_index_cleanup.mysql.up.sql @@ -0,0 +1,5 @@ +-- This index is replaced by identity_credential_identifiers_ici_nid_i_idx (included in the previous OEL release) +DROP INDEX identity_credential_identifiers_nid_ici_i_idx ON identity_credential_identifiers; + +-- This index is replaced by identity_credential_identifiers_ici_nid_i_idx (included in the previous OEL release) +DROP INDEX identity_credential_identifiers_identity_credential_id_idx ON identity_credential_identifiers; diff --git a/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.down.sql b/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.down.sql new file mode 100644 index 0000000..fced320 --- /dev/null +++ b/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'saml'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.up.sql b/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.up.sql new file mode 100644 index 0000000..de88b5d --- /dev/null +++ b/persistence/sql/migrations/sql/20241203105600000000_saml_credential_type.up.sql @@ -0,0 +1,3 @@ +INSERT INTO identity_credential_types (id, name) +SELECT '7bddcf6c-f50e-4a18-9b0f-429114c33419', 'saml' + WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'saml'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.down.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.down.sql new file mode 100644 index 0000000..1954c6d --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_login_codes ALTER COLUMN address_type TYPE CHAR(36); +ALTER TABLE identity_registration_codes ALTER COLUMN address_type TYPE CHAR(36); diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.up.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.up.sql new file mode 100644 index 0000000..a980906 --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.autocommit.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_login_codes ALTER COLUMN address_type TYPE VARCHAR(36); +ALTER TABLE identity_registration_codes ALTER COLUMN address_type TYPE VARCHAR(36); diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.down.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.down.sql new file mode 100644 index 0000000..e5a5c53 --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_login_codes MODIFY address_type CHAR(36); +ALTER TABLE identity_registration_codes MODIFY address_type CHAR(36); diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.up.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.up.sql new file mode 100644 index 0000000..3935dc7 --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_login_codes MODIFY address_type VARCHAR(36); +ALTER TABLE identity_registration_codes MODIFY address_type VARCHAR(36); diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.down.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.down.sql new file mode 100644 index 0000000..4cfa9c7 --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE identity_login_codes ADD COLUMN address_type_old CHAR(36); +UPDATE identity_login_codes SET address_type_old = address_type; +ALTER TABLE identity_login_codes DROP COLUMN address_type; +ALTER TABLE identity_login_codes RENAME COLUMN address_type_old TO address_type; + +ALTER TABLE identity_registration_codes ADD COLUMN address_type_old CHAR(36); +UPDATE identity_registration_codes SET address_type_old = address_type; +ALTER TABLE identity_registration_codes DROP COLUMN address_type; +ALTER TABLE identity_registration_codes RENAME COLUMN address_type_old TO address_type; diff --git a/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.up.sql b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.up.sql new file mode 100644 index 0000000..6745e71 --- /dev/null +++ b/persistence/sql/migrations/sql/20250505150900000000_code_address_type.sqlite.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE identity_login_codes ADD COLUMN address_type_old VARCHAR(36); +UPDATE identity_login_codes SET address_type_old = address_type; +ALTER TABLE identity_login_codes DROP COLUMN address_type; +ALTER TABLE identity_login_codes RENAME COLUMN address_type_old TO address_type; + +ALTER TABLE identity_registration_codes ADD COLUMN address_type_old VARCHAR(36); +UPDATE identity_registration_codes SET address_type_old = address_type; +ALTER TABLE identity_registration_codes DROP COLUMN address_type; +ALTER TABLE identity_registration_codes RENAME COLUMN address_type_old TO address_type; diff --git a/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.down.sql b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.down.sql new file mode 100644 index 0000000..84b2a98 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.down.sql @@ -0,0 +1 @@ +ALTER TABLE identities DROP COLUMN external_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.up.sql b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.up.sql new file mode 100644 index 0000000..f7f1f52 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.autocommit.up.sql @@ -0,0 +1 @@ +ALTER TABLE identities ADD COLUMN external_id VARCHAR(64) NULL CHECK (external_id IS NULL OR external_id != ''); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.down.sql b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.down.sql new file mode 100644 index 0000000..518de27 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.down.sql @@ -0,0 +1 @@ +ALTER TABLE identities DROP COLUMN IF EXISTS external_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.up.sql b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.up.sql new file mode 100644 index 0000000..7cf3af3 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000000_identities_external_id.cockroach.autocommit.up.sql @@ -0,0 +1 @@ +ALTER TABLE identities ADD COLUMN IF NOT EXISTS external_id VARCHAR(64) NULL CHECK (external_id IS NULL OR external_id != ''); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.cockroach.autocommit.up.sql b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.cockroach.autocommit.up.sql new file mode 100644 index 0000000..de34ab8 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.cockroach.autocommit.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX IF NOT EXISTS identities_nid_external_id_idx + ON identities (external_id, nid) USING HASH WHERE external_id IS NOT NULL AND external_id != ''; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.down.sql b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.down.sql new file mode 100644 index 0000000..d3c94d5 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identities_nid_external_id_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.down.sql b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.down.sql new file mode 100644 index 0000000..95eb57c --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identities_nid_external_id_idx ON identities; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.up.sql b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.up.sql new file mode 100644 index 0000000..454b85b --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.mysql.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX identities_nid_external_id_idx + ON identities (nid, external_id); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.up.sql b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.up.sql new file mode 100644 index 0000000..84b7351 --- /dev/null +++ b/persistence/sql/migrations/sql/20250708190000000001_identities_external_id_index.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX IF NOT EXISTS identities_nid_external_id_idx + ON identities (nid, external_id) WHERE external_id IS NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.down.sql b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.down.sql new file mode 100644 index 0000000..fb8722d --- /dev/null +++ b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_login_flows DROP COLUMN IF EXISTS identity_schema_id; +ALTER TABLE selfservice_registration_flows DROP COLUMN IF EXISTS identity_schema_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.up.sql b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.up.sql new file mode 100644 index 0000000..1df0468 --- /dev/null +++ b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.cockroach.autocommit.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_login_flows ADD COLUMN IF NOT EXISTS identity_schema_id VARCHAR(128) NULL; +ALTER TABLE selfservice_registration_flows ADD COLUMN IF NOT EXISTS identity_schema_id VARCHAR(128) NULL; diff --git a/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.down.sql b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.down.sql new file mode 100644 index 0000000..4df7b20 --- /dev/null +++ b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_login_flows DROP COLUMN identity_schema_id; +ALTER TABLE selfservice_registration_flows DROP COLUMN identity_schema_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.up.sql b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.up.sql new file mode 100644 index 0000000..23a55fc --- /dev/null +++ b/persistence/sql/migrations/sql/20250710085000000000_add_schema_id.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE selfservice_login_flows ADD COLUMN identity_schema_id VARCHAR(128) NULL; +ALTER TABLE selfservice_registration_flows ADD COLUMN identity_schema_id VARCHAR(128) NULL; diff --git a/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.down.sql b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.down.sql new file mode 100644 index 0000000..01d5050 --- /dev/null +++ b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_credential_identifiers DROP COLUMN identity_id; +ALTER TABLE session_devices DROP COLUMN identity_id; diff --git a/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.mysql.up.sql b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.mysql.up.sql new file mode 100644 index 0000000..c1b23fd --- /dev/null +++ b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_credential_identifiers ADD COLUMN identity_id char(36) NULL; +ALTER TABLE session_devices ADD COLUMN identity_id char(36) NULL; diff --git a/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.down.sql b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.down.sql new file mode 100644 index 0000000..18adff0 --- /dev/null +++ b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.down.sql @@ -0,0 +1,50 @@ +-- For SQLite, we do all operations in a single migration for simplicity. + +CREATE TABLE "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"identity_credential_type_id" char(36) NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE NO ACTION ON DELETE CASCADE +); + +INSERT INTO _identity_credential_identifiers_tmp (id, identifier, identity_credential_id, created_at, updated_at, nid, identity_credential_type_id) + SELECT id, identifier, identity_credential_id, created_at, updated_at, nid, identity_credential_type_id + FROM identity_credential_identifiers; + +DROP TABLE identity_credential_identifiers; +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; + + +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx" ON "identity_credential_identifiers" (nid, identity_credential_type_id, identifier); +CREATE INDEX identity_credential_identifiers_nid_i_ici_idx ON "identity_credential_identifiers" (nid, identifier, identity_credential_id); +CREATE INDEX identity_credential_identifiers_ici_nid_i_idx ON "identity_credential_identifiers" (identity_credential_id ASC, nid ASC, identifier ASC); + + +CREATE TABLE IF NOT EXISTS "_session_devices_tmp" +( + "id" UUID PRIMARY KEY NOT NULL, + "ip_address" VARCHAR(50) DEFAULT '', + "user_agent" VARCHAR(512) DEFAULT '', + "location" VARCHAR(512) DEFAULT '', + "nid" UUID NOT NULL, + "session_id" UUID NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "session_metadata_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "sessions" ("id") ON DELETE cascade, + CONSTRAINT "session_metadata_nid_fk" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON DELETE cascade, + CONSTRAINT unique_session_device UNIQUE (nid, session_id, ip_address, user_agent) +); + +INSERT INTO "_session_devices_tmp" (id, ip_address, user_agent, location, nid, session_id, created_at, updated_at) + SELECT id, ip_address, user_agent, location, nid, session_id, created_at, updated_at + FROM session_devices; + +DROP TABLE session_devices; +ALTER TABLE "_session_devices_tmp" RENAME TO "session_devices"; + +CREATE INDEX session_devices_nid_idx ON session_devices (nid ASC); +CREATE INDEX session_devices_session_id_idx ON session_devices (session_id ASC); diff --git a/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql new file mode 100644 index 0000000..fc87a72 --- /dev/null +++ b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql @@ -0,0 +1,57 @@ +-- For SQLite, we do all operations in a single migration for simplicity. + +CREATE TABLE IF NOT EXISTS "_identity_credential_identifiers_tmp" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE, +FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON UPDATE RESTRICT ON DELETE CASCADE, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON UPDATE RESTRICT ON DELETE CASCADE +); + + +INSERT INTO _identity_credential_identifiers_tmp (id, identifier, identity_credential_id, created_at, updated_at, nid, identity_credential_type_id, identity_id) + SELECT ici.id, ici.identifier, ici.identity_credential_id, ici.created_at, ici.updated_at, ici.nid, ici.identity_credential_type_id, ic.identity_id + FROM identity_credential_identifiers ici + INNER JOIN identity_credentials ic ON ici.identity_credential_id = ic.id AND ici.nid = ic.nid; + +DROP TABLE identity_credential_identifiers; +ALTER TABLE "_identity_credential_identifiers_tmp" RENAME TO "identity_credential_identifiers"; + +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_nid_type_uq_idx" ON "identity_credential_identifiers" (nid, identity_credential_type_id, identifier); +CREATE INDEX identity_credential_identifiers_nid_i_ici_idx ON "identity_credential_identifiers" (nid, identifier, identity_credential_id); +CREATE INDEX identity_credential_identifiers_ici_nid_i_idx ON "identity_credential_identifiers" (identity_credential_id ASC, nid ASC, identifier ASC); + + +CREATE TABLE IF NOT EXISTS "_session_devices_tmp" +( + "id" UUID PRIMARY KEY NOT NULL, + "identity_id" UUID NOT NULL, + "ip_address" VARCHAR(50) DEFAULT '', + "user_agent" VARCHAR(512) DEFAULT '', + "location" VARCHAR(512) DEFAULT '', + "nid" UUID NOT NULL, + "session_id" UUID NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "session_metadata_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "sessions" ("id") ON DELETE cascade, + CONSTRAINT "session_metadata_nid_fk" FOREIGN KEY ("nid") REFERENCES "networks" ("id") ON DELETE cascade, + CONSTRAINT "session_devices_identity_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, + CONSTRAINT unique_session_device UNIQUE (nid, session_id, ip_address, user_agent) +); + +INSERT INTO "_session_devices_tmp" (id, identity_id, ip_address, user_agent, location, nid, session_id, created_at, updated_at) + SELECT sd.id, s.identity_id, sd.ip_address, sd.user_agent, sd.location, sd.nid, sd.session_id, sd.created_at, sd.updated_at + FROM session_devices sd JOIN sessions s ON sd.session_id = s.id; + +DROP TABLE session_devices; +ALTER TABLE "_session_devices_tmp" RENAME TO "session_devices"; + +CREATE INDEX session_devices_nid_idx ON session_devices (nid ASC); +CREATE INDEX session_devices_session_id_idx ON session_devices (session_id ASC); diff --git a/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.up.sql b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.up.sql new file mode 100644 index 0000000..3db2341 --- /dev/null +++ b/persistence/sql/migrations/sql/20251104000000000000_identifiers_devices_identity_id.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_credential_identifiers ADD COLUMN identity_id UUID NULL; +ALTER TABLE session_devices ADD COLUMN identity_id UUID NULL; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.cockroach.up.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.cockroach.up.sql new file mode 100644 index 0000000..949de05 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.cockroach.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE identity_credential_identifiers + ALTER identity_id SET NOT NULL, + ADD CONSTRAINT IF NOT EXISTS "identity_credential_identifiers_identities_id_fk" FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; + + +ALTER TABLE session_devices + ALTER identity_id SET NOT NULL, + ADD CONSTRAINT IF NOT EXISTS "session_devices_identities_id_fk" FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.down.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.down.sql new file mode 100644 index 0000000..811aeaa --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE identity_credential_identifiers + DROP CONSTRAINT identity_credential_identifiers_identities_id_fk, + ALTER identity_id DROP NOT NULL; + +ALTER TABLE session_devices + DROP CONSTRAINT session_devices_identities_id_fk, + ALTER identity_id DROP NOT NULL; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.down.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.down.sql new file mode 100644 index 0000000..97f8f39 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE identity_credential_identifiers + DROP CONSTRAINT `identity_credential_identifiers_identity_id_fk`, + MODIFY identity_id char(36) NULL; + +ALTER TABLE session_devices + DROP CONSTRAINT `session_devices_identity_id_fk`, + MODIFY identity_id char(36) NULL; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.up.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.up.sql new file mode 100644 index 0000000..9f364cd --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.mysql.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE identity_credential_identifiers + MODIFY identity_id char(36) NOT NULL, + ADD CONSTRAINT `identity_credential_identifiers_identity_id_fk` FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE session_devices + MODIFY identity_id char(36) NOT NULL, + ADD CONSTRAINT `session_devices_identity_id_fk` FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.postgres.up.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.postgres.up.sql new file mode 100644 index 0000000..e47fa53 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.postgres.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE identity_credential_identifiers + ALTER identity_id SET NOT NULL, + ADD CONSTRAINT "identity_credential_identifiers_identities_id_fk" FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; + + +ALTER TABLE session_devices + ALTER identity_id SET NOT NULL, + ADD CONSTRAINT "session_devices_identities_id_fk" FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE RESTRICT ON DELETE CASCADE; diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.down.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.down.sql new file mode 100644 index 0000000..291bc2f --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.down.sql @@ -0,0 +1 @@ +-- Nothing, all done in 20251104000000000000_identifiers_devices_identity_id.sqlite.down.sql diff --git a/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.up.sql b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.up.sql new file mode 100644 index 0000000..6dac535 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000003_identity_id_not_null_fks.sqlite.up.sql @@ -0,0 +1 @@ +-- Nothing, all done in 20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.down.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.down.sql new file mode 100644 index 0000000..ca1277f --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS identity_credential_identifiers@identity_credential_identifiers_identities_id_fk_idx; +DROP INDEX IF EXISTS session_devices@session_devices_identities_id_fk_idx; diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.up.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.up.sql new file mode 100644 index 0000000..662eae0 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.cockroach.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_identities_id_fk_idx ON identity_credential_identifiers (identity_id); +CREATE INDEX IF NOT EXISTS session_devices_identities_id_fk_idx ON session_devices (identity_id); diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.down.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.down.sql new file mode 100644 index 0000000..63a6b80 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.down.sql @@ -0,0 +1,4 @@ +-- Cannot drop index '...': needed in a foreign key constraint + +-- DROP INDEX identity_credential_identifiers_identity_id_fk_idx ON identity_credential_identifiers; +-- DROP INDEX session_devices_identity_id_fk_idx ON session_devices; diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.up.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.up.sql new file mode 100644 index 0000000..655719d --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.mysql.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX identity_credential_identifiers_identity_id_fk_idx ON identity_credential_identifiers (identity_id); +CREATE INDEX session_devices_identity_id_fk_idx ON session_devices (identity_id); diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.down.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.down.sql new file mode 100644 index 0000000..6665a18 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS identity_credential_identifiers_identities_id_fk_idx; +DROP INDEX IF EXISTS session_devices_identities_id_fk_idx; diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.up.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.up.sql new file mode 100644 index 0000000..662eae0 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.postgres.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_identities_id_fk_idx ON identity_credential_identifiers (identity_id); +CREATE INDEX IF NOT EXISTS session_devices_identities_id_fk_idx ON session_devices (identity_id); diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.down.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.down.sql new file mode 100644 index 0000000..291bc2f --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.down.sql @@ -0,0 +1 @@ +-- Nothing, all done in 20251104000000000000_identifiers_devices_identity_id.sqlite.down.sql diff --git a/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.up.sql b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.up.sql new file mode 100644 index 0000000..6dac535 --- /dev/null +++ b/persistence/sql/migrations/sql/20251105000000000004_identity_id_not_null_fks.sqlite.up.sql @@ -0,0 +1 @@ +-- Nothing, all done in 20251104000000000000_identifiers_devices_identity_id.sqlite.up.sql diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.down.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.down.sql new file mode 100644 index 0000000..ba69640 --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.down.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages DROP COLUMN IF EXISTS request_headers; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.down.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.down.sql new file mode 100644 index 0000000..bda1bb1 --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages DROP COLUMN request_headers; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.up.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.up.sql new file mode 100644 index 0000000..16f3638 --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE courier_messages + ADD COLUMN request_headers JSON; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.down.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.down.sql new file mode 100644 index 0000000..bda1bb1 --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.down.sql @@ -0,0 +1 @@ +ALTER TABLE courier_messages DROP COLUMN request_headers; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.up.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.up.sql new file mode 100644 index 0000000..17178a2 --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.sqlite.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE courier_messages + ADD COLUMN request_headers JSONB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.up.sql b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.up.sql new file mode 100644 index 0000000..0ba0eae --- /dev/null +++ b/persistence/sql/migrations/sql/20251215000000000000_courier_messages_add_request_headers.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE courier_messages + ADD COLUMN IF NOT EXISTS request_headers JSONB; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.down.sql b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.down.sql new file mode 100644 index 0000000..bc92056 --- /dev/null +++ b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.down.sql @@ -0,0 +1 @@ +CREATE INDEX identity_credentials_nid_idx ON identity_credentials (nid); diff --git a/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.down.sql b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.down.sql new file mode 100644 index 0000000..ad213f3 --- /dev/null +++ b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.down.sql @@ -0,0 +1,2 @@ +-- CREATE INDEX identity_credentials_nid_idx ON identity_credentials (nid); +-- didn't actually drop it in the up migration because "needed in a foreign key constraint" diff --git a/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.up.sql b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.up.sql new file mode 100644 index 0000000..a0b9ec7 --- /dev/null +++ b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.mysql.up.sql @@ -0,0 +1,2 @@ +-- DROP INDEX identity_credentials_nid_idx ON identity_credentials; +-- can't drop because "needed in a foreign key constraint" diff --git a/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.up.sql b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.up.sql new file mode 100644 index 0000000..8a00835 --- /dev/null +++ b/persistence/sql/migrations/sql/20260114175904000000_drop_identity_credentials_nid_idx.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identity_credentials_nid_idx; diff --git a/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.down.sql b/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.down.sql new file mode 100644 index 0000000..039c063 --- /dev/null +++ b/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'deviceauthn'; diff --git a/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.up.sql b/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.up.sql new file mode 100644 index 0000000..3628ccf --- /dev/null +++ b/persistence/sql/migrations/sql/20260127000000000001_add_deviceauthn_keys.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '26433aa2-c33a-4870-85dc-230af354e0bf', 'deviceauthn' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'deviceauthn'); diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.down.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..23a2254 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses DROP COLUMN IF EXISTS break_glass_for_organization; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.up.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..c14da13 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses ADD COLUMN IF NOT EXISTS break_glass_for_organization UUID; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.down.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.down.sql new file mode 100644 index 0000000..e2f7661 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses DROP COLUMN break_glass_for_organization; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e2f7661 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses DROP COLUMN break_glass_for_organization; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..18ea040 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses ADD COLUMN break_glass_for_organization CHAR(36); diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.down.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..23a2254 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses DROP COLUMN IF EXISTS break_glass_for_organization; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.up.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..c14da13 --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses ADD COLUMN IF NOT EXISTS break_glass_for_organization UUID; diff --git a/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.up.sql b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.up.sql new file mode 100644 index 0000000..b67f44e --- /dev/null +++ b/persistence/sql/migrations/sql/20260327101213000000_add_break_glass_to_recovery_addresses.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_addresses ADD COLUMN break_glass_for_organization UUID; diff --git a/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.down.sql b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.down.sql new file mode 100644 index 0000000..b12241c --- /dev/null +++ b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS identity_pending_traits_changes; diff --git a/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.mysql.up.sql b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.mysql.up.sql new file mode 100644 index 0000000..fbc8f37 --- /dev/null +++ b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.mysql.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE identity_pending_traits_changes ( + id CHAR(36) NOT NULL PRIMARY KEY, + identity_id CHAR(36) NOT NULL, + nid CHAR(36) NOT NULL, + new_address_value VARCHAR(255) NOT NULL, + new_address_via VARCHAR(16) NOT NULL, + original_traits_hash VARCHAR(64) NOT NULL, + proposed_traits JSON NOT NULL, + verification_flow_id CHAR(36) NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'pending', + pending_identity_key CHAR(36) GENERATED ALWAYS AS ( + CASE WHEN status = 'pending' THEN identity_id ELSE NULL END + ) VIRTUAL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT identity_pending_traits_changes_identities_id_fk FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_networks_id_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_verification_flows_id_fk FOREIGN KEY (verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE INDEX identity_pending_traits_changes_nid_identity_id_status_idx ON identity_pending_traits_changes (nid, identity_id, status); +CREATE INDEX identity_pending_traits_changes_nid_verification_flow_id_idx ON identity_pending_traits_changes (nid, verification_flow_id); +CREATE UNIQUE INDEX identity_pending_traits_changes_nid_identity_pending_idx + ON identity_pending_traits_changes (nid, pending_identity_key); diff --git a/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.sqlite3.up.sql b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.sqlite3.up.sql new file mode 100644 index 0000000..3775a44 --- /dev/null +++ b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.sqlite3.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE identity_pending_traits_changes ( + "id" TEXT NOT NULL PRIMARY KEY, + "identity_id" char(36) NOT NULL, + "nid" char(36) NOT NULL, + "new_address_value" VARCHAR(255) NOT NULL, + "new_address_via" VARCHAR(16) NOT NULL, + "original_traits_hash" VARCHAR(64) NOT NULL, + "proposed_traits" TEXT NOT NULL, + "verification_flow_id" char(36) NOT NULL, + "status" VARCHAR(16) NOT NULL DEFAULT 'pending', + "created_at" DATETIME NOT NULL, + "updated_at" DATETIME NOT NULL, + CONSTRAINT identity_pending_traits_changes_identities_id_fk FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_networks_id_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_verification_flows_id_fk FOREIGN KEY (verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE CASCADE +); + +CREATE INDEX identity_pending_traits_changes_nid_identity_id_status_idx ON identity_pending_traits_changes (nid, identity_id, status); +CREATE INDEX identity_pending_traits_changes_nid_verification_flow_id_idx ON identity_pending_traits_changes (nid, verification_flow_id); +CREATE UNIQUE INDEX identity_pending_traits_changes_nid_identity_pending_idx + ON identity_pending_traits_changes (nid, identity_id) + WHERE status = 'pending'; diff --git a/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.up.sql b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.up.sql new file mode 100644 index 0000000..2c4b73c --- /dev/null +++ b/persistence/sql/migrations/sql/20260408000000000000_create_pending_traits_changes.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE identity_pending_traits_changes ( + "id" UUID NOT NULL PRIMARY KEY, + "identity_id" UUID NOT NULL, + "nid" UUID NOT NULL, + "new_address_value" VARCHAR(255) NOT NULL, + "new_address_via" VARCHAR(16) NOT NULL, + "original_traits_hash" VARCHAR(64) NOT NULL, + "proposed_traits" jsonb NOT NULL, + "verification_flow_id" UUID NOT NULL, + "status" VARCHAR(16) NOT NULL DEFAULT 'pending', + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT identity_pending_traits_changes_identities_id_fk FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_networks_id_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_pending_traits_changes_verification_flows_id_fk FOREIGN KEY (verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE CASCADE +); + +CREATE INDEX identity_pending_traits_changes_nid_identity_id_status_idx ON identity_pending_traits_changes (nid, identity_id, status); +CREATE INDEX identity_pending_traits_changes_nid_verification_flow_id_idx ON identity_pending_traits_changes (nid, verification_flow_id); +CREATE UNIQUE INDEX identity_pending_traits_changes_nid_identity_pending_idx + ON identity_pending_traits_changes (nid, identity_id) + WHERE status = 'pending'; diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.down.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.down.sql new file mode 100644 index 0000000..752ca4c --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.down.sql @@ -0,0 +1,2 @@ +DELETE FROM identity_verification_tokens WHERE identity_verifiable_address_id IS NULL; +ALTER TABLE identity_verification_tokens ALTER COLUMN identity_verifiable_address_id SET NOT NULL; diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.down.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.down.sql new file mode 100644 index 0000000..450d6c6 --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.down.sql @@ -0,0 +1,2 @@ +DELETE FROM identity_verification_tokens WHERE identity_verifiable_address_id IS NULL; +ALTER TABLE identity_verification_tokens MODIFY identity_verifiable_address_id CHAR(36) NOT NULL; diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.up.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.up.sql new file mode 100644 index 0000000..b01dcb7 --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verification_tokens MODIFY identity_verifiable_address_id CHAR(36) NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.down.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.down.sql new file mode 100644 index 0000000..60aec6f --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.down.sql @@ -0,0 +1,32 @@ +DELETE FROM identity_verification_tokens WHERE identity_verifiable_address_id IS NULL; + +ALTER TABLE identity_verification_tokens +RENAME TO _identity_verification_tokens_old; + +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); + +INSERT INTO identity_verification_tokens +SELECT * FROM _identity_verification_tokens_old; + +DROP TABLE _identity_verification_tokens_old; + +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid, token, used, selfservice_verification_flow_id); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.up.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.up.sql new file mode 100644 index 0000000..84dd48a --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.sqlite3.up.sql @@ -0,0 +1,30 @@ +ALTER TABLE identity_verification_tokens +RENAME TO _identity_verification_tokens_old; + +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36), +"selfservice_verification_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"nid" char(36), +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); + +INSERT INTO identity_verification_tokens +SELECT * FROM _identity_verification_tokens_old; + +DROP TABLE _identity_verification_tokens_old; + +CREATE INDEX identity_verification_tokens_id_nid_idx ON identity_verification_tokens (id, nid); +CREATE INDEX identity_verification_tokens_nid_id_idx ON identity_verification_tokens (nid, id); +CREATE INDEX identity_verification_tokens_token_nid_used_flow_id_idx ON identity_verification_tokens (nid, token, used, selfservice_verification_flow_id); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); diff --git a/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.up.sql b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.up.sql new file mode 100644 index 0000000..ecb8910 --- /dev/null +++ b/persistence/sql/migrations/sql/20260413000000000000_make_verification_token_address_nullable.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verification_tokens ALTER COLUMN identity_verifiable_address_id DROP NOT NULL; diff --git a/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.down.sql b/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.down.sql new file mode 100644 index 0000000..5318402 --- /dev/null +++ b/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.down.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_login_flows DROP COLUMN test_flow; diff --git a/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.up.sql b/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.up.sql new file mode 100644 index 0000000..2e5b418 --- /dev/null +++ b/persistence/sql/migrations/sql/20260417000000000001_add_test_flow_to_selfservice_login_flows.up.sql @@ -0,0 +1 @@ +ALTER TABLE selfservice_login_flows ADD COLUMN test_flow BOOL; diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.down.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.down.sql new file mode 100644 index 0000000..fb8cea8 --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.down.sql @@ -0,0 +1,11 @@ +DROP INDEX IF EXISTS identity_pending_traits_changes_nid_origin_settings_flow_id_idx; +DROP INDEX IF EXISTS identity_pending_traits_changes_nid_session_id_idx; + +ALTER TABLE identity_pending_traits_changes + DROP CONSTRAINT IF EXISTS identity_pending_traits_changes_settings_flow_id_fk; + +ALTER TABLE identity_pending_traits_changes + DROP CONSTRAINT IF EXISTS identity_pending_traits_changes_sessions_id_fk; + +ALTER TABLE identity_pending_traits_changes DROP COLUMN IF EXISTS origin_settings_flow_id; +ALTER TABLE identity_pending_traits_changes DROP COLUMN IF EXISTS session_id; diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.down.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.down.sql new file mode 100644 index 0000000..76d0397 --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.down.sql @@ -0,0 +1,8 @@ +DROP INDEX identity_pending_traits_changes_nid_origin_settings_flow_id_idx ON identity_pending_traits_changes; +DROP INDEX identity_pending_traits_changes_nid_session_id_idx ON identity_pending_traits_changes; + +ALTER TABLE identity_pending_traits_changes + DROP FOREIGN KEY identity_pending_traits_changes_settings_flow_id_fk, + DROP FOREIGN KEY identity_pending_traits_changes_sessions_id_fk, + DROP COLUMN origin_settings_flow_id, + DROP COLUMN session_id; diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.up.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.up.sql new file mode 100644 index 0000000..d87e8f5 --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.mysql.up.sql @@ -0,0 +1,13 @@ +ALTER TABLE identity_pending_traits_changes + ADD COLUMN session_id CHAR(36) NULL, + ADD CONSTRAINT identity_pending_traits_changes_sessions_id_fk + FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE SET NULL, + ADD COLUMN origin_settings_flow_id CHAR(36) NULL, + ADD CONSTRAINT identity_pending_traits_changes_settings_flow_id_fk + FOREIGN KEY (origin_settings_flow_id) REFERENCES selfservice_settings_flows (id) ON DELETE CASCADE; + +CREATE INDEX identity_pending_traits_changes_nid_session_id_idx + ON identity_pending_traits_changes (nid, session_id); + +CREATE INDEX identity_pending_traits_changes_nid_origin_settings_flow_id_idx + ON identity_pending_traits_changes (nid, origin_settings_flow_id); diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.down.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.down.sql new file mode 100644 index 0000000..c5a7af0 --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS identity_pending_traits_changes_nid_origin_settings_flow_id_idx; +DROP INDEX IF EXISTS identity_pending_traits_changes_nid_session_id_idx; + +ALTER TABLE identity_pending_traits_changes DROP COLUMN origin_settings_flow_id; +ALTER TABLE identity_pending_traits_changes DROP COLUMN session_id; diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.up.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.up.sql new file mode 100644 index 0000000..62ca76c --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.sqlite3.up.sql @@ -0,0 +1,11 @@ +ALTER TABLE identity_pending_traits_changes + ADD COLUMN "session_id" char(36) NULL REFERENCES sessions (id) ON DELETE SET NULL; + +ALTER TABLE identity_pending_traits_changes + ADD COLUMN "origin_settings_flow_id" char(36) NULL REFERENCES selfservice_settings_flows (id) ON DELETE CASCADE; + +CREATE INDEX identity_pending_traits_changes_nid_session_id_idx + ON identity_pending_traits_changes (nid, session_id); + +CREATE INDEX identity_pending_traits_changes_nid_origin_settings_flow_id_idx + ON identity_pending_traits_changes (nid, origin_settings_flow_id); diff --git a/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.up.sql b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.up.sql new file mode 100644 index 0000000..27e947e --- /dev/null +++ b/persistence/sql/migrations/sql/20260422000000000000_pending_traits_changes_add_session.up.sql @@ -0,0 +1,13 @@ +ALTER TABLE identity_pending_traits_changes + ADD COLUMN "session_id" UUID NULL, + ADD CONSTRAINT identity_pending_traits_changes_sessions_id_fk + FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE SET NULL, + ADD COLUMN "origin_settings_flow_id" UUID NULL, + ADD CONSTRAINT identity_pending_traits_changes_settings_flow_id_fk + FOREIGN KEY (origin_settings_flow_id) REFERENCES selfservice_settings_flows (id) ON DELETE CASCADE; + +CREATE INDEX identity_pending_traits_changes_nid_session_id_idx + ON identity_pending_traits_changes (nid, session_id); + +CREATE INDEX identity_pending_traits_changes_nid_origin_settings_flow_id_idx + ON identity_pending_traits_changes (nid, origin_settings_flow_id); diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.down.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.down.sql new file mode 100644 index 0000000..ff96520 --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.down.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows drop column if exists organization_id; diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.down.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.down.sql new file mode 100644 index 0000000..4b30576 --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.down.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows drop column organization_id; diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.up.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.up.sql new file mode 100644 index 0000000..5c16899 --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.mysql.up.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows add column organization_id char(36) null; diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.down.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.down.sql new file mode 100644 index 0000000..4b30576 --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.down.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows drop column organization_id; diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.up.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.up.sql new file mode 100644 index 0000000..557ce46 --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.sqlite.up.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows add column organization_id uuid null; diff --git a/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.up.sql b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.up.sql new file mode 100644 index 0000000..b7639bf --- /dev/null +++ b/persistence/sql/migrations/sql/20260430000000000000_settings_flows_add_organization_id.up.sql @@ -0,0 +1 @@ +alter table selfservice_settings_flows add column if not exists organization_id uuid null; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.down.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.down.sql new file mode 100644 index 0000000..281d48b --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN IF EXISTS "internal_context"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN IF EXISTS "internal_context"; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.up.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.up.sql new file mode 100644 index 0000000..bfa87e1 --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.cockroach.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN IF NOT EXISTS "internal_context" json; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN IF NOT EXISTS "internal_context" json; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.down.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.down.sql new file mode 100644 index 0000000..8e9ff21 --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_recovery_flows` DROP COLUMN `internal_context`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `internal_context`; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.up.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.up.sql new file mode 100644 index 0000000..a1e282a --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_recovery_flows` ADD COLUMN `internal_context` JSON; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `internal_context` JSON; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.down.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.down.sql new file mode 100644 index 0000000..281d48b --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN IF EXISTS "internal_context"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN IF EXISTS "internal_context"; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.up.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.up.sql new file mode 100644 index 0000000..200604f --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.postgres.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN IF NOT EXISTS "internal_context" jsonb; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN IF NOT EXISTS "internal_context" jsonb; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.down.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.down.sql new file mode 100644 index 0000000..580d79d --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" DROP COLUMN "internal_context"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "internal_context"; diff --git a/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.up.sql b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.up.sql new file mode 100644 index 0000000..26f9a5d --- /dev/null +++ b/persistence/sql/migrations/sql/20260506000000000000_add_internal_context_to_recovery_verification_flows.sqlite3.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_recovery_flows" ADD COLUMN "internal_context" TEXT; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "internal_context" TEXT; diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.down.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.down.sql new file mode 100644 index 0000000..97aa028 --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS courier_messages_nid_created_at_id_idx; diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.up.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.up.sql new file mode 100644 index 0000000..9b2c8c2 --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.autocommit.up.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS courier_messages_nid_created_at_id_idx ON courier_messages (nid ASC, created_at DESC, id ASC); diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.down.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.down.sql new file mode 100644 index 0000000..2109c3d --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX courier_messages_nid_created_at_id_idx ON courier_messages; diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.up.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.up.sql new file mode 100644 index 0000000..a7b3366 --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX courier_messages_nid_created_at_id_idx ON courier_messages (nid ASC, created_at DESC, id ASC); diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.down.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.down.sql new file mode 100644 index 0000000..994f875 --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.down.sql @@ -0,0 +1 @@ +DROP INDEX CONCURRENTLY IF EXISTS courier_messages_nid_created_at_id_idx; diff --git a/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.up.sql b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.up.sql new file mode 100644 index 0000000..38efddb --- /dev/null +++ b/persistence/sql/migrations/sql/20260616000000000000_courier_messages_restore_list_index.postgres.autocommit.up.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS courier_messages_nid_created_at_id_idx ON courier_messages (nid ASC, created_at DESC, id ASC); diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.down.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.down.sql new file mode 100644 index 0000000..6df5bff --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS courier_messages_status_created_at_idx; diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.up.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.up.sql new file mode 100644 index 0000000..61c2632 --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.autocommit.up.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS courier_messages_status_created_at_idx ON courier_messages (status ASC, created_at ASC); diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.down.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.down.sql new file mode 100644 index 0000000..4eee745 --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX courier_messages_status_created_at_idx ON courier_messages; diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.up.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.up.sql new file mode 100644 index 0000000..6931739 --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX courier_messages_status_created_at_idx ON courier_messages (status ASC, created_at ASC); diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.down.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.down.sql new file mode 100644 index 0000000..7d7b654 --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.down.sql @@ -0,0 +1 @@ +DROP INDEX CONCURRENTLY IF EXISTS courier_messages_status_created_at_idx; diff --git a/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.up.sql b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.up.sql new file mode 100644 index 0000000..d936d28 --- /dev/null +++ b/persistence/sql/migrations/sql/20260703000000000000_courier_messages_status_created_at_idx.postgres.autocommit.up.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS courier_messages_status_created_at_idx ON courier_messages (status ASC, created_at ASC); diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go new file mode 100644 index 0000000..92c3460 --- /dev/null +++ b/persistence/sql/persister.go @@ -0,0 +1,237 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "embed" + "io/fs" + "slices" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/persistence/sql/devices" + idpersistence "github.com/ory/kratos/persistence/sql/identity" + gomigrations "github.com/ory/kratos/persistence/sql/migrations/go" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/session" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/contextx" + "github.com/ory/x/fsx" + "github.com/ory/x/logrusx" + "github.com/ory/x/networkx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" +) + +var _ persistence.Persister = new(Persister) + +//go:embed migrations/sql/*.sql +var Migrations embed.FS + +type ( + persisterDependencies interface { + logrusx.Provider + config.Provider + clock.Provider + contextx.Provider + otelx.Provider + schema.IdentitySchemaProvider + identity.ValidationProvider + } + Persister struct { + nid uuid.UUID + c *pop.Connection + mb *popx.MigrationBox + mbs popx.MigrationStatuses + r persisterDependencies + + identity.PrivilegedPool + session.DevicePersister + } +) + +type options struct { + extraMigrations []fs.FS + extraGoMigrations popx.Migrations + disableLogging bool +} + +type Option = func(o *options) + +func WithExtraMigrations(fss ...fs.FS) Option { + return func(o *options) { + o.extraMigrations = fss + } +} + +func WithExtraGoMigrations(ms ...popx.Migration) Option { + return func(o *options) { + o.extraGoMigrations = ms + } +} + +func WithDisabledLogging(v bool) Option { + return func(o *options) { + o.disableLogging = v + } +} + +func NewPersister(r persisterDependencies, c *pop.Connection, opts ...Option) (*Persister, error) { + o := &options{} + for _, f := range opts { + f(o) + } + logger := r.Logger() + if o.disableLogging { + logger.Logrus().SetLevel(logrus.WarnLevel) + } + m, err := popx.NewMigrationBox( + fsx.Merge(append([]fs.FS{Migrations, networkx.Migrations}, o.extraMigrations...)...), + c, logger, + popx.WithGoMigrations(slices.Concat(gomigrations.All, o.extraGoMigrations)), + ) + if err != nil { + return nil, err + } + + return &Persister{ + c: c, + mb: m, + r: r, + PrivilegedPool: idpersistence.NewPersister(r, c), + DevicePersister: devices.NewPersister(r, c), + }, nil +} + +func (p *Persister) NetworkID(ctx context.Context) uuid.UUID { + return p.r.Contextualizer().Network(ctx, p.nid) +} + +func (p Persister) WithNetworkID(nid uuid.UUID) persistence.Persister { + p.nid = nid + if pp, ok := p.PrivilegedPool.(interface { + WithNetworkID(uuid.UUID) identity.PrivilegedPool + }); ok { + p.PrivilegedPool = pp.WithNetworkID(nid) + } + if dp, ok := p.DevicePersister.(interface { + WithNetworkID(uuid.UUID) session.DevicePersister + }); ok { + p.DevicePersister = dp.WithNetworkID(nid) + } + return &p +} + +func (p *Persister) DetermineNetwork(ctx context.Context) (*networkx.Network, error) { + return networkx.Determine(p.Connection(ctx)) +} + +func (p *Persister) Connection(ctx context.Context) *pop.Connection { + return p.c.WithContext(ctx) +} + +func (p *Persister) MigrationStatus(ctx context.Context) (_ popx.MigrationStatuses, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.MigrationStatus") + defer otelx.End(span, &err) + + if p.mbs != nil { + return p.mbs, nil + } + + status, err := p.mb.Status(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + if !status.HasPending() { + p.mbs = status + } + + return status, nil +} + +func (p *Persister) MigrateDown(ctx context.Context, steps int) error { + return p.mb.Down(ctx, steps) +} + +func (p *Persister) MigrateUp(ctx context.Context) error { + return p.mb.Up(ctx) +} + +func (p *Persister) MigrationBox() *popx.MigrationBox { + return p.mb +} + +func (p *Persister) Close(ctx context.Context) error { + return errors.WithStack(p.GetConnection(ctx).Close()) +} + +func (p *Persister) Ping(ctx context.Context) error { + return errors.WithStack(p.c.Store.SQLDB().PingContext(ctx)) +} + +func (p *Persister) CleanupDatabase(ctx context.Context, wait time.Duration, older time.Duration, batchSize int) error { + currentTime := time.Now().Add(-older) + p.r.Logger().Printf("Cleaning up records older than %s\n", currentTime) + + p.r.Logger().Println("Cleaning up expired sessions") + if err := p.DeleteExpiredSessions(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired continuity containers") + if err := p.DeleteExpiredContinuitySessions(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired login flows") + if err := p.DeleteExpiredLoginFlows(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired recovery flows") + if err := p.DeleteExpiredRecoveryFlows(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired registration flows") + if err := p.DeleteExpiredRegistrationFlows(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired settings flows") + if err := p.DeleteExpiredSettingsFlows(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired verification flows") + if err := p.DeleteExpiredVerificationFlows(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Cleaning up expired session token exchangers") + if err := p.DeleteExpiredExchangers(ctx, currentTime, batchSize); err != nil { + return err + } + time.Sleep(wait) + + p.r.Logger().Println("Successfully cleaned up the latest batch of the SQL database! " + + "This should be re-run periodically, to be sure that all expired data is purged.") + return nil +} diff --git a/persistence/sql/persister_cleanup_test.go b/persistence/sql/persister_cleanup_test.go new file mode 100644 index 0000000..c09e41d --- /dev/null +++ b/persistence/sql/persister_cleanup_test.go @@ -0,0 +1,176 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" +) + +func TestPersister_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup", func(t *testing.T) { + assert.Nil(t, p.CleanupDatabase(ctx, 0, 0, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.CleanupDatabase(ctx, 0, 0, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Continuity_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup continuity sessions", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredContinuitySessions(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup continuity sessions", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredContinuitySessions(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Login_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup login flows", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredLoginFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup login flows", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredLoginFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Recovery_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup recovery flows", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredRecoveryFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup recovery flows", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredRecoveryFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Registration_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup registration flows", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredRegistrationFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup registration flows", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredRegistrationFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Session_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup sessions", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredSessions(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup sessions", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredSessions(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Settings_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup setting flows", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredSettingsFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup setting flows", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredSettingsFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_Verification_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup verification flows", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredVerificationFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup verification flows", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredVerificationFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} + +func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + currentTime := time.Now() + ctx := context.Background() + + t.Run("case=should not throw error on cleanup session token exchangers", func(t *testing.T) { + assert.Nil(t, p.DeleteExpiredExchangers(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) + + t.Run("case=should throw error on cleanup session token exchangers if DB is closed", func(t *testing.T) { + require.NoError(t, p.GetConnection(ctx).Close()) + assert.Error(t, p.DeleteExpiredExchangers(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx))) + }) +} diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go new file mode 100644 index 0000000..77cea81 --- /dev/null +++ b/persistence/sql/persister_code.go @@ -0,0 +1,176 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "crypto/subtle" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +type oneTimeCodeProvider interface { + GetID() uuid.UUID + Validate(clock.Clock) error + TableName(ctx context.Context) string + GetHMACCode() string +} + +type codeOptions struct { + IdentityID *uuid.UUID +} + +type codeOption func(o *codeOptions) + +func withCheckIdentityID(id uuid.UUID) codeOption { + return func(o *codeOptions) { + o.IdentityID = &id + } +} + +func useOneTimeCode[P any, U interface { + *P + oneTimeCodeProvider +}](ctx context.Context, p *Persister, flowID uuid.UUID, userProvidedCode, flowTableName, foreignKeyName string, opts ...codeOption, +) (target U, err error) { + maxSubmissions := p.r.Config().SelfServiceCodeMethodMaxSubmissions(ctx) + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.useOneTimeCode", trace.WithAttributes(attribute.Int("max_submissions", maxSubmissions))) + defer otelx.End(span, &err) + + o := new(codeOptions) + for _, opt := range opts { + opt(o) + } + + // Before we do anything else, increment the submit count and check if we're + // being brute-forced. This is a separate statement/transaction to the rest + // of the operations so that it is correct for all transaction isolation + // levels. + submitCount, err := incrementOTPCodeSubmitCount(ctx, p, flowID, flowTableName) + if err != nil { + return nil, err + } + + if submitCount > maxSubmissions { + return nil, errors.WithStack(code.ErrCodeSubmittedTooOften()) + } + + nid := p.NetworkID(ctx) + + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + var codes []U + codesQuery := tx.Where(fmt.Sprintf("nid = ? AND %s = ?", foreignKeyName), nid, flowID) + if o.IdentityID != nil { + codesQuery = codesQuery.Where("identity_id = ?", *o.IdentityID) + } + + if err := sqlcon.HandleError(codesQuery.All(&codes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + return errors.WithStack(code.ErrCodeNotFound()) + } + return err + } + + secrets: + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(hmacValueWithSecret(userProvidedCode, secret)) + for i := range codes { + c := codes[i] + if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { + // Not the supplied code + continue + } + target = c + break secrets + } + } + + if target.Validate(p.r.Clock()) != nil { + // Return no error, as that would roll back the transaction. We re-validate the code after the transaction. + return nil + } + + // Consume the code atomically. The `used_at IS NULL` guard ensures that + // on READ COMMITTED backends two concurrent submissions of the same code + // cannot both succeed: only the first UPDATE affects a row. If no row was + // affected, another transaction already consumed the code. + //#nosec G201 -- TableName is static + count, err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ? AND used_at IS NULL", target.TableName(ctx)), time.Now().UTC(), target.GetID(), nid).ExecWithCount() + if err != nil { + return err + } + if count == 0 { + return errors.WithStack(code.ErrCodeAlreadyUsed()) + } + return nil + }); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := target.Validate(p.r.Clock()); err != nil { + return nil, err + } + + return target, nil +} + +func incrementOTPCodeSubmitCount(ctx context.Context, p *Persister, flowID uuid.UUID, flowTableName string) (submitCount int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.incrementOTPCodeSubmitCount", + trace.WithAttributes(attribute.Stringer("flow_id", flowID), attribute.String("flow_table_name", flowTableName))) + defer otelx.End(span, &err) + defer func() { + span.SetAttributes(attribute.Int("submit_count", submitCount)) + }() + + nid := p.NetworkID(ctx) + + // The branch below is a marginal performance optimization for databases + // supporting RETURNING (one query instead of two). There is no real + // security difference here, but there is an observable difference in + // behavior. + // + // Databases supporting RETURNING will perform the increment+select + // atomically. That means that always exactly 5 attempts will be allowed for + // each flow, no matter how many concurrent attempts are made. + // + // Databases without support for RETURNING (MySQL) will perform the UPDATE + // and SELECT in two queries, which are not atomic. The effect is that there + // will still never be more than 5 attempts for each flow, but there may be + // fewer before we reject. Under normal operation, this is never a problem + // because a human will never submit their code as quickly as would be + // required to trigger this race condition. + // + // In a very strict sense of the word, the MySQL implementation is even more + // secure than the RETURNING implementation. But we're ok either way :) + if p.c.Dialect.Name() == "mysql" { + //#nosec G201 -- TableName is static + qUpdate := fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName) + if err := p.GetConnection(ctx).RawQuery(qUpdate, flowID, nid).Exec(); err != nil { + return 0, sqlcon.HandleError(err) + } + //#nosec G201 -- TableName is static + qSelect := fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName) + err = sqlcon.HandleError(p.GetConnection(ctx).RawQuery(qSelect, flowID, nid).First(&submitCount)) + } else { + //#nosec G201 -- TableName is static + q := fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ? RETURNING submit_count", flowTableName) + err = sqlcon.HandleError(p.Connection(ctx).RawQuery(q, flowID, nid).First(&submitCount)) + } + if errors.Is(err, sqlcon.ErrNoRows()) { + return 0, errors.WithStack(code.ErrCodeNotFound()) + } + + return submitCount, err +} diff --git a/persistence/sql/persister_continuity.go b/persistence/sql/persister_continuity.go new file mode 100644 index 0000000..36ce9e5 --- /dev/null +++ b/persistence/sql/persister_continuity.go @@ -0,0 +1,88 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/gofrs/uuid" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/continuity" +) + +var _ continuity.Persister = new(Persister) + +func (p *Persister) SaveContinuitySession(ctx context.Context, c *continuity.Container) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.SaveContinuitySession") + defer otelx.End(span, &err) + + c.NID = p.NetworkID(ctx) + 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) + + var c continuity.Container + if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&c); err != nil { + return nil, sqlcon.HandleError(err) + } + return &c, nil +} + +func (p *Persister) DeleteContinuitySession(ctx context.Context, id uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteContinuitySession") + defer otelx.End(span, &err) + + if count, err := p.GetConnection(ctx).RawQuery( + //#nosec G201 -- TableName is static + fmt.Sprintf("DELETE FROM %s WHERE id=? AND nid=?", + continuity.Container{}.TableName()), id, p.NetworkID(ctx)).ExecWithCount(); err != nil { + return sqlcon.HandleError(err) + } else if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +func (p *Persister) DeleteExpiredContinuitySessions(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredContinuitySessions") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + continuity.Container{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_courier.go b/persistence/sql/persister_courier.go new file mode 100644 index 0000000..a66218a --- /dev/null +++ b/persistence/sql/persister_courier.go @@ -0,0 +1,229 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/otelx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/sqlcon" + "github.com/ory/x/uuidx" +) + +var _ courier.Persister = new(Persister) + +func (p *Persister) AddMessage(ctx context.Context, m *courier.Message) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.AddMessage") + defer otelx.End(span, &err) + + m.NID = p.NetworkID(ctx) + m.Status = courier.MessageStatusQueued + return sqlcon.HandleError(p.GetConnection(ctx).Create(m)) // do not create eager to avoid identity injection. +} + +func (p *Persister) ListMessages(ctx context.Context, filter courier.ListCourierMessagesParameters, opts []keysetpagination.Option) (_ []courier.Message, _ *keysetpagination.Paginator, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListMessages") + defer otelx.End(span, &err) + + q := p.GetConnection(ctx).Where("nid=?", p.NetworkID(ctx)) + + if filter.Status != nil { + q = q.Where("status=?", *filter.Status) + } + + if filter.Recipient != "" { + q = q.Where("recipient=?", filter.Recipient) + } + + opts = append(opts, keysetpagination.WithDefaultToken(courier.Message{}.DefaultPageToken())) + opts = append(opts, keysetpagination.WithDefaultSize(10)) + paginator, err := keysetpagination.NewPaginator(opts...) + if err != nil { + return nil, nil, err + } + + messages := make([]courier.Message, paginator.Size()) + if err := q.Scope(keysetpagination.Paginate[courier.Message](paginator)). + All(&messages); err != nil { + return nil, nil, sqlcon.HandleError(err) + } + + messages, nextPage := keysetpagination.Result(messages, paginator) + + return messages, nextPage, nil +} + +func (p *Persister) NextMessages(ctx context.Context, limit uint8) (messages []courier.Message, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.NextMessages") + defer otelx.End(span, &err) + + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + q := tx.Where("nid = ? AND status = ?", + p.NetworkID(ctx), + courier.MessageStatusQueued, + ) + if tx.Dialect.Name() == dbal.DriverCockroachDB { + // On CockroachDB, exclude messages enqueued within the last second. + // Concurrent INSERTs land at created_at ~ now; keeping them out of + // the scanned span avoids read-write contention between this poll + // and message enqueueing, which surfaces as transaction retries + // (ReadWithinUncertaintyIntervalError). Skipped messages are picked + // up by a subsequent poll. The other databases serve this query + // from an MVCC snapshot that does not conflict with concurrent + // inserts, so they keep the exact current behavior. + q = q.Where("created_at < ?", time.Now().UTC().Add(-time.Second)) + } + + var m []courier.Message + if err := q. + Order("created_at ASC"). + Limit(int(limit)). + All(&m); err != nil { + return err + } + + if len(m) == 0 { + return sql.ErrNoRows + } + + for i := range m { + message := &m[i] + message.Status = courier.MessageStatusProcessing + if err := update.Generic(ctx, tx, p.r.Tracer(ctx).Tracer(), message, "status"); err != nil { + return err + } + } + + messages = m + return nil + }); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, errors.WithStack(courier.ErrQueueEmpty) + } + return nil, sqlcon.HandleError(err) + } + + if len(messages) == 0 { + return nil, errors.WithStack(courier.ErrQueueEmpty) + } + + return messages, nil +} + +func (p *Persister) LatestQueuedMessage(ctx context.Context) (_ *courier.Message, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.LatestQueuedMessage") + defer otelx.End(span, &err) + + var m courier.Message + if err := p.GetConnection(ctx). + Where("nid = ? AND status = ?", + p.NetworkID(ctx), + courier.MessageStatusQueued, + ). + Order("created_at DESC"). + First(&m); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, errors.WithStack(courier.ErrQueueEmpty) + } + return nil, sqlcon.HandleError(err) + } + + return &m, nil +} + +func (p *Persister) SetMessageStatus(ctx context.Context, id uuid.UUID, ms courier.MessageStatus) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.SetMessageStatus") + defer otelx.End(span, &err) + + count, err := p.GetConnection(ctx).RawQuery( + "UPDATE courier_messages SET status = ? WHERE id = ? AND nid = ?", + ms, + id, + p.NetworkID(ctx), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + + return nil +} + +func (p *Persister) IncrementMessageSendCount(ctx context.Context, id uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.IncrementMessageSendCount") + defer otelx.End(span, &err) + + count, err := p.GetConnection(ctx).RawQuery( + "UPDATE courier_messages SET send_count = send_count + 1 WHERE id = ? AND nid = ?", + id, + p.NetworkID(ctx), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + + return nil +} + +func (p *Persister) FetchMessage(ctx context.Context, msgID uuid.UUID) (_ *courier.Message, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FetchMessage") + defer otelx.End(span, &err) + + var message courier.Message + if err := p.GetConnection(ctx). + Where("id = ? AND nid = ?", msgID, p.NetworkID(ctx)). + Eager(). + First(&message); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &message, nil +} + +func (p *Persister) RecordDispatch(ctx context.Context, msgID uuid.UUID, status courier.CourierMessageDispatchStatus, err error) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RecordDispatch") + defer otelx.End(span, &err) + + dispatch := courier.MessageDispatch{ + ID: uuidx.NewV4(), + MessageID: msgID, + Status: status, + NID: p.NetworkID(ctx), + } + + if err != nil { + // We use herodot as a carrier for the error's data + her := herodot.ToDefaultError(err, "") + content, mErr := json.Marshal(her) + if mErr != nil { + return errors.WithStack(mErr) + } + dispatch.Error = content + } + + if err := p.GetConnection(ctx).Create(&dispatch); err != nil { + return sqlcon.HandleError(err) + } + + return nil +} diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go new file mode 100644 index 0000000..77f7980 --- /dev/null +++ b/persistence/sql/persister_errorx.go @@ -0,0 +1,110 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var _ errorx.Persister = new(Persister) + +func (p *Persister) CreateErrorContainer(ctx context.Context, csrfToken string, errs error) (containerID uuid.UUID, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateErrorContainer") + defer otelx.End(span, &err) + + message, err := encodeSelfServiceErrors(errs) + if err != nil { + return uuid.Nil, err + } + + c := &errorx.ErrorContainer{ + ID: uuid.Nil, + NID: p.NetworkID(ctx), + CSRFToken: csrfToken, + Errors: message, + WasSeen: false, + } + + if err := p.GetConnection(ctx).Create(c); err != nil { + return uuid.Nil, sqlcon.HandleError(err) + } + + span.SetAttributes(attribute.String("id", c.ID.String())) + + return c.ID, nil +} + +func (p *Persister) ReadErrorContainer(ctx context.Context, id uuid.UUID) (_ *errorx.ErrorContainer, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ReadErrorContainer") + defer otelx.End(span, &err) + + var ec errorx.ErrorContainer + 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 +} + +func (p *Persister) ClearErrorContainers(ctx context.Context, olderThan time.Duration, force bool) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ClearErrorContainers") + defer otelx.End(span, &err) + + if force { + err = p.GetConnection(ctx).RawQuery( + "DELETE FROM selfservice_errors WHERE nid = ? AND seen_at < ? AND seen_at IS NOT NULL", + p.NetworkID(ctx), time.Now().UTC().Add(-olderThan)).Exec() + } else { + err = p.GetConnection(ctx).RawQuery( + "DELETE FROM selfservice_errors WHERE nid = ? AND was_seen=true AND seen_at < ? AND seen_at IS NOT NULL", + p.NetworkID(ctx), time.Now().UTC().Add(-olderThan)).Exec() + } + + return sqlcon.HandleError(err) +} + +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.")) + } + + if c := new(herodot.DefaultError); errors.As(e, &c) { + e = c + } else if c := new(jsonschema.ValidationError); errors.As(e, &c) { + e = c + } else { + e = herodot.ToDefaultError(e, "") + } + + enc, err := json.Marshal(e) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("Unable to encode error messages.").WithDebug(err.Error())) + } + + return enc, nil +} diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go new file mode 100644 index 0000000..0eea4bd --- /dev/null +++ b/persistence/sql/persister_hmac.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "crypto/hmac" + "crypto/sha512" + "fmt" +) + +func (p *Persister) hmacValue(ctx context.Context, value string) string { + return hmacValueWithSecret(value, p.r.Config().SecretsSession(ctx)[0]) +} + +func hmacValueWithSecret(value string, secret []byte) string { + h := hmac.New(sha512.New512_256, secret) + _, _ = h.Write([]byte(value)) + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go new file mode 100644 index 0000000..ed77e1b --- /dev/null +++ b/persistence/sql/persister_hmac_test.go @@ -0,0 +1,102 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/embedx" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/dbal" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type logRegistryOnly struct { + l *logrusx.Logger + c *config.Config +} + +func (l *logRegistryOnly) Config() *config.Config { + return l.c +} + +func (l *logRegistryOnly) Contextualizer() contextx.Contextualizer { + // TODO implement me + panic("implement me") +} + +func (l *logRegistryOnly) Logger() *logrusx.Logger { + if l.l == nil { + l.l = logrusx.New("kratos", "testing") + } + return l.l +} + +func (l *logRegistryOnly) Audit() *logrusx.Logger { + panic("implement me") +} + +func (l *logRegistryOnly) Tracer(context.Context) *otelx.Tracer { + return otelx.NewNoop() +} + +func (l *logRegistryOnly) Clock() clock.Clock { + return clock.New() +} + +func (l *logRegistryOnly) IdentityTraitsSchemas(context.Context) (schema.IdentitySchemaList, error) { + panic("implement me") +} + +func (l *logRegistryOnly) IdentityValidator() *identity.Validator { + panic("implement me") +} + +var _ persisterDependencies = &logRegistryOnly{} + +func TestPersisterHMAC(t *testing.T) { + t.Parallel() + + ctx := context.Background() + baseSecret := "foobarbaz" + baseSecretBytes := []byte(baseSecret) + opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} + conf := config.MustNew(t, logrusx.New("", ""), contextx.NewTestConfigProvider(embedx.ConfigSchema, opts...), opts...) + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dbal.NewSQLiteTestDatabase(t)}) + require.NoError(t, err) + p, err := NewPersister(&logRegistryOnly{c: conf}, c) + require.NoError(t, err) + + t.Run("case=behaves deterministically", func(t *testing.T) { + assert.Equal(t, hmacValueWithSecret("hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret("notme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret("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 = contextx.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + assert.NotEqual(t, hmacValueWithSecret("hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.Equal(t, hmacValueWithSecret("hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + }) + + t.Run("case=with new and old secret", func(t *testing.T) { + ctx = contextx.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + assert.Equal(t, hmacValueWithSecret("hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) + }) +} diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go new file mode 100644 index 0000000..1b8b4a6 --- /dev/null +++ b/persistence/sql/persister_login.go @@ -0,0 +1,106 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var _ login.FlowPersister = new(Persister) + +func (p *Persister) CreateLoginFlow(ctx context.Context, r *login.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginFlow") + defer otelx.End(span, &err) + + r.NID = p.NetworkID(ctx) + r.EnsureInternalContext() + return p.GetConnection(ctx).Create(r) +} + +func (p *Persister) UpdateLoginFlow(ctx context.Context, r *login.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateLoginFlow") + defer otelx.End(span, &err) + + r.EnsureInternalContext() + cp := *r + cp.NID = p.NetworkID(ctx) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), cp) +} + +func (p *Persister) GetLoginFlow(ctx context.Context, id uuid.UUID) (_ *login.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetLoginFlow") + defer otelx.End(span, &err) + + conn := p.GetConnection(ctx) + + var r login.Flow + if err := conn.Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&r); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &r, nil +} + +func (p *Persister) ForceLoginFlow(ctx context.Context, id uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ForceLoginFlow") + defer otelx.End(span, &err) + + return p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + lr, err := p.GetLoginFlow(ctx, id) + if err != nil { + return err + } + + lr.Refresh = true + return tx.Save(lr, "nid") + }) +} + +func (p *Persister) DeleteTestLoginFlow(ctx context.Context, id uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteTestLoginFlow") + defer otelx.End(span, &err) + + count, err := p.GetConnection(ctx).RawQuery(` + DELETE FROM selfservice_login_flows + WHERE id = ? + AND nid = ? + AND test_flow = true`, + id, + p.NetworkID(ctx), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +func (p *Persister) DeleteExpiredLoginFlows(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredLoginFlows") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + login.Flow{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_login_code.go b/persistence/sql/persister_login_code.go new file mode 100644 index 0000000..e4efdcd --- /dev/null +++ b/persistence/sql/persister_login_code.go @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/persistence/sql/batch" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateLoginCode(ctx context.Context, params *code.CreateLoginCodeParams) (_ *code.LoginCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginCode") + defer otelx.End(span, &err) + + now := time.Now().UTC() + loginCode := &code.LoginCode{ + IdentityID: params.IdentityID, + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + // Extra columns are values the caller already knows for columns that are + // not part of the OSS model (e.g. crdb_region on CockroachDB + // multi-region). Writing them with the insert avoids the server-side + // fallback (a column default or a lookup derived from a foreign key), + // which for region columns costs a cross-region round trip per statement. + if len(params.InsertExtraColumns) > 0 { + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + return batch.Create(ctx, + &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: tx}, + []*code.LoginCode{loginCode}, + batch.WithExtraColumns(params.InsertExtraColumns)) + }); err != nil { + return nil, sqlcon.HandleError(err) + } + return loginCode, nil + } + + if err := p.GetConnection(ctx).Create(loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return loginCode, nil +} + +func (p *Persister) UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, userProvidedCode string) (_ *code.LoginCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseLoginCode") + defer otelx.End(span, &err) + + codeRow, err := useOneTimeCode[code.LoginCode](ctx, p, flowID, userProvidedCode, login.Flow{}.TableName(), "selfservice_login_flow_id", withCheckIdentityID(identityID)) + if err != nil { + return nil, err + } + + return codeRow, nil +} + +func (p *Persister) GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (_ *code.LoginCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedLoginCode") + defer otelx.End(span, &err) + + var loginCode code.LoginCode + if err := p.Connection(ctx).Where("selfservice_login_flow_id = ? AND nid = ? AND used_at IS NOT NULL", flowID, p.NetworkID(ctx)).First(&loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return &loginCode, nil +} + +func (p *Persister) DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteLoginCodesOfFlow") + defer otelx.End(span, &err) + + return p.GetConnection(ctx).Where("selfservice_login_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.LoginCode{}) +} diff --git a/persistence/sql/persister_onetime_secret_race_test.go b/persistence/sql/persister_onetime_secret_race_test.go new file mode 100644 index 0000000..b218fb2 --- /dev/null +++ b/persistence/sql/persister_onetime_secret_race_test.go @@ -0,0 +1,215 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + "fmt" + "net" + "os/exec" + "path/filepath" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/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/link" + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" +) + +// TestOneTimeSecretSingleUseUnderReadCommitted is the regression test for +// HackerOne #3692149 and its duplicate #3695625. +// +// One-time secrets (codes and tokens) must be consumed atomically. On READ +// COMMITTED backends (self-hosted Postgres/MySQL) the consume previously did a +// SELECT (matching only unused rows) followed by an unguarded UPDATE. Two +// concurrent submissions of the same valid secret could both read the row as +// unused and both run the UPDATE, so both succeeded — a lost update that let an +// attacker mint duplicate sessions for the same identity. +// +// Postgres defaults to READ COMMITTED, so this test reproduces the race +// deterministically against a real Postgres instance using real production +// code: before the fix multiple concurrent consumers succeed, after the fix +// exactly one does. The test starts a throwaway local Postgres cluster and +// skips if the Postgres binaries are not installed. +func TestOneTimeSecretSingleUseUnderReadCommitted(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres-backed race test under -short") + } + + dsn := startLocalPostgres(t) + + ctx := testhelpers.WithDefaultIdentitySchema(t.Context(), "file://./stub/identity.schema.json") + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + + // assertSingleUse runs many concurrent consumers of the same one-time + // secret and asserts that no more than one of them succeeds. + assertSingleUse := func(t *testing.T, consume func() error) { + const concurrency = 16 + var success int32 + var start sync.WaitGroup + var done sync.WaitGroup + start.Add(1) + for range concurrency { + done.Add(1) + go func() { + defer done.Done() + start.Wait() + if err := consume(); err == nil { + atomic.AddInt32(&success, 1) + } + }() + } + start.Done() + done.Wait() + + require.EqualValues(t, 1, atomic.LoadInt32(&success), + "exactly one concurrent consumer may use the one-time secret") + } + + newIdentityWithRecoveryAddress := func(t *testing.T, email string) *identity.Identity { + var i identity.Identity + i.Traits = identity.Traits(`{}`) + i.RecoveryAddresses = append(i.RecoveryAddresses, identity.RecoveryAddress{ + Value: email, Via: identity.AddressTypeEmail, IdentityID: i.ID, + }) + require.NoError(t, p.CreateIdentity(ctx, &i)) + return &i + } + + newRecoveryFlow := func(t *testing.T) *recovery.Flow { + f := &recovery.Flow{ + ID: x.NewUUID(), + Type: flow.TypeBrowser, + State: flow.StateChooseMethod, + ExpiresAt: time.Now().Add(time.Hour), + IssuedAt: time.Now(), + } + require.NoError(t, p.CreateRecoveryFlow(ctx, f)) + return f + } + + t.Run("case=recovery code", func(t *testing.T) { + f := newRecoveryFlow(t) + i := newIdentityWithRecoveryAddress(t, "rc-race@ory.sh") + dto := &code.CreateRecoveryCodeParams{ + RawCode: "12345678", + FlowID: f.ID, + RecoveryAddress: &i.RecoveryAddresses[0], + ExpiresIn: time.Hour, + IdentityID: i.ID, + } + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + assertSingleUse(t, func() error { + _, err := p.UseRecoveryCode(ctx, f.ID, dto.RawCode) + return err + }) + }) + + t.Run("case=recovery token", func(t *testing.T) { + f := newRecoveryFlow(t) + i := newIdentityWithRecoveryAddress(t, "rt-race@ory.sh") + token := &link.RecoveryToken{ + Token: x.NewUUID().String(), + FlowID: uuid.NullUUID{UUID: f.ID, Valid: true}, + RecoveryAddress: &i.RecoveryAddresses[0], + ExpiresAt: time.Now().Add(time.Hour), + IssuedAt: time.Now(), + IdentityID: i.ID, + TokenType: link.RecoveryTokenTypeAdmin, + } + require.NoError(t, p.CreateRecoveryToken(ctx, token)) + + raw := token.Token + assertSingleUse(t, func() error { + _, err := p.UseRecoveryToken(ctx, f.ID, raw) + return err + }) + }) +} + +// startLocalPostgres boots a throwaway PostgreSQL cluster for the test and +// returns its DSN. It skips the test when the Postgres binaries are not +// available. Postgres defaults to READ COMMITTED isolation, which is required +// to reproduce the one-time-secret consume race. +func startLocalPostgres(t *testing.T) string { + t.Helper() + + bin := lookupPostgresBin(t) + dataDir := filepath.Join(t.TempDir(), "pgdata") + port := freePort(t) + + run := func(name string, args ...string) { + //#nosec G204 -- test helper runs trusted Postgres binaries from a fixed, test-controlled path. + cmd := exec.Command(filepath.Join(bin, name), args...) + out, err := cmd.CombinedOutput() + require.NoErrorf(t, err, "%s failed: %s", name, string(out)) + } + + run("initdb", "-D", dataDir, "-U", "postgres", "--auth=trust", "-E", "UTF8") + + logFile := filepath.Join(t.TempDir(), "pg.log") + run("pg_ctl", "-D", dataDir, "-l", logFile, "-w", + "-o", fmt.Sprintf("-p %d -c listen_addresses=127.0.0.1 -c fsync=off", port), + "start") + t.Cleanup(func() { + //#nosec G204 -- test helper runs trusted Postgres binaries from a fixed, test-controlled path. + stop := exec.Command(filepath.Join(bin, "pg_ctl"), "-D", dataDir, "-m", "immediate", "stop") + _ = stop.Run() + }) + + dsn := fmt.Sprintf("postgres://postgres@127.0.0.1:%d/postgres?sslmode=disable", port) + + // Wait for the server to accept connections. + require.EventuallyWithT(t, func(t *assert.CollectT) { + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dsn}) + if !assert.NoError(t, err) { + return + } + assert.NoError(t, c.Open()) + assert.NoError(t, c.RawQuery("SELECT 1").Exec()) + _ = c.Close() + }, 30*time.Second, 200*time.Millisecond) + + return dsn +} + +func lookupPostgresBin(t *testing.T) string { + t.Helper() + // Prefer the Homebrew keg-only path, then fall back to PATH. + for _, candidate := range []string{ + "/opt/homebrew/opt/postgresql@14/bin", + "/usr/local/opt/postgresql@14/bin", + } { + if _, err := exec.LookPath(filepath.Join(candidate, "pg_ctl")); err == nil { + return candidate + } + } + p, err := exec.LookPath("pg_ctl") + if err != nil { + t.Skip("PostgreSQL binaries (pg_ctl/initdb) not found; skipping READ COMMITTED race test") + } + return filepath.Dir(p) +} + +func freePort(t *testing.T) int { + t.Helper() + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer func() { _ = l.Close() }() + return l.Addr().(*net.TCPAddr).Port +} diff --git a/persistence/sql/persister_pending_traits_change.go b/persistence/sql/persister_pending_traits_change.go new file mode 100644 index 0000000..6fbdf2c --- /dev/null +++ b/persistence/sql/persister_pending_traits_change.go @@ -0,0 +1,81 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var _ identity.PendingTraitsChangePersister = new(Persister) + +func (p *Persister) CreatePendingTraitsChange(ctx context.Context, ptc *identity.PendingTraitsChange) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreatePendingTraitsChange") + defer otelx.End(span, &err) + + ptc.ID = uuid.Must(uuid.NewV4()) + ptc.NID = p.NetworkID(ctx) + ptc.CreatedAt = time.Now().UTC().Truncate(time.Microsecond) + ptc.UpdatedAt = ptc.CreatedAt + + if err := sqlcon.HandleError(p.GetConnection(ctx).Create(ptc)); err != nil { + if !errors.Is(err, sqlcon.ErrUniqueViolation()) { + return err + } + + // Race: another concurrent request created a pending change after our delete. + // Delete again and retry once. + if err := p.DeletePendingTraitsChangesByIdentity(ctx, ptc.IdentityID); err != nil { + return err + } + // TODO(jonas): We could do this on the SQL level with an ON CONFLICT DO UPDATE, but that would require a more complex query and we want to keep the logic in Go for better observability and error handling. + if err := sqlcon.HandleError(p.GetConnection(ctx).Create(ptc)); err != nil { + return err + } + } + return nil +} + +func (p *Persister) GetPendingTraitsChangeByVerificationFlow(ctx context.Context, flowID uuid.UUID) (_ *identity.PendingTraitsChange, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetPendingTraitsChangeByVerificationFlow") + defer otelx.End(span, &err) + + var ptc identity.PendingTraitsChange + if err := p.GetConnection(ctx). + Where("nid = ? AND verification_flow_id = ? AND status = ?", + p.NetworkID(ctx), flowID, identity.PendingTraitsChangeStatusPending). + First(&ptc); err != nil { + return nil, sqlcon.HandleError(err) + } + return &ptc, nil +} + +func (p *Persister) DeletePendingTraitsChangesByIdentity(ctx context.Context, identityID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeletePendingTraitsChangesByIdentity") + defer otelx.End(span, &err) + + return sqlcon.HandleError( + p.GetConnection(ctx).RawQuery( + "DELETE FROM identity_pending_traits_changes WHERE nid = ? AND identity_id = ? AND status = 'pending'", + p.NetworkID(ctx), + identityID, + ).Exec(), + ) +} + +func (p *Persister) UpdatePendingTraitsChange(ctx context.Context, ptc *identity.PendingTraitsChange) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdatePendingTraitsChange") + defer otelx.End(span, &err) + + ptc.UpdatedAt = time.Now().UTC().Truncate(time.Microsecond) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), ptc) +} diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go new file mode 100644 index 0000000..dd53f51 --- /dev/null +++ b/persistence/sql/persister_recovery.go @@ -0,0 +1,146 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var ( + _ recovery.FlowPersister = new(Persister) + _ link.RecoveryTokenPersister = new(Persister) +) + +func (p *Persister) CreateRecoveryFlow(ctx context.Context, r *recovery.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryFlow") + defer otelx.End(span, &err) + + r.NID = p.NetworkID(ctx) + return p.GetConnection(ctx).Create(r) +} + +func (p *Persister) GetRecoveryFlow(ctx context.Context, id uuid.UUID) (_ *recovery.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetRecoveryFlow") + defer otelx.End(span, &err) + + var r recovery.Flow + if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&r); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &r, nil +} + +func (p *Persister) UpdateRecoveryFlow(ctx context.Context, r *recovery.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateRecoveryFlow") + defer otelx.End(span, &err) + + cp := *r + cp.NID = p.NetworkID(ctx) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), cp) +} + +func (p *Persister) CreateRecoveryToken(ctx context.Context, token *link.RecoveryToken) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryToken") + defer otelx.End(span, &err) + + t := token.Token + token.Token = p.hmacValue(ctx, t) + token.NID = p.NetworkID(ctx) + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(token); err != nil { + return err + } + + token.Token = t + return nil +} + +func (p *Persister) UseRecoveryToken(ctx context.Context, fID uuid.UUID, token string) (_ *link.RecoveryToken, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryToken") + defer otelx.End(span, &err) + + var rt link.RecoveryToken + + 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 = ?", hmacValueWithSecret(token, secret), nid, fID).First(&rt); err != nil { + if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows()) { + return err + } + } else { + break + } + } + if err != nil { + return err + } + + var ra identity.RecoveryAddress + if err := tx.Where("id = ? AND nid = ?", rt.RecoveryAddressID, nid).First(&ra); err != nil { + if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows()) { + return err + } + } + rt.RecoveryAddress = &ra + + // Consume the token atomically. The `NOT used` guard ensures that on + // READ COMMITTED backends two concurrent submissions of the same token + // cannot both succeed: only the first UPDATE affects a row. If no row + // was affected, another transaction already consumed the token. + //#nosec G201 -- TableName is static + count, err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET used=true, used_at=? WHERE id=? AND nid = ? AND NOT used", rt.TableName(ctx)), time.Now().UTC(), rt.ID, nid).ExecWithCount() + if err != nil { + return err + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil + })); err != nil { + return nil, err + } + + return &rt, nil +} + +func (p *Persister) DeleteRecoveryToken(ctx context.Context, token string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryToken") + defer otelx.End(span, &err) + + //#nosec G201 -- TableName is static + return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE token=? AND nid = ?", new(link.RecoveryToken).TableName(ctx)), token, p.NetworkID(ctx)).Exec() +} + +func (p *Persister) DeleteExpiredRecoveryFlows(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredRecoveryFlows") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + recovery.Flow{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_recovery_code.go b/persistence/sql/persister_recovery_code.go new file mode 100644 index 0000000..d684a1c --- /dev/null +++ b/persistence/sql/persister_recovery_code.go @@ -0,0 +1,85 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRecoveryCode(ctx context.Context, params *code.CreateRecoveryCodeParams) (_ *code.RecoveryCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryCode") + defer otelx.End(span, &err) + + now := time.Now() + recoveryCode := &code.RecoveryCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.UTC().Add(params.ExpiresIn), + IssuedAt: now, + CodeType: params.CodeType, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + IdentityID: params.IdentityID, + } + + if params.RecoveryAddress != nil { + recoveryCode.RecoveryAddress = params.RecoveryAddress + recoveryCode.RecoveryAddressID = uuid.NullUUID{ + UUID: params.RecoveryAddress.ID, + Valid: true, + } + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(recoveryCode); err != nil { + return nil, err + } + + return recoveryCode, nil +} + +// UseRecoveryCode attempts to "use" the supplied code in the flow +// +// If the supplied code matched a code from the flow, no error is returned +// If an invalid code was submitted with this flow more than 5 times, an error is returned +func (p *Persister) UseRecoveryCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (_ *code.RecoveryCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryCode") + defer otelx.End(span, &err) + + codeRow, err := useOneTimeCode[code.RecoveryCode](ctx, p, flowID, userProvidedCode, recovery.Flow{}.TableName(), "selfservice_recovery_flow_id") + if err != nil { + return nil, err + } + + var ra identity.RecoveryAddress + if err := sqlcon.HandleError(p.GetConnection(ctx).Where("id = ? AND nid = ?", codeRow.RecoveryAddressID, p.NetworkID(ctx)).First(&ra)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + // This is ok, it can happen when an administrator initiates account recovery. This works even if the + // user has no recovery address! + } else { + return nil, err + } + } + codeRow.RecoveryAddress = &ra + + return codeRow, nil +} + +func (p *Persister) DeleteRecoveryCodesOfFlow(ctx context.Context, flowID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryCodesOfFlow") + defer otelx.End(span, &err) + + return p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RecoveryCode{}) +} diff --git a/persistence/sql/persister_registration.go b/persistence/sql/persister_registration.go new file mode 100644 index 0000000..1afc797 --- /dev/null +++ b/persistence/sql/persister_registration.go @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow/registration" +) + +func (p *Persister) CreateRegistrationFlow(ctx context.Context, r *registration.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRegistrationFlow") + defer otelx.End(span, &err) + + r.NID = p.NetworkID(ctx) + r.EnsureInternalContext() + return p.GetConnection(ctx).Create(r) +} + +func (p *Persister) UpdateRegistrationFlow(ctx context.Context, r *registration.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateRegistrationFlow") + defer otelx.End(span, &err) + + r.EnsureInternalContext() + cp := *r + cp.NID = p.NetworkID(ctx) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), cp) +} + +func (p *Persister) GetRegistrationFlow(ctx context.Context, id uuid.UUID) (_ *registration.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetRegistrationFlow") + defer otelx.End(span, &err) + + var r registration.Flow + if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", + id, p.NetworkID(ctx)).First(&r); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &r, nil +} + +func (p *Persister) DeleteExpiredRegistrationFlows(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredRegistrationFlows") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + registration.Flow{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_registration_code.go b/persistence/sql/persister_registration_code.go new file mode 100644 index 0000000..7a08430 --- /dev/null +++ b/persistence/sql/persister_registration_code.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/go-faker/faker/v4/pkg/slice" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRegistrationCode(ctx context.Context, params *code.CreateRegistrationCodeParams) (_ *code.RegistrationCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRegistrationCode") + defer otelx.End(span, &err) + + now := time.Now().UTC() + registrationCode := &code.RegistrationCode{ + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(registrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return registrationCode, nil +} + +func (p *Persister) UseRegistrationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string, addresses ...string) (_ *code.RegistrationCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRegistrationCode") + defer otelx.End(span, &err) + + codeRow, err := useOneTimeCode[code.RegistrationCode](ctx, p, flowID, userProvidedCode, registration.Flow{}.TableName(), "selfservice_registration_flow_id") + if err != nil { + return nil, err + } + + // ensure that the identifiers extracted from the traits are contained in the registration code + if !slice.Contains(addresses, codeRow.Address) { + return nil, errors.WithStack(code.ErrCodeNotFound()) + } + + return codeRow, nil +} + +func (p *Persister) GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (_ *code.RegistrationCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedRegistrationCode") + defer otelx.End(span, &err) + + var registrationCode code.RegistrationCode + if err := p.Connection(ctx).Where("selfservice_registration_flow_id = ? AND used_at IS NOT NULL AND nid = ?", flowID, p.NetworkID(ctx)).First(®istrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return ®istrationCode, nil +} + +func (p *Persister) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRegistrationCodesOfFlow") + defer otelx.End(span, &err) + + return p.GetConnection(ctx).Where("selfservice_registration_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RegistrationCode{}) +} diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go new file mode 100644 index 0000000..53dc365 --- /dev/null +++ b/persistence/sql/persister_session.go @@ -0,0 +1,724 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/otelx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/stringsx" +) + +var _ session.Persister = new(Persister) + +const ( + SessionDeviceUserAgentMaxLength = 512 + SessionDeviceLocationMaxLength = 512 + paginationMaxItemsSize = 1000 + paginationDefaultItemsSize = 250 +) + +func (p *Persister) GetSession(ctx context.Context, sid uuid.UUID, expandables session.Expandables) (_ *session.Session, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetSession") + defer otelx.End(span, &err) + + var s session.Session + s.Devices = make([]session.Device, 0) + nid := p.NetworkID(ctx) + + q := p.GetConnection(ctx).Q() + // if len(expandables) > 0 { + if expandables.Has(session.ExpandSessionDevices) { + q = q.Eager(expandables.ToEager()...) + } + + if err := q.Where("id = ? AND nid = ?", sid, nid).First(&s); err != nil { + return nil, sqlcon.HandleError(err) + } + + if expandables.Has(session.ExpandSessionIdentity) { + // This is needed because of how identities are fetched from the store (if we use eager not all fields are + // available!). + i, err := p.GetIdentity(ctx, s.IdentityID, identity.ExpandDefault) + if err != nil { + return nil, err + } + s.Identity = i + } + + s.Active = s.IsActive() + return &s, nil +} + +func (p *Persister) ListSessions(ctx context.Context, active *bool, paginatorOpts []keysetpagination.Option, expandables session.Expandables) (_ []session.Session, _ *keysetpagination.Paginator, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListSessions") + defer otelx.End(span, &err) + + var s []session.Session + nid := p.NetworkID(ctx) + + paginatorOpts = append(paginatorOpts, keysetpagination.WithDefaultSize(paginationDefaultItemsSize)) + paginatorOpts = append(paginatorOpts, keysetpagination.WithMaxSize(paginationMaxItemsSize)) + paginatorOpts = append(paginatorOpts, keysetpagination.WithDefaultToken(new(session.Session).DefaultPageToken())) + 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, nil, errors.WithStack(x.PageTokenInvalid) + } + + if err := p.listWithinReadCommittedReadOnlyTx(ctx, func(ctx context.Context, c *pop.Connection) error { + s = make([]session.Session, 0) + + q := c.Where("nid = ?", nid) + if active != nil { + if *active { + q.Where("active = ? AND expires_at >= ?", *active, time.Now().UTC()) + } else { + q.Where("(active = ? OR expires_at < ?)", *active, time.Now().UTC()) + } + } + + if len(expandables) > 0 { + q = q.EagerPreload(expandables.ToEager()...) + } + + if err := q.Scope(keysetpagination.Paginate[session.Session](paginator)).All(&s); err != nil { + return sqlcon.HandleError(err) + } + + return nil + }); err != nil { + return nil, nil, err + } + + for k := range s { + if s[k].Identity == nil { + continue + } + if err := p.InjectTraitsSchemaURL(ctx, s[k].Identity); err != nil { + return nil, nil, err + } + } + + s, nextPage := keysetpagination.Result(s, paginator) + return s, nextPage, nil +} + +// ListSessionsByIdentity retrieves sessions for an identity from the store. +func (p *Persister) ListSessionsByIdentity( + ctx context.Context, + iID uuid.UUID, + active *bool, + page, perPage int, + except uuid.UUID, + expandables session.Expandables, +) (_ []session.Session, _ int64, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListSessionsByIdentity") + defer otelx.End(span, &err) + + var s []session.Session + t := int64(0) + nid := p.NetworkID(ctx) + + if err := p.listWithinReadCommittedReadOnlyTx(ctx, func(ctx context.Context, c *pop.Connection) error { + s = make([]session.Session, 0) + + q := c.Where("identity_id = ? AND nid = ?", iID, nid) + if except != uuid.Nil { + q = q.Where("id != ?", except) + } + if active != nil { + if *active { + q.Where("active = ? AND expires_at >= ?", *active, time.Now().UTC()) + } else { + q.Where("(active = ? OR expires_at < ?)", *active, time.Now().UTC()) + } + } + + if len(expandables) > 0 { + q = q.EagerPreload(expandables.ToEager()...) + } + + total, err := q.Count(new(session.Session)) + if err != nil { + return sqlcon.HandleError(err) + } + t = int64(total) + + q.Order("created_at DESC") + + if err := q.Paginate(page, perPage).All(&s); err != nil { + return sqlcon.HandleError(err) + } + return nil + }); err != nil { + return nil, 0, err + } + + 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. +func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpsertSession") + defer otelx.End(span, &err) + + s.NID = p.NetworkID(ctx) + if s.Identity != nil { + s.IdentityID = s.Identity.ID + } else if s.IdentityID.IsNil() { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("cannot upsert session without an identity or identity ID set")) + } + + 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() { + exists, err = tx.Where("id = ? AND nid = ?", s.ID, s.NID).Exists(new(session.Session)) + if err != nil { + return sqlcon.HandleError(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, "issued_at", "identity_id", "nid"); err != nil { + return sqlcon.HandleError(err) + } + updated = true + return nil + } + + // This must not be eager or identities will be created / updated + if err := sqlcon.HandleError(tx.Create(s)); err != nil { + return err + } + + for i := range s.Devices { + device := &(s.Devices[i]) + device.SessionID = s.ID + device.NID = s.NID + device.IdentityID = new(s.IdentityID) + + if device.Location != nil { + device.Location = new(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) + } + if device.UserAgent != nil { + device.UserAgent = new(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) + } + + if err := p.CreateDevice(ctx, device); err != nil { + return err + } + } + + return nil + })) +} + +// DeleteSession permanently deletes a single session. Returns +// sqlcon.ErrNoRows() when no matching session is found in the caller's +// network. +func (p *Persister) DeleteSession(ctx context.Context, sid uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSession") + defer otelx.End(span, &err) + + n, err := p.DeleteSessionsByIDs(ctx, []uuid.UUID{sid}) + if err != nil { + return err + } + if n == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +func (p *Persister) DeleteSessionsByIdentity(ctx context.Context, identityID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIdentity") + defer otelx.End(span, &err) + + //#nosec G201 -- TableName is static + count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %s WHERE identity_id = ? AND nid = ?", + session.Session{}.TableName(), + ), + identityID, + p.NetworkID(ctx), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +func (p *Persister) GetSessionByToken(ctx context.Context, token string, expand session.Expandables, identityExpand identity.Expandables) (res *session.Session, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetSessionByToken") + defer otelx.End(span, &err) + + var s session.Session + s.Devices = make([]session.Device, 0) + nid := p.NetworkID(ctx) + + con := p.GetConnection(ctx) + if err := con.Where("token = ? AND nid = ?", token, nid).First(&s); err != nil { + return nil, sqlcon.HandleError(err) + } + + var ( + i *identity.Identity + sd []session.Device + ) + + eg, ctx := errgroup.WithContext(ctx) + if expand.Has(session.ExpandSessionDevices) { + eg.Go(func() error { + return sqlcon.HandleError(con.WithContext(ctx). + Where("session_id = ? AND nid = ?", s.ID, nid).All(&sd)) + }) + } + + // This is needed because of how identities are fetched from the store (if we use eager not all fields are + // available!). + if expand.Has(session.ExpandSessionIdentity) { + eg.Go(func() (err error) { + i, err = p.GetIdentity(ctx, s.IdentityID, identityExpand) + return err + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + s.Identity = i + s.Devices = sd + + return &s, nil +} + +func (p *Persister) DeleteSessionByToken(ctx context.Context, token string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionByToken") + defer otelx.End(span, &err) + + //#nosec G201 -- TableName is static + count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %s WHERE token = ? AND nid = ?", + session.Session{}.TableName(), + ), + token, + p.NetworkID(ctx), + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +// RevokeSessionByToken marks the session with the given token inactive and +// returns its IDs so the caller can attach them to observability events without +// a separate GetSessionByToken round trip. +// Returns sqlcon.ErrNoRows() when no matching session exists in the caller's +// network; the returned RevokedSession is the zero value in that case. +func (p *Persister) RevokeSessionByToken(ctx context.Context, token string) (revoked session.RevokedSession, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionByToken") + defer otelx.End(span, &err) + + con := p.GetConnection(ctx) + nid := p.NetworkID(ctx) + var dst struct { + ID uuid.UUID `db:"id"` + IdentityID uuid.UUID `db:"identity_id"` + } + + if dbal.IsPostgresCompatible(con.Dialect.Name()) { + // CTE: identify the row by (token, nid), conditionally flip active=false + // only when currently true, and return the matched row's identifiers in + // one round trip. See revokeMatchingSessions for the contention rationale. + const query = `WITH found AS (SELECT id, identity_id FROM sessions WHERE token = ? AND nid = ?), + upd AS (UPDATE sessions SET active = false FROM found WHERE sessions.id = found.id AND sessions.active = true RETURNING 1) +SELECT id, identity_id FROM found` + + err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + return c.RawQuery(query, token, nid).First(&dst) + }) + } else { + // SQLite and MySQL: data-modifying CTEs are not portable here, so issue + // a separate SELECT followed by the legacy UPDATE. Same two-statement + // shape as today's caller (GetSessionByToken + RevokeSessionByToken), + // so no regression on these dialects. + err = con.RawQuery("SELECT id, identity_id FROM sessions WHERE token = ? AND nid = ?", token, nid).First(&dst) + if err == nil { + err = con.RawQuery("UPDATE sessions SET active = false WHERE token = ? AND nid = ?", token, nid).Exec() + } + } + if err != nil { + return session.RevokedSession{}, sqlcon.HandleError(err) + } + return session.RevokedSession{ID: dst.ID, IdentityID: dst.IdentityID}, nil +} + +// RevokeSessionById revokes a given session. Returns sqlcon.ErrNoRows() when +// no matching session is found in the caller's network. +func (p *Persister) RevokeSessionById(ctx context.Context, sID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionById") + defer otelx.End(span, &err) + + count, err := p.revokeMatchingSessions(ctx, "id = ? AND nid = ?", sID, p.NetworkID(ctx)) + if err != nil { + return err + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil +} + +// RevokeSession revokes a given session. If the session does not exist or was not modified, +// it effectively has been revoked already, and therefore that case does not return an error. +func (p *Persister) RevokeSession(ctx context.Context, iID, sID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSession") + defer otelx.End(span, &err) + + return p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + return c.RawQuery( + "UPDATE sessions SET active = false WHERE id = ? AND identity_id = ? AND nid = ? AND active = true", + sID, iID, p.NetworkID(ctx), + ).Exec() + }) +} + +// RevokeSessionsIdentityExcept marks all except the given session of an identity inactive. +func (p *Persister) RevokeSessionsIdentityExcept(ctx context.Context, iID, sID uuid.UUID) (res int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsIdentityExcept") + defer otelx.End(span, &err) + + return p.revokeMatchingSessions(ctx, "identity_id = ? AND id != ? AND nid = ?", iID, sID, p.NetworkID(ctx)) +} + +// RevokeSessionsByIdentities marks all currently active sessions inactive for the given identity IDs. +func (p *Persister) RevokeSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsByIdentities") + defer otelx.End(span, &err) + + if len(identityIDs) == 0 { + return 0, nil + } + + err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + var inner error + count, inner = c.RawQuery( + "UPDATE sessions SET active = false WHERE identity_id IN (?) AND active = true AND nid = ?", + identityIDs, p.NetworkID(ctx), + ).ExecWithCount() + return inner + }) + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// RevokeSessionsByIDs marks the listed sessions inactive (only ones currently active). +func (p *Persister) RevokeSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsByIDs") + defer otelx.End(span, &err) + + if len(sessionIDs) == 0 { + return 0, nil + } + + err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + var inner error + count, inner = c.RawQuery( + "UPDATE sessions SET active = false WHERE id IN (?) AND active = true AND nid = ?", + sessionIDs, p.NetworkID(ctx), + ).ExecWithCount() + return inner + }) + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// RevokeAllSessions deactivates up to `limit` currently-active sessions in +// the caller's network in a single SQL statement and returns the number of +// rows actually updated (in the range [0, limit]). +func (p *Persister) RevokeAllSessions(ctx context.Context, limit int) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeAllSessions") + defer otelx.End(span, &err) + + err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + var inner error + count, inner = c.RawQuery( + "UPDATE sessions SET active = false WHERE id IN (SELECT id FROM (SELECT id FROM sessions c WHERE active = true AND nid = ? LIMIT ?) AS s)", + p.NetworkID(ctx), limit, + ).ExecWithCount() + return inner + }) + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// revokeMatchingSessions flips active=false on every sessions row matching the +// given WHERE clause and returns the number of rows that matched the clause +// before the update (irrespective of whether they were already inactive). Used +// by the revoke entry points whose caller relies on the matched-count +// distinction — either to map count==0 to sqlcon.ErrNoRows() or to surface +// the count as an API response field. +// +// On CockroachDB and PostgreSQL, the update is expressed as a CTE so that +// only rows whose current active=true get rewritten. CockroachDB's sessions +// table is LOCALITY GLOBAL: every write commits at a future timestamp and +// any concurrent write to the same key inside that closed-timestamp window +// raises WriteTooOldError + a serializable refresh. Suppressing the +// redundant rewrite of already-inactive rows eliminates the dominant +// same-fingerprint contention pattern observed in production (concurrent +// revokes for the same token from logout retries / multiple clients). +// SQLite and MySQL retain the single-statement UPDATE; neither is subject +// to the GLOBAL-table closed-timestamp contention. +func (p *Persister) revokeMatchingSessions(ctx context.Context, predicate string, args ...any) (int, error) { + con := p.GetConnection(ctx) + var ( + count int + err error + ) + + if dbal.IsPostgresCompatible(con.Dialect.Name()) { + //#nosec G201 -- predicate is a static persister-internal constant, not user input + query := fmt.Sprintf(`WITH found AS (SELECT id FROM sessions WHERE %s), + upd AS (UPDATE sessions SET active = false FROM found WHERE sessions.id = found.id AND sessions.active = true RETURNING 1) +SELECT count(*) FROM found`, predicate) + + err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error { + return c.RawQuery(query, args...).First(&count) + }) + } else { + //#nosec G201 -- predicate is a static persister-internal constant, not user input + count, err = con.RawQuery( + fmt.Sprintf("UPDATE sessions SET active = false WHERE %s", predicate), + args..., + ).ExecWithCount() + } + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// runInReadCommittedOnCRDB invokes fn against a connection bound to a READ +// COMMITTED transaction on CockroachDB, and against the bare persister +// connection on every other dialect. CRDB needs the explicit isolation +// downgrade so the cluster transparently advances the statement read +// timestamp on WriteTooOldError instead of surfacing the serializable +// retry. Postgres already runs single statements at READ COMMITTED by +// default; SQLite and MySQL are not subject to the GLOBAL-table +// closed-timestamp contention. YugabyteDB does not get CockroachDB's isolation +// override and relies on TransactionWithOptions retrying serialization +// failures instead. +func (p *Persister) runInReadCommittedOnCRDB(ctx context.Context, fn func(*pop.Connection) error) error { + con := p.GetConnection(ctx) + if con.Dialect.Name() != dbal.DriverCockroachDB { + return fn(con) + } + return popx.TransactionWithOptions(ctx, con, + &sql.TxOptions{Isolation: sql.LevelReadCommitted}, + func(ctx context.Context, tx *pop.Connection) error { return fn(tx) }, + ) +} + +// listWithinReadCommittedReadOnlyTx invokes fn, which must only read, inside a READ +// COMMITTED read-only transaction on CockroachDB and PostgreSQL, and inside a +// regular transaction on every other dialect. The paginated session list +// queries run more than one statement per transaction (count + page, or page +// + eager preloads); under SERIALIZABLE a concurrent write to the scanned +// rows — a login upsert, extension, or revocation — between those statements +// invalidates the transaction's read timestamp and surfaces as a client-side +// RETRY_SERIALIZABLE retry. READ COMMITTED gives each statement its own read +// timestamp so CockroachDB absorbs those conflicts server-side. READ +// COMMITTED is already the PostgreSQL default; there the options make the +// read-only intent explicit and let the server skip write-path bookkeeping. +// SQLite and MySQL keep the plain transaction. +func (p *Persister) listWithinReadCommittedReadOnlyTx(ctx context.Context, fn func(ctx context.Context, c *pop.Connection) error) error { + con := p.GetConnection(ctx) + if dbal.IsPostgresCompatible(con.Dialect.Name()) { + return popx.TransactionWithOptions(ctx, con, + &sql.TxOptions{Isolation: sql.LevelReadCommitted, ReadOnly: true}, + fn, + ) + } + return p.Transaction(ctx, fn) +} + +// DeleteSessionsByIdentities permanently deletes all sessions belonging to the given identity IDs. +func (p *Persister) DeleteSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIdentities") + defer otelx.End(span, &err) + + if len(identityIDs) == 0 { + return 0, nil + } + + //#nosec G201 -- TableName is static. + count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %s WHERE identity_id IN (?) AND nid = ?", + session.Session{}.TableName(), + ), identityIDs, p.NetworkID(ctx)).ExecWithCount() + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// DeleteSessionsByIDs permanently deletes the listed sessions. +func (p *Persister) DeleteSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIDs") + defer otelx.End(span, &err) + + if len(sessionIDs) == 0 { + return 0, nil + } + + //#nosec G201 -- TableName is static. + count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %s WHERE id IN (?) AND nid = ?", + session.Session{}.TableName(), + ), sessionIDs, p.NetworkID(ctx)).ExecWithCount() + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +// DeleteAllSessions permanently deletes up to `limit` sessions in the +// caller's network in a single SQL statement and returns the number of rows +// actually deleted (in the range [0, limit]). +func (p *Persister) DeleteAllSessions(ctx context.Context, limit int) (count int, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteAllSessions") + defer otelx.End(span, &err) + + //#nosec G201 -- TableName is static + count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id IN (SELECT id FROM (SELECT id FROM %[1]s c WHERE nid = ? LIMIT ?) AS s)", + session.Session{}.TableName(), + ), p.NetworkID(ctx), limit).ExecWithCount() + if err != nil { + return 0, sqlcon.HandleError(err) + } + return count, nil +} + +func (p *Persister) DeleteExpiredSessions(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredSessions") + defer otelx.End(span, &err) + + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + session.Session{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_sessiontokenexchanger.go b/persistence/sql/persister_sessiontokenexchanger.go new file mode 100644 index 0000000..b4cf5a2 --- /dev/null +++ b/persistence/sql/persister_sessiontokenexchanger.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/otelx" + "github.com/ory/x/randx" + "github.com/ory/x/sqlcon" +) + +var _ sessiontokenexchange.Persister = new(Persister) + +func updateLimitClause(conn *pop.Connection) string { + // Not all databases support limiting in update clauses. + switch conn.Dialect.Name() { + case "sqlite3", dbal.DriverPostgreSQL, dbal.DriverYugabyteDB: + return "" + default: + return "LIMIT 1" + } +} + +func (p *Persister) CreateSessionTokenExchanger(ctx context.Context, flowID uuid.UUID) (e *sessiontokenexchange.Exchanger, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateSessionTokenExchanger") + defer otelx.End(span, &err) + + e = &sessiontokenexchange.Exchanger{ + NID: p.NetworkID(ctx), + FlowID: flowID, + InitCode: randx.MustString(64, randx.AlphaNum), + ReturnToCode: randx.MustString(64, randx.AlphaNum), + } + + return e, p.GetConnection(ctx).Create(e) +} + +func (p *Persister) GetExchangerFromCode(ctx context.Context, initCode string, returnToCode string) (e *sessiontokenexchange.Exchanger, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetExchangerFromCode") + defer otelx.End(span, &err) + + e = new(sessiontokenexchange.Exchanger) + conn := p.GetConnection(ctx) + if err = conn.Where(` +nid = ? AND +init_code = ? AND init_code <> '' AND +return_to_code = ? AND return_to_code <> '' AND +session_id IS NOT NULL`, + p.NetworkID(ctx), initCode, returnToCode).First(e); err != nil { + return nil, sqlcon.HandleError(err) + } + + return e, nil +} + +func (p *Persister) GetExchangerFromCodeAllowPending(ctx context.Context, initCode string, returnToCode string) (e *sessiontokenexchange.Exchanger, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetExchangerFromCodeAllowPending") + defer otelx.End(span, &err) + + e = new(sessiontokenexchange.Exchanger) + conn := p.GetConnection(ctx) + if err = conn.Where(` +nid = ? AND +init_code = ? AND init_code <> '' AND +return_to_code = ? AND return_to_code <> ''`, + p.NetworkID(ctx), initCode, returnToCode).First(e); err != nil { + return nil, sqlcon.HandleError(err) + } + + return e, nil +} + +func (p *Persister) UpdateSessionOnExchanger(ctx context.Context, flowID uuid.UUID, sessionID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateSessionOnExchanger") + defer otelx.End(span, &err) + + conn := p.GetConnection(ctx) + query := fmt.Sprintf("UPDATE %s SET session_id = ? WHERE flow_id = ? AND nid = ? %s", + conn.Dialect.Quote(new(sessiontokenexchange.Exchanger).TableName()), + updateLimitClause(conn), + ) + + return sqlcon.HandleError(conn.RawQuery(query, sessionID, flowID, p.NetworkID(ctx)).Exec()) +} + +func (p *Persister) CodeForFlow(ctx context.Context, flowID uuid.UUID) (codes *sessiontokenexchange.Codes, found bool, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CodeForFlow") + defer otelx.End(span, &err) + + var e sessiontokenexchange.Exchanger + switch err = sqlcon.HandleError(p.GetConnection(ctx). + Where("flow_id = ? AND nid = ? AND init_code <> '' and return_to_code <> ''", flowID, p.NetworkID(ctx)). + First(&e)); { + case err == nil: + return &sessiontokenexchange.Codes{ + InitCode: e.InitCode, + ReturnToCode: e.ReturnToCode, + }, true, nil + case errors.Is(err, sqlcon.ErrNoRows()): + return nil, false, nil + default: + return nil, false, err + } +} + +func (p *Persister) MoveToNewFlow(ctx context.Context, oldFlow, newFlow uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.MoveToNewFlow") + defer otelx.End(span, &err) + + conn := p.GetConnection(ctx) + query := fmt.Sprintf("UPDATE %s SET flow_id = ? WHERE flow_id = ? AND nid = ? %s", + conn.Dialect.Quote(new(sessiontokenexchange.Exchanger).TableName()), + updateLimitClause(conn), + ) + + return sqlcon.HandleError(conn.RawQuery(query, newFlow, oldFlow, p.NetworkID(ctx)).Exec()) +} + +func (p *Persister) DeleteExpiredExchangers(ctx context.Context, at time.Time, limit int) error { + expiredAfter := at.Add(1 * time.Hour) + conn := p.GetConnection(ctx) + + //#nosec G201 -- TableName is static + err := conn.RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE created_at <= ? and nid = ? ORDER BY created_at ASC LIMIT ?) AS s)", + sessiontokenexchange.Exchanger{}.TableName(), + ), + expiredAfter, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_settings.go b/persistence/sql/persister_settings.go new file mode 100644 index 0000000..07b3600 --- /dev/null +++ b/persistence/sql/persister_settings.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/sql/update" + + "github.com/gofrs/uuid" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/selfservice/flow/settings" +) + +var _ settings.FlowPersister = new(Persister) + +func (p *Persister) CreateSettingsFlow(ctx context.Context, r *settings.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateSettingsFlow") + defer otelx.End(span, &err) + + r.NID = p.NetworkID(ctx) + r.EnsureInternalContext() + return sqlcon.HandleError(p.GetConnection(ctx).Create(r)) +} + +func (p *Persister) GetSettingsFlow(ctx context.Context, id uuid.UUID) (_ *settings.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetSettingsFlow") + defer otelx.End(span, &err) + + var r settings.Flow + + err = p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&r) + if err != nil { + return nil, sqlcon.HandleError(err) + } + + r.Identity, err = p.GetIdentity(ctx, r.IdentityID, identity.ExpandDefault) + if err != nil { + return nil, err + } + + return &r, nil +} + +func (p *Persister) UpdateSettingsFlow(ctx context.Context, r *settings.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateSettingsFlow") + defer otelx.End(span, &err) + + r.EnsureInternalContext() + cp := *r + cp.NID = p.NetworkID(ctx) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), cp) +} + +func (p *Persister) DeleteExpiredSettingsFlows(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredSettingsFlows") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + settings.Flow{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go new file mode 100644 index 0000000..04768df --- /dev/null +++ b/persistence/sql/persister_test.go @@ -0,0 +1,513 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + "context" + "fmt" + "os" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/cockroach-go/v2/testserver" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + continuity "github.com/ory/kratos/continuity/test" + "github.com/ory/kratos/corpx" + courier "github.com/ory/kratos/courier/test" + "github.com/ory/kratos/driver/config" + ri "github.com/ory/kratos/identity" + identity "github.com/ory/kratos/identity/test" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + errorx "github.com/ory/kratos/selfservice/errorx/test" + lf "github.com/ory/kratos/selfservice/flow/login" + login "github.com/ory/kratos/selfservice/flow/login/test" + recovery "github.com/ory/kratos/selfservice/flow/recovery/test" + registration "github.com/ory/kratos/selfservice/flow/registration/test" + settings "github.com/ory/kratos/selfservice/flow/settings/test" + verification "github.com/ory/kratos/selfservice/flow/verification/test" + sessiontokenexchange "github.com/ory/kratos/selfservice/sessiontokenexchange/test" + code "github.com/ory/kratos/selfservice/strategy/code/test" + link "github.com/ory/kratos/selfservice/strategy/link/test" + session "github.com/ory/kratos/session/test" + "github.com/ory/kratos/x" + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/logging" + "github.com/ory/x/configx" + "github.com/ory/x/dbal" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlcon/dockertest" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func init() { + corpx.RegisterFakes() + pop.SetNowFunc(func() time.Time { + return time.Now().UTC().Round(time.Second) + }) +} + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +func pl(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + if pop.Debug == false { + return + } + + if lvl == logging.SQL { + if len(args) > 0 { + xargs := make([]string, len(args)) + for i, a := range args { + switch a.(type) { + case string: + xargs[i] = fmt.Sprintf("%q", a) + default: + xargs[i] = fmt.Sprintf("%v", a) + } + } + s = fmt.Sprintf("%s - %s | %s", lvl, s, xargs) + } else { + s = fmt.Sprintf("%s - %s", lvl, s) + } + } else { + s = fmt.Sprintf(s, args...) + s = fmt.Sprintf("%s - %s", lvl, s) + } + t.Log(s) + } +} + +var dbNames = []string{"sqlite", "postgres", "mysql", "cockroach"} + +func setupDatabase(t testing.TB, name string) string { + var dsn string + switch name { + case "sqlite": + dsn = dbal.NewSQLiteTestDatabase(t) + case "postgres": + dsn = dockertest.RunTestPostgreSQLWithVersion(t, "16") + case "mysql": + dsn = dockertest.RunTestMySQLWithVersion(t, "8.4") + case "cockroach": + dsn = newLocalTestCRDBServer(t) + default: + t.Fatalf("unknown database: %s", name) + } + + if name != "sqlite" { + require.EventuallyWithT(t, func(t *assert.CollectT) { + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dsn}) + require.NoError(t, err) + require.NoError(t, c.Open()) + dbName := "testdb" + strings.ReplaceAll(x.NewUUID().String(), "-", "") + require.NoError(t, c.RawQuery("CREATE DATABASE "+dbName).Exec()) + dsn = regexp.MustCompile(`/[a-z0-9]+\?`).ReplaceAllString(dsn, "/"+dbName+"?") + }, 60*time.Second, 200*time.Millisecond) + } + + t.Logf("Connecting to %s: %s", name, dsn) + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn) + p := reg.Persister().(*sql.Persister) + + t.Logf("Applying %s migrations", name) + pop.SetLogger(pl(t)) + require.NoError(t, p.MigrateUp(t.Context())) + t.Logf("%s migrations applied", name) + status, err := p.MigrationStatus(t.Context()) + require.NoError(t, err) + require.False(t, status.HasPending()) + + t.Logf("Database %s initialized successfully", name) + return dsn +} + +func TestPersister(t *testing.T) { + t.Parallel() + + ctx := t.Context() + // Bake the default identity schema into each registry's base config below + // instead of layering it onto ctx. A context overlay makes + // TestConfigProvider.Config rebuild the whole koanf tree on every config read + // (the same reason identity.TestPool bakes PoolConfigValues), which the + // contracts here would otherwise pay on every read. + defaultSchema := configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")) + + for _, name := range dbNames { + t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) { + t.Parallel() + + if name != "sqlite" && testing.Short() { + t.Skip("skipping non-sqlite under -short") + } + + dsn := setupDatabase(t, name) + t.Logf("DSN: %s", dsn) + + t.Run("racy identity creation", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + // Pre-run migrations so concurrent goroutine startups below + // don't race to apply the same migrations on a fresh DB. + pkg.NewRegistryDefaultWithDSN(t, dsn) + } + + var wg sync.WaitGroup + + for i := range 10 { + wg.Add(1) + go func() { + defer wg.Done() + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, ps := testhelpers.NewNetwork(t, ctx, reg.Persister()) + id := ri.NewIdentity("") + id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{ + Type: ri.CredentialsTypePassword, + Identifiers: []string{fmt.Sprintf("racy identity %d", i)}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + }) + id.Traits = ri.Traits("{}") + + require.NoError(t, ps.CreateIdentity(ctx, id)) + }() + } + + wg.Wait() + }) + + t.Run("case=credential types exist", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} { + require.NoError(t, p.(*sql.Persister).Connection(t.Context()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{})) + } + }) + + t.Run("contract=identity.TestPool", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + // Unlike the sibling contracts (which bake only the single default + // identity schema via defaultSchema), this contract needs the full + // PoolConfigValues schema set baked into the registry's base config. + ctx := t.Context() + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, configx.WithValues(identity.PoolConfigValues())) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + identity.TestPool(ctx, p, reg.IdentityManager(), name)(t) + }) + t.Run("contract=registration.TestFlowPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + registration.TestFlowPersister(ctx, p)(t) + }) + t.Run("contract=errorx.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + errorx.TestPersister(ctx, p)(t) + }) + t.Run("contract=login.TestFlowPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + login.TestFlowPersister(ctx, p)(t) + }) + t.Run("contract=settings.TestFlowPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + settings.TestFlowPersister(ctx, p)(t) + }) + t.Run("contract=session.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + session.TestPersister(ctx, reg.Config(), p)(t) + }) + t.Run("contract=sessiontokenexchange.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + sessiontokenexchange.TestPersister(ctx, p)(t) + }) + t.Run("contract=courier.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + upsert, insert := sqltesthelpers.DefaultNetworkWrapper(p) + courier.TestPersister(ctx, upsert, insert)(t) + }) + t.Run("contract=verification.TestFlowPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + verification.TestFlowPersister(ctx, p)(t) + }) + t.Run("contract=recovery.TestFlowPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + recovery.TestFlowPersister(ctx, p)(t) + }) + t.Run("contract=link.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + link.TestPersister(ctx, p)(t) + }) + t.Run("contract=code.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + code.TestPersister(ctx, p)(t) + }) + t.Run("contract=continuity.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + continuity.TestPersister(ctx, p)(t) + }) + t.Run("contract=batch.TestPersister", func(t *testing.T) { + t.Parallel() + dsn := dsn + // Just have a separate DB for sqlite to speed it up. + if name == "sqlite" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + + _, reg := pkg.NewRegistryDefaultWithDSN(t, dsn, defaultSchema) + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) + batch.TestPersister(ctx, reg.Tracer(ctx), p)(t) + }) + }) + } +} + +func getErr(args ...interface{}) error { + if len(args) == 0 { + return nil + } + lastArg := args[len(args)-1] + if e, ok := lastArg.(error); ok { + return e + } + return nil +} + +func TestPersister_Transaction(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := reg.Persister() + + t.Run("case=should not create identity because callback returned error", func(t *testing.T) { + i := &ri.Identity{ + ID: x.NewUUID(), + State: ri.StateActive, + Traits: ri.Traits(`{}`), + } + errMessage := "failing because why not" + err := p.Transaction(t.Context(), func(_ context.Context, connection *pop.Connection) error { + require.NoError(t, connection.Create(i)) + return errors.New(errMessage) + }) + require.Error(t, err) + assert.Contains(t, err.Error(), errMessage) + _, err = p.GetIdentity(t.Context(), i.ID, ri.ExpandNothing) + require.Error(t, err) + assert.Equal(t, sqlcon.ErrNoRows().Error(), err.Error()) + }) + + t.Run("case=functions should use the context connection", func(t *testing.T) { + c := p.GetConnection(t.Context()) + errMessage := "some stupid error you can't debug" + lr := &lf.Flow{ + ID: x.NewUUID(), + } + err := c.Transaction(func(tx *pop.Connection) error { + ctx := popx.WithTransaction(t.Context(), tx) + require.NoError(t, p.CreateLoginFlow(ctx, lr), "%+v", lr) + require.NoError(t, getErr(p.GetLoginFlow(ctx, lr.ID)), "%+v", lr) + return errors.New(errMessage) + }) + require.Error(t, err) + assert.Contains(t, err.Error(), errMessage) + _, err = p.GetLoginFlow(t.Context(), lr.ID) + require.Error(t, err) + assert.Equal(t, sqlcon.ErrNoRows().Error(), err.Error()) + }) +} + +func Benchmark_BatchCreateIdentities(b *testing.B) { + ctx := b.Context() + batchSizes := []int{1, 10, 100, 500, 800, 900, 1000, 2000, 3000} + parallelRequests := []int{1, 4, 8, 16} + + for _, name := range dbNames { + b.Run(fmt.Sprintf("database=%s", name), func(b *testing.B) { + dsn := setupDatabase(b, name) + _, reg := pkg.NewRegistryDefaultWithDSN(b, dsn) + conf := reg.Config() + _, p := testhelpers.NewNetwork(b, ctx, reg.Persister()) + multipleEmailsSchema := schema.Schema{ + ID: "multiple_emails", + 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: multipleEmailsSchema.ID, + URL: multipleEmailsSchema.RawURL, + }, + }) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/") + + run := 0 + for _, batchSize := range batchSizes { + b.Run(fmt.Sprintf("batch-size=%d", batchSize), func(b *testing.B) { + for _, paralellism := range parallelRequests { + b.Run(fmt.Sprintf("parallelism=%d", paralellism), func(b *testing.B) { + start := time.Now() + for i := 0; i < b.N; i++ { + wg := new(errgroup.Group) + for paralell := 0; paralell < paralellism; paralell++ { + paralell := paralell + wg.Go(func() error { + identities := make([]*ri.Identity, batchSize) + prefix := fmt.Sprintf("bench-insert-run-%d", run+paralell) + for j := range identities { + identities[j] = identity.NewTestIdentity(1, prefix, j) + } + + return p.CreateIdentities(ctx, identities) + }) + } + assert.NoError(b, wg.Wait()) + run += paralellism + } + end := time.Now() + b.ReportMetric(float64(paralellism*batchSize*b.N), "identites_created") + b.ReportMetric(float64(paralellism*batchSize*b.N)/end.Sub(start).Seconds(), "identities/s") + }) + } + }) + } + }) + } +} + +func newLocalTestCRDBServer(t testing.TB) string { + ts, err := testserver.NewTestServer(testserver.CustomVersionOpt("v25.4.1")) + require.NoError(t, err) + t.Cleanup(ts.Stop) + + require.NoError(t, ts.WaitForInit()) + + ts.PGURL().Scheme = "cockroach" + return ts.PGURL().String() +} diff --git a/persistence/sql/persister_transaction_helpers.go b/persistence/sql/persister_transaction_helpers.go new file mode 100644 index 0000000..6b00a0e --- /dev/null +++ b/persistence/sql/persister_transaction_helpers.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + + "github.com/ory/x/popx" + + "github.com/ory/pop/v6" +) + +func (p *Persister) Transaction(ctx context.Context, callback func(ctx context.Context, connection *pop.Connection) error) error { + return popx.Transaction(ctx, p.c.WithContext(ctx), callback) +} + +func (p *Persister) GetConnection(ctx context.Context) *pop.Connection { + return popx.GetConnection(ctx, p.c.WithContext(ctx)) +} diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go new file mode 100644 index 0000000..236882e --- /dev/null +++ b/persistence/sql/persister_verification.go @@ -0,0 +1,145 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var _ verification.FlowPersister = new(Persister) + +func (p *Persister) CreateVerificationFlow(ctx context.Context, r *verification.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationFlow") + defer otelx.End(span, &err) + + r.NID = p.NetworkID(ctx) + // This should not create the request eagerly because otherwise we might accidentally create an address + // that isn't supposed to be in the database. + return p.GetConnection(ctx).Create(r) +} + +func (p *Persister) GetVerificationFlow(ctx context.Context, id uuid.UUID) (_ *verification.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetVerificationFlow") + defer otelx.End(span, &err) + + var r verification.Flow + if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&r); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &r, nil +} + +func (p *Persister) UpdateVerificationFlow(ctx context.Context, r *verification.Flow) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateVerificationFlow") + defer otelx.End(span, &err) + + cp := *r + cp.NID = p.NetworkID(ctx) + return update.Generic(ctx, p.GetConnection(ctx), p.r.Tracer(ctx).Tracer(), cp) +} + +func (p *Persister) CreateVerificationToken(ctx context.Context, token *link.VerificationToken) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationToken") + defer otelx.End(span, &err) + + t := token.Token + token.Token = p.hmacValue(ctx, t) + token.NID = p.NetworkID(ctx) + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(token); err != nil { + return err + } + token.Token = t + return nil +} + +func (p *Persister) UseVerificationToken(ctx context.Context, fID uuid.UUID, token string) (_ *link.VerificationToken, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationToken") + defer otelx.End(span, &err) + + var rt link.VerificationToken + + 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 = ?", hmacValueWithSecret(token, secret), nid, fID).First(&rt); err != nil { + if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows()) { + return err + } + } else { + break + } + } + if err != nil { + return err + } + + if rt.VerifiableAddressID.Valid { + var va identity.VerifiableAddress + if err := tx.Where("id = ? AND nid = ?", rt.VerifiableAddressID.UUID, nid).First(&va); err != nil { + return sqlcon.HandleError(err) + } + rt.VerifiableAddress = &va + } + + // Consume the token atomically. The `NOT used` guard ensures that on + // READ COMMITTED backends two concurrent submissions of the same token + // cannot both succeed: only the first UPDATE affects a row. If no row + // was affected, another transaction already consumed the token. + //#nosec G201 -- TableName is static + count, err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET used=true, used_at=? WHERE id=? AND nid = ? AND NOT used", rt.TableName(ctx)), time.Now().UTC(), rt.ID, nid).ExecWithCount() + if err != nil { + return err + } + if count == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + return nil + })); err != nil { + return nil, err + } + + return &rt, nil +} + +func (p *Persister) DeleteVerificationToken(ctx context.Context, token string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationToken") + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + //#nosec G201 -- TableName is static + return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE token=? AND nid = ?", new(link.VerificationToken).TableName(ctx)), token, nid).Exec() +} + +func (p *Persister) DeleteExpiredVerificationFlows(ctx context.Context, expiresAt time.Time, limit int) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredVerificationFlows") + defer otelx.End(span, &err) + //#nosec G201 -- TableName is static + err = p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)", + verification.Flow{}.TableName(), + ), + expiresAt, + p.NetworkID(ctx), + limit, + ).Exec() + + return sqlcon.HandleError(err) +} diff --git a/persistence/sql/persister_verification_code.go b/persistence/sql/persister_verification_code.go new file mode 100644 index 0000000..0ba9f4c --- /dev/null +++ b/persistence/sql/persister_verification_code.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateVerificationCode(ctx context.Context, params *code.CreateVerificationCodeParams) (_ *code.VerificationCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationCode") + defer otelx.End(span, &err) + + now := time.Now().UTC() + verificationCode := &code.VerificationCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.Add(params.ExpiresIn), + IssuedAt: now, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + } + + if params.VerifiableAddress == nil { + return nil, errors.WithStack(herodot.ErrNotFound().WithReason("can't create a verification code without a verifiable address")) + } + + // Can be nil when the code is for a pending change, in which case the address doesn't exist yet. See VerifyNewAddress hook. + if va, ok := params.VerifiableAddress.ToPersistable(); ok { + verificationCode.VerifiableAddress = va + verificationCode.VerifiableAddressID = uuid.NullUUID{UUID: va.ID, Valid: true} + } else { + verificationCode.VerifiableAddressID = uuid.NullUUID{Valid: false} + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(verificationCode); err != nil { + return nil, err + } + + return verificationCode, nil +} + +func (p *Persister) UseVerificationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (_ *code.VerificationCode, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationCode") + defer otelx.End(span, &err) + + codeRow, err := useOneTimeCode[code.VerificationCode](ctx, p, flowID, userProvidedCode, verification.Flow{}.TableName(), "selfservice_verification_flow_id") + if err != nil { + return nil, err + } + + if codeRow.VerifiableAddressID.Valid { + var va identity.VerifiableAddress + if err := p.Connection(ctx).Where("id = ? AND nid = ?", codeRow.VerifiableAddressID, p.NetworkID(ctx)).First(&va); err != nil { + return nil, sqlcon.HandleError(err) + } + codeRow.VerifiableAddress = &va + } + + return codeRow, nil +} + +func (p *Persister) DeleteVerificationCodesOfFlow(ctx context.Context, fID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationCodesOfFlow") + defer otelx.End(span, &err) + + return p.GetConnection(ctx).Where("selfservice_verification_flow_id = ? AND nid = ?", fID, p.NetworkID(ctx)).Delete(&code.VerificationCode{}) +} diff --git a/persistence/sql/stub/expand.schema.json b/persistence/sql/stub/expand.schema.json new file mode 100644 index 0000000..5cb8a8e --- /dev/null +++ b/persistence/sql/stub/expand.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "traits": { + "additionalProperties": false, + "properties": { + "email": { + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + }, + "title": "Email address", + "type": "string", + "maxLength": 320 + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "email", + "name" + ], + "type": "object" + } + }, + "title": "Person", + "type": "object" +} diff --git a/persistence/sql/stub/identity-2.schema.json b/persistence/sql/stub/identity-2.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/persistence/sql/stub/identity-2.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/persistence/sql/stub/identity.schema.json b/persistence/sql/stub/identity.schema.json new file mode 100644 index 0000000..0337894 --- /dev/null +++ b/persistence/sql/stub/identity.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/persistence/sql/stub/phone.schema.json b/persistence/sql/stub/phone.schema.json new file mode 100644 index 0000000..71a17c9 --- /dev/null +++ b/persistence/sql/stub/phone.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "traits": { + "additionalProperties": false, + "properties": { + "phone": { + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "sms" + }, + "verification": { + "via": "sms" + } + }, + "title": "Phone number", + "type": "string", + "maxLength": 32 + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "phone", + "name" + ], + "type": "object" + } + }, + "title": "Person", + "type": "object" +} diff --git a/persistence/sql/testhelpers/network.go b/persistence/sql/testhelpers/network.go new file mode 100644 index 0000000..81cb360 --- /dev/null +++ b/persistence/sql/testhelpers/network.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "testing" + + db "github.com/gofrs/uuid" + + courier "github.com/ory/kratos/courier/test" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" +) + +func DefaultNetworkWrapper(p persistence.Persister) (courier.NetworkWrapper, courier.NetworkWrapper) { + return func(t *testing.T, ctx context.Context) (db.UUID, courier.PersisterWrapper) { + return testhelpers.NewNetworkUnlessExisting(t, ctx, p) + }, func(t *testing.T, ctx context.Context) (db.UUID, courier.PersisterWrapper) { + return testhelpers.NewNetwork(t, ctx, p) + } +} diff --git a/persistence/sql/update/update.go b/persistence/sql/update/update.go new file mode 100644 index 0000000..1df80fc --- /dev/null +++ b/persistence/sql/update/update.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package update + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/columns" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func Generic(ctx context.Context, c *pop.Connection, tracer trace.Tracer, v any, columnNames ...string) (err error) { + ctx, span := tracer.Start(ctx, "persistence.sql.update") + defer otelx.End(span, &err) + + quoter, ok := c.Dialect.(interface{ Quote(key string) string }) + if !ok { + return errors.Errorf("store is not a quoter: %T", c.Store) + } + + model := pop.NewModel(v, ctx) + tn := model.TableName() + + cols := columns.Columns{} + if len(columnNames) > 0 && tn == model.TableName() { + cols = columns.NewColumnsWithAlias(tn, model.As, model.IDField()) + cols.Add(columnNames...) + } else { + cols = columns.ForStructWithAlias(v, tn, model.As, model.IDField()) + } + + //#nosec G201 -- TableName is static + stmt := fmt.Sprintf("UPDATE %s AS %s SET %s WHERE (%s) AND %s.nid = :nid", + quoter.Quote(model.TableName()), + model.Alias(), + cols.Writeable().QuotedUpdateString(quoter), + model.WhereNamedID(), + model.Alias(), + ) + + if result, err := c.Store.NamedExecContext(ctx, stmt, v); err != nil { + return sqlcon.HandleError(err) + } else if affected, err := result.RowsAffected(); err != nil { + return sqlcon.HandleError(err) + } else if affected == 0 { + return errors.WithStack(sqlcon.ErrNoRows()) + } + + return nil +} + +// GenericExcept updates v like Generic, but omits excludeColumns from the +// UPDATE ... SET list (every other writeable column is still written). +// +// Leaving a column that backs a unique index out of the SET list — when its +// value has not changed — lets the database skip the uniqueness check it would +// otherwise plan for that index, which can be expensive. +func GenericExcept(ctx context.Context, c *pop.Connection, tracer trace.Tracer, v any, excludeColumns ...string) error { + model := pop.NewModel(v, ctx) + cols := columns.ForStructWithAlias(v, model.TableName(), model.As, model.IDField()) + cols.Remove(excludeColumns...) + + writeable := cols.Writeable().Cols + names := make([]string, 0, len(writeable)) + for name := range writeable { + names = append(names, name) + } + return Generic(ctx, c, tracer, v, names...) +} diff --git a/persistence/sql/update/update_test.go b/persistence/sql/update/update_test.go new file mode 100644 index 0000000..82ce3f3 --- /dev/null +++ b/persistence/sql/update/update_test.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package update_test + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + + "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" +) + +type updateTestModel struct { + ID uuid.UUID `db:"id"` + NID uuid.UUID `db:"nid"` + Name string `db:"name"` + UniqueVal string `db:"unique_val"` +} + +func (updateTestModel) TableName() string { return "update_test_models" } + +func newUpdateTestConn(t *testing.T) *pop.Connection { + t.Helper() + + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dbal.NewSQLiteTestDatabase(t)}) + require.NoError(t, err) + require.NoError(t, c.Open()) + t.Cleanup(func() { _ = c.Close() }) + + require.NoError(t, c.RawQuery(`CREATE TABLE update_test_models ( + id TEXT PRIMARY KEY, + nid TEXT NOT NULL, + name TEXT NOT NULL, + unique_val TEXT NOT NULL + )`).Exec()) + require.NoError(t, c.RawQuery( + `CREATE UNIQUE INDEX update_test_models_nid_unique_val ON update_test_models (nid, unique_val)`).Exec()) + + return c +} + +func TestGeneric(t *testing.T) { + t.Parallel() + ctx := t.Context() + tracer := noop.NewTracerProvider().Tracer("") + + seed := func(t *testing.T, c *pop.Connection) (nid, id uuid.UUID) { + nid = uuid.Must(uuid.NewV4()) + id = uuid.Must(uuid.NewV4()) + require.NoError(t, c.Create(&updateTestModel{ + ID: id, NID: nid, Name: "original", UniqueVal: "original-unique", + })) + return nid, id + } + + reload := func(t *testing.T, c *pop.Connection, nid, id uuid.UUID) updateTestModel { + var m updateTestModel + require.NoError(t, c.Where("id = ? AND nid = ?", id, nid).First(&m)) + return m + } + + t.Run("case=GenericExcept leaves the excluded column untouched", func(t *testing.T) { + t.Parallel() + c := newUpdateTestConn(t) + nid, id := seed(t, c) + + // The in-memory model carries a different unique_val, but excluding it + // must keep the column out of the SET list so the stored value stays. + m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "must-not-be-written"} + require.NoError(t, update.GenericExcept(ctx, c, tracer, m, "unique_val")) + + got := reload(t, c, nid, id) + assert.Equal(t, "changed", got.Name, "a non-excluded column is written") + assert.Equal(t, "original-unique", got.UniqueVal, "the excluded column is left untouched") + }) + + t.Run("case=Generic writes every column including the unique one", func(t *testing.T) { + t.Parallel() + c := newUpdateTestConn(t) + nid, id := seed(t, c) + + m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "now-written"} + require.NoError(t, update.Generic(ctx, c, tracer, m)) + + got := reload(t, c, nid, id) + assert.Equal(t, "changed", got.Name) + assert.Equal(t, "now-written", got.UniqueVal, "Generic writes the unique column") + }) + + t.Run("case=Generic with an explicit column list writes only those columns", func(t *testing.T) { + t.Parallel() + c := newUpdateTestConn(t) + nid, id := seed(t, c) + + m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "must-not-be-written"} + require.NoError(t, update.Generic(ctx, c, tracer, m, "name")) + + got := reload(t, c, nid, id) + assert.Equal(t, "changed", got.Name) + assert.Equal(t, "original-unique", got.UniqueVal, "a column absent from the list is left untouched") + }) + + t.Run("case=returns ErrNoRows when the row does not exist", func(t *testing.T) { + t.Parallel() + c := newUpdateTestConn(t) + + m := &updateTestModel{ID: uuid.Must(uuid.NewV4()), NID: uuid.Must(uuid.NewV4()), Name: "x", UniqueVal: "y"} + assert.Error(t, update.GenericExcept(ctx, c, tracer, m, "unique_val")) + assert.Error(t, update.Generic(ctx, c, tracer, m)) + }) +} diff --git a/pkg/client-go/.gitignore b/pkg/client-go/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/pkg/client-go/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/pkg/client-go/.openapi-generator-ignore b/pkg/client-go/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/pkg/client-go/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/pkg/client-go/.openapi-generator/FILES b/pkg/client-go/.openapi-generator/FILES new file mode 100644 index 0000000..cdc0e6a --- /dev/null +++ b/pkg/client-go/.openapi-generator/FILES @@ -0,0 +1,352 @@ +.gitignore +.openapi-generator-ignore +.travis.yml +README.md +api/openapi.yaml +api_courier.go +api_frontend.go +api_identity.go +api_metadata.go +client.go +configuration.go +docs/AdminIdentityImportCredentialsLookupSecret.md +docs/AdminIdentityImportCredentialsLookupSecretConfig.md +docs/AuthenticatorAssuranceLevel.md +docs/BatchPatchIdentitiesResponse.md +docs/ConsistencyRequestParameters.md +docs/ContinueWith.md +docs/ContinueWithDeviceAuthnPinEntryUi.md +docs/ContinueWithDeviceAuthnPinEntryUiData.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/CourierMessageStatus.md +docs/CourierMessageType.md +docs/CreateFedcmFlowResponse.md +docs/CreateIdentityBody.md +docs/CreateRecoveryCodeForIdentityBody.md +docs/CreateRecoveryLinkForIdentityBody.md +docs/CreateTestLoginFlowBody.md +docs/DeleteMySessionsCount.md +docs/DeviceAuthnAndroidAuthorizationList.md +docs/DeviceAuthnAndroidKeyDescription.md +docs/DeviceAuthnAndroidRootOfTrust.md +docs/DeviceAuthnAttestation.md +docs/DeviceAuthnIOSAttStmt.md +docs/DeviceAuthnIOSAttestation.md +docs/DeviceAuthnKey.md +docs/DeviceType.md +docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md +docs/ErrorBrowserLocationChangeRequired.md +docs/ErrorFlowReplaced.md +docs/ErrorGeneric.md +docs/FlowError.md +docs/FrontendAPI.md +docs/GenericError.md +docs/GetVersion200Response.md +docs/HealthNotReadyStatus.md +docs/HealthStatus.md +docs/Identity.md +docs/IdentityAPI.md +docs/IdentityCredentials.md +docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md +docs/IdentityCredentialsLookupSecretCode.md +docs/IdentityCredentialsOidc.md +docs/IdentityCredentialsOidcProvider.md +docs/IdentityCredentialsPassword.md +docs/IdentityCredentialsWebAuthn.md +docs/IdentityCredentialsWebAuthnAttestation.md +docs/IdentityCredentialsWebAuthnAuthenticator.md +docs/IdentityCredentialsWebAuthnFlags.md +docs/IdentityPatch.md +docs/IdentityPatchResponse.md +docs/IdentitySchemaContainer.md +docs/IdentityWithCredentials.md +docs/IdentityWithCredentialsOidc.md +docs/IdentityWithCredentialsOidcConfig.md +docs/IdentityWithCredentialsOidcConfigProvider.md +docs/IdentityWithCredentialsPasskey.md +docs/IdentityWithCredentialsPasskeyConfig.md +docs/IdentityWithCredentialsPassword.md +docs/IdentityWithCredentialsPasswordConfig.md +docs/IdentityWithCredentialsSaml.md +docs/IdentityWithCredentialsSamlConfig.md +docs/IdentityWithCredentialsSamlConfigProvider.md +docs/IdentityWithCredentialsTotp.md +docs/IdentityWithCredentialsTotpConfig.md +docs/IdentityWithCredentialsWebAuthn.md +docs/IdentityWithCredentialsWebAuthnConfig.md +docs/IsAlive200Response.md +docs/IsReady503Response.md +docs/JsonPatch.md +docs/KeyState.md +docs/LoginFlow.md +docs/LoginFlowState.md +docs/LoginFlowTestContext.md +docs/LoginFlowTestDebugPayload.md +docs/LoginFlowTestSchemaValidationError.md +docs/LoginFlowTestStepError.md +docs/LogoutFlow.md +docs/ManageSessionsBody.md +docs/ManageSessionsResponse.md +docs/Message.md +docs/MessageDispatch.md +docs/MetadataAPI.md +docs/NeedsPrivilegedSessionError.md +docs/OAuth2Client.md +docs/OAuth2ConsentRequestOpenIDConnectContext.md +docs/OAuth2LoginRequest.md +docs/PINConfig.md +docs/PatchIdentitiesBody.md +docs/PerformNativeLogoutBody.md +docs/Provider.md +docs/RecoveryCodeForIdentity.md +docs/RecoveryFlow.md +docs/RecoveryFlowState.md +docs/RecoveryIdentityAddress.md +docs/RecoveryLinkForIdentity.md +docs/RegistrationFlow.md +docs/RegistrationFlowState.md +docs/SelfServiceFlowExpiredError.md +docs/Session.md +docs/SessionAuthenticationMethod.md +docs/SessionDevice.md +docs/SettingsFlow.md +docs/SettingsFlowState.md +docs/SuccessfulCodeExchangeResponse.md +docs/SuccessfulNativeLogin.md +docs/SuccessfulNativeRegistration.md +docs/TokenPagination.md +docs/TokenPaginationHeaders.md +docs/UiContainer.md +docs/UiNode.md +docs/UiNodeAnchorAttributes.md +docs/UiNodeAttributes.md +docs/UiNodeDivisionAttributes.md +docs/UiNodeImageAttributes.md +docs/UiNodeInputAttributes.md +docs/UiNodeInputAttributesOption.md +docs/UiNodeMeta.md +docs/UiNodeScriptAttributes.md +docs/UiNodeTextAttributes.md +docs/UiText.md +docs/UpdateFedcmFlowBody.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/UpdateLoginFlowWithSamlMethod.md +docs/UpdateLoginFlowWithTotpMethod.md +docs/UpdateLoginFlowWithWebAuthnMethod.md +docs/UpdateRecoveryFlowBody.md +docs/UpdateRecoveryFlowWithCodeMethod.md +docs/UpdateRecoveryFlowWithLinkMethod.md +docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md +docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md +docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md +docs/UpdateRegistrationFlowWithSamlMethod.md +docs/UpdateRegistrationFlowWithWebAuthnMethod.md +docs/UpdateSettingsFlowBody.md +docs/UpdateSettingsFlowWithLookupMethod.md +docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md +docs/UpdateSettingsFlowWithPasswordMethod.md +docs/UpdateSettingsFlowWithProfileMethod.md +docs/UpdateSettingsFlowWithSamlMethod.md +docs/UpdateSettingsFlowWithTotpMethod.md +docs/UpdateSettingsFlowWithWebAuthnMethod.md +docs/UpdateVerificationFlowBody.md +docs/UpdateVerificationFlowWithCodeMethod.md +docs/UpdateVerificationFlowWithLinkMethod.md +docs/UserVerification.md +docs/VerifiableIdentityAddress.md +docs/VerificationFlow.md +docs/VerificationFlowState.md +docs/Version.md +git_push.sh +go.mod +go.sum +model_admin_identity_import_credentials_lookup_secret.go +model_admin_identity_import_credentials_lookup_secret_config.go +model_authenticator_assurance_level.go +model_batch_patch_identities_response.go +model_consistency_request_parameters.go +model_continue_with.go +model_continue_with_device_authn_pin_entry_ui.go +model_continue_with_device_authn_pin_entry_ui_data.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 +model_continue_with_verification_ui.go +model_continue_with_verification_ui_flow.go +model_courier_message_status.go +model_courier_message_type.go +model_create_fedcm_flow_response.go +model_create_identity_body.go +model_create_recovery_code_for_identity_body.go +model_create_recovery_link_for_identity_body.go +model_create_test_login_flow_body.go +model_delete_my_sessions_count.go +model_device_authn_android_authorization_list.go +model_device_authn_android_key_description.go +model_device_authn_android_root_of_trust.go +model_device_authn_attestation.go +model_device_authn_ios_att_stmt.go +model_device_authn_ios_attestation.go +model_device_authn_key.go +model_device_type.go +model_error_authenticator_assurance_level_not_satisfied.go +model_error_browser_location_change_required.go +model_error_flow_replaced.go +model_error_generic.go +model_flow_error.go +model_generic_error.go +model_get_version_200_response.go +model_health_not_ready_status.go +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_lookup_secret_code.go +model_identity_credentials_oidc.go +model_identity_credentials_oidc_provider.go +model_identity_credentials_password.go +model_identity_credentials_web_authn.go +model_identity_credentials_web_authn_attestation.go +model_identity_credentials_web_authn_authenticator.go +model_identity_credentials_web_authn_flags.go +model_identity_patch.go +model_identity_patch_response.go +model_identity_schema_container.go +model_identity_with_credentials.go +model_identity_with_credentials_oidc.go +model_identity_with_credentials_oidc_config.go +model_identity_with_credentials_oidc_config_provider.go +model_identity_with_credentials_passkey.go +model_identity_with_credentials_passkey_config.go +model_identity_with_credentials_password.go +model_identity_with_credentials_password_config.go +model_identity_with_credentials_saml.go +model_identity_with_credentials_saml_config.go +model_identity_with_credentials_saml_config_provider.go +model_identity_with_credentials_totp.go +model_identity_with_credentials_totp_config.go +model_identity_with_credentials_web_authn.go +model_identity_with_credentials_web_authn_config.go +model_is_alive_200_response.go +model_is_ready_503_response.go +model_json_patch.go +model_key_state.go +model_login_flow.go +model_login_flow_state.go +model_login_flow_test_context.go +model_login_flow_test_debug_payload.go +model_login_flow_test_schema_validation_error.go +model_login_flow_test_step_error.go +model_logout_flow.go +model_manage_sessions_body.go +model_manage_sessions_response.go +model_message.go +model_message_dispatch.go +model_needs_privileged_session_error.go +model_o_auth2_client.go +model_o_auth2_consent_request_open_id_connect_context.go +model_o_auth2_login_request.go +model_patch_identities_body.go +model_perform_native_logout_body.go +model_pin_config.go +model_provider.go +model_recovery_code_for_identity.go +model_recovery_flow.go +model_recovery_flow_state.go +model_recovery_identity_address.go +model_recovery_link_for_identity.go +model_registration_flow.go +model_registration_flow_state.go +model_self_service_flow_expired_error.go +model_session.go +model_session_authentication_method.go +model_session_device.go +model_settings_flow.go +model_settings_flow_state.go +model_successful_code_exchange_response.go +model_successful_native_login.go +model_successful_native_registration.go +model_token_pagination.go +model_token_pagination_headers.go +model_ui_container.go +model_ui_node.go +model_ui_node_anchor_attributes.go +model_ui_node_attributes.go +model_ui_node_division_attributes.go +model_ui_node_image_attributes.go +model_ui_node_input_attributes.go +model_ui_node_input_attributes_option.go +model_ui_node_meta.go +model_ui_node_script_attributes.go +model_ui_node_text_attributes.go +model_ui_text.go +model_update_fedcm_flow_body.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_saml_method.go +model_update_login_flow_with_totp_method.go +model_update_login_flow_with_web_authn_method.go +model_update_recovery_flow_body.go +model_update_recovery_flow_with_code_method.go +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_saml_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_saml_method.go +model_update_settings_flow_with_totp_method.go +model_update_settings_flow_with_web_authn_method.go +model_update_verification_flow_body.go +model_update_verification_flow_with_code_method.go +model_update_verification_flow_with_link_method.go +model_user_verification.go +model_verifiable_identity_address.go +model_verification_flow.go +model_verification_flow_state.go +model_version.go +response.go +test/api_courier_test.go +test/api_frontend_test.go +test/api_identity_test.go +test/api_metadata_test.go +utils.go diff --git a/pkg/client-go/.openapi-generator/VERSION b/pkg/client-go/.openapi-generator/VERSION new file mode 100644 index 0000000..5f84a81 --- /dev/null +++ b/pkg/client-go/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.12.0 diff --git a/pkg/client-go/.travis.yml b/pkg/client-go/.travis.yml new file mode 100644 index 0000000..f5cb2ce --- /dev/null +++ b/pkg/client-go/.travis.yml @@ -0,0 +1,8 @@ +language: go + +install: + - go get -d -v . + +script: + - go build -v ./ + diff --git a/pkg/client-go/README.md b/pkg/client-go/README.md new file mode 100644 index 0000000..a40c775 --- /dev/null +++ b/pkg/client-go/README.md @@ -0,0 +1,357 @@ +# Go API client for client + +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. + + +## 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: +- Package version: 1.0.0 +- Generator version: 7.12.0 +- Build package: org.openapitools.codegen.languages.GoClientCodegen + +## Installation + +Install the following dependencies: + +```sh +go get github.com/stretchr/testify/assert +go get golang.org/x/net/context +``` + +Put the package under your project folder and add the following in import: + +```go +import client "github.com/ory/client-go" +``` + +To use a proxy, set the environment variable `HTTP_PROXY`: + +```go +os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") +``` + +## Configuration of Server URL + +Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. + +### Select Server Configuration + +For using other server than the one defined on index 0 set context value `client.ContextServerIndex` of type `int`. + +```go +ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) +``` + +### Templated Server URL + +Templated server URL is formatted using default variables from configuration or from context value `client.ContextServerVariables` of type `map[string]string`. + +```go +ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ + "basePath": "v2", +}) +``` + +Note, enum values are always validated and all unused variables are silently ignored. + +### URLs Configuration per Operation + +Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. +An operation is uniquely identified by `"{classname}Service.{nickname}"` string. +Similar rules for overriding default operation server index and variables applies by using `client.ContextOperationServerIndices` and `client.ContextOperationServerVariables` context maps. + +```go +ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ + "{classname}Service.{nickname}": 2, +}) +ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ + "{classname}Service.{nickname}": { + "port": "8443", + }, +}) +``` + +## Documentation for API Endpoints + +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* | [**CreateFedcmFlow**](docs/FrontendAPI.md#createfedcmflow) | **Get** /self-service/fed-cm/parameters | Get FedCM Parameters +*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* | [**DeleteTestLoginFlow**](docs/FrontendAPI.md#deletetestloginflow) | **Delete** /self-service/login/test | Delete a test OIDC login flow +*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* | [**GetWellKnownChangePassword**](docs/FrontendAPI.md#getwellknownchangepassword) | **Get** /.well-known/change-password | Change Password URL +*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* | [**UpdateFedcmFlow**](docs/FrontendAPI.md#updatefedcmflow) | **Post** /self-service/fed-cm/token | Submit a FedCM token +*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* | [**CreateTestLoginFlow**](docs/IdentityAPI.md#createtestloginflow) | **Post** /admin/test-login-flows | Create a test OIDC login flow +*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* | [**GetIdentityByExternalID**](docs/IdentityAPI.md#getidentitybyexternalid) | **Get** /admin/identities/by/external/{externalID} | Get an Identity by its External ID +*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* | [**ManageSessions**](docs/IdentityAPI.md#managesessions) | **Post** /admin/sessions | Manage sessions in bulk +*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 + + - [AdminIdentityImportCredentialsLookupSecret](docs/AdminIdentityImportCredentialsLookupSecret.md) + - [AdminIdentityImportCredentialsLookupSecretConfig](docs/AdminIdentityImportCredentialsLookupSecretConfig.md) + - [AuthenticatorAssuranceLevel](docs/AuthenticatorAssuranceLevel.md) + - [BatchPatchIdentitiesResponse](docs/BatchPatchIdentitiesResponse.md) + - [ConsistencyRequestParameters](docs/ConsistencyRequestParameters.md) + - [ContinueWith](docs/ContinueWith.md) + - [ContinueWithDeviceAuthnPinEntryUi](docs/ContinueWithDeviceAuthnPinEntryUi.md) + - [ContinueWithDeviceAuthnPinEntryUiData](docs/ContinueWithDeviceAuthnPinEntryUiData.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) + - [ContinueWithVerificationUi](docs/ContinueWithVerificationUi.md) + - [ContinueWithVerificationUiFlow](docs/ContinueWithVerificationUiFlow.md) + - [CourierMessageStatus](docs/CourierMessageStatus.md) + - [CourierMessageType](docs/CourierMessageType.md) + - [CreateFedcmFlowResponse](docs/CreateFedcmFlowResponse.md) + - [CreateIdentityBody](docs/CreateIdentityBody.md) + - [CreateRecoveryCodeForIdentityBody](docs/CreateRecoveryCodeForIdentityBody.md) + - [CreateRecoveryLinkForIdentityBody](docs/CreateRecoveryLinkForIdentityBody.md) + - [CreateTestLoginFlowBody](docs/CreateTestLoginFlowBody.md) + - [DeleteMySessionsCount](docs/DeleteMySessionsCount.md) + - [DeviceAuthnAndroidAuthorizationList](docs/DeviceAuthnAndroidAuthorizationList.md) + - [DeviceAuthnAndroidKeyDescription](docs/DeviceAuthnAndroidKeyDescription.md) + - [DeviceAuthnAndroidRootOfTrust](docs/DeviceAuthnAndroidRootOfTrust.md) + - [DeviceAuthnAttestation](docs/DeviceAuthnAttestation.md) + - [DeviceAuthnIOSAttStmt](docs/DeviceAuthnIOSAttStmt.md) + - [DeviceAuthnIOSAttestation](docs/DeviceAuthnIOSAttestation.md) + - [DeviceAuthnKey](docs/DeviceAuthnKey.md) + - [DeviceType](docs/DeviceType.md) + - [ErrorAuthenticatorAssuranceLevelNotSatisfied](docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md) + - [ErrorBrowserLocationChangeRequired](docs/ErrorBrowserLocationChangeRequired.md) + - [ErrorFlowReplaced](docs/ErrorFlowReplaced.md) + - [ErrorGeneric](docs/ErrorGeneric.md) + - [FlowError](docs/FlowError.md) + - [GenericError](docs/GenericError.md) + - [GetVersion200Response](docs/GetVersion200Response.md) + - [HealthNotReadyStatus](docs/HealthNotReadyStatus.md) + - [HealthStatus](docs/HealthStatus.md) + - [Identity](docs/Identity.md) + - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) + - [IdentityCredentialsLookupSecretCode](docs/IdentityCredentialsLookupSecretCode.md) + - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) + - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) + - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) + - [IdentityCredentialsWebAuthn](docs/IdentityCredentialsWebAuthn.md) + - [IdentityCredentialsWebAuthnAttestation](docs/IdentityCredentialsWebAuthnAttestation.md) + - [IdentityCredentialsWebAuthnAuthenticator](docs/IdentityCredentialsWebAuthnAuthenticator.md) + - [IdentityCredentialsWebAuthnFlags](docs/IdentityCredentialsWebAuthnFlags.md) + - [IdentityPatch](docs/IdentityPatch.md) + - [IdentityPatchResponse](docs/IdentityPatchResponse.md) + - [IdentitySchemaContainer](docs/IdentitySchemaContainer.md) + - [IdentityWithCredentials](docs/IdentityWithCredentials.md) + - [IdentityWithCredentialsOidc](docs/IdentityWithCredentialsOidc.md) + - [IdentityWithCredentialsOidcConfig](docs/IdentityWithCredentialsOidcConfig.md) + - [IdentityWithCredentialsOidcConfigProvider](docs/IdentityWithCredentialsOidcConfigProvider.md) + - [IdentityWithCredentialsPasskey](docs/IdentityWithCredentialsPasskey.md) + - [IdentityWithCredentialsPasskeyConfig](docs/IdentityWithCredentialsPasskeyConfig.md) + - [IdentityWithCredentialsPassword](docs/IdentityWithCredentialsPassword.md) + - [IdentityWithCredentialsPasswordConfig](docs/IdentityWithCredentialsPasswordConfig.md) + - [IdentityWithCredentialsSaml](docs/IdentityWithCredentialsSaml.md) + - [IdentityWithCredentialsSamlConfig](docs/IdentityWithCredentialsSamlConfig.md) + - [IdentityWithCredentialsSamlConfigProvider](docs/IdentityWithCredentialsSamlConfigProvider.md) + - [IdentityWithCredentialsTotp](docs/IdentityWithCredentialsTotp.md) + - [IdentityWithCredentialsTotpConfig](docs/IdentityWithCredentialsTotpConfig.md) + - [IdentityWithCredentialsWebAuthn](docs/IdentityWithCredentialsWebAuthn.md) + - [IdentityWithCredentialsWebAuthnConfig](docs/IdentityWithCredentialsWebAuthnConfig.md) + - [IsAlive200Response](docs/IsAlive200Response.md) + - [IsReady503Response](docs/IsReady503Response.md) + - [JsonPatch](docs/JsonPatch.md) + - [KeyState](docs/KeyState.md) + - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) + - [LoginFlowTestContext](docs/LoginFlowTestContext.md) + - [LoginFlowTestDebugPayload](docs/LoginFlowTestDebugPayload.md) + - [LoginFlowTestSchemaValidationError](docs/LoginFlowTestSchemaValidationError.md) + - [LoginFlowTestStepError](docs/LoginFlowTestStepError.md) + - [LogoutFlow](docs/LogoutFlow.md) + - [ManageSessionsBody](docs/ManageSessionsBody.md) + - [ManageSessionsResponse](docs/ManageSessionsResponse.md) + - [Message](docs/Message.md) + - [MessageDispatch](docs/MessageDispatch.md) + - [NeedsPrivilegedSessionError](docs/NeedsPrivilegedSessionError.md) + - [OAuth2Client](docs/OAuth2Client.md) + - [OAuth2ConsentRequestOpenIDConnectContext](docs/OAuth2ConsentRequestOpenIDConnectContext.md) + - [OAuth2LoginRequest](docs/OAuth2LoginRequest.md) + - [PINConfig](docs/PINConfig.md) + - [PatchIdentitiesBody](docs/PatchIdentitiesBody.md) + - [PerformNativeLogoutBody](docs/PerformNativeLogoutBody.md) + - [Provider](docs/Provider.md) + - [RecoveryCodeForIdentity](docs/RecoveryCodeForIdentity.md) + - [RecoveryFlow](docs/RecoveryFlow.md) + - [RecoveryFlowState](docs/RecoveryFlowState.md) + - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) + - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) + - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) + - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) + - [Session](docs/Session.md) + - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) + - [SessionDevice](docs/SessionDevice.md) + - [SettingsFlow](docs/SettingsFlow.md) + - [SettingsFlowState](docs/SettingsFlowState.md) + - [SuccessfulCodeExchangeResponse](docs/SuccessfulCodeExchangeResponse.md) + - [SuccessfulNativeLogin](docs/SuccessfulNativeLogin.md) + - [SuccessfulNativeRegistration](docs/SuccessfulNativeRegistration.md) + - [TokenPagination](docs/TokenPagination.md) + - [TokenPaginationHeaders](docs/TokenPaginationHeaders.md) + - [UiContainer](docs/UiContainer.md) + - [UiNode](docs/UiNode.md) + - [UiNodeAnchorAttributes](docs/UiNodeAnchorAttributes.md) + - [UiNodeAttributes](docs/UiNodeAttributes.md) + - [UiNodeDivisionAttributes](docs/UiNodeDivisionAttributes.md) + - [UiNodeImageAttributes](docs/UiNodeImageAttributes.md) + - [UiNodeInputAttributes](docs/UiNodeInputAttributes.md) + - [UiNodeInputAttributesOption](docs/UiNodeInputAttributesOption.md) + - [UiNodeMeta](docs/UiNodeMeta.md) + - [UiNodeScriptAttributes](docs/UiNodeScriptAttributes.md) + - [UiNodeTextAttributes](docs/UiNodeTextAttributes.md) + - [UiText](docs/UiText.md) + - [UpdateFedcmFlowBody](docs/UpdateFedcmFlowBody.md) + - [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) + - [UpdateLoginFlowWithSamlMethod](docs/UpdateLoginFlowWithSamlMethod.md) + - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) + - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) + - [UpdateRecoveryFlowBody](docs/UpdateRecoveryFlowBody.md) + - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) + - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) + - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) + - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) + - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) + - [UpdateRegistrationFlowWithSamlMethod](docs/UpdateRegistrationFlowWithSamlMethod.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) + - [UpdateSettingsFlowWithSamlMethod](docs/UpdateSettingsFlowWithSamlMethod.md) + - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) + - [UpdateSettingsFlowWithWebAuthnMethod](docs/UpdateSettingsFlowWithWebAuthnMethod.md) + - [UpdateVerificationFlowBody](docs/UpdateVerificationFlowBody.md) + - [UpdateVerificationFlowWithCodeMethod](docs/UpdateVerificationFlowWithCodeMethod.md) + - [UpdateVerificationFlowWithLinkMethod](docs/UpdateVerificationFlowWithLinkMethod.md) + - [UserVerification](docs/UserVerification.md) + - [VerifiableIdentityAddress](docs/VerifiableIdentityAddress.md) + - [VerificationFlow](docs/VerificationFlow.md) + - [VerificationFlowState](docs/VerificationFlowState.md) + - [Version](docs/Version.md) + + +## Documentation For Authorization + + +Authentication schemes defined for the API: +### oryAccessToken + +- **Type**: API key +- **API key parameter name**: Authorization +- **Location**: HTTP header + +Note, each API key must be added to a map of `map[string]APIKey` where the key is: oryAccessToken and passed in as the auth context for each request. + +Example + +```go +auth := context.WithValue( + context.Background(), + client.ContextAPIKeys, + map[string]client.APIKey{ + "oryAccessToken": {Key: "API_KEY_STRING"}, + }, + ) +r, err := client.Service.Operation(auth, args) +``` + + +## Documentation for Utility Methods + +Due to the fact that model structure members are all pointers, this package contains +a number of utility functions to easily obtain pointers to values of basic types. +Each of these functions takes a value of the given basic type and returns a pointer to it: + +* `PtrBool` +* `PtrInt` +* `PtrInt32` +* `PtrInt64` +* `PtrFloat` +* `PtrFloat32` +* `PtrFloat64` +* `PtrString` +* `PtrTime` + +## Author + +office@ory.sh + diff --git a/pkg/client-go/api_courier.go b/pkg/client-go/api_courier.go new file mode 100644 index 0000000..0fc15fc --- /dev/null +++ b/pkg/client-go/api_courier.go @@ -0,0 +1,369 @@ +/* +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 ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + +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 CourierAPIGetCourierMessageRequest + */ + GetCourierMessage(ctx context.Context, id string) CourierAPIGetCourierMessageRequest + + // GetCourierMessageExecute executes the request + // @return Message + GetCourierMessageExecute(r CourierAPIGetCourierMessageRequest) (*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 CourierAPIListCourierMessagesRequest + */ + ListCourierMessages(ctx context.Context) CourierAPIListCourierMessagesRequest + + // ListCourierMessagesExecute executes the request + // @return []Message + ListCourierMessagesExecute(r CourierAPIListCourierMessagesRequest) ([]Message, *http.Response, error) +} + +// CourierAPIService CourierAPI service +type CourierAPIService service + +type CourierAPIGetCourierMessageRequest struct { + ctx context.Context + ApiService CourierAPI + id string +} + +func (r CourierAPIGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { + return r.ApiService.GetCourierMessageExecute(r) +} + +/* +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 CourierAPIGetCourierMessageRequest +*/ +func (a *CourierAPIService) GetCourierMessage(ctx context.Context, id string) CourierAPIGetCourierMessageRequest { + return CourierAPIGetCourierMessageRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +// Execute executes the request +// +// @return Message +func (a *CourierAPIService) GetCourierMessageExecute(r CourierAPIGetCourierMessageRequest) (*Message, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *Message + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.GetCourierMessage") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/courier/messages/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type CourierAPIListCourierMessagesRequest struct { + ctx context.Context + ApiService CourierAPI + pageSize *int64 + pageToken *string + status *CourierMessageStatus + recipient *string +} + +// Items per Page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +func (r CourierAPIListCourierMessagesRequest) PageSize(pageSize int64) CourierAPIListCourierMessagesRequest { + r.pageSize = &pageSize + return r +} + +// Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +func (r CourierAPIListCourierMessagesRequest) PageToken(pageToken string) CourierAPIListCourierMessagesRequest { + r.pageToken = &pageToken + return r +} + +// Status filters out messages based on status. If no value is provided, it doesn't take effect on filter. +func (r CourierAPIListCourierMessagesRequest) Status(status CourierMessageStatus) CourierAPIListCourierMessagesRequest { + r.status = &status + return r +} + +// Recipient filters out messages based on recipient. If no value is provided, it doesn't take effect on filter. +func (r CourierAPIListCourierMessagesRequest) Recipient(recipient string) CourierAPIListCourierMessagesRequest { + r.recipient = &recipient + return r +} + +func (r CourierAPIListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { + return r.ApiService.ListCourierMessagesExecute(r) +} + +/* +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 CourierAPIListCourierMessagesRequest +*/ +func (a *CourierAPIService) ListCourierMessages(ctx context.Context) CourierAPIListCourierMessagesRequest { + return CourierAPIListCourierMessagesRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return []Message +func (a *CourierAPIService) ListCourierMessagesExecute(r CourierAPIListCourierMessagesRequest) ([]Message, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue []Message + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.ListCourierMessages") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/courier/messages" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.pageSize != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "page_size", r.pageSize, "form", "") + } else { + var defaultValue int64 = 250 + r.pageSize = &defaultValue + } + if r.pageToken != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "page_token", r.pageToken, "form", "") + } + if r.status != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "status", r.status, "form", "") + } + if r.recipient != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "recipient", r.recipient, "form", "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/pkg/client-go/api_metadata.go b/pkg/client-go/api_metadata.go new file mode 100644 index 0000000..498c7d3 --- /dev/null +++ b/pkg/client-go/api_metadata.go @@ -0,0 +1,435 @@ +/* +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 ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +type MetadataAPI interface { + + /* + GetVersion Return Running Software Version. + + This endpoint returns the version of Ory Kratos. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIGetVersionRequest + */ + GetVersion(ctx context.Context) MetadataAPIGetVersionRequest + + // GetVersionExecute executes the request + // @return GetVersion200Response + GetVersionExecute(r MetadataAPIGetVersionRequest) (*GetVersion200Response, *http.Response, error) + + /* + IsAlive Check HTTP Server Status + + This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIIsAliveRequest + */ + IsAlive(ctx context.Context) MetadataAPIIsAliveRequest + + // IsAliveExecute executes the request + // @return IsAlive200Response + IsAliveExecute(r MetadataAPIIsAliveRequest) (*IsAlive200Response, *http.Response, error) + + /* + IsReady Check HTTP Server and Database Status + + This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIIsReadyRequest + */ + IsReady(ctx context.Context) MetadataAPIIsReadyRequest + + // IsReadyExecute executes the request + // @return IsAlive200Response + IsReadyExecute(r MetadataAPIIsReadyRequest) (*IsAlive200Response, *http.Response, error) +} + +// MetadataAPIService MetadataAPI service +type MetadataAPIService service + +type MetadataAPIGetVersionRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { + return r.ApiService.GetVersionExecute(r) +} + +/* +GetVersion Return Running Software Version. + +This endpoint returns the version of Ory Kratos. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIGetVersionRequest +*/ +func (a *MetadataAPIService) GetVersion(ctx context.Context) MetadataAPIGetVersionRequest { + return MetadataAPIGetVersionRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return GetVersion200Response +func (a *MetadataAPIService) GetVersionExecute(r MetadataAPIGetVersionRequest) (*GetVersion200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *GetVersion200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.GetVersion") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/version" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataAPIIsAliveRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsAliveExecute(r) +} + +/* +IsAlive Check HTTP Server Status + +This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming +HTTP requests. This status does currently not include checks whether the database connection is working. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIIsAliveRequest +*/ +func (a *MetadataAPIService) IsAlive(ctx context.Context) MetadataAPIIsAliveRequest { + return MetadataAPIIsAliveRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return IsAlive200Response +func (a *MetadataAPIService) IsAliveExecute(r MetadataAPIIsAliveRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsAlive") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/alive" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataAPIIsReadyRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsReadyExecute(r) +} + +/* +IsReady Check HTTP Server and Database Status + +This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g. +the database) are responsive as well. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIIsReadyRequest +*/ +func (a *MetadataAPIService) IsReady(ctx context.Context) MetadataAPIIsReadyRequest { + return MetadataAPIIsReadyRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return IsAlive200Response +func (a *MetadataAPIService) IsReadyExecute(r MetadataAPIIsReadyRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsReady") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/ready" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 503 { + var v IsReady503Response + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/pkg/client-go/client.go b/pkg/client-go/client.go new file mode 100644 index 0000000..8246c31 --- /dev/null +++ b/pkg/client-go/client.go @@ -0,0 +1,665 @@ +/* +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 ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +var ( + JsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?json)`) + XmlCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?xml)`) + queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`) + queryDescape = strings.NewReplacer("%5B", "[", "%5D", "]") +) + +// APIClient manages communication with the Ory Identities API API v +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services + + CourierAPI CourierAPI + + FrontendAPI FrontendAPI + + IdentityAPI IdentityAPI + + MetadataAPI MetadataAPI +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + 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) + + return c +} + +func atoi(in string) (int, error) { + return strconv.Atoi(in) +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insensitive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.EqualFold(a, needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +func parameterValueToString(obj interface{}, key string) string { + if reflect.TypeOf(obj).Kind() != reflect.Ptr { + if actualObj, ok := obj.(interface{ GetActualInstanceValue() interface{} }); ok { + return fmt.Sprintf("%v", actualObj.GetActualInstanceValue()) + } + + return fmt.Sprintf("%v", obj) + } + var param, ok = obj.(MappedNullable) + if !ok { + return "" + } + dataMap, err := param.ToMap() + if err != nil { + return "" + } + return fmt.Sprintf("%v", dataMap[key]) +} + +// parameterAddToHeaderOrQuery adds the provided object to the request header or url query +// supporting deep object syntax +func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix string, obj interface{}, style string, collectionType string) { + var v = reflect.ValueOf(obj) + var value = "" + if v == reflect.ValueOf(nil) { + value = "null" + } else { + switch v.Kind() { + case reflect.Invalid: + value = "invalid" + + case reflect.Struct: + if t, ok := obj.(MappedNullable); ok { + dataMap, err := t.ToMap() + if err != nil { + return + } + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, style, collectionType) + return + } + if t, ok := obj.(time.Time); ok { + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339Nano), style, collectionType) + return + } + value = v.Type().String() + " value" + case reflect.Slice: + var indValue = reflect.ValueOf(obj) + if indValue == reflect.ValueOf(nil) { + return + } + var lenIndValue = indValue.Len() + for i := 0; i < lenIndValue; i++ { + var arrayValue = indValue.Index(i) + var keyPrefixForCollectionType = keyPrefix + if style == "deepObject" { + keyPrefixForCollectionType = keyPrefix + "[" + strconv.Itoa(i) + "]" + } + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), style, collectionType) + } + return + + case reflect.Map: + var indValue = reflect.ValueOf(obj) + if indValue == reflect.ValueOf(nil) { + return + } + iter := indValue.MapRange() + for iter.Next() { + k, v := iter.Key(), iter.Value() + parameterAddToHeaderOrQuery(headerOrQueryParams, fmt.Sprintf("%s[%s]", keyPrefix, k.String()), v.Interface(), style, collectionType) + } + return + + case reflect.Interface: + fallthrough + case reflect.Ptr: + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, v.Elem().Interface(), style, collectionType) + return + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + value = strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + value = strconv.FormatUint(v.Uint(), 10) + case reflect.Float32, reflect.Float64: + value = strconv.FormatFloat(v.Float(), 'g', -1, 32) + case reflect.Bool: + value = strconv.FormatBool(v.Bool()) + case reflect.String: + value = v.String() + default: + value = v.Type().String() + " value" + } + } + + switch valuesMap := headerOrQueryParams.(type) { + case url.Values: + if collectionType == "csv" && valuesMap.Get(keyPrefix) != "" { + valuesMap.Set(keyPrefix, valuesMap.Get(keyPrefix)+","+value) + } else { + valuesMap.Add(keyPrefix, value) + } + break + case map[string]string: + valuesMap[keyPrefix] = value + break + } +} + +// helper for converting interface{} parameters to json strings +func parameterToJson(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + +// callAPI do the request. +func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + return resp, err +} + +// Allow modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +type formFile struct { + fileBytes []byte + fileName string + formFileName string +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams url.Values, + formFiles []formFile) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // add form parameters and file if available. + if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(formFiles) > 0) { + if body != nil { + return nil, errors.New("Cannot specify postBody and multipart form at the same time.") + } + body = &bytes.Buffer{} + w := multipart.NewWriter(body) + + for k, v := range formParams { + for _, iv := range v { + if strings.HasPrefix(k, "@") { // file + err = addFile(w, k[1:], iv) + if err != nil { + return nil, err + } + } else { // form value + w.WriteField(k, iv) + } + } + } + for _, formFile := range formFiles { + if len(formFile.fileBytes) > 0 && formFile.fileName != "" { + w.Boundary() + part, err := w.CreateFormFile(formFile.formFileName, filepath.Base(formFile.fileName)) + if err != nil { + return nil, err + } + _, err = part.Write(formFile.fileBytes) + if err != nil { + return nil, err + } + } + } + + // Set the Boundary in the Content-Type + headerParams["Content-Type"] = w.FormDataContentType() + + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + w.Close() + } + + if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { + if body != nil { + return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") + } + body = &bytes.Buffer{} + body.WriteString(formParams.Encode()) + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Adding Query Param + query := url.Query() + for k, v := range queryParams { + for _, iv := range v { + query.Add(k, iv) + } + } + + // Encode the parameters. + url.RawQuery = queryParamSplit.ReplaceAllStringFunc(query.Encode(), func(s string) string { + pieces := strings.Split(s, "=") + pieces[0] = queryDescape.Replace(pieces[0]) + return strings.Join(pieces, "=") + }) + + // Generate a new request + if body != nil { + localVarRequest, err = http.NewRequest(method, url.String(), body) + } else { + localVarRequest, err = http.NewRequest(method, url.String(), nil) + } + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers[h] = []string{v} + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + + // Walk through any authentication. + + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if f, ok := v.(*os.File); ok { + f, err = os.CreateTemp("", "HttpClientFile") + if err != nil { + return + } + _, err = f.Write(b) + if err != nil { + return + } + _, err = f.Seek(0, io.SeekStart) + return + } + if f, ok := v.(**os.File); ok { + *f, err = os.CreateTemp("", "HttpClientFile") + if err != nil { + return + } + _, err = (*f).Write(b) + if err != nil { + return + } + _, err = (*f).Seek(0, io.SeekStart) + return + } + if XmlCheck.MatchString(contentType) { + if err = xml.Unmarshal(b, v); err != nil { + return err + } + return nil + } + if JsonCheck.MatchString(contentType) { + if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas + if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined + if err = unmarshalObj.UnmarshalJSON(b); err != nil { + return err + } + } else { + return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") + } + } else if err = json.Unmarshal(b, v); err != nil { // simple model + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Add a file to the multipart request +func addFile(w *multipart.Writer, fieldName, path string) error { + file, err := os.Open(filepath.Clean(path)) + if err != nil { + return err + } + err = file.Close() + if err != nil { + return err + } + + part, err := w.CreateFormFile(fieldName, filepath.Base(path)) + if err != nil { + return err + } + _, err = io.Copy(part, file) + + return err +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if fp, ok := body.(*os.File); ok { + _, err = bodyBuf.ReadFrom(fp) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if JsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } else if XmlCheck.MatchString(contentType) { + var bs []byte + bs, err = xml.Marshal(body) + if err == nil { + bodyBuf.Write(bs) + } + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("invalid body type %s\n", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + return contentType +} + +// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go +type cacheControl map[string]string + +func parseCacheControl(headers http.Header) cacheControl { + cc := cacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc +} + +// CacheExpires helper function to determine remaining time before repeating a request. +func CacheExpires(r *http.Response) time.Time { + // Figure out when the cache expires. + var expires time.Time + now, err := time.Parse(time.RFC1123, r.Header.Get("date")) + if err != nil { + return time.Now() + } + respCacheControl := parseCacheControl(r.Header) + + if maxAge, ok := respCacheControl["max-age"]; ok { + lifetime, err := time.ParseDuration(maxAge + "s") + if err != nil { + expires = now + } else { + expires = now.Add(lifetime) + } + } else { + expiresHeader := r.Header.Get("Expires") + if expiresHeader != "" { + expires, err = time.Parse(time.RFC1123, expiresHeader) + if err != nil { + expires = now + } + } + } + return expires +} + +func strlen(s string) int { + return utf8.RuneCountInString(s) +} + +// GenericOpenAPIError Provides access to the body, error and model on returned errors. +type GenericOpenAPIError struct { + body []byte + error string + model interface{} +} + +// Error returns non-empty string if there was an error. +func (e GenericOpenAPIError) Error() string { + return e.error +} + +// Body returns the raw bytes of the response +func (e GenericOpenAPIError) Body() []byte { + return e.body +} + +// Model returns the unpacked model of the error +func (e GenericOpenAPIError) Model() interface{} { + return e.model +} + +// format error message using title and detail when model implements rfc7807 +func formatErrorMessage(status string, v interface{}) string { + str := "" + metaValue := reflect.ValueOf(v).Elem() + + if metaValue.Kind() == reflect.Struct { + field := metaValue.FieldByName("Title") + if field != (reflect.Value{}) { + str = fmt.Sprintf("%s", field.Interface()) + } + + field = metaValue.FieldByName("Detail") + if field != (reflect.Value{}) { + str = fmt.Sprintf("%s (%s)", str, field.Interface()) + } + } + + return strings.TrimSpace(fmt.Sprintf("%s %s", status, str)) +} diff --git a/pkg/client-go/configuration.go b/pkg/client-go/configuration.go new file mode 100644 index 0000000..c383daa --- /dev/null +++ b/pkg/client-go/configuration.go @@ -0,0 +1,218 @@ +/* +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 ( + "context" + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextAPIKeys takes a string apikey as authentication for the request + ContextAPIKeys = contextKey("apiKeys") + + // ContextServerIndex uses a server configuration from the index. + ContextServerIndex = contextKey("serverIndex") + + // ContextOperationServerIndices uses a server configuration from the index mapping. + ContextOperationServerIndices = contextKey("serverOperationIndices") + + // ContextServerVariables overrides a server configuration variables. + ContextServerVariables = contextKey("serverVariables") + + // ContextOperationServerVariables overrides a server configuration variables using operation specific values. + ContextOperationServerVariables = contextKey("serverOperationVariables") +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + URL string + Description string + Variables map[string]ServerVariable +} + +// ServerConfigurations stores multiple ServerConfiguration items +type ServerConfigurations []ServerConfiguration + +// Configuration stores the configuration of the API client +type Configuration struct { + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers ServerConfigurations + OperationServers map[string]ServerConfigurations + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration() *Configuration { + cfg := &Configuration{ + DefaultHeader: make(map[string]string), + UserAgent: "OpenAPI-Generator/1.0.0/go", + Debug: false, + Servers: ServerConfigurations{ + { + URL: "", + Description: "No description provided", + }, + }, + OperationServers: map[string]ServerConfigurations{}, + } + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// URL formats template on a index using given variables +func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { + if index < 0 || len(sc) <= index { + return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1) + } + server := sc[index] + url := server.URL + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} + +// ServerURL returns URL based on server settings +func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { + return c.Servers.URL(index, variables) +} + +func getServerIndex(ctx context.Context) (int, error) { + si := ctx.Value(ContextServerIndex) + if si != nil { + if index, ok := si.(int); ok { + return index, nil + } + return 0, reportError("Invalid type %T should be int", si) + } + return 0, nil +} + +func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { + osi := ctx.Value(ContextOperationServerIndices) + if osi != nil { + if operationIndices, ok := osi.(map[string]int); !ok { + return 0, reportError("Invalid type %T should be map[string]int", osi) + } else { + index, ok := operationIndices[endpoint] + if ok { + return index, nil + } + } + } + return getServerIndex(ctx) +} + +func getServerVariables(ctx context.Context) (map[string]string, error) { + sv := ctx.Value(ContextServerVariables) + if sv != nil { + if variables, ok := sv.(map[string]string); ok { + return variables, nil + } + return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) + } + return nil, nil +} + +func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { + osv := ctx.Value(ContextOperationServerVariables) + if osv != nil { + if operationVariables, ok := osv.(map[string]map[string]string); !ok { + return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) + } else { + variables, ok := operationVariables[endpoint] + if ok { + return variables, nil + } + } + } + return getServerVariables(ctx) +} + +// ServerURLWithContext returns a new server URL given an endpoint +func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { + sc, ok := c.OperationServers[endpoint] + if !ok { + sc = c.Servers + } + + if ctx == nil { + return sc.URL(0, nil) + } + + index, err := getServerOperationIndex(ctx, endpoint) + if err != nil { + return "", err + } + + variables, err := getServerOperationVariables(ctx, endpoint) + if err != nil { + return "", err + } + + return sc.URL(index, variables) +} diff --git a/pkg/client-go/git_push.sh b/pkg/client-go/git_push.sh new file mode 100644 index 0000000..b036751 --- /dev/null +++ b/pkg/client-go/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="ory" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="client-go" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/pkg/client-go/go.mod b/pkg/client-go/go.mod new file mode 100644 index 0000000..6e768c9 --- /dev/null +++ b/pkg/client-go/go.mod @@ -0,0 +1,3 @@ +module github.com/ory/client-go + +go 1.18 diff --git a/pkg/client-go/go.sum b/pkg/client-go/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/pkg/client-go/model_admin_identity_import_credentials_lookup_secret.go b/pkg/client-go/model_admin_identity_import_credentials_lookup_secret.go new file mode 100644 index 0000000..ee04513 --- /dev/null +++ b/pkg/client-go/model_admin_identity_import_credentials_lookup_secret.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the AdminIdentityImportCredentialsLookupSecret type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AdminIdentityImportCredentialsLookupSecret{} + +// AdminIdentityImportCredentialsLookupSecret Create Identity and Import Lookup Secret Credentials +type AdminIdentityImportCredentialsLookupSecret struct { + Config *AdminIdentityImportCredentialsLookupSecretConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _AdminIdentityImportCredentialsLookupSecret AdminIdentityImportCredentialsLookupSecret + +// NewAdminIdentityImportCredentialsLookupSecret instantiates a new AdminIdentityImportCredentialsLookupSecret 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 NewAdminIdentityImportCredentialsLookupSecret() *AdminIdentityImportCredentialsLookupSecret { + this := AdminIdentityImportCredentialsLookupSecret{} + return &this +} + +// NewAdminIdentityImportCredentialsLookupSecretWithDefaults instantiates a new AdminIdentityImportCredentialsLookupSecret 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 NewAdminIdentityImportCredentialsLookupSecretWithDefaults() *AdminIdentityImportCredentialsLookupSecret { + this := AdminIdentityImportCredentialsLookupSecret{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *AdminIdentityImportCredentialsLookupSecret) GetConfig() AdminIdentityImportCredentialsLookupSecretConfig { + if o == nil || IsNil(o.Config) { + var ret AdminIdentityImportCredentialsLookupSecretConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AdminIdentityImportCredentialsLookupSecret) GetConfigOk() (*AdminIdentityImportCredentialsLookupSecretConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *AdminIdentityImportCredentialsLookupSecret) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given AdminIdentityImportCredentialsLookupSecretConfig and assigns it to the Config field. +func (o *AdminIdentityImportCredentialsLookupSecret) SetConfig(v AdminIdentityImportCredentialsLookupSecretConfig) { + o.Config = &v +} + +func (o AdminIdentityImportCredentialsLookupSecret) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AdminIdentityImportCredentialsLookupSecret) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *AdminIdentityImportCredentialsLookupSecret) UnmarshalJSON(data []byte) (err error) { + varAdminIdentityImportCredentialsLookupSecret := _AdminIdentityImportCredentialsLookupSecret{} + + err = json.Unmarshal(data, &varAdminIdentityImportCredentialsLookupSecret) + + if err != nil { + return err + } + + *o = AdminIdentityImportCredentialsLookupSecret(varAdminIdentityImportCredentialsLookupSecret) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableAdminIdentityImportCredentialsLookupSecret struct { + value *AdminIdentityImportCredentialsLookupSecret + isSet bool +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) Get() *AdminIdentityImportCredentialsLookupSecret { + return v.value +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) Set(val *AdminIdentityImportCredentialsLookupSecret) { + v.value = val + v.isSet = true +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) IsSet() bool { + return v.isSet +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAdminIdentityImportCredentialsLookupSecret(val *AdminIdentityImportCredentialsLookupSecret) *NullableAdminIdentityImportCredentialsLookupSecret { + return &NullableAdminIdentityImportCredentialsLookupSecret{value: val, isSet: true} +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_admin_identity_import_credentials_lookup_secret_config.go b/pkg/client-go/model_admin_identity_import_credentials_lookup_secret_config.go new file mode 100644 index 0000000..87c5428 --- /dev/null +++ b/pkg/client-go/model_admin_identity_import_credentials_lookup_secret_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the AdminIdentityImportCredentialsLookupSecretConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AdminIdentityImportCredentialsLookupSecretConfig{} + +// AdminIdentityImportCredentialsLookupSecretConfig Create Identity and Import Lookup Secret Credentials Configuration +type AdminIdentityImportCredentialsLookupSecretConfig struct { + // Codes is a list of \"lookup secret\" codes configured for the user. + Codes []IdentityCredentialsLookupSecretCode `json:"codes,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _AdminIdentityImportCredentialsLookupSecretConfig AdminIdentityImportCredentialsLookupSecretConfig + +// NewAdminIdentityImportCredentialsLookupSecretConfig instantiates a new AdminIdentityImportCredentialsLookupSecretConfig 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 NewAdminIdentityImportCredentialsLookupSecretConfig() *AdminIdentityImportCredentialsLookupSecretConfig { + this := AdminIdentityImportCredentialsLookupSecretConfig{} + return &this +} + +// NewAdminIdentityImportCredentialsLookupSecretConfigWithDefaults instantiates a new AdminIdentityImportCredentialsLookupSecretConfig 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 NewAdminIdentityImportCredentialsLookupSecretConfigWithDefaults() *AdminIdentityImportCredentialsLookupSecretConfig { + this := AdminIdentityImportCredentialsLookupSecretConfig{} + return &this +} + +// GetCodes returns the Codes field value if set, zero value otherwise. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) GetCodes() []IdentityCredentialsLookupSecretCode { + if o == nil || IsNil(o.Codes) { + var ret []IdentityCredentialsLookupSecretCode + return ret + } + return o.Codes +} + +// GetCodesOk returns a tuple with the Codes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) GetCodesOk() ([]IdentityCredentialsLookupSecretCode, bool) { + if o == nil || IsNil(o.Codes) { + return nil, false + } + return o.Codes, true +} + +// HasCodes returns a boolean if a field has been set. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) HasCodes() bool { + if o != nil && !IsNil(o.Codes) { + return true + } + + return false +} + +// SetCodes gets a reference to the given []IdentityCredentialsLookupSecretCode and assigns it to the Codes field. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) SetCodes(v []IdentityCredentialsLookupSecretCode) { + o.Codes = v +} + +func (o AdminIdentityImportCredentialsLookupSecretConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AdminIdentityImportCredentialsLookupSecretConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Codes) { + toSerialize["codes"] = o.Codes + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *AdminIdentityImportCredentialsLookupSecretConfig) UnmarshalJSON(data []byte) (err error) { + varAdminIdentityImportCredentialsLookupSecretConfig := _AdminIdentityImportCredentialsLookupSecretConfig{} + + err = json.Unmarshal(data, &varAdminIdentityImportCredentialsLookupSecretConfig) + + if err != nil { + return err + } + + *o = AdminIdentityImportCredentialsLookupSecretConfig(varAdminIdentityImportCredentialsLookupSecretConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "codes") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableAdminIdentityImportCredentialsLookupSecretConfig struct { + value *AdminIdentityImportCredentialsLookupSecretConfig + isSet bool +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) Get() *AdminIdentityImportCredentialsLookupSecretConfig { + return v.value +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) Set(val *AdminIdentityImportCredentialsLookupSecretConfig) { + v.value = val + v.isSet = true +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAdminIdentityImportCredentialsLookupSecretConfig(val *AdminIdentityImportCredentialsLookupSecretConfig) *NullableAdminIdentityImportCredentialsLookupSecretConfig { + return &NullableAdminIdentityImportCredentialsLookupSecretConfig{value: val, isSet: true} +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_authenticator_assurance_level.go b/pkg/client-go/model_authenticator_assurance_level.go new file mode 100644 index 0000000..58c1786 --- /dev/null +++ b/pkg/client-go/model_authenticator_assurance_level.go @@ -0,0 +1,115 @@ +/* +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" + "fmt" +) + +// AuthenticatorAssuranceLevel The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder for an attacker to compromise the account. Generally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g. password + TOTP) have been used. To learn more about these levels please head over to: https://www.ory.com/kratos/docs/concepts/credentials +type AuthenticatorAssuranceLevel string + +// List of authenticatorAssuranceLevel +const ( + AUTHENTICATORASSURANCELEVEL_AAL0 AuthenticatorAssuranceLevel = "aal0" + AUTHENTICATORASSURANCELEVEL_AAL1 AuthenticatorAssuranceLevel = "aal1" + AUTHENTICATORASSURANCELEVEL_AAL2 AuthenticatorAssuranceLevel = "aal2" + AUTHENTICATORASSURANCELEVEL_AAL3 AuthenticatorAssuranceLevel = "aal3" +) + +// All allowed values of AuthenticatorAssuranceLevel enum +var AllowedAuthenticatorAssuranceLevelEnumValues = []AuthenticatorAssuranceLevel{ + "aal0", + "aal1", + "aal2", + "aal3", +} + +func (v *AuthenticatorAssuranceLevel) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := AuthenticatorAssuranceLevel(value) + for _, existing := range AllowedAuthenticatorAssuranceLevelEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid AuthenticatorAssuranceLevel", value) +} + +// NewAuthenticatorAssuranceLevelFromValue returns a pointer to a valid AuthenticatorAssuranceLevel +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewAuthenticatorAssuranceLevelFromValue(v string) (*AuthenticatorAssuranceLevel, error) { + ev := AuthenticatorAssuranceLevel(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for AuthenticatorAssuranceLevel: valid values are %v", v, AllowedAuthenticatorAssuranceLevelEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v AuthenticatorAssuranceLevel) IsValid() bool { + for _, existing := range AllowedAuthenticatorAssuranceLevelEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to authenticatorAssuranceLevel value +func (v AuthenticatorAssuranceLevel) Ptr() *AuthenticatorAssuranceLevel { + return &v +} + +type NullableAuthenticatorAssuranceLevel struct { + value *AuthenticatorAssuranceLevel + isSet bool +} + +func (v NullableAuthenticatorAssuranceLevel) Get() *AuthenticatorAssuranceLevel { + return v.value +} + +func (v *NullableAuthenticatorAssuranceLevel) Set(val *AuthenticatorAssuranceLevel) { + v.value = val + v.isSet = true +} + +func (v NullableAuthenticatorAssuranceLevel) IsSet() bool { + return v.isSet +} + +func (v *NullableAuthenticatorAssuranceLevel) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAuthenticatorAssuranceLevel(val *AuthenticatorAssuranceLevel) *NullableAuthenticatorAssuranceLevel { + return &NullableAuthenticatorAssuranceLevel{value: val, isSet: true} +} + +func (v NullableAuthenticatorAssuranceLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAuthenticatorAssuranceLevel) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_batch_patch_identities_response.go b/pkg/client-go/model_batch_patch_identities_response.go new file mode 100644 index 0000000..d66356e --- /dev/null +++ b/pkg/client-go/model_batch_patch_identities_response.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the BatchPatchIdentitiesResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &BatchPatchIdentitiesResponse{} + +// BatchPatchIdentitiesResponse Patch identities response +type BatchPatchIdentitiesResponse struct { + // The patch responses for the individual identities. + Identities []IdentityPatchResponse `json:"identities,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _BatchPatchIdentitiesResponse BatchPatchIdentitiesResponse + +// NewBatchPatchIdentitiesResponse instantiates a new BatchPatchIdentitiesResponse 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 NewBatchPatchIdentitiesResponse() *BatchPatchIdentitiesResponse { + this := BatchPatchIdentitiesResponse{} + return &this +} + +// NewBatchPatchIdentitiesResponseWithDefaults instantiates a new BatchPatchIdentitiesResponse 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 NewBatchPatchIdentitiesResponseWithDefaults() *BatchPatchIdentitiesResponse { + this := BatchPatchIdentitiesResponse{} + return &this +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *BatchPatchIdentitiesResponse) GetIdentities() []IdentityPatchResponse { + if o == nil || IsNil(o.Identities) { + var ret []IdentityPatchResponse + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BatchPatchIdentitiesResponse) GetIdentitiesOk() ([]IdentityPatchResponse, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *BatchPatchIdentitiesResponse) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []IdentityPatchResponse and assigns it to the Identities field. +func (o *BatchPatchIdentitiesResponse) SetIdentities(v []IdentityPatchResponse) { + o.Identities = v +} + +func (o BatchPatchIdentitiesResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o BatchPatchIdentitiesResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *BatchPatchIdentitiesResponse) UnmarshalJSON(data []byte) (err error) { + varBatchPatchIdentitiesResponse := _BatchPatchIdentitiesResponse{} + + err = json.Unmarshal(data, &varBatchPatchIdentitiesResponse) + + if err != nil { + return err + } + + *o = BatchPatchIdentitiesResponse(varBatchPatchIdentitiesResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "identities") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableBatchPatchIdentitiesResponse struct { + value *BatchPatchIdentitiesResponse + isSet bool +} + +func (v NullableBatchPatchIdentitiesResponse) Get() *BatchPatchIdentitiesResponse { + return v.value +} + +func (v *NullableBatchPatchIdentitiesResponse) Set(val *BatchPatchIdentitiesResponse) { + v.value = val + v.isSet = true +} + +func (v NullableBatchPatchIdentitiesResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableBatchPatchIdentitiesResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBatchPatchIdentitiesResponse(val *BatchPatchIdentitiesResponse) *NullableBatchPatchIdentitiesResponse { + return &NullableBatchPatchIdentitiesResponse{value: val, isSet: true} +} + +func (v NullableBatchPatchIdentitiesResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBatchPatchIdentitiesResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_consistency_request_parameters.go b/pkg/client-go/model_consistency_request_parameters.go new file mode 100644 index 0000000..0628cba --- /dev/null +++ b/pkg/client-go/model_consistency_request_parameters.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the ConsistencyRequestParameters type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ConsistencyRequestParameters{} + +// ConsistencyRequestParameters Control API consistency guarantees +type ConsistencyRequestParameters struct { + // Read Consistency Level (preview) The read consistency level determines the consistency guarantee for reads: strong (slow): The read is guaranteed to return the most recent data committed at the start of the read. eventual (very fast): The result will return data that is about 4.8 seconds old. The default consistency guarantee can be changed in the Ory Network Console or using the Ory CLI with `ory patch project --replace '/previews/default_read_consistency_level=\"strong\"'`. Setting the default consistency level to `eventual` may cause regressions in the future as we add consistency controls to more APIs. Currently, the following APIs will be affected by this setting: `GET /admin/identities` This feature is in preview and only available in Ory Network. ConsistencyLevelUnset ConsistencyLevelUnset is the unset / default consistency level. strong ConsistencyLevelStrong ConsistencyLevelStrong is the strong consistency level. eventual ConsistencyLevelEventual ConsistencyLevelEventual is the eventual consistency level using follower read timestamps. + Consistency *string `json:"consistency,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ConsistencyRequestParameters ConsistencyRequestParameters + +// NewConsistencyRequestParameters instantiates a new ConsistencyRequestParameters 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 NewConsistencyRequestParameters() *ConsistencyRequestParameters { + this := ConsistencyRequestParameters{} + return &this +} + +// NewConsistencyRequestParametersWithDefaults instantiates a new ConsistencyRequestParameters 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 NewConsistencyRequestParametersWithDefaults() *ConsistencyRequestParameters { + this := ConsistencyRequestParameters{} + return &this +} + +// GetConsistency returns the Consistency field value if set, zero value otherwise. +func (o *ConsistencyRequestParameters) GetConsistency() string { + if o == nil || IsNil(o.Consistency) { + var ret string + return ret + } + return *o.Consistency +} + +// GetConsistencyOk returns a tuple with the Consistency field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ConsistencyRequestParameters) GetConsistencyOk() (*string, bool) { + if o == nil || IsNil(o.Consistency) { + return nil, false + } + return o.Consistency, true +} + +// HasConsistency returns a boolean if a field has been set. +func (o *ConsistencyRequestParameters) HasConsistency() bool { + if o != nil && !IsNil(o.Consistency) { + return true + } + + return false +} + +// SetConsistency gets a reference to the given string and assigns it to the Consistency field. +func (o *ConsistencyRequestParameters) SetConsistency(v string) { + o.Consistency = &v +} + +func (o ConsistencyRequestParameters) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ConsistencyRequestParameters) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Consistency) { + toSerialize["consistency"] = o.Consistency + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ConsistencyRequestParameters) UnmarshalJSON(data []byte) (err error) { + varConsistencyRequestParameters := _ConsistencyRequestParameters{} + + err = json.Unmarshal(data, &varConsistencyRequestParameters) + + if err != nil { + return err + } + + *o = ConsistencyRequestParameters(varConsistencyRequestParameters) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "consistency") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableConsistencyRequestParameters struct { + value *ConsistencyRequestParameters + isSet bool +} + +func (v NullableConsistencyRequestParameters) Get() *ConsistencyRequestParameters { + return v.value +} + +func (v *NullableConsistencyRequestParameters) Set(val *ConsistencyRequestParameters) { + v.value = val + v.isSet = true +} + +func (v NullableConsistencyRequestParameters) IsSet() bool { + return v.isSet +} + +func (v *NullableConsistencyRequestParameters) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableConsistencyRequestParameters(val *ConsistencyRequestParameters) *NullableConsistencyRequestParameters { + return &NullableConsistencyRequestParameters{value: val, isSet: true} +} + +func (v NullableConsistencyRequestParameters) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableConsistencyRequestParameters) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with.go b/pkg/client-go/model_continue_with.go new file mode 100644 index 0000000..af6ec30 --- /dev/null +++ b/pkg/client-go/model_continue_with.go @@ -0,0 +1,354 @@ +/* +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" + "fmt" +) + +// ContinueWith - struct for ContinueWith +type ContinueWith struct { + ContinueWithDeviceAuthnPinEntryUi *ContinueWithDeviceAuthnPinEntryUi + ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo + ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken + ContinueWithSettingsUi *ContinueWithSettingsUi + ContinueWithVerificationUi *ContinueWithVerificationUi +} + +// ContinueWithDeviceAuthnPinEntryUiAsContinueWith is a convenience function that returns ContinueWithDeviceAuthnPinEntryUi wrapped in ContinueWith +func ContinueWithDeviceAuthnPinEntryUiAsContinueWith(v *ContinueWithDeviceAuthnPinEntryUi) ContinueWith { + return ContinueWith{ + ContinueWithDeviceAuthnPinEntryUi: v, + } +} + +// ContinueWithRecoveryUiAsContinueWith is a convenience function that returns ContinueWithRecoveryUi wrapped in ContinueWith +func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWith { + return ContinueWith{ + ContinueWithRecoveryUi: v, + } +} + +// 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{ + ContinueWithSetOrySessionToken: v, + } +} + +// ContinueWithSettingsUiAsContinueWith is a convenience function that returns ContinueWithSettingsUi wrapped in ContinueWith +func ContinueWithSettingsUiAsContinueWith(v *ContinueWithSettingsUi) ContinueWith { + return ContinueWith{ + ContinueWithSettingsUi: v, + } +} + +// ContinueWithVerificationUiAsContinueWith is a convenience function that returns ContinueWithVerificationUi wrapped in ContinueWith +func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) ContinueWith { + return ContinueWith{ + ContinueWithVerificationUi: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *ContinueWith) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + + // 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_pin_entry_ui' + if jsonDict["action"] == "show_pin_entry_ui" { + // try to unmarshal JSON data into ContinueWithDeviceAuthnPinEntryUi + err = json.Unmarshal(data, &dst.ContinueWithDeviceAuthnPinEntryUi) + if err == nil { + return nil // data stored in dst.ContinueWithDeviceAuthnPinEntryUi, return on the first match + } else { + dst.ContinueWithDeviceAuthnPinEntryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithDeviceAuthnPinEntryUi: %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 { + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) + } + } + + // 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 { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } + + // 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 'continueWithDeviceAuthnPinEntryUi' + if jsonDict["action"] == "continueWithDeviceAuthnPinEntryUi" { + // try to unmarshal JSON data into ContinueWithDeviceAuthnPinEntryUi + err = json.Unmarshal(data, &dst.ContinueWithDeviceAuthnPinEntryUi) + if err == nil { + return nil // data stored in dst.ContinueWithDeviceAuthnPinEntryUi, return on the first match + } else { + dst.ContinueWithDeviceAuthnPinEntryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithDeviceAuthnPinEntryUi: %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 { + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src ContinueWith) MarshalJSON() ([]byte, error) { + if src.ContinueWithDeviceAuthnPinEntryUi != nil { + return json.Marshal(&src.ContinueWithDeviceAuthnPinEntryUi) + } + + if src.ContinueWithRecoveryUi != nil { + return json.Marshal(&src.ContinueWithRecoveryUi) + } + + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + + if src.ContinueWithSetOrySessionToken != nil { + return json.Marshal(&src.ContinueWithSetOrySessionToken) + } + + if src.ContinueWithSettingsUi != nil { + return json.Marshal(&src.ContinueWithSettingsUi) + } + + if src.ContinueWithVerificationUi != nil { + return json.Marshal(&src.ContinueWithVerificationUi) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *ContinueWith) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.ContinueWithDeviceAuthnPinEntryUi != nil { + return obj.ContinueWithDeviceAuthnPinEntryUi + } + + if obj.ContinueWithRecoveryUi != nil { + return obj.ContinueWithRecoveryUi + } + + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + + if obj.ContinueWithSetOrySessionToken != nil { + return obj.ContinueWithSetOrySessionToken + } + + if obj.ContinueWithSettingsUi != nil { + return obj.ContinueWithSettingsUi + } + + if obj.ContinueWithVerificationUi != nil { + return obj.ContinueWithVerificationUi + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj ContinueWith) GetActualInstanceValue() interface{} { + if obj.ContinueWithDeviceAuthnPinEntryUi != nil { + return *obj.ContinueWithDeviceAuthnPinEntryUi + } + + if obj.ContinueWithRecoveryUi != nil { + return *obj.ContinueWithRecoveryUi + } + + if obj.ContinueWithRedirectBrowserTo != nil { + return *obj.ContinueWithRedirectBrowserTo + } + + if obj.ContinueWithSetOrySessionToken != nil { + return *obj.ContinueWithSetOrySessionToken + } + + if obj.ContinueWithSettingsUi != nil { + return *obj.ContinueWithSettingsUi + } + + if obj.ContinueWithVerificationUi != nil { + return *obj.ContinueWithVerificationUi + } + + // all schemas are nil + return nil +} + +type NullableContinueWith struct { + value *ContinueWith + isSet bool +} + +func (v NullableContinueWith) Get() *ContinueWith { + return v.value +} + +func (v *NullableContinueWith) Set(val *ContinueWith) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWith) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWith) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWith(val *ContinueWith) *NullableContinueWith { + return &NullableContinueWith{value: val, isSet: true} +} + +func (v NullableContinueWith) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWith) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_device_authn_pin_entry_ui.go b/pkg/client-go/model_continue_with_device_authn_pin_entry_ui.go new file mode 100644 index 0000000..b54a5eb --- /dev/null +++ b/pkg/client-go/model_continue_with_device_authn_pin_entry_ui.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithDeviceAuthnPinEntryUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithDeviceAuthnPinEntryUi{} + +// ContinueWithDeviceAuthnPinEntryUi Returned after a PIN-protected DeviceAuthn enrollment or secret rotation. It carries the one-time HPKE-sealed pin_secret, which the device opens with the transport private key it generated for this enrollment and binds to the user's PIN. The plaintext secret never leaves the device; the server only ever stores its at-rest ciphertext. The enrolled key's client_key_id is not included: it is the SHA-256 fingerprint of the device's own public key, which the device derives locally. Non-PIN keys need no client action at all and therefore produce no continue_with. +type ContinueWithDeviceAuthnPinEntryUi struct { + // Action will always be `show_pin_entry_ui` show_pin_entry_ui ContinueWithActionShowPINEntryUIString + Action string `json:"action"` + // The one-time HPKE-sealed pin_secret material the device opens to bind the user's PIN after a PIN-protected DeviceAuthn enrollment or secret rotation. + Data ContinueWithDeviceAuthnPinEntryUiData `json:"data"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithDeviceAuthnPinEntryUi ContinueWithDeviceAuthnPinEntryUi + +// NewContinueWithDeviceAuthnPinEntryUi instantiates a new ContinueWithDeviceAuthnPinEntryUi 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 NewContinueWithDeviceAuthnPinEntryUi(action string, data ContinueWithDeviceAuthnPinEntryUiData) *ContinueWithDeviceAuthnPinEntryUi { + this := ContinueWithDeviceAuthnPinEntryUi{} + this.Action = action + this.Data = data + return &this +} + +// NewContinueWithDeviceAuthnPinEntryUiWithDefaults instantiates a new ContinueWithDeviceAuthnPinEntryUi 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 NewContinueWithDeviceAuthnPinEntryUiWithDefaults() *ContinueWithDeviceAuthnPinEntryUi { + this := ContinueWithDeviceAuthnPinEntryUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithDeviceAuthnPinEntryUi) 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 *ContinueWithDeviceAuthnPinEntryUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithDeviceAuthnPinEntryUi) SetAction(v string) { + o.Action = v +} + +// GetData returns the Data field value +func (o *ContinueWithDeviceAuthnPinEntryUi) GetData() ContinueWithDeviceAuthnPinEntryUiData { + if o == nil { + var ret ContinueWithDeviceAuthnPinEntryUiData + return ret + } + + return o.Data +} + +// GetDataOk returns a tuple with the Data field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUi) GetDataOk() (*ContinueWithDeviceAuthnPinEntryUiData, bool) { + if o == nil { + return nil, false + } + return &o.Data, true +} + +// SetData sets field value +func (o *ContinueWithDeviceAuthnPinEntryUi) SetData(v ContinueWithDeviceAuthnPinEntryUiData) { + o.Data = v +} + +func (o ContinueWithDeviceAuthnPinEntryUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithDeviceAuthnPinEntryUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["data"] = o.Data + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithDeviceAuthnPinEntryUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "data", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithDeviceAuthnPinEntryUi := _ContinueWithDeviceAuthnPinEntryUi{} + + err = json.Unmarshal(data, &varContinueWithDeviceAuthnPinEntryUi) + + if err != nil { + return err + } + + *o = ContinueWithDeviceAuthnPinEntryUi(varContinueWithDeviceAuthnPinEntryUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "data") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithDeviceAuthnPinEntryUi struct { + value *ContinueWithDeviceAuthnPinEntryUi + isSet bool +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) Get() *ContinueWithDeviceAuthnPinEntryUi { + return v.value +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) Set(val *ContinueWithDeviceAuthnPinEntryUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithDeviceAuthnPinEntryUi(val *ContinueWithDeviceAuthnPinEntryUi) *NullableContinueWithDeviceAuthnPinEntryUi { + return &NullableContinueWithDeviceAuthnPinEntryUi{value: val, isSet: true} +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_device_authn_pin_entry_ui_data.go b/pkg/client-go/model_continue_with_device_authn_pin_entry_ui_data.go new file mode 100644 index 0000000..446cfde --- /dev/null +++ b/pkg/client-go/model_continue_with_device_authn_pin_entry_ui_data.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithDeviceAuthnPinEntryUiData type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithDeviceAuthnPinEntryUiData{} + +// ContinueWithDeviceAuthnPinEntryUiData Contains the encapsulated key and the sealed ciphertext that the device opens with the X25519 transport private key it generated for this enrollment or rotation. Open it with HPKE (RFC 9180) using the suite DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, the ASCII info string \"ory/deviceauthn/pin-secret/v1\", and the key's client_key_id (its ASCII hex form) as the AAD. +type ContinueWithDeviceAuthnPinEntryUiData struct { + // The base64-encoded HPKE ciphertext of the sealed pin_secret. + Ciphertext string `json:"ciphertext"` + // The base64-encoded HPKE encapsulated key (the `enc` output of the seal operation). + Enc string `json:"enc"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithDeviceAuthnPinEntryUiData ContinueWithDeviceAuthnPinEntryUiData + +// NewContinueWithDeviceAuthnPinEntryUiData instantiates a new ContinueWithDeviceAuthnPinEntryUiData 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 NewContinueWithDeviceAuthnPinEntryUiData(ciphertext string, enc string) *ContinueWithDeviceAuthnPinEntryUiData { + this := ContinueWithDeviceAuthnPinEntryUiData{} + this.Ciphertext = ciphertext + this.Enc = enc + return &this +} + +// NewContinueWithDeviceAuthnPinEntryUiDataWithDefaults instantiates a new ContinueWithDeviceAuthnPinEntryUiData 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 NewContinueWithDeviceAuthnPinEntryUiDataWithDefaults() *ContinueWithDeviceAuthnPinEntryUiData { + this := ContinueWithDeviceAuthnPinEntryUiData{} + return &this +} + +// GetCiphertext returns the Ciphertext field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetCiphertext() string { + if o == nil { + var ret string + return ret + } + + return o.Ciphertext +} + +// GetCiphertextOk returns a tuple with the Ciphertext field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetCiphertextOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Ciphertext, true +} + +// SetCiphertext sets field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) SetCiphertext(v string) { + o.Ciphertext = v +} + +// GetEnc returns the Enc field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetEnc() string { + if o == nil { + var ret string + return ret + } + + return o.Enc +} + +// GetEncOk returns a tuple with the Enc field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetEncOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Enc, true +} + +// SetEnc sets field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) SetEnc(v string) { + o.Enc = v +} + +func (o ContinueWithDeviceAuthnPinEntryUiData) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithDeviceAuthnPinEntryUiData) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["ciphertext"] = o.Ciphertext + toSerialize["enc"] = o.Enc + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithDeviceAuthnPinEntryUiData) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "ciphertext", + "enc", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithDeviceAuthnPinEntryUiData := _ContinueWithDeviceAuthnPinEntryUiData{} + + err = json.Unmarshal(data, &varContinueWithDeviceAuthnPinEntryUiData) + + if err != nil { + return err + } + + *o = ContinueWithDeviceAuthnPinEntryUiData(varContinueWithDeviceAuthnPinEntryUiData) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "ciphertext") + delete(additionalProperties, "enc") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithDeviceAuthnPinEntryUiData struct { + value *ContinueWithDeviceAuthnPinEntryUiData + isSet bool +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) Get() *ContinueWithDeviceAuthnPinEntryUiData { + return v.value +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) Set(val *ContinueWithDeviceAuthnPinEntryUiData) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithDeviceAuthnPinEntryUiData(val *ContinueWithDeviceAuthnPinEntryUiData) *NullableContinueWithDeviceAuthnPinEntryUiData { + return &NullableContinueWithDeviceAuthnPinEntryUiData{value: val, isSet: true} +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_recovery_ui.go b/pkg/client-go/model_continue_with_recovery_ui.go new file mode 100644 index 0000000..4ecc198 --- /dev/null +++ b/pkg/client-go/model_continue_with_recovery_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRecoveryUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRecoveryUi{} + +// ContinueWithRecoveryUi Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRecoveryUi struct { + // Action will always be `show_recovery_ui` show_recovery_ui ContinueWithActionShowRecoveryUIString + Action string `json:"action"` + Flow ContinueWithRecoveryUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRecoveryUi ContinueWithRecoveryUi + +// NewContinueWithRecoveryUi instantiates a new ContinueWithRecoveryUi 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 NewContinueWithRecoveryUi(action string, flow ContinueWithRecoveryUiFlow) *ContinueWithRecoveryUi { + this := ContinueWithRecoveryUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithRecoveryUiWithDefaults instantiates a new ContinueWithRecoveryUi 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 NewContinueWithRecoveryUiWithDefaults() *ContinueWithRecoveryUi { + this := ContinueWithRecoveryUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithRecoveryUi) 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 *ContinueWithRecoveryUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRecoveryUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithRecoveryUi) GetFlow() ContinueWithRecoveryUiFlow { + if o == nil { + var ret ContinueWithRecoveryUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRecoveryUi) GetFlowOk() (*ContinueWithRecoveryUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithRecoveryUi) SetFlow(v ContinueWithRecoveryUiFlow) { + o.Flow = v +} + +func (o ContinueWithRecoveryUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRecoveryUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRecoveryUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRecoveryUi := _ContinueWithRecoveryUi{} + + err = json.Unmarshal(data, &varContinueWithRecoveryUi) + + if err != nil { + return err + } + + *o = ContinueWithRecoveryUi(varContinueWithRecoveryUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithRecoveryUi struct { + value *ContinueWithRecoveryUi + isSet bool +} + +func (v NullableContinueWithRecoveryUi) Get() *ContinueWithRecoveryUi { + return v.value +} + +func (v *NullableContinueWithRecoveryUi) Set(val *ContinueWithRecoveryUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRecoveryUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRecoveryUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRecoveryUi(val *ContinueWithRecoveryUi) *NullableContinueWithRecoveryUi { + return &NullableContinueWithRecoveryUi{value: val, isSet: true} +} + +func (v NullableContinueWithRecoveryUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRecoveryUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_recovery_ui_flow.go b/pkg/client-go/model_continue_with_recovery_ui_flow.go new file mode 100644 index 0000000..9190751 --- /dev/null +++ b/pkg/client-go/model_continue_with_recovery_ui_flow.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRecoveryUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRecoveryUiFlow{} + +// ContinueWithRecoveryUiFlow struct for ContinueWithRecoveryUiFlow +type ContinueWithRecoveryUiFlow struct { + // The ID of the recovery flow + Id string `json:"id"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRecoveryUiFlow ContinueWithRecoveryUiFlow + +// NewContinueWithRecoveryUiFlow instantiates a new ContinueWithRecoveryUiFlow 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 NewContinueWithRecoveryUiFlow(id string) *ContinueWithRecoveryUiFlow { + this := ContinueWithRecoveryUiFlow{} + this.Id = id + return &this +} + +// NewContinueWithRecoveryUiFlowWithDefaults instantiates a new ContinueWithRecoveryUiFlow 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 NewContinueWithRecoveryUiFlowWithDefaults() *ContinueWithRecoveryUiFlow { + this := ContinueWithRecoveryUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithRecoveryUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRecoveryUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *ContinueWithRecoveryUiFlow) SetId(v string) { + o.Id = v +} + +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithRecoveryUiFlow) GetUrl() string { + if o == nil || IsNil(o.Url) { + 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 *ContinueWithRecoveryUiFlow) GetUrlOk() (*string, bool) { + if o == nil || IsNil(o.Url) { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithRecoveryUiFlow) HasUrl() bool { + if o != nil && !IsNil(o.Url) { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithRecoveryUiFlow) SetUrl(v string) { + o.Url = &v +} + +func (o ContinueWithRecoveryUiFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRecoveryUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRecoveryUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRecoveryUiFlow := _ContinueWithRecoveryUiFlow{} + + err = json.Unmarshal(data, &varContinueWithRecoveryUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithRecoveryUiFlow(varContinueWithRecoveryUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithRecoveryUiFlow struct { + value *ContinueWithRecoveryUiFlow + isSet bool +} + +func (v NullableContinueWithRecoveryUiFlow) Get() *ContinueWithRecoveryUiFlow { + return v.value +} + +func (v *NullableContinueWithRecoveryUiFlow) Set(val *ContinueWithRecoveryUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRecoveryUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRecoveryUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRecoveryUiFlow(val *ContinueWithRecoveryUiFlow) *NullableContinueWithRecoveryUiFlow { + return &NullableContinueWithRecoveryUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithRecoveryUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRecoveryUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_redirect_browser_to.go b/pkg/client-go/model_continue_with_redirect_browser_to.go new file mode 100644 index 0000000..aa5dc91 --- /dev/null +++ b/pkg/client-go/model_continue_with_redirect_browser_to.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRedirectBrowserTo type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRedirectBrowserTo{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRedirectBrowserTo ContinueWithRedirectBrowserTo + +// 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRedirectBrowserTo) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRedirectBrowserTo) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "redirect_browser_to", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRedirectBrowserTo := _ContinueWithRedirectBrowserTo{} + + err = json.Unmarshal(data, &varContinueWithRedirectBrowserTo) + + if err != nil { + return err + } + + *o = ContinueWithRedirectBrowserTo(varContinueWithRedirectBrowserTo) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_continue_with_set_ory_session_token.go b/pkg/client-go/model_continue_with_set_ory_session_token.go new file mode 100644 index 0000000..c8f9b62 --- /dev/null +++ b/pkg/client-go/model_continue_with_set_ory_session_token.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSetOrySessionToken type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSetOrySessionToken{} + +// ContinueWithSetOrySessionToken Indicates that a session was issued, and the application should use this token for authenticated requests +type ContinueWithSetOrySessionToken struct { + // Action will always be `set_ory_session_token` set_ory_session_token ContinueWithActionSetOrySessionTokenString + Action string `json:"action"` + // Token is the token of the session + OrySessionToken string `json:"ory_session_token"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSetOrySessionToken ContinueWithSetOrySessionToken + +// NewContinueWithSetOrySessionToken instantiates a new ContinueWithSetOrySessionToken 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 NewContinueWithSetOrySessionToken(action string, orySessionToken string) *ContinueWithSetOrySessionToken { + this := ContinueWithSetOrySessionToken{} + this.Action = action + this.OrySessionToken = orySessionToken + return &this +} + +// NewContinueWithSetOrySessionTokenWithDefaults instantiates a new ContinueWithSetOrySessionToken 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 NewContinueWithSetOrySessionTokenWithDefaults() *ContinueWithSetOrySessionToken { + this := ContinueWithSetOrySessionToken{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithSetOrySessionToken) 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 *ContinueWithSetOrySessionToken) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithSetOrySessionToken) SetAction(v string) { + o.Action = v +} + +// GetOrySessionToken returns the OrySessionToken field value +func (o *ContinueWithSetOrySessionToken) GetOrySessionToken() string { + if o == nil { + var ret string + return ret + } + + return o.OrySessionToken +} + +// GetOrySessionTokenOk returns a tuple with the OrySessionToken field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSetOrySessionToken) GetOrySessionTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.OrySessionToken, true +} + +// SetOrySessionToken sets field value +func (o *ContinueWithSetOrySessionToken) SetOrySessionToken(v string) { + o.OrySessionToken = v +} + +func (o ContinueWithSetOrySessionToken) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSetOrySessionToken) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["ory_session_token"] = o.OrySessionToken + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSetOrySessionToken) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "ory_session_token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSetOrySessionToken := _ContinueWithSetOrySessionToken{} + + err = json.Unmarshal(data, &varContinueWithSetOrySessionToken) + + if err != nil { + return err + } + + *o = ContinueWithSetOrySessionToken(varContinueWithSetOrySessionToken) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "ory_session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSetOrySessionToken struct { + value *ContinueWithSetOrySessionToken + isSet bool +} + +func (v NullableContinueWithSetOrySessionToken) Get() *ContinueWithSetOrySessionToken { + return v.value +} + +func (v *NullableContinueWithSetOrySessionToken) Set(val *ContinueWithSetOrySessionToken) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSetOrySessionToken) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSetOrySessionToken) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSetOrySessionToken(val *ContinueWithSetOrySessionToken) *NullableContinueWithSetOrySessionToken { + return &NullableContinueWithSetOrySessionToken{value: val, isSet: true} +} + +func (v NullableContinueWithSetOrySessionToken) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSetOrySessionToken) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_settings_ui.go b/pkg/client-go/model_continue_with_settings_ui.go new file mode 100644 index 0000000..d9903db --- /dev/null +++ b/pkg/client-go/model_continue_with_settings_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSettingsUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSettingsUi{} + +// ContinueWithSettingsUi Indicates, that the UI flow could be continued by showing a settings ui +type ContinueWithSettingsUi struct { + // Action will always be `show_settings_ui` show_settings_ui ContinueWithActionShowSettingsUIString + Action string `json:"action"` + Flow ContinueWithSettingsUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSettingsUi ContinueWithSettingsUi + +// NewContinueWithSettingsUi instantiates a new ContinueWithSettingsUi 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 NewContinueWithSettingsUi(action string, flow ContinueWithSettingsUiFlow) *ContinueWithSettingsUi { + this := ContinueWithSettingsUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithSettingsUiWithDefaults instantiates a new ContinueWithSettingsUi 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 NewContinueWithSettingsUiWithDefaults() *ContinueWithSettingsUi { + this := ContinueWithSettingsUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithSettingsUi) 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 *ContinueWithSettingsUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithSettingsUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithSettingsUi) GetFlow() ContinueWithSettingsUiFlow { + if o == nil { + var ret ContinueWithSettingsUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUi) GetFlowOk() (*ContinueWithSettingsUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithSettingsUi) SetFlow(v ContinueWithSettingsUiFlow) { + o.Flow = v +} + +func (o ContinueWithSettingsUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSettingsUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSettingsUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSettingsUi := _ContinueWithSettingsUi{} + + err = json.Unmarshal(data, &varContinueWithSettingsUi) + + if err != nil { + return err + } + + *o = ContinueWithSettingsUi(varContinueWithSettingsUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSettingsUi struct { + value *ContinueWithSettingsUi + isSet bool +} + +func (v NullableContinueWithSettingsUi) Get() *ContinueWithSettingsUi { + return v.value +} + +func (v *NullableContinueWithSettingsUi) Set(val *ContinueWithSettingsUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSettingsUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSettingsUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSettingsUi(val *ContinueWithSettingsUi) *NullableContinueWithSettingsUi { + return &NullableContinueWithSettingsUi{value: val, isSet: true} +} + +func (v NullableContinueWithSettingsUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSettingsUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_settings_ui_flow.go b/pkg/client-go/model_continue_with_settings_ui_flow.go new file mode 100644 index 0000000..37c95fa --- /dev/null +++ b/pkg/client-go/model_continue_with_settings_ui_flow.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSettingsUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSettingsUiFlow{} + +// ContinueWithSettingsUiFlow struct for ContinueWithSettingsUiFlow +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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSettingsUiFlow ContinueWithSettingsUiFlow + +// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow 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 NewContinueWithSettingsUiFlow(id string) *ContinueWithSettingsUiFlow { + this := ContinueWithSettingsUiFlow{} + this.Id = id + return &this +} + +// NewContinueWithSettingsUiFlowWithDefaults instantiates a new ContinueWithSettingsUiFlow 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 NewContinueWithSettingsUiFlowWithDefaults() *ContinueWithSettingsUiFlow { + this := ContinueWithSettingsUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithSettingsUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +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 || IsNil(o.Url) { + 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 || IsNil(o.Url) { + 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 && !IsNil(o.Url) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSettingsUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSettingsUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSettingsUiFlow := _ContinueWithSettingsUiFlow{} + + err = json.Unmarshal(data, &varContinueWithSettingsUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithSettingsUiFlow(varContinueWithSettingsUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSettingsUiFlow struct { + value *ContinueWithSettingsUiFlow + isSet bool +} + +func (v NullableContinueWithSettingsUiFlow) Get() *ContinueWithSettingsUiFlow { + return v.value +} + +func (v *NullableContinueWithSettingsUiFlow) Set(val *ContinueWithSettingsUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSettingsUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSettingsUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSettingsUiFlow(val *ContinueWithSettingsUiFlow) *NullableContinueWithSettingsUiFlow { + return &NullableContinueWithSettingsUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSettingsUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_verification_ui.go b/pkg/client-go/model_continue_with_verification_ui.go new file mode 100644 index 0000000..6a84f48 --- /dev/null +++ b/pkg/client-go/model_continue_with_verification_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithVerificationUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithVerificationUi{} + +// ContinueWithVerificationUi Indicates, that the UI flow could be continued by showing a verification ui +type ContinueWithVerificationUi struct { + // Action will always be `show_verification_ui` show_verification_ui ContinueWithActionShowVerificationUIString + Action string `json:"action"` + Flow ContinueWithVerificationUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithVerificationUi ContinueWithVerificationUi + +// NewContinueWithVerificationUi instantiates a new ContinueWithVerificationUi 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 NewContinueWithVerificationUi(action string, flow ContinueWithVerificationUiFlow) *ContinueWithVerificationUi { + this := ContinueWithVerificationUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithVerificationUiWithDefaults instantiates a new ContinueWithVerificationUi 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 NewContinueWithVerificationUiWithDefaults() *ContinueWithVerificationUi { + this := ContinueWithVerificationUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithVerificationUi) 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 *ContinueWithVerificationUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithVerificationUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithVerificationUi) GetFlow() ContinueWithVerificationUiFlow { + if o == nil { + var ret ContinueWithVerificationUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUi) GetFlowOk() (*ContinueWithVerificationUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithVerificationUi) SetFlow(v ContinueWithVerificationUiFlow) { + o.Flow = v +} + +func (o ContinueWithVerificationUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithVerificationUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithVerificationUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithVerificationUi := _ContinueWithVerificationUi{} + + err = json.Unmarshal(data, &varContinueWithVerificationUi) + + if err != nil { + return err + } + + *o = ContinueWithVerificationUi(varContinueWithVerificationUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithVerificationUi struct { + value *ContinueWithVerificationUi + isSet bool +} + +func (v NullableContinueWithVerificationUi) Get() *ContinueWithVerificationUi { + return v.value +} + +func (v *NullableContinueWithVerificationUi) Set(val *ContinueWithVerificationUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithVerificationUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithVerificationUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithVerificationUi(val *ContinueWithVerificationUi) *NullableContinueWithVerificationUi { + return &NullableContinueWithVerificationUi{value: val, isSet: true} +} + +func (v NullableContinueWithVerificationUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithVerificationUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_continue_with_verification_ui_flow.go b/pkg/client-go/model_continue_with_verification_ui_flow.go new file mode 100644 index 0000000..398b9d4 --- /dev/null +++ b/pkg/client-go/model_continue_with_verification_ui_flow.go @@ -0,0 +1,236 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithVerificationUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithVerificationUiFlow{} + +// ContinueWithVerificationUiFlow struct for ContinueWithVerificationUiFlow +type ContinueWithVerificationUiFlow struct { + // The ID of the verification flow + Id string `json:"id"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithVerificationUiFlow ContinueWithVerificationUiFlow + +// NewContinueWithVerificationUiFlow instantiates a new ContinueWithVerificationUiFlow 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 NewContinueWithVerificationUiFlow(id string, verifiableAddress string) *ContinueWithVerificationUiFlow { + this := ContinueWithVerificationUiFlow{} + this.Id = id + this.VerifiableAddress = verifiableAddress + return &this +} + +// NewContinueWithVerificationUiFlowWithDefaults instantiates a new ContinueWithVerificationUiFlow 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 NewContinueWithVerificationUiFlowWithDefaults() *ContinueWithVerificationUiFlow { + this := ContinueWithVerificationUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithVerificationUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *ContinueWithVerificationUiFlow) SetId(v string) { + o.Id = v +} + +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithVerificationUiFlow) GetUrl() string { + if o == nil || IsNil(o.Url) { + 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 *ContinueWithVerificationUiFlow) GetUrlOk() (*string, bool) { + if o == nil || IsNil(o.Url) { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithVerificationUiFlow) HasUrl() bool { + if o != nil && !IsNil(o.Url) { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithVerificationUiFlow) SetUrl(v string) { + o.Url = &v +} + +// GetVerifiableAddress returns the VerifiableAddress field value +func (o *ContinueWithVerificationUiFlow) GetVerifiableAddress() string { + if o == nil { + var ret string + return ret + } + + return o.VerifiableAddress +} + +// GetVerifiableAddressOk returns a tuple with the VerifiableAddress field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUiFlow) GetVerifiableAddressOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.VerifiableAddress, true +} + +// SetVerifiableAddress sets field value +func (o *ContinueWithVerificationUiFlow) SetVerifiableAddress(v string) { + o.VerifiableAddress = v +} + +func (o ContinueWithVerificationUiFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithVerificationUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + toSerialize["verifiable_address"] = o.VerifiableAddress + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithVerificationUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "verifiable_address", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithVerificationUiFlow := _ContinueWithVerificationUiFlow{} + + err = json.Unmarshal(data, &varContinueWithVerificationUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithVerificationUiFlow(varContinueWithVerificationUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + delete(additionalProperties, "verifiable_address") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithVerificationUiFlow struct { + value *ContinueWithVerificationUiFlow + isSet bool +} + +func (v NullableContinueWithVerificationUiFlow) Get() *ContinueWithVerificationUiFlow { + return v.value +} + +func (v *NullableContinueWithVerificationUiFlow) Set(val *ContinueWithVerificationUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithVerificationUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithVerificationUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithVerificationUiFlow(val *ContinueWithVerificationUiFlow) *NullableContinueWithVerificationUiFlow { + return &NullableContinueWithVerificationUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithVerificationUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithVerificationUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_courier_message_status.go b/pkg/client-go/model_courier_message_status.go new file mode 100644 index 0000000..d152440 --- /dev/null +++ b/pkg/client-go/model_courier_message_status.go @@ -0,0 +1,115 @@ +/* +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" + "fmt" +) + +// CourierMessageStatus A Message's Status +type CourierMessageStatus string + +// List of courierMessageStatus +const ( + COURIERMESSAGESTATUS_QUEUED CourierMessageStatus = "queued" + COURIERMESSAGESTATUS_SENT CourierMessageStatus = "sent" + COURIERMESSAGESTATUS_PROCESSING CourierMessageStatus = "processing" + COURIERMESSAGESTATUS_ABANDONED CourierMessageStatus = "abandoned" +) + +// All allowed values of CourierMessageStatus enum +var AllowedCourierMessageStatusEnumValues = []CourierMessageStatus{ + "queued", + "sent", + "processing", + "abandoned", +} + +func (v *CourierMessageStatus) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := CourierMessageStatus(value) + for _, existing := range AllowedCourierMessageStatusEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid CourierMessageStatus", value) +} + +// NewCourierMessageStatusFromValue returns a pointer to a valid CourierMessageStatus +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewCourierMessageStatusFromValue(v string) (*CourierMessageStatus, error) { + ev := CourierMessageStatus(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for CourierMessageStatus: valid values are %v", v, AllowedCourierMessageStatusEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v CourierMessageStatus) IsValid() bool { + for _, existing := range AllowedCourierMessageStatusEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to courierMessageStatus value +func (v CourierMessageStatus) Ptr() *CourierMessageStatus { + return &v +} + +type NullableCourierMessageStatus struct { + value *CourierMessageStatus + isSet bool +} + +func (v NullableCourierMessageStatus) Get() *CourierMessageStatus { + return v.value +} + +func (v *NullableCourierMessageStatus) Set(val *CourierMessageStatus) { + v.value = val + v.isSet = true +} + +func (v NullableCourierMessageStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableCourierMessageStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCourierMessageStatus(val *CourierMessageStatus) *NullableCourierMessageStatus { + return &NullableCourierMessageStatus{value: val, isSet: true} +} + +func (v NullableCourierMessageStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCourierMessageStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_courier_message_type.go b/pkg/client-go/model_courier_message_type.go new file mode 100644 index 0000000..28e0a35 --- /dev/null +++ b/pkg/client-go/model_courier_message_type.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// CourierMessageType It can either be `email` or `phone` +type CourierMessageType string + +// List of courierMessageType +const ( + COURIERMESSAGETYPE_EMAIL CourierMessageType = "email" + COURIERMESSAGETYPE_PHONE CourierMessageType = "phone" +) + +// All allowed values of CourierMessageType enum +var AllowedCourierMessageTypeEnumValues = []CourierMessageType{ + "email", + "phone", +} + +func (v *CourierMessageType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := CourierMessageType(value) + for _, existing := range AllowedCourierMessageTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid CourierMessageType", value) +} + +// NewCourierMessageTypeFromValue returns a pointer to a valid CourierMessageType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewCourierMessageTypeFromValue(v string) (*CourierMessageType, error) { + ev := CourierMessageType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for CourierMessageType: valid values are %v", v, AllowedCourierMessageTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v CourierMessageType) IsValid() bool { + for _, existing := range AllowedCourierMessageTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to courierMessageType value +func (v CourierMessageType) Ptr() *CourierMessageType { + return &v +} + +type NullableCourierMessageType struct { + value *CourierMessageType + isSet bool +} + +func (v NullableCourierMessageType) Get() *CourierMessageType { + return v.value +} + +func (v *NullableCourierMessageType) Set(val *CourierMessageType) { + v.value = val + v.isSet = true +} + +func (v NullableCourierMessageType) IsSet() bool { + return v.isSet +} + +func (v *NullableCourierMessageType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCourierMessageType(val *CourierMessageType) *NullableCourierMessageType { + return &NullableCourierMessageType{value: val, isSet: true} +} + +func (v NullableCourierMessageType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCourierMessageType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_create_fedcm_flow_response.go b/pkg/client-go/model_create_fedcm_flow_response.go new file mode 100644 index 0000000..499f8e5 --- /dev/null +++ b/pkg/client-go/model_create_fedcm_flow_response.go @@ -0,0 +1,191 @@ +/* +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" +) + +// checks if the CreateFedcmFlowResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateFedcmFlowResponse{} + +// CreateFedcmFlowResponse Contains a list of all available FedCM providers. +type CreateFedcmFlowResponse struct { + CsrfToken *string `json:"csrf_token,omitempty"` + Providers []Provider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _CreateFedcmFlowResponse CreateFedcmFlowResponse + +// NewCreateFedcmFlowResponse instantiates a new CreateFedcmFlowResponse 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 NewCreateFedcmFlowResponse() *CreateFedcmFlowResponse { + this := CreateFedcmFlowResponse{} + return &this +} + +// NewCreateFedcmFlowResponseWithDefaults instantiates a new CreateFedcmFlowResponse 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 NewCreateFedcmFlowResponseWithDefaults() *CreateFedcmFlowResponse { + this := CreateFedcmFlowResponse{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *CreateFedcmFlowResponse) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *CreateFedcmFlowResponse) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *CreateFedcmFlowResponse) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *CreateFedcmFlowResponse) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *CreateFedcmFlowResponse) GetProviders() []Provider { + if o == nil || IsNil(o.Providers) { + var ret []Provider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateFedcmFlowResponse) GetProvidersOk() ([]Provider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *CreateFedcmFlowResponse) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []Provider and assigns it to the Providers field. +func (o *CreateFedcmFlowResponse) SetProviders(v []Provider) { + o.Providers = v +} + +func (o CreateFedcmFlowResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateFedcmFlowResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateFedcmFlowResponse) UnmarshalJSON(data []byte) (err error) { + varCreateFedcmFlowResponse := _CreateFedcmFlowResponse{} + + err = json.Unmarshal(data, &varCreateFedcmFlowResponse) + + if err != nil { + return err + } + + *o = CreateFedcmFlowResponse(varCreateFedcmFlowResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateFedcmFlowResponse struct { + value *CreateFedcmFlowResponse + isSet bool +} + +func (v NullableCreateFedcmFlowResponse) Get() *CreateFedcmFlowResponse { + return v.value +} + +func (v *NullableCreateFedcmFlowResponse) Set(val *CreateFedcmFlowResponse) { + v.value = val + v.isSet = true +} + +func (v NullableCreateFedcmFlowResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateFedcmFlowResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateFedcmFlowResponse(val *CreateFedcmFlowResponse) *NullableCreateFedcmFlowResponse { + return &NullableCreateFedcmFlowResponse{value: val, isSet: true} +} + +func (v NullableCreateFedcmFlowResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateFedcmFlowResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_create_identity_body.go b/pkg/client-go/model_create_identity_body.go new file mode 100644 index 0000000..2caeb1f --- /dev/null +++ b/pkg/client-go/model_create_identity_body.go @@ -0,0 +1,551 @@ +/* +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" + "fmt" +) + +// checks if the CreateIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateIdentityBody{} + +// CreateIdentityBody Create Identity Body +type CreateIdentityBody struct { + Credentials *IdentityWithCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // Store metadata about the identity which the identity itself can see when calling for example the session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic interface{} `json:"metadata_public,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RecoveryAddresses contains all the addresses that can be used to recover an identity. Use this structure to import recovery addresses for an identity. Please keep in mind that the address needs to be represented in the Identity Schema or this field will be overwritten on the next identity update. + RecoveryAddresses []RecoveryIdentityAddress `json:"recovery_addresses,omitempty"` + // Region is the Ory Network region this identity will be created in. Optional; defaults to the project home region if omitted. Only effective on the Ory Network. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + SchemaId string `json:"schema_id"` + // State is the identity's state. active StateActive inactive StateInactive + State *string `json:"state,omitempty"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_url`. + Traits map[string]interface{} `json:"traits"` + // VerifiableAddresses contains all the addresses that can be verified by the user. Use this structure to import verified addresses for an identity. Please keep in mind that the address needs to be represented in the Identity Schema or this field will be overwritten on the next identity update. + VerifiableAddresses []VerifiableIdentityAddress `json:"verifiable_addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _CreateIdentityBody CreateIdentityBody + +// NewCreateIdentityBody instantiates a new CreateIdentityBody 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 NewCreateIdentityBody(schemaId string, traits map[string]interface{}) *CreateIdentityBody { + this := CreateIdentityBody{} + this.SchemaId = schemaId + this.Traits = traits + return &this +} + +// NewCreateIdentityBodyWithDefaults instantiates a new CreateIdentityBody 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 NewCreateIdentityBodyWithDefaults() *CreateIdentityBody { + this := CreateIdentityBody{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetCredentials() IdentityWithCredentials { + if o == nil || IsNil(o.Credentials) { + var ret IdentityWithCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetCredentialsOk() (*IdentityWithCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given IdentityWithCredentials and assigns it to the Credentials field. +func (o *CreateIdentityBody) SetCredentials(v IdentityWithCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *CreateIdentityBody) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *CreateIdentityBody) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *CreateIdentityBody) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *CreateIdentityBody) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *CreateIdentityBody) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *CreateIdentityBody) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *CreateIdentityBody) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *CreateIdentityBody) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *CreateIdentityBody) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRecoveryAddresses returns the RecoveryAddresses field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetRecoveryAddresses() []RecoveryIdentityAddress { + if o == nil || IsNil(o.RecoveryAddresses) { + var ret []RecoveryIdentityAddress + return ret + } + return o.RecoveryAddresses +} + +// GetRecoveryAddressesOk returns a tuple with the RecoveryAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetRecoveryAddressesOk() ([]RecoveryIdentityAddress, bool) { + if o == nil || IsNil(o.RecoveryAddresses) { + return nil, false + } + return o.RecoveryAddresses, true +} + +// HasRecoveryAddresses returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasRecoveryAddresses() bool { + if o != nil && !IsNil(o.RecoveryAddresses) { + return true + } + + return false +} + +// SetRecoveryAddresses gets a reference to the given []RecoveryIdentityAddress and assigns it to the RecoveryAddresses field. +func (o *CreateIdentityBody) SetRecoveryAddresses(v []RecoveryIdentityAddress) { + o.RecoveryAddresses = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *CreateIdentityBody) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *CreateIdentityBody) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *CreateIdentityBody) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetState() string { + if o == nil || IsNil(o.State) { + var ret string + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetStateOk() (*string, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given string and assigns it to the State field. +func (o *CreateIdentityBody) SetState(v string) { + o.State = &v +} + +// GetTraits returns the Traits field value +func (o *CreateIdentityBody) 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 *CreateIdentityBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *CreateIdentityBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetVerifiableAddresses returns the VerifiableAddresses field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetVerifiableAddresses() []VerifiableIdentityAddress { + if o == nil || IsNil(o.VerifiableAddresses) { + var ret []VerifiableIdentityAddress + return ret + } + return o.VerifiableAddresses +} + +// GetVerifiableAddressesOk returns a tuple with the VerifiableAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetVerifiableAddressesOk() ([]VerifiableIdentityAddress, bool) { + if o == nil || IsNil(o.VerifiableAddresses) { + return nil, false + } + return o.VerifiableAddresses, true +} + +// HasVerifiableAddresses returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasVerifiableAddresses() bool { + if o != nil && !IsNil(o.VerifiableAddresses) { + return true + } + + return false +} + +// SetVerifiableAddresses gets a reference to the given []VerifiableIdentityAddress and assigns it to the VerifiableAddresses field. +func (o *CreateIdentityBody) SetVerifiableAddresses(v []VerifiableIdentityAddress) { + o.VerifiableAddresses = v +} + +func (o CreateIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.RecoveryAddresses) { + toSerialize["recovery_addresses"] = o.RecoveryAddresses + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + toSerialize["traits"] = o.Traits + if !IsNil(o.VerifiableAddresses) { + toSerialize["verifiable_addresses"] = o.VerifiableAddresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "schema_id", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateIdentityBody := _CreateIdentityBody{} + + err = json.Unmarshal(data, &varCreateIdentityBody) + + if err != nil { + return err + } + + *o = CreateIdentityBody(varCreateIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "recovery_addresses") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "state") + delete(additionalProperties, "traits") + delete(additionalProperties, "verifiable_addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateIdentityBody struct { + value *CreateIdentityBody + isSet bool +} + +func (v NullableCreateIdentityBody) Get() *CreateIdentityBody { + return v.value +} + +func (v *NullableCreateIdentityBody) Set(val *CreateIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateIdentityBody(val *CreateIdentityBody) *NullableCreateIdentityBody { + return &NullableCreateIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_create_recovery_code_for_identity_body.go b/pkg/client-go/model_create_recovery_code_for_identity_body.go new file mode 100644 index 0000000..e23a27f --- /dev/null +++ b/pkg/client-go/model_create_recovery_code_for_identity_body.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the CreateRecoveryCodeForIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateRecoveryCodeForIdentityBody{} + +// CreateRecoveryCodeForIdentityBody Create Recovery Code for Identity Request Body +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" validate:"regexp=^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _CreateRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody + +// NewCreateRecoveryCodeForIdentityBody instantiates a new CreateRecoveryCodeForIdentityBody 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 NewCreateRecoveryCodeForIdentityBody(identityId string) *CreateRecoveryCodeForIdentityBody { + this := CreateRecoveryCodeForIdentityBody{} + this.IdentityId = identityId + return &this +} + +// NewCreateRecoveryCodeForIdentityBodyWithDefaults instantiates a new CreateRecoveryCodeForIdentityBody 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 NewCreateRecoveryCodeForIdentityBodyWithDefaults() *CreateRecoveryCodeForIdentityBody { + this := CreateRecoveryCodeForIdentityBody{} + return &this +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetExpiresIn() string { + if o == nil || IsNil(o.ExpiresIn) { + var ret string + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetExpiresInOk() (*string, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given string and assigns it to the ExpiresIn field. +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 || IsNil(o.FlowType) { + 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 || IsNil(o.FlowType) { + 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 && !IsNil(o.FlowType) { + 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 { + var ret string + return ret + } + + return o.IdentityId +} + +// GetIdentityIdOk returns a tuple with the IdentityId field value +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetIdentityIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.IdentityId, true +} + +// SetIdentityId sets field value +func (o *CreateRecoveryCodeForIdentityBody) SetIdentityId(v string) { + o.IdentityId = v +} + +func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateRecoveryCodeForIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + if !IsNil(o.FlowType) { + toSerialize["flow_type"] = o.FlowType + } + toSerialize["identity_id"] = o.IdentityId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateRecoveryCodeForIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateRecoveryCodeForIdentityBody := _CreateRecoveryCodeForIdentityBody{} + + err = json.Unmarshal(data, &varCreateRecoveryCodeForIdentityBody) + + if err != nil { + return err + } + + *o = CreateRecoveryCodeForIdentityBody(varCreateRecoveryCodeForIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_in") + delete(additionalProperties, "flow_type") + delete(additionalProperties, "identity_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateRecoveryCodeForIdentityBody struct { + value *CreateRecoveryCodeForIdentityBody + isSet bool +} + +func (v NullableCreateRecoveryCodeForIdentityBody) Get() *CreateRecoveryCodeForIdentityBody { + return v.value +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) Set(val *CreateRecoveryCodeForIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateRecoveryCodeForIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateRecoveryCodeForIdentityBody(val *CreateRecoveryCodeForIdentityBody) *NullableCreateRecoveryCodeForIdentityBody { + return &NullableCreateRecoveryCodeForIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_create_recovery_link_for_identity_body.go b/pkg/client-go/model_create_recovery_link_for_identity_body.go new file mode 100644 index 0000000..2a1ce64 --- /dev/null +++ b/pkg/client-go/model_create_recovery_link_for_identity_body.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the CreateRecoveryLinkForIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateRecoveryLinkForIdentityBody{} + +// CreateRecoveryLinkForIdentityBody Create Recovery Link for Identity Request Body +type CreateRecoveryLinkForIdentityBody struct { + // Link Expires In The recovery link 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" validate:"regexp=^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$"` + // Identity to Recover The identity's ID you wish to recover. + IdentityId string `json:"identity_id"` + AdditionalProperties map[string]interface{} +} + +type _CreateRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody + +// NewCreateRecoveryLinkForIdentityBody instantiates a new CreateRecoveryLinkForIdentityBody 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 NewCreateRecoveryLinkForIdentityBody(identityId string) *CreateRecoveryLinkForIdentityBody { + this := CreateRecoveryLinkForIdentityBody{} + this.IdentityId = identityId + return &this +} + +// NewCreateRecoveryLinkForIdentityBodyWithDefaults instantiates a new CreateRecoveryLinkForIdentityBody 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 NewCreateRecoveryLinkForIdentityBodyWithDefaults() *CreateRecoveryLinkForIdentityBody { + this := CreateRecoveryLinkForIdentityBody{} + return &this +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *CreateRecoveryLinkForIdentityBody) GetExpiresIn() string { + if o == nil || IsNil(o.ExpiresIn) { + var ret string + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryLinkForIdentityBody) GetExpiresInOk() (*string, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *CreateRecoveryLinkForIdentityBody) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given string and assigns it to the ExpiresIn field. +func (o *CreateRecoveryLinkForIdentityBody) SetExpiresIn(v string) { + o.ExpiresIn = &v +} + +// GetIdentityId returns the IdentityId field value +func (o *CreateRecoveryLinkForIdentityBody) GetIdentityId() string { + if o == nil { + var ret string + return ret + } + + return o.IdentityId +} + +// GetIdentityIdOk returns a tuple with the IdentityId field value +// and a boolean to check if the value has been set. +func (o *CreateRecoveryLinkForIdentityBody) GetIdentityIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.IdentityId, true +} + +// SetIdentityId sets field value +func (o *CreateRecoveryLinkForIdentityBody) SetIdentityId(v string) { + o.IdentityId = v +} + +func (o CreateRecoveryLinkForIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateRecoveryLinkForIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + toSerialize["identity_id"] = o.IdentityId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateRecoveryLinkForIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateRecoveryLinkForIdentityBody := _CreateRecoveryLinkForIdentityBody{} + + err = json.Unmarshal(data, &varCreateRecoveryLinkForIdentityBody) + + if err != nil { + return err + } + + *o = CreateRecoveryLinkForIdentityBody(varCreateRecoveryLinkForIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_in") + delete(additionalProperties, "identity_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateRecoveryLinkForIdentityBody struct { + value *CreateRecoveryLinkForIdentityBody + isSet bool +} + +func (v NullableCreateRecoveryLinkForIdentityBody) Get() *CreateRecoveryLinkForIdentityBody { + return v.value +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) Set(val *CreateRecoveryLinkForIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateRecoveryLinkForIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateRecoveryLinkForIdentityBody(val *CreateRecoveryLinkForIdentityBody) *NullableCreateRecoveryLinkForIdentityBody { + return &NullableCreateRecoveryLinkForIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateRecoveryLinkForIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_create_test_login_flow_body.go b/pkg/client-go/model_create_test_login_flow_body.go new file mode 100644 index 0000000..e74173a --- /dev/null +++ b/pkg/client-go/model_create_test_login_flow_body.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the CreateTestLoginFlowBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateTestLoginFlowBody{} + +// CreateTestLoginFlowBody struct for CreateTestLoginFlowBody +type CreateTestLoginFlowBody struct { + // ID of the OIDC provider to test. Must match a provider configured on the project that serves this request. + ProviderId string `json:"provider_id"` + AdditionalProperties map[string]interface{} +} + +type _CreateTestLoginFlowBody CreateTestLoginFlowBody + +// NewCreateTestLoginFlowBody instantiates a new CreateTestLoginFlowBody 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 NewCreateTestLoginFlowBody(providerId string) *CreateTestLoginFlowBody { + this := CreateTestLoginFlowBody{} + this.ProviderId = providerId + return &this +} + +// NewCreateTestLoginFlowBodyWithDefaults instantiates a new CreateTestLoginFlowBody 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 NewCreateTestLoginFlowBodyWithDefaults() *CreateTestLoginFlowBody { + this := CreateTestLoginFlowBody{} + return &this +} + +// GetProviderId returns the ProviderId field value +func (o *CreateTestLoginFlowBody) GetProviderId() string { + if o == nil { + var ret string + return ret + } + + return o.ProviderId +} + +// GetProviderIdOk returns a tuple with the ProviderId field value +// and a boolean to check if the value has been set. +func (o *CreateTestLoginFlowBody) GetProviderIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ProviderId, true +} + +// SetProviderId sets field value +func (o *CreateTestLoginFlowBody) SetProviderId(v string) { + o.ProviderId = v +} + +func (o CreateTestLoginFlowBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateTestLoginFlowBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["provider_id"] = o.ProviderId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateTestLoginFlowBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateTestLoginFlowBody := _CreateTestLoginFlowBody{} + + err = json.Unmarshal(data, &varCreateTestLoginFlowBody) + + if err != nil { + return err + } + + *o = CreateTestLoginFlowBody(varCreateTestLoginFlowBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "provider_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateTestLoginFlowBody struct { + value *CreateTestLoginFlowBody + isSet bool +} + +func (v NullableCreateTestLoginFlowBody) Get() *CreateTestLoginFlowBody { + return v.value +} + +func (v *NullableCreateTestLoginFlowBody) Set(val *CreateTestLoginFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateTestLoginFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateTestLoginFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateTestLoginFlowBody(val *CreateTestLoginFlowBody) *NullableCreateTestLoginFlowBody { + return &NullableCreateTestLoginFlowBody{value: val, isSet: true} +} + +func (v NullableCreateTestLoginFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateTestLoginFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_delete_my_sessions_count.go b/pkg/client-go/model_delete_my_sessions_count.go new file mode 100644 index 0000000..ca207b0 --- /dev/null +++ b/pkg/client-go/model_delete_my_sessions_count.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the DeleteMySessionsCount type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeleteMySessionsCount{} + +// DeleteMySessionsCount Deleted Session Count +type DeleteMySessionsCount struct { + // The number of sessions that were revoked. + Count *int64 `json:"count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeleteMySessionsCount DeleteMySessionsCount + +// NewDeleteMySessionsCount instantiates a new DeleteMySessionsCount 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 NewDeleteMySessionsCount() *DeleteMySessionsCount { + this := DeleteMySessionsCount{} + return &this +} + +// NewDeleteMySessionsCountWithDefaults instantiates a new DeleteMySessionsCount 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 NewDeleteMySessionsCountWithDefaults() *DeleteMySessionsCount { + this := DeleteMySessionsCount{} + return &this +} + +// GetCount returns the Count field value if set, zero value otherwise. +func (o *DeleteMySessionsCount) GetCount() int64 { + if o == nil || IsNil(o.Count) { + var ret int64 + return ret + } + return *o.Count +} + +// GetCountOk returns a tuple with the Count field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeleteMySessionsCount) GetCountOk() (*int64, bool) { + if o == nil || IsNil(o.Count) { + return nil, false + } + return o.Count, true +} + +// HasCount returns a boolean if a field has been set. +func (o *DeleteMySessionsCount) HasCount() bool { + if o != nil && !IsNil(o.Count) { + return true + } + + return false +} + +// SetCount gets a reference to the given int64 and assigns it to the Count field. +func (o *DeleteMySessionsCount) SetCount(v int64) { + o.Count = &v +} + +func (o DeleteMySessionsCount) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeleteMySessionsCount) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Count) { + toSerialize["count"] = o.Count + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeleteMySessionsCount) UnmarshalJSON(data []byte) (err error) { + varDeleteMySessionsCount := _DeleteMySessionsCount{} + + err = json.Unmarshal(data, &varDeleteMySessionsCount) + + if err != nil { + return err + } + + *o = DeleteMySessionsCount(varDeleteMySessionsCount) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeleteMySessionsCount struct { + value *DeleteMySessionsCount + isSet bool +} + +func (v NullableDeleteMySessionsCount) Get() *DeleteMySessionsCount { + return v.value +} + +func (v *NullableDeleteMySessionsCount) Set(val *DeleteMySessionsCount) { + v.value = val + v.isSet = true +} + +func (v NullableDeleteMySessionsCount) IsSet() bool { + return v.isSet +} + +func (v *NullableDeleteMySessionsCount) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeleteMySessionsCount(val *DeleteMySessionsCount) *NullableDeleteMySessionsCount { + return &NullableDeleteMySessionsCount{value: val, isSet: true} +} + +func (v NullableDeleteMySessionsCount) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeleteMySessionsCount) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_android_authorization_list.go b/pkg/client-go/model_device_authn_android_authorization_list.go new file mode 100644 index 0000000..26dc776 --- /dev/null +++ b/pkg/client-go/model_device_authn_android_authorization_list.go @@ -0,0 +1,1116 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidAuthorizationList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidAuthorizationList{} + +// DeviceAuthnAndroidAuthorizationList Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +type DeviceAuthnAndroidAuthorizationList struct { + ActiveDateTime *int64 `json:"active_date_time,omitempty"` + Algorithm *int64 `json:"algorithm,omitempty"` + ApplicationId []int32 `json:"application_id,omitempty"` + AttestationApplicationId []int32 `json:"attestation_application_id,omitempty"` + AttestationIdBrand []int32 `json:"attestation_id_brand,omitempty"` + AttestationIdDevice []int32 `json:"attestation_id_device,omitempty"` + AttestationIdImei []int32 `json:"attestation_id_imei,omitempty"` + AttestationIdManufacturer []int32 `json:"attestation_id_manufacturer,omitempty"` + AttestationIdMeid []int32 `json:"attestation_id_meid,omitempty"` + AttestationIdModel []int32 `json:"attestation_id_model,omitempty"` + AttestationIdProduct []int32 `json:"attestation_id_product,omitempty"` + AttestationIdSerial []int32 `json:"attestation_id_serial,omitempty"` + AuthTimeout *int64 `json:"auth_timeout,omitempty"` + BootPatchLevel *int64 `json:"boot_patch_level,omitempty"` + CreationDateTime *int64 `json:"creation_date_time,omitempty"` + Digest []int64 `json:"digest,omitempty"` + EcCurve *int64 `json:"ec_curve,omitempty"` + KeySize *int64 `json:"key_size,omitempty"` + Origin *int64 `json:"origin,omitempty"` + OriginationExpireDateTime *int64 `json:"origination_expire_date_time,omitempty"` + OsPatchLevel *int64 `json:"os_patch_level,omitempty"` + OsVersion *int64 `json:"os_version,omitempty"` + Purpose []int64 `json:"purpose,omitempty"` + RootOfTrust *DeviceAuthnAndroidRootOfTrust `json:"root_of_trust,omitempty"` + UsageExpireDateTime *int64 `json:"usage_expire_date_time,omitempty"` + UserAuthType *int64 `json:"user_auth_type,omitempty"` + VendorPatchLevel *int64 `json:"vendor_patch_level,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidAuthorizationList DeviceAuthnAndroidAuthorizationList + +// NewDeviceAuthnAndroidAuthorizationList instantiates a new DeviceAuthnAndroidAuthorizationList 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 NewDeviceAuthnAndroidAuthorizationList() *DeviceAuthnAndroidAuthorizationList { + this := DeviceAuthnAndroidAuthorizationList{} + return &this +} + +// NewDeviceAuthnAndroidAuthorizationListWithDefaults instantiates a new DeviceAuthnAndroidAuthorizationList 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 NewDeviceAuthnAndroidAuthorizationListWithDefaults() *DeviceAuthnAndroidAuthorizationList { + this := DeviceAuthnAndroidAuthorizationList{} + return &this +} + +// GetActiveDateTime returns the ActiveDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetActiveDateTime() int64 { + if o == nil || IsNil(o.ActiveDateTime) { + var ret int64 + return ret + } + return *o.ActiveDateTime +} + +// GetActiveDateTimeOk returns a tuple with the ActiveDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetActiveDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.ActiveDateTime) { + return nil, false + } + return o.ActiveDateTime, true +} + +// HasActiveDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasActiveDateTime() bool { + if o != nil && !IsNil(o.ActiveDateTime) { + return true + } + + return false +} + +// SetActiveDateTime gets a reference to the given int64 and assigns it to the ActiveDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetActiveDateTime(v int64) { + o.ActiveDateTime = &v +} + +// GetAlgorithm returns the Algorithm field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAlgorithm() int64 { + if o == nil || IsNil(o.Algorithm) { + var ret int64 + return ret + } + return *o.Algorithm +} + +// GetAlgorithmOk returns a tuple with the Algorithm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAlgorithmOk() (*int64, bool) { + if o == nil || IsNil(o.Algorithm) { + return nil, false + } + return o.Algorithm, true +} + +// HasAlgorithm returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAlgorithm() bool { + if o != nil && !IsNil(o.Algorithm) { + return true + } + + return false +} + +// SetAlgorithm gets a reference to the given int64 and assigns it to the Algorithm field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAlgorithm(v int64) { + o.Algorithm = &v +} + +// GetApplicationId returns the ApplicationId field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetApplicationId() []int32 { + if o == nil || IsNil(o.ApplicationId) { + var ret []int32 + return ret + } + return o.ApplicationId +} + +// GetApplicationIdOk returns a tuple with the ApplicationId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetApplicationIdOk() ([]int32, bool) { + if o == nil || IsNil(o.ApplicationId) { + return nil, false + } + return o.ApplicationId, true +} + +// HasApplicationId returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasApplicationId() bool { + if o != nil && !IsNil(o.ApplicationId) { + return true + } + + return false +} + +// SetApplicationId gets a reference to the given []int32 and assigns it to the ApplicationId field. +func (o *DeviceAuthnAndroidAuthorizationList) SetApplicationId(v []int32) { + o.ApplicationId = v +} + +// GetAttestationApplicationId returns the AttestationApplicationId field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationApplicationId() []int32 { + if o == nil || IsNil(o.AttestationApplicationId) { + var ret []int32 + return ret + } + return o.AttestationApplicationId +} + +// GetAttestationApplicationIdOk returns a tuple with the AttestationApplicationId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationApplicationIdOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationApplicationId) { + return nil, false + } + return o.AttestationApplicationId, true +} + +// HasAttestationApplicationId returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationApplicationId() bool { + if o != nil && !IsNil(o.AttestationApplicationId) { + return true + } + + return false +} + +// SetAttestationApplicationId gets a reference to the given []int32 and assigns it to the AttestationApplicationId field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationApplicationId(v []int32) { + o.AttestationApplicationId = v +} + +// GetAttestationIdBrand returns the AttestationIdBrand field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdBrand() []int32 { + if o == nil || IsNil(o.AttestationIdBrand) { + var ret []int32 + return ret + } + return o.AttestationIdBrand +} + +// GetAttestationIdBrandOk returns a tuple with the AttestationIdBrand field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdBrandOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdBrand) { + return nil, false + } + return o.AttestationIdBrand, true +} + +// HasAttestationIdBrand returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdBrand() bool { + if o != nil && !IsNil(o.AttestationIdBrand) { + return true + } + + return false +} + +// SetAttestationIdBrand gets a reference to the given []int32 and assigns it to the AttestationIdBrand field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdBrand(v []int32) { + o.AttestationIdBrand = v +} + +// GetAttestationIdDevice returns the AttestationIdDevice field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdDevice() []int32 { + if o == nil || IsNil(o.AttestationIdDevice) { + var ret []int32 + return ret + } + return o.AttestationIdDevice +} + +// GetAttestationIdDeviceOk returns a tuple with the AttestationIdDevice field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdDeviceOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdDevice) { + return nil, false + } + return o.AttestationIdDevice, true +} + +// HasAttestationIdDevice returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdDevice() bool { + if o != nil && !IsNil(o.AttestationIdDevice) { + return true + } + + return false +} + +// SetAttestationIdDevice gets a reference to the given []int32 and assigns it to the AttestationIdDevice field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdDevice(v []int32) { + o.AttestationIdDevice = v +} + +// GetAttestationIdImei returns the AttestationIdImei field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdImei() []int32 { + if o == nil || IsNil(o.AttestationIdImei) { + var ret []int32 + return ret + } + return o.AttestationIdImei +} + +// GetAttestationIdImeiOk returns a tuple with the AttestationIdImei field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdImeiOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdImei) { + return nil, false + } + return o.AttestationIdImei, true +} + +// HasAttestationIdImei returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdImei() bool { + if o != nil && !IsNil(o.AttestationIdImei) { + return true + } + + return false +} + +// SetAttestationIdImei gets a reference to the given []int32 and assigns it to the AttestationIdImei field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdImei(v []int32) { + o.AttestationIdImei = v +} + +// GetAttestationIdManufacturer returns the AttestationIdManufacturer field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdManufacturer() []int32 { + if o == nil || IsNil(o.AttestationIdManufacturer) { + var ret []int32 + return ret + } + return o.AttestationIdManufacturer +} + +// GetAttestationIdManufacturerOk returns a tuple with the AttestationIdManufacturer field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdManufacturerOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdManufacturer) { + return nil, false + } + return o.AttestationIdManufacturer, true +} + +// HasAttestationIdManufacturer returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdManufacturer() bool { + if o != nil && !IsNil(o.AttestationIdManufacturer) { + return true + } + + return false +} + +// SetAttestationIdManufacturer gets a reference to the given []int32 and assigns it to the AttestationIdManufacturer field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdManufacturer(v []int32) { + o.AttestationIdManufacturer = v +} + +// GetAttestationIdMeid returns the AttestationIdMeid field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdMeid() []int32 { + if o == nil || IsNil(o.AttestationIdMeid) { + var ret []int32 + return ret + } + return o.AttestationIdMeid +} + +// GetAttestationIdMeidOk returns a tuple with the AttestationIdMeid field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdMeidOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdMeid) { + return nil, false + } + return o.AttestationIdMeid, true +} + +// HasAttestationIdMeid returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdMeid() bool { + if o != nil && !IsNil(o.AttestationIdMeid) { + return true + } + + return false +} + +// SetAttestationIdMeid gets a reference to the given []int32 and assigns it to the AttestationIdMeid field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdMeid(v []int32) { + o.AttestationIdMeid = v +} + +// GetAttestationIdModel returns the AttestationIdModel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdModel() []int32 { + if o == nil || IsNil(o.AttestationIdModel) { + var ret []int32 + return ret + } + return o.AttestationIdModel +} + +// GetAttestationIdModelOk returns a tuple with the AttestationIdModel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdModelOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdModel) { + return nil, false + } + return o.AttestationIdModel, true +} + +// HasAttestationIdModel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdModel() bool { + if o != nil && !IsNil(o.AttestationIdModel) { + return true + } + + return false +} + +// SetAttestationIdModel gets a reference to the given []int32 and assigns it to the AttestationIdModel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdModel(v []int32) { + o.AttestationIdModel = v +} + +// GetAttestationIdProduct returns the AttestationIdProduct field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdProduct() []int32 { + if o == nil || IsNil(o.AttestationIdProduct) { + var ret []int32 + return ret + } + return o.AttestationIdProduct +} + +// GetAttestationIdProductOk returns a tuple with the AttestationIdProduct field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdProductOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdProduct) { + return nil, false + } + return o.AttestationIdProduct, true +} + +// HasAttestationIdProduct returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdProduct() bool { + if o != nil && !IsNil(o.AttestationIdProduct) { + return true + } + + return false +} + +// SetAttestationIdProduct gets a reference to the given []int32 and assigns it to the AttestationIdProduct field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdProduct(v []int32) { + o.AttestationIdProduct = v +} + +// GetAttestationIdSerial returns the AttestationIdSerial field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdSerial() []int32 { + if o == nil || IsNil(o.AttestationIdSerial) { + var ret []int32 + return ret + } + return o.AttestationIdSerial +} + +// GetAttestationIdSerialOk returns a tuple with the AttestationIdSerial field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdSerialOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdSerial) { + return nil, false + } + return o.AttestationIdSerial, true +} + +// HasAttestationIdSerial returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdSerial() bool { + if o != nil && !IsNil(o.AttestationIdSerial) { + return true + } + + return false +} + +// SetAttestationIdSerial gets a reference to the given []int32 and assigns it to the AttestationIdSerial field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdSerial(v []int32) { + o.AttestationIdSerial = v +} + +// GetAuthTimeout returns the AuthTimeout field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAuthTimeout() int64 { + if o == nil || IsNil(o.AuthTimeout) { + var ret int64 + return ret + } + return *o.AuthTimeout +} + +// GetAuthTimeoutOk returns a tuple with the AuthTimeout field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAuthTimeoutOk() (*int64, bool) { + if o == nil || IsNil(o.AuthTimeout) { + return nil, false + } + return o.AuthTimeout, true +} + +// HasAuthTimeout returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAuthTimeout() bool { + if o != nil && !IsNil(o.AuthTimeout) { + return true + } + + return false +} + +// SetAuthTimeout gets a reference to the given int64 and assigns it to the AuthTimeout field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAuthTimeout(v int64) { + o.AuthTimeout = &v +} + +// GetBootPatchLevel returns the BootPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetBootPatchLevel() int64 { + if o == nil || IsNil(o.BootPatchLevel) { + var ret int64 + return ret + } + return *o.BootPatchLevel +} + +// GetBootPatchLevelOk returns a tuple with the BootPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetBootPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.BootPatchLevel) { + return nil, false + } + return o.BootPatchLevel, true +} + +// HasBootPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasBootPatchLevel() bool { + if o != nil && !IsNil(o.BootPatchLevel) { + return true + } + + return false +} + +// SetBootPatchLevel gets a reference to the given int64 and assigns it to the BootPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetBootPatchLevel(v int64) { + o.BootPatchLevel = &v +} + +// GetCreationDateTime returns the CreationDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetCreationDateTime() int64 { + if o == nil || IsNil(o.CreationDateTime) { + var ret int64 + return ret + } + return *o.CreationDateTime +} + +// GetCreationDateTimeOk returns a tuple with the CreationDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetCreationDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.CreationDateTime) { + return nil, false + } + return o.CreationDateTime, true +} + +// HasCreationDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasCreationDateTime() bool { + if o != nil && !IsNil(o.CreationDateTime) { + return true + } + + return false +} + +// SetCreationDateTime gets a reference to the given int64 and assigns it to the CreationDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetCreationDateTime(v int64) { + o.CreationDateTime = &v +} + +// GetDigest returns the Digest field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetDigest() []int64 { + if o == nil || IsNil(o.Digest) { + var ret []int64 + return ret + } + return o.Digest +} + +// GetDigestOk returns a tuple with the Digest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetDigestOk() ([]int64, bool) { + if o == nil || IsNil(o.Digest) { + return nil, false + } + return o.Digest, true +} + +// HasDigest returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasDigest() bool { + if o != nil && !IsNil(o.Digest) { + return true + } + + return false +} + +// SetDigest gets a reference to the given []int64 and assigns it to the Digest field. +func (o *DeviceAuthnAndroidAuthorizationList) SetDigest(v []int64) { + o.Digest = v +} + +// GetEcCurve returns the EcCurve field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetEcCurve() int64 { + if o == nil || IsNil(o.EcCurve) { + var ret int64 + return ret + } + return *o.EcCurve +} + +// GetEcCurveOk returns a tuple with the EcCurve field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetEcCurveOk() (*int64, bool) { + if o == nil || IsNil(o.EcCurve) { + return nil, false + } + return o.EcCurve, true +} + +// HasEcCurve returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasEcCurve() bool { + if o != nil && !IsNil(o.EcCurve) { + return true + } + + return false +} + +// SetEcCurve gets a reference to the given int64 and assigns it to the EcCurve field. +func (o *DeviceAuthnAndroidAuthorizationList) SetEcCurve(v int64) { + o.EcCurve = &v +} + +// GetKeySize returns the KeySize field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetKeySize() int64 { + if o == nil || IsNil(o.KeySize) { + var ret int64 + return ret + } + return *o.KeySize +} + +// GetKeySizeOk returns a tuple with the KeySize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetKeySizeOk() (*int64, bool) { + if o == nil || IsNil(o.KeySize) { + return nil, false + } + return o.KeySize, true +} + +// HasKeySize returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasKeySize() bool { + if o != nil && !IsNil(o.KeySize) { + return true + } + + return false +} + +// SetKeySize gets a reference to the given int64 and assigns it to the KeySize field. +func (o *DeviceAuthnAndroidAuthorizationList) SetKeySize(v int64) { + o.KeySize = &v +} + +// GetOrigin returns the Origin field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOrigin() int64 { + if o == nil || IsNil(o.Origin) { + var ret int64 + return ret + } + return *o.Origin +} + +// GetOriginOk returns a tuple with the Origin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginOk() (*int64, bool) { + if o == nil || IsNil(o.Origin) { + return nil, false + } + return o.Origin, true +} + +// HasOrigin returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOrigin() bool { + if o != nil && !IsNil(o.Origin) { + return true + } + + return false +} + +// SetOrigin gets a reference to the given int64 and assigns it to the Origin field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOrigin(v int64) { + o.Origin = &v +} + +// GetOriginationExpireDateTime returns the OriginationExpireDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginationExpireDateTime() int64 { + if o == nil || IsNil(o.OriginationExpireDateTime) { + var ret int64 + return ret + } + return *o.OriginationExpireDateTime +} + +// GetOriginationExpireDateTimeOk returns a tuple with the OriginationExpireDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginationExpireDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.OriginationExpireDateTime) { + return nil, false + } + return o.OriginationExpireDateTime, true +} + +// HasOriginationExpireDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOriginationExpireDateTime() bool { + if o != nil && !IsNil(o.OriginationExpireDateTime) { + return true + } + + return false +} + +// SetOriginationExpireDateTime gets a reference to the given int64 and assigns it to the OriginationExpireDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOriginationExpireDateTime(v int64) { + o.OriginationExpireDateTime = &v +} + +// GetOsPatchLevel returns the OsPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsPatchLevel() int64 { + if o == nil || IsNil(o.OsPatchLevel) { + var ret int64 + return ret + } + return *o.OsPatchLevel +} + +// GetOsPatchLevelOk returns a tuple with the OsPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.OsPatchLevel) { + return nil, false + } + return o.OsPatchLevel, true +} + +// HasOsPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOsPatchLevel() bool { + if o != nil && !IsNil(o.OsPatchLevel) { + return true + } + + return false +} + +// SetOsPatchLevel gets a reference to the given int64 and assigns it to the OsPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOsPatchLevel(v int64) { + o.OsPatchLevel = &v +} + +// GetOsVersion returns the OsVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsVersion() int64 { + if o == nil || IsNil(o.OsVersion) { + var ret int64 + return ret + } + return *o.OsVersion +} + +// GetOsVersionOk returns a tuple with the OsVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsVersionOk() (*int64, bool) { + if o == nil || IsNil(o.OsVersion) { + return nil, false + } + return o.OsVersion, true +} + +// HasOsVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOsVersion() bool { + if o != nil && !IsNil(o.OsVersion) { + return true + } + + return false +} + +// SetOsVersion gets a reference to the given int64 and assigns it to the OsVersion field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOsVersion(v int64) { + o.OsVersion = &v +} + +// GetPurpose returns the Purpose field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetPurpose() []int64 { + if o == nil || IsNil(o.Purpose) { + var ret []int64 + return ret + } + return o.Purpose +} + +// GetPurposeOk returns a tuple with the Purpose field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetPurposeOk() ([]int64, bool) { + if o == nil || IsNil(o.Purpose) { + return nil, false + } + return o.Purpose, true +} + +// HasPurpose returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasPurpose() bool { + if o != nil && !IsNil(o.Purpose) { + return true + } + + return false +} + +// SetPurpose gets a reference to the given []int64 and assigns it to the Purpose field. +func (o *DeviceAuthnAndroidAuthorizationList) SetPurpose(v []int64) { + o.Purpose = v +} + +// GetRootOfTrust returns the RootOfTrust field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetRootOfTrust() DeviceAuthnAndroidRootOfTrust { + if o == nil || IsNil(o.RootOfTrust) { + var ret DeviceAuthnAndroidRootOfTrust + return ret + } + return *o.RootOfTrust +} + +// GetRootOfTrustOk returns a tuple with the RootOfTrust field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetRootOfTrustOk() (*DeviceAuthnAndroidRootOfTrust, bool) { + if o == nil || IsNil(o.RootOfTrust) { + return nil, false + } + return o.RootOfTrust, true +} + +// HasRootOfTrust returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasRootOfTrust() bool { + if o != nil && !IsNil(o.RootOfTrust) { + return true + } + + return false +} + +// SetRootOfTrust gets a reference to the given DeviceAuthnAndroidRootOfTrust and assigns it to the RootOfTrust field. +func (o *DeviceAuthnAndroidAuthorizationList) SetRootOfTrust(v DeviceAuthnAndroidRootOfTrust) { + o.RootOfTrust = &v +} + +// GetUsageExpireDateTime returns the UsageExpireDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetUsageExpireDateTime() int64 { + if o == nil || IsNil(o.UsageExpireDateTime) { + var ret int64 + return ret + } + return *o.UsageExpireDateTime +} + +// GetUsageExpireDateTimeOk returns a tuple with the UsageExpireDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetUsageExpireDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.UsageExpireDateTime) { + return nil, false + } + return o.UsageExpireDateTime, true +} + +// HasUsageExpireDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasUsageExpireDateTime() bool { + if o != nil && !IsNil(o.UsageExpireDateTime) { + return true + } + + return false +} + +// SetUsageExpireDateTime gets a reference to the given int64 and assigns it to the UsageExpireDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetUsageExpireDateTime(v int64) { + o.UsageExpireDateTime = &v +} + +// GetUserAuthType returns the UserAuthType field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetUserAuthType() int64 { + if o == nil || IsNil(o.UserAuthType) { + var ret int64 + return ret + } + return *o.UserAuthType +} + +// GetUserAuthTypeOk returns a tuple with the UserAuthType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetUserAuthTypeOk() (*int64, bool) { + if o == nil || IsNil(o.UserAuthType) { + return nil, false + } + return o.UserAuthType, true +} + +// HasUserAuthType returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasUserAuthType() bool { + if o != nil && !IsNil(o.UserAuthType) { + return true + } + + return false +} + +// SetUserAuthType gets a reference to the given int64 and assigns it to the UserAuthType field. +func (o *DeviceAuthnAndroidAuthorizationList) SetUserAuthType(v int64) { + o.UserAuthType = &v +} + +// GetVendorPatchLevel returns the VendorPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetVendorPatchLevel() int64 { + if o == nil || IsNil(o.VendorPatchLevel) { + var ret int64 + return ret + } + return *o.VendorPatchLevel +} + +// GetVendorPatchLevelOk returns a tuple with the VendorPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetVendorPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.VendorPatchLevel) { + return nil, false + } + return o.VendorPatchLevel, true +} + +// HasVendorPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasVendorPatchLevel() bool { + if o != nil && !IsNil(o.VendorPatchLevel) { + return true + } + + return false +} + +// SetVendorPatchLevel gets a reference to the given int64 and assigns it to the VendorPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetVendorPatchLevel(v int64) { + o.VendorPatchLevel = &v +} + +func (o DeviceAuthnAndroidAuthorizationList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidAuthorizationList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ActiveDateTime) { + toSerialize["active_date_time"] = o.ActiveDateTime + } + if !IsNil(o.Algorithm) { + toSerialize["algorithm"] = o.Algorithm + } + if !IsNil(o.ApplicationId) { + toSerialize["application_id"] = o.ApplicationId + } + if !IsNil(o.AttestationApplicationId) { + toSerialize["attestation_application_id"] = o.AttestationApplicationId + } + if !IsNil(o.AttestationIdBrand) { + toSerialize["attestation_id_brand"] = o.AttestationIdBrand + } + if !IsNil(o.AttestationIdDevice) { + toSerialize["attestation_id_device"] = o.AttestationIdDevice + } + if !IsNil(o.AttestationIdImei) { + toSerialize["attestation_id_imei"] = o.AttestationIdImei + } + if !IsNil(o.AttestationIdManufacturer) { + toSerialize["attestation_id_manufacturer"] = o.AttestationIdManufacturer + } + if !IsNil(o.AttestationIdMeid) { + toSerialize["attestation_id_meid"] = o.AttestationIdMeid + } + if !IsNil(o.AttestationIdModel) { + toSerialize["attestation_id_model"] = o.AttestationIdModel + } + if !IsNil(o.AttestationIdProduct) { + toSerialize["attestation_id_product"] = o.AttestationIdProduct + } + if !IsNil(o.AttestationIdSerial) { + toSerialize["attestation_id_serial"] = o.AttestationIdSerial + } + if !IsNil(o.AuthTimeout) { + toSerialize["auth_timeout"] = o.AuthTimeout + } + if !IsNil(o.BootPatchLevel) { + toSerialize["boot_patch_level"] = o.BootPatchLevel + } + if !IsNil(o.CreationDateTime) { + toSerialize["creation_date_time"] = o.CreationDateTime + } + if !IsNil(o.Digest) { + toSerialize["digest"] = o.Digest + } + if !IsNil(o.EcCurve) { + toSerialize["ec_curve"] = o.EcCurve + } + if !IsNil(o.KeySize) { + toSerialize["key_size"] = o.KeySize + } + if !IsNil(o.Origin) { + toSerialize["origin"] = o.Origin + } + if !IsNil(o.OriginationExpireDateTime) { + toSerialize["origination_expire_date_time"] = o.OriginationExpireDateTime + } + if !IsNil(o.OsPatchLevel) { + toSerialize["os_patch_level"] = o.OsPatchLevel + } + if !IsNil(o.OsVersion) { + toSerialize["os_version"] = o.OsVersion + } + if !IsNil(o.Purpose) { + toSerialize["purpose"] = o.Purpose + } + if !IsNil(o.RootOfTrust) { + toSerialize["root_of_trust"] = o.RootOfTrust + } + if !IsNil(o.UsageExpireDateTime) { + toSerialize["usage_expire_date_time"] = o.UsageExpireDateTime + } + if !IsNil(o.UserAuthType) { + toSerialize["user_auth_type"] = o.UserAuthType + } + if !IsNil(o.VendorPatchLevel) { + toSerialize["vendor_patch_level"] = o.VendorPatchLevel + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidAuthorizationList) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidAuthorizationList := _DeviceAuthnAndroidAuthorizationList{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidAuthorizationList) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidAuthorizationList(varDeviceAuthnAndroidAuthorizationList) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active_date_time") + delete(additionalProperties, "algorithm") + delete(additionalProperties, "application_id") + delete(additionalProperties, "attestation_application_id") + delete(additionalProperties, "attestation_id_brand") + delete(additionalProperties, "attestation_id_device") + delete(additionalProperties, "attestation_id_imei") + delete(additionalProperties, "attestation_id_manufacturer") + delete(additionalProperties, "attestation_id_meid") + delete(additionalProperties, "attestation_id_model") + delete(additionalProperties, "attestation_id_product") + delete(additionalProperties, "attestation_id_serial") + delete(additionalProperties, "auth_timeout") + delete(additionalProperties, "boot_patch_level") + delete(additionalProperties, "creation_date_time") + delete(additionalProperties, "digest") + delete(additionalProperties, "ec_curve") + delete(additionalProperties, "key_size") + delete(additionalProperties, "origin") + delete(additionalProperties, "origination_expire_date_time") + delete(additionalProperties, "os_patch_level") + delete(additionalProperties, "os_version") + delete(additionalProperties, "purpose") + delete(additionalProperties, "root_of_trust") + delete(additionalProperties, "usage_expire_date_time") + delete(additionalProperties, "user_auth_type") + delete(additionalProperties, "vendor_patch_level") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidAuthorizationList struct { + value *DeviceAuthnAndroidAuthorizationList + isSet bool +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) Get() *DeviceAuthnAndroidAuthorizationList { + return v.value +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) Set(val *DeviceAuthnAndroidAuthorizationList) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidAuthorizationList(val *DeviceAuthnAndroidAuthorizationList) *NullableDeviceAuthnAndroidAuthorizationList { + return &NullableDeviceAuthnAndroidAuthorizationList{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_android_key_description.go b/pkg/client-go/model_device_authn_android_key_description.go new file mode 100644 index 0000000..5f55985 --- /dev/null +++ b/pkg/client-go/model_device_authn_android_key_description.go @@ -0,0 +1,339 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidKeyDescription type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidKeyDescription{} + +// DeviceAuthnAndroidKeyDescription Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +type DeviceAuthnAndroidKeyDescription struct { + AttestationSecurityLevel *int64 `json:"attestation_security_level,omitempty"` + AttestationVersion *int64 `json:"attestation_version,omitempty"` + KeymasterSecurityLevel *int64 `json:"keymaster_security_level,omitempty"` + KeymasterVersion *int64 `json:"keymaster_version,omitempty"` + SoftwareEnforced *DeviceAuthnAndroidAuthorizationList `json:"software_enforced,omitempty"` + TeeEnforced *DeviceAuthnAndroidAuthorizationList `json:"tee_enforced,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidKeyDescription DeviceAuthnAndroidKeyDescription + +// NewDeviceAuthnAndroidKeyDescription instantiates a new DeviceAuthnAndroidKeyDescription 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 NewDeviceAuthnAndroidKeyDescription() *DeviceAuthnAndroidKeyDescription { + this := DeviceAuthnAndroidKeyDescription{} + return &this +} + +// NewDeviceAuthnAndroidKeyDescriptionWithDefaults instantiates a new DeviceAuthnAndroidKeyDescription 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 NewDeviceAuthnAndroidKeyDescriptionWithDefaults() *DeviceAuthnAndroidKeyDescription { + this := DeviceAuthnAndroidKeyDescription{} + return &this +} + +// GetAttestationSecurityLevel returns the AttestationSecurityLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationSecurityLevel() int64 { + if o == nil || IsNil(o.AttestationSecurityLevel) { + var ret int64 + return ret + } + return *o.AttestationSecurityLevel +} + +// GetAttestationSecurityLevelOk returns a tuple with the AttestationSecurityLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationSecurityLevelOk() (*int64, bool) { + if o == nil || IsNil(o.AttestationSecurityLevel) { + return nil, false + } + return o.AttestationSecurityLevel, true +} + +// HasAttestationSecurityLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasAttestationSecurityLevel() bool { + if o != nil && !IsNil(o.AttestationSecurityLevel) { + return true + } + + return false +} + +// SetAttestationSecurityLevel gets a reference to the given int64 and assigns it to the AttestationSecurityLevel field. +func (o *DeviceAuthnAndroidKeyDescription) SetAttestationSecurityLevel(v int64) { + o.AttestationSecurityLevel = &v +} + +// GetAttestationVersion returns the AttestationVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationVersion() int64 { + if o == nil || IsNil(o.AttestationVersion) { + var ret int64 + return ret + } + return *o.AttestationVersion +} + +// GetAttestationVersionOk returns a tuple with the AttestationVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationVersionOk() (*int64, bool) { + if o == nil || IsNil(o.AttestationVersion) { + return nil, false + } + return o.AttestationVersion, true +} + +// HasAttestationVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasAttestationVersion() bool { + if o != nil && !IsNil(o.AttestationVersion) { + return true + } + + return false +} + +// SetAttestationVersion gets a reference to the given int64 and assigns it to the AttestationVersion field. +func (o *DeviceAuthnAndroidKeyDescription) SetAttestationVersion(v int64) { + o.AttestationVersion = &v +} + +// GetKeymasterSecurityLevel returns the KeymasterSecurityLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterSecurityLevel() int64 { + if o == nil || IsNil(o.KeymasterSecurityLevel) { + var ret int64 + return ret + } + return *o.KeymasterSecurityLevel +} + +// GetKeymasterSecurityLevelOk returns a tuple with the KeymasterSecurityLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterSecurityLevelOk() (*int64, bool) { + if o == nil || IsNil(o.KeymasterSecurityLevel) { + return nil, false + } + return o.KeymasterSecurityLevel, true +} + +// HasKeymasterSecurityLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasKeymasterSecurityLevel() bool { + if o != nil && !IsNil(o.KeymasterSecurityLevel) { + return true + } + + return false +} + +// SetKeymasterSecurityLevel gets a reference to the given int64 and assigns it to the KeymasterSecurityLevel field. +func (o *DeviceAuthnAndroidKeyDescription) SetKeymasterSecurityLevel(v int64) { + o.KeymasterSecurityLevel = &v +} + +// GetKeymasterVersion returns the KeymasterVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterVersion() int64 { + if o == nil || IsNil(o.KeymasterVersion) { + var ret int64 + return ret + } + return *o.KeymasterVersion +} + +// GetKeymasterVersionOk returns a tuple with the KeymasterVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterVersionOk() (*int64, bool) { + if o == nil || IsNil(o.KeymasterVersion) { + return nil, false + } + return o.KeymasterVersion, true +} + +// HasKeymasterVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasKeymasterVersion() bool { + if o != nil && !IsNil(o.KeymasterVersion) { + return true + } + + return false +} + +// SetKeymasterVersion gets a reference to the given int64 and assigns it to the KeymasterVersion field. +func (o *DeviceAuthnAndroidKeyDescription) SetKeymasterVersion(v int64) { + o.KeymasterVersion = &v +} + +// GetSoftwareEnforced returns the SoftwareEnforced field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetSoftwareEnforced() DeviceAuthnAndroidAuthorizationList { + if o == nil || IsNil(o.SoftwareEnforced) { + var ret DeviceAuthnAndroidAuthorizationList + return ret + } + return *o.SoftwareEnforced +} + +// GetSoftwareEnforcedOk returns a tuple with the SoftwareEnforced field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetSoftwareEnforcedOk() (*DeviceAuthnAndroidAuthorizationList, bool) { + if o == nil || IsNil(o.SoftwareEnforced) { + return nil, false + } + return o.SoftwareEnforced, true +} + +// HasSoftwareEnforced returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasSoftwareEnforced() bool { + if o != nil && !IsNil(o.SoftwareEnforced) { + return true + } + + return false +} + +// SetSoftwareEnforced gets a reference to the given DeviceAuthnAndroidAuthorizationList and assigns it to the SoftwareEnforced field. +func (o *DeviceAuthnAndroidKeyDescription) SetSoftwareEnforced(v DeviceAuthnAndroidAuthorizationList) { + o.SoftwareEnforced = &v +} + +// GetTeeEnforced returns the TeeEnforced field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetTeeEnforced() DeviceAuthnAndroidAuthorizationList { + if o == nil || IsNil(o.TeeEnforced) { + var ret DeviceAuthnAndroidAuthorizationList + return ret + } + return *o.TeeEnforced +} + +// GetTeeEnforcedOk returns a tuple with the TeeEnforced field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetTeeEnforcedOk() (*DeviceAuthnAndroidAuthorizationList, bool) { + if o == nil || IsNil(o.TeeEnforced) { + return nil, false + } + return o.TeeEnforced, true +} + +// HasTeeEnforced returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasTeeEnforced() bool { + if o != nil && !IsNil(o.TeeEnforced) { + return true + } + + return false +} + +// SetTeeEnforced gets a reference to the given DeviceAuthnAndroidAuthorizationList and assigns it to the TeeEnforced field. +func (o *DeviceAuthnAndroidKeyDescription) SetTeeEnforced(v DeviceAuthnAndroidAuthorizationList) { + o.TeeEnforced = &v +} + +func (o DeviceAuthnAndroidKeyDescription) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidKeyDescription) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AttestationSecurityLevel) { + toSerialize["attestation_security_level"] = o.AttestationSecurityLevel + } + if !IsNil(o.AttestationVersion) { + toSerialize["attestation_version"] = o.AttestationVersion + } + if !IsNil(o.KeymasterSecurityLevel) { + toSerialize["keymaster_security_level"] = o.KeymasterSecurityLevel + } + if !IsNil(o.KeymasterVersion) { + toSerialize["keymaster_version"] = o.KeymasterVersion + } + if !IsNil(o.SoftwareEnforced) { + toSerialize["software_enforced"] = o.SoftwareEnforced + } + if !IsNil(o.TeeEnforced) { + toSerialize["tee_enforced"] = o.TeeEnforced + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidKeyDescription) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidKeyDescription := _DeviceAuthnAndroidKeyDescription{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidKeyDescription) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidKeyDescription(varDeviceAuthnAndroidKeyDescription) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attestation_security_level") + delete(additionalProperties, "attestation_version") + delete(additionalProperties, "keymaster_security_level") + delete(additionalProperties, "keymaster_version") + delete(additionalProperties, "software_enforced") + delete(additionalProperties, "tee_enforced") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidKeyDescription struct { + value *DeviceAuthnAndroidKeyDescription + isSet bool +} + +func (v NullableDeviceAuthnAndroidKeyDescription) Get() *DeviceAuthnAndroidKeyDescription { + return v.value +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) Set(val *DeviceAuthnAndroidKeyDescription) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidKeyDescription) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidKeyDescription(val *DeviceAuthnAndroidKeyDescription) *NullableDeviceAuthnAndroidKeyDescription { + return &NullableDeviceAuthnAndroidKeyDescription{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidKeyDescription) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_android_root_of_trust.go b/pkg/client-go/model_device_authn_android_root_of_trust.go new file mode 100644 index 0000000..d462bf2 --- /dev/null +++ b/pkg/client-go/model_device_authn_android_root_of_trust.go @@ -0,0 +1,265 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidRootOfTrust type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidRootOfTrust{} + +// DeviceAuthnAndroidRootOfTrust struct for DeviceAuthnAndroidRootOfTrust +type DeviceAuthnAndroidRootOfTrust struct { + DeviceLocked *bool `json:"device_locked,omitempty"` + VerifiedBootHash []int32 `json:"verified_boot_hash,omitempty"` + VerifiedBootKey []int32 `json:"verified_boot_key,omitempty"` + VerifiedBootState *int64 `json:"verified_boot_state,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidRootOfTrust DeviceAuthnAndroidRootOfTrust + +// NewDeviceAuthnAndroidRootOfTrust instantiates a new DeviceAuthnAndroidRootOfTrust 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 NewDeviceAuthnAndroidRootOfTrust() *DeviceAuthnAndroidRootOfTrust { + this := DeviceAuthnAndroidRootOfTrust{} + return &this +} + +// NewDeviceAuthnAndroidRootOfTrustWithDefaults instantiates a new DeviceAuthnAndroidRootOfTrust 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 NewDeviceAuthnAndroidRootOfTrustWithDefaults() *DeviceAuthnAndroidRootOfTrust { + this := DeviceAuthnAndroidRootOfTrust{} + return &this +} + +// GetDeviceLocked returns the DeviceLocked field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetDeviceLocked() bool { + if o == nil || IsNil(o.DeviceLocked) { + var ret bool + return ret + } + return *o.DeviceLocked +} + +// GetDeviceLockedOk returns a tuple with the DeviceLocked field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetDeviceLockedOk() (*bool, bool) { + if o == nil || IsNil(o.DeviceLocked) { + return nil, false + } + return o.DeviceLocked, true +} + +// HasDeviceLocked returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasDeviceLocked() bool { + if o != nil && !IsNil(o.DeviceLocked) { + return true + } + + return false +} + +// SetDeviceLocked gets a reference to the given bool and assigns it to the DeviceLocked field. +func (o *DeviceAuthnAndroidRootOfTrust) SetDeviceLocked(v bool) { + o.DeviceLocked = &v +} + +// GetVerifiedBootHash returns the VerifiedBootHash field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootHash() []int32 { + if o == nil || IsNil(o.VerifiedBootHash) { + var ret []int32 + return ret + } + return o.VerifiedBootHash +} + +// GetVerifiedBootHashOk returns a tuple with the VerifiedBootHash field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootHashOk() ([]int32, bool) { + if o == nil || IsNil(o.VerifiedBootHash) { + return nil, false + } + return o.VerifiedBootHash, true +} + +// HasVerifiedBootHash returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootHash() bool { + if o != nil && !IsNil(o.VerifiedBootHash) { + return true + } + + return false +} + +// SetVerifiedBootHash gets a reference to the given []int32 and assigns it to the VerifiedBootHash field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootHash(v []int32) { + o.VerifiedBootHash = v +} + +// GetVerifiedBootKey returns the VerifiedBootKey field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootKey() []int32 { + if o == nil || IsNil(o.VerifiedBootKey) { + var ret []int32 + return ret + } + return o.VerifiedBootKey +} + +// GetVerifiedBootKeyOk returns a tuple with the VerifiedBootKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootKeyOk() ([]int32, bool) { + if o == nil || IsNil(o.VerifiedBootKey) { + return nil, false + } + return o.VerifiedBootKey, true +} + +// HasVerifiedBootKey returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootKey() bool { + if o != nil && !IsNil(o.VerifiedBootKey) { + return true + } + + return false +} + +// SetVerifiedBootKey gets a reference to the given []int32 and assigns it to the VerifiedBootKey field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootKey(v []int32) { + o.VerifiedBootKey = v +} + +// GetVerifiedBootState returns the VerifiedBootState field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootState() int64 { + if o == nil || IsNil(o.VerifiedBootState) { + var ret int64 + return ret + } + return *o.VerifiedBootState +} + +// GetVerifiedBootStateOk returns a tuple with the VerifiedBootState field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootStateOk() (*int64, bool) { + if o == nil || IsNil(o.VerifiedBootState) { + return nil, false + } + return o.VerifiedBootState, true +} + +// HasVerifiedBootState returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootState() bool { + if o != nil && !IsNil(o.VerifiedBootState) { + return true + } + + return false +} + +// SetVerifiedBootState gets a reference to the given int64 and assigns it to the VerifiedBootState field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootState(v int64) { + o.VerifiedBootState = &v +} + +func (o DeviceAuthnAndroidRootOfTrust) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidRootOfTrust) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DeviceLocked) { + toSerialize["device_locked"] = o.DeviceLocked + } + if !IsNil(o.VerifiedBootHash) { + toSerialize["verified_boot_hash"] = o.VerifiedBootHash + } + if !IsNil(o.VerifiedBootKey) { + toSerialize["verified_boot_key"] = o.VerifiedBootKey + } + if !IsNil(o.VerifiedBootState) { + toSerialize["verified_boot_state"] = o.VerifiedBootState + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidRootOfTrust) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidRootOfTrust := _DeviceAuthnAndroidRootOfTrust{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidRootOfTrust) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidRootOfTrust(varDeviceAuthnAndroidRootOfTrust) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "device_locked") + delete(additionalProperties, "verified_boot_hash") + delete(additionalProperties, "verified_boot_key") + delete(additionalProperties, "verified_boot_state") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidRootOfTrust struct { + value *DeviceAuthnAndroidRootOfTrust + isSet bool +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) Get() *DeviceAuthnAndroidRootOfTrust { + return v.value +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) Set(val *DeviceAuthnAndroidRootOfTrust) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidRootOfTrust(val *DeviceAuthnAndroidRootOfTrust) *NullableDeviceAuthnAndroidRootOfTrust { + return &NullableDeviceAuthnAndroidRootOfTrust{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_attestation.go b/pkg/client-go/model_device_authn_attestation.go new file mode 100644 index 0000000..5280065 --- /dev/null +++ b/pkg/client-go/model_device_authn_attestation.go @@ -0,0 +1,191 @@ +/* +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" +) + +// checks if the DeviceAuthnAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAttestation{} + +// DeviceAuthnAttestation Exactly one of `android` or `ios` is set, matching the key's device_type. +type DeviceAuthnAttestation struct { + Android *DeviceAuthnAndroidKeyDescription `json:"android,omitempty"` + Ios *DeviceAuthnIOSAttestation `json:"ios,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAttestation DeviceAuthnAttestation + +// NewDeviceAuthnAttestation instantiates a new DeviceAuthnAttestation 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 NewDeviceAuthnAttestation() *DeviceAuthnAttestation { + this := DeviceAuthnAttestation{} + return &this +} + +// NewDeviceAuthnAttestationWithDefaults instantiates a new DeviceAuthnAttestation 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 NewDeviceAuthnAttestationWithDefaults() *DeviceAuthnAttestation { + this := DeviceAuthnAttestation{} + return &this +} + +// GetAndroid returns the Android field value if set, zero value otherwise. +func (o *DeviceAuthnAttestation) GetAndroid() DeviceAuthnAndroidKeyDescription { + if o == nil || IsNil(o.Android) { + var ret DeviceAuthnAndroidKeyDescription + return ret + } + return *o.Android +} + +// GetAndroidOk returns a tuple with the Android field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAttestation) GetAndroidOk() (*DeviceAuthnAndroidKeyDescription, bool) { + if o == nil || IsNil(o.Android) { + return nil, false + } + return o.Android, true +} + +// HasAndroid returns a boolean if a field has been set. +func (o *DeviceAuthnAttestation) HasAndroid() bool { + if o != nil && !IsNil(o.Android) { + return true + } + + return false +} + +// SetAndroid gets a reference to the given DeviceAuthnAndroidKeyDescription and assigns it to the Android field. +func (o *DeviceAuthnAttestation) SetAndroid(v DeviceAuthnAndroidKeyDescription) { + o.Android = &v +} + +// GetIos returns the Ios field value if set, zero value otherwise. +func (o *DeviceAuthnAttestation) GetIos() DeviceAuthnIOSAttestation { + if o == nil || IsNil(o.Ios) { + var ret DeviceAuthnIOSAttestation + return ret + } + return *o.Ios +} + +// GetIosOk returns a tuple with the Ios field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAttestation) GetIosOk() (*DeviceAuthnIOSAttestation, bool) { + if o == nil || IsNil(o.Ios) { + return nil, false + } + return o.Ios, true +} + +// HasIos returns a boolean if a field has been set. +func (o *DeviceAuthnAttestation) HasIos() bool { + if o != nil && !IsNil(o.Ios) { + return true + } + + return false +} + +// SetIos gets a reference to the given DeviceAuthnIOSAttestation and assigns it to the Ios field. +func (o *DeviceAuthnAttestation) SetIos(v DeviceAuthnIOSAttestation) { + o.Ios = &v +} + +func (o DeviceAuthnAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Android) { + toSerialize["android"] = o.Android + } + if !IsNil(o.Ios) { + toSerialize["ios"] = o.Ios + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAttestation) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAttestation := _DeviceAuthnAttestation{} + + err = json.Unmarshal(data, &varDeviceAuthnAttestation) + + if err != nil { + return err + } + + *o = DeviceAuthnAttestation(varDeviceAuthnAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "android") + delete(additionalProperties, "ios") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAttestation struct { + value *DeviceAuthnAttestation + isSet bool +} + +func (v NullableDeviceAuthnAttestation) Get() *DeviceAuthnAttestation { + return v.value +} + +func (v *NullableDeviceAuthnAttestation) Set(val *DeviceAuthnAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAttestation(val *DeviceAuthnAttestation) *NullableDeviceAuthnAttestation { + return &NullableDeviceAuthnAttestation{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_ios_att_stmt.go b/pkg/client-go/model_device_authn_ios_att_stmt.go new file mode 100644 index 0000000..fb35706 --- /dev/null +++ b/pkg/client-go/model_device_authn_ios_att_stmt.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the DeviceAuthnIOSAttStmt type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnIOSAttStmt{} + +// DeviceAuthnIOSAttStmt Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +type DeviceAuthnIOSAttStmt struct { + // X5c is the leaf-first certificate chain encoded as base64(DER), per the JOSE convention defined in RFC 7515 §4.1.6. + X5c [][]int32 `json:"x5c,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnIOSAttStmt DeviceAuthnIOSAttStmt + +// NewDeviceAuthnIOSAttStmt instantiates a new DeviceAuthnIOSAttStmt 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 NewDeviceAuthnIOSAttStmt() *DeviceAuthnIOSAttStmt { + this := DeviceAuthnIOSAttStmt{} + return &this +} + +// NewDeviceAuthnIOSAttStmtWithDefaults instantiates a new DeviceAuthnIOSAttStmt 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 NewDeviceAuthnIOSAttStmtWithDefaults() *DeviceAuthnIOSAttStmt { + this := DeviceAuthnIOSAttStmt{} + return &this +} + +// GetX5c returns the X5c field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttStmt) GetX5c() [][]int32 { + if o == nil || IsNil(o.X5c) { + var ret [][]int32 + return ret + } + return o.X5c +} + +// GetX5cOk returns a tuple with the X5c field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttStmt) GetX5cOk() ([][]int32, bool) { + if o == nil || IsNil(o.X5c) { + return nil, false + } + return o.X5c, true +} + +// HasX5c returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttStmt) HasX5c() bool { + if o != nil && !IsNil(o.X5c) { + return true + } + + return false +} + +// SetX5c gets a reference to the given [][]int32 and assigns it to the X5c field. +func (o *DeviceAuthnIOSAttStmt) SetX5c(v [][]int32) { + o.X5c = v +} + +func (o DeviceAuthnIOSAttStmt) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnIOSAttStmt) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.X5c) { + toSerialize["x5c"] = o.X5c + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnIOSAttStmt) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnIOSAttStmt := _DeviceAuthnIOSAttStmt{} + + err = json.Unmarshal(data, &varDeviceAuthnIOSAttStmt) + + if err != nil { + return err + } + + *o = DeviceAuthnIOSAttStmt(varDeviceAuthnIOSAttStmt) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "x5c") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnIOSAttStmt struct { + value *DeviceAuthnIOSAttStmt + isSet bool +} + +func (v NullableDeviceAuthnIOSAttStmt) Get() *DeviceAuthnIOSAttStmt { + return v.value +} + +func (v *NullableDeviceAuthnIOSAttStmt) Set(val *DeviceAuthnIOSAttStmt) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnIOSAttStmt) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnIOSAttStmt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnIOSAttStmt(val *DeviceAuthnIOSAttStmt) *NullableDeviceAuthnIOSAttStmt { + return &NullableDeviceAuthnIOSAttStmt{value: val, isSet: true} +} + +func (v NullableDeviceAuthnIOSAttStmt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnIOSAttStmt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_ios_attestation.go b/pkg/client-go/model_device_authn_ios_attestation.go new file mode 100644 index 0000000..be826db --- /dev/null +++ b/pkg/client-go/model_device_authn_ios_attestation.go @@ -0,0 +1,228 @@ +/* +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" +) + +// checks if the DeviceAuthnIOSAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnIOSAttestation{} + +// DeviceAuthnIOSAttestation Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +type DeviceAuthnIOSAttestation struct { + AttStmt *DeviceAuthnIOSAttStmt `json:"att_stmt,omitempty"` + AuthData []int32 `json:"auth_data,omitempty"` + Fmt *string `json:"fmt,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnIOSAttestation DeviceAuthnIOSAttestation + +// NewDeviceAuthnIOSAttestation instantiates a new DeviceAuthnIOSAttestation 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 NewDeviceAuthnIOSAttestation() *DeviceAuthnIOSAttestation { + this := DeviceAuthnIOSAttestation{} + return &this +} + +// NewDeviceAuthnIOSAttestationWithDefaults instantiates a new DeviceAuthnIOSAttestation 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 NewDeviceAuthnIOSAttestationWithDefaults() *DeviceAuthnIOSAttestation { + this := DeviceAuthnIOSAttestation{} + return &this +} + +// GetAttStmt returns the AttStmt field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetAttStmt() DeviceAuthnIOSAttStmt { + if o == nil || IsNil(o.AttStmt) { + var ret DeviceAuthnIOSAttStmt + return ret + } + return *o.AttStmt +} + +// GetAttStmtOk returns a tuple with the AttStmt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetAttStmtOk() (*DeviceAuthnIOSAttStmt, bool) { + if o == nil || IsNil(o.AttStmt) { + return nil, false + } + return o.AttStmt, true +} + +// HasAttStmt returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasAttStmt() bool { + if o != nil && !IsNil(o.AttStmt) { + return true + } + + return false +} + +// SetAttStmt gets a reference to the given DeviceAuthnIOSAttStmt and assigns it to the AttStmt field. +func (o *DeviceAuthnIOSAttestation) SetAttStmt(v DeviceAuthnIOSAttStmt) { + o.AttStmt = &v +} + +// GetAuthData returns the AuthData field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetAuthData() []int32 { + if o == nil || IsNil(o.AuthData) { + var ret []int32 + return ret + } + return o.AuthData +} + +// GetAuthDataOk returns a tuple with the AuthData field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetAuthDataOk() ([]int32, bool) { + if o == nil || IsNil(o.AuthData) { + return nil, false + } + return o.AuthData, true +} + +// HasAuthData returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasAuthData() bool { + if o != nil && !IsNil(o.AuthData) { + return true + } + + return false +} + +// SetAuthData gets a reference to the given []int32 and assigns it to the AuthData field. +func (o *DeviceAuthnIOSAttestation) SetAuthData(v []int32) { + o.AuthData = v +} + +// GetFmt returns the Fmt field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetFmt() string { + if o == nil || IsNil(o.Fmt) { + var ret string + return ret + } + return *o.Fmt +} + +// GetFmtOk returns a tuple with the Fmt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetFmtOk() (*string, bool) { + if o == nil || IsNil(o.Fmt) { + return nil, false + } + return o.Fmt, true +} + +// HasFmt returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasFmt() bool { + if o != nil && !IsNil(o.Fmt) { + return true + } + + return false +} + +// SetFmt gets a reference to the given string and assigns it to the Fmt field. +func (o *DeviceAuthnIOSAttestation) SetFmt(v string) { + o.Fmt = &v +} + +func (o DeviceAuthnIOSAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnIOSAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AttStmt) { + toSerialize["att_stmt"] = o.AttStmt + } + if !IsNil(o.AuthData) { + toSerialize["auth_data"] = o.AuthData + } + if !IsNil(o.Fmt) { + toSerialize["fmt"] = o.Fmt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnIOSAttestation) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnIOSAttestation := _DeviceAuthnIOSAttestation{} + + err = json.Unmarshal(data, &varDeviceAuthnIOSAttestation) + + if err != nil { + return err + } + + *o = DeviceAuthnIOSAttestation(varDeviceAuthnIOSAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "att_stmt") + delete(additionalProperties, "auth_data") + delete(additionalProperties, "fmt") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnIOSAttestation struct { + value *DeviceAuthnIOSAttestation + isSet bool +} + +func (v NullableDeviceAuthnIOSAttestation) Get() *DeviceAuthnIOSAttestation { + return v.value +} + +func (v *NullableDeviceAuthnIOSAttestation) Set(val *DeviceAuthnIOSAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnIOSAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnIOSAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnIOSAttestation(val *DeviceAuthnIOSAttestation) *NullableDeviceAuthnIOSAttestation { + return &NullableDeviceAuthnIOSAttestation{value: val, isSet: true} +} + +func (v NullableDeviceAuthnIOSAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnIOSAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_authn_key.go b/pkg/client-go/model_device_authn_key.go new file mode 100644 index 0000000..931f179 --- /dev/null +++ b/pkg/client-go/model_device_authn_key.go @@ -0,0 +1,531 @@ +/* +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" + "time" +) + +// checks if the DeviceAuthnKey type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnKey{} + +// DeviceAuthnKey Represents a hardware-backed signing key enrolled from a mobile device. The private key resides inside the device and never exists on the server. To list the identity's enrolled keys, fetch a settings flow: each key's remove button (a `ui.nodes` entry named `deviceauthn_remove` in group `deviceauthn`) carries the key, with its PIN state redacted, in the node label's `context`. +type DeviceAuthnKey struct { + Attestation *DeviceAuthnAttestation `json:"attestation,omitempty"` + // The key's stable id, unique per identity. Submit it as the `client_key_id` when logging in with the key, deleting it, or rotating its pin_secret. The device can also compute the id without reading it back from the server: it is the lowercase-hex SHA-256 of `public_key` (the key's PKIX, ASN.1 DER encoding). Keys enrolled before the server derived the id keep their original client-chosen value, so prefer reading this field over recomputing it for older keys. + ClientKeyId *string `json:"client_key_id,omitempty"` + // When the key was enrolled. Only used for troubleshooting and UI. + CreatedAt *time.Time `json:"created_at,omitempty"` + // A human-readable name for the device, helping the user tell this key apart from others. + DeviceName *string `json:"device_name,omitempty"` + DeviceType *DeviceType `json:"device_type,omitempty"` + Pin *PINConfig `json:"pin,omitempty"` + // The device's public key (an elliptic-curve key on P-224, P-256, P-384, or P-521 in version 1) in PKIX, ASN.1 DER (SubjectPublicKeyInfo) form, base64-encoded. Signatures are verified against this key. + PublicKey *string `json:"public_key,omitempty"` + // Set only when the key's attestation chain was accepted under relaxed rules (software roots, expired certificates, software security level) rather than strict hardware attestation. Such keys are refused at login after this time, or immediately once relaxed attestation is turned off. Absent for hardware-attested keys that pass strict validation. + RelaxedAttestationExpiresAt *time.Time `json:"relaxed_attestation_expires_at,omitempty"` + State *KeyState `json:"state,omitempty"` + UserVerification *UserVerification `json:"user_verification,omitempty"` + // The cryptography version of the key. Version 1 uses ECDSA with SHA-256 on an elliptic curve (P-224, P-256, P-384, or P-521); further versions are reserved for future signature suites. + Version *int64 `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnKey DeviceAuthnKey + +// NewDeviceAuthnKey instantiates a new DeviceAuthnKey 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 NewDeviceAuthnKey() *DeviceAuthnKey { + this := DeviceAuthnKey{} + return &this +} + +// NewDeviceAuthnKeyWithDefaults instantiates a new DeviceAuthnKey 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 NewDeviceAuthnKeyWithDefaults() *DeviceAuthnKey { + this := DeviceAuthnKey{} + return &this +} + +// GetAttestation returns the Attestation field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetAttestation() DeviceAuthnAttestation { + if o == nil || IsNil(o.Attestation) { + var ret DeviceAuthnAttestation + return ret + } + return *o.Attestation +} + +// GetAttestationOk returns a tuple with the Attestation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetAttestationOk() (*DeviceAuthnAttestation, bool) { + if o == nil || IsNil(o.Attestation) { + return nil, false + } + return o.Attestation, true +} + +// HasAttestation returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasAttestation() bool { + if o != nil && !IsNil(o.Attestation) { + return true + } + + return false +} + +// SetAttestation gets a reference to the given DeviceAuthnAttestation and assigns it to the Attestation field. +func (o *DeviceAuthnKey) SetAttestation(v DeviceAuthnAttestation) { + o.Attestation = &v +} + +// GetClientKeyId returns the ClientKeyId field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetClientKeyId() string { + if o == nil || IsNil(o.ClientKeyId) { + var ret string + return ret + } + return *o.ClientKeyId +} + +// GetClientKeyIdOk returns a tuple with the ClientKeyId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetClientKeyIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientKeyId) { + return nil, false + } + return o.ClientKeyId, true +} + +// HasClientKeyId returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasClientKeyId() bool { + if o != nil && !IsNil(o.ClientKeyId) { + return true + } + + return false +} + +// SetClientKeyId gets a reference to the given string and assigns it to the ClientKeyId field. +func (o *DeviceAuthnKey) SetClientKeyId(v string) { + o.ClientKeyId = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *DeviceAuthnKey) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetDeviceName returns the DeviceName field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetDeviceName() string { + if o == nil || IsNil(o.DeviceName) { + var ret string + return ret + } + return *o.DeviceName +} + +// GetDeviceNameOk returns a tuple with the DeviceName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetDeviceNameOk() (*string, bool) { + if o == nil || IsNil(o.DeviceName) { + return nil, false + } + return o.DeviceName, true +} + +// HasDeviceName returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasDeviceName() bool { + if o != nil && !IsNil(o.DeviceName) { + return true + } + + return false +} + +// SetDeviceName gets a reference to the given string and assigns it to the DeviceName field. +func (o *DeviceAuthnKey) SetDeviceName(v string) { + o.DeviceName = &v +} + +// GetDeviceType returns the DeviceType field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetDeviceType() DeviceType { + if o == nil || IsNil(o.DeviceType) { + var ret DeviceType + return ret + } + return *o.DeviceType +} + +// GetDeviceTypeOk returns a tuple with the DeviceType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetDeviceTypeOk() (*DeviceType, bool) { + if o == nil || IsNil(o.DeviceType) { + return nil, false + } + return o.DeviceType, true +} + +// HasDeviceType returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasDeviceType() bool { + if o != nil && !IsNil(o.DeviceType) { + return true + } + + return false +} + +// SetDeviceType gets a reference to the given DeviceType and assigns it to the DeviceType field. +func (o *DeviceAuthnKey) SetDeviceType(v DeviceType) { + o.DeviceType = &v +} + +// GetPin returns the Pin field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetPin() PINConfig { + if o == nil || IsNil(o.Pin) { + var ret PINConfig + return ret + } + return *o.Pin +} + +// GetPinOk returns a tuple with the Pin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetPinOk() (*PINConfig, bool) { + if o == nil || IsNil(o.Pin) { + return nil, false + } + return o.Pin, true +} + +// HasPin returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasPin() bool { + if o != nil && !IsNil(o.Pin) { + return true + } + + return false +} + +// SetPin gets a reference to the given PINConfig and assigns it to the Pin field. +func (o *DeviceAuthnKey) SetPin(v PINConfig) { + o.Pin = &v +} + +// GetPublicKey returns the PublicKey field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetPublicKey() string { + if o == nil || IsNil(o.PublicKey) { + var ret string + return ret + } + return *o.PublicKey +} + +// GetPublicKeyOk returns a tuple with the PublicKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetPublicKeyOk() (*string, bool) { + if o == nil || IsNil(o.PublicKey) { + return nil, false + } + return o.PublicKey, true +} + +// HasPublicKey returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasPublicKey() bool { + if o != nil && !IsNil(o.PublicKey) { + return true + } + + return false +} + +// SetPublicKey gets a reference to the given string and assigns it to the PublicKey field. +func (o *DeviceAuthnKey) SetPublicKey(v string) { + o.PublicKey = &v +} + +// GetRelaxedAttestationExpiresAt returns the RelaxedAttestationExpiresAt field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetRelaxedAttestationExpiresAt() time.Time { + if o == nil || IsNil(o.RelaxedAttestationExpiresAt) { + var ret time.Time + return ret + } + return *o.RelaxedAttestationExpiresAt +} + +// GetRelaxedAttestationExpiresAtOk returns a tuple with the RelaxedAttestationExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetRelaxedAttestationExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.RelaxedAttestationExpiresAt) { + return nil, false + } + return o.RelaxedAttestationExpiresAt, true +} + +// HasRelaxedAttestationExpiresAt returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasRelaxedAttestationExpiresAt() bool { + if o != nil && !IsNil(o.RelaxedAttestationExpiresAt) { + return true + } + + return false +} + +// SetRelaxedAttestationExpiresAt gets a reference to the given time.Time and assigns it to the RelaxedAttestationExpiresAt field. +func (o *DeviceAuthnKey) SetRelaxedAttestationExpiresAt(v time.Time) { + o.RelaxedAttestationExpiresAt = &v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetState() KeyState { + if o == nil || IsNil(o.State) { + var ret KeyState + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetStateOk() (*KeyState, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given KeyState and assigns it to the State field. +func (o *DeviceAuthnKey) SetState(v KeyState) { + o.State = &v +} + +// GetUserVerification returns the UserVerification field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetUserVerification() UserVerification { + if o == nil || IsNil(o.UserVerification) { + var ret UserVerification + return ret + } + return *o.UserVerification +} + +// GetUserVerificationOk returns a tuple with the UserVerification field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetUserVerificationOk() (*UserVerification, bool) { + if o == nil || IsNil(o.UserVerification) { + return nil, false + } + return o.UserVerification, true +} + +// HasUserVerification returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasUserVerification() bool { + if o != nil && !IsNil(o.UserVerification) { + return true + } + + return false +} + +// SetUserVerification gets a reference to the given UserVerification and assigns it to the UserVerification field. +func (o *DeviceAuthnKey) SetUserVerification(v UserVerification) { + o.UserVerification = &v +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetVersion() int64 { + if o == nil || IsNil(o.Version) { + var ret int64 + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetVersionOk() (*int64, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given int64 and assigns it to the Version field. +func (o *DeviceAuthnKey) SetVersion(v int64) { + o.Version = &v +} + +func (o DeviceAuthnKey) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnKey) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Attestation) { + toSerialize["attestation"] = o.Attestation + } + if !IsNil(o.ClientKeyId) { + toSerialize["client_key_id"] = o.ClientKeyId + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.DeviceName) { + toSerialize["device_name"] = o.DeviceName + } + if !IsNil(o.DeviceType) { + toSerialize["device_type"] = o.DeviceType + } + if !IsNil(o.Pin) { + toSerialize["pin"] = o.Pin + } + if !IsNil(o.PublicKey) { + toSerialize["public_key"] = o.PublicKey + } + if !IsNil(o.RelaxedAttestationExpiresAt) { + toSerialize["relaxed_attestation_expires_at"] = o.RelaxedAttestationExpiresAt + } + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + if !IsNil(o.UserVerification) { + toSerialize["user_verification"] = o.UserVerification + } + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnKey) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnKey := _DeviceAuthnKey{} + + err = json.Unmarshal(data, &varDeviceAuthnKey) + + if err != nil { + return err + } + + *o = DeviceAuthnKey(varDeviceAuthnKey) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attestation") + delete(additionalProperties, "client_key_id") + delete(additionalProperties, "created_at") + delete(additionalProperties, "device_name") + delete(additionalProperties, "device_type") + delete(additionalProperties, "pin") + delete(additionalProperties, "public_key") + delete(additionalProperties, "relaxed_attestation_expires_at") + delete(additionalProperties, "state") + delete(additionalProperties, "user_verification") + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnKey struct { + value *DeviceAuthnKey + isSet bool +} + +func (v NullableDeviceAuthnKey) Get() *DeviceAuthnKey { + return v.value +} + +func (v *NullableDeviceAuthnKey) Set(val *DeviceAuthnKey) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnKey) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnKey) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnKey(val *DeviceAuthnKey) *NullableDeviceAuthnKey { + return &NullableDeviceAuthnKey{value: val, isSet: true} +} + +func (v NullableDeviceAuthnKey) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnKey) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_device_type.go b/pkg/client-go/model_device_type.go new file mode 100644 index 0000000..e00804f --- /dev/null +++ b/pkg/client-go/model_device_type.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// DeviceType \"Android\" and \"iOS\" use different attestation and signature encodings. +type DeviceType string + +// List of DeviceType +const ( + DEVICETYPE_ANDROID DeviceType = "Android" + DEVICETYPE_IOS DeviceType = "iOS" +) + +// All allowed values of DeviceType enum +var AllowedDeviceTypeEnumValues = []DeviceType{ + "Android", + "iOS", +} + +func (v *DeviceType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := DeviceType(value) + for _, existing := range AllowedDeviceTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid DeviceType", value) +} + +// NewDeviceTypeFromValue returns a pointer to a valid DeviceType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewDeviceTypeFromValue(v string) (*DeviceType, error) { + ev := DeviceType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for DeviceType: valid values are %v", v, AllowedDeviceTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v DeviceType) IsValid() bool { + for _, existing := range AllowedDeviceTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to DeviceType value +func (v DeviceType) Ptr() *DeviceType { + return &v +} + +type NullableDeviceType struct { + value *DeviceType + isSet bool +} + +func (v NullableDeviceType) Get() *DeviceType { + return v.value +} + +func (v *NullableDeviceType) Set(val *DeviceType) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceType) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceType(val *DeviceType) *NullableDeviceType { + return &NullableDeviceType{value: val, isSet: true} +} + +func (v NullableDeviceType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_error_authenticator_assurance_level_not_satisfied.go b/pkg/client-go/model_error_authenticator_assurance_level_not_satisfied.go new file mode 100644 index 0000000..62f965a --- /dev/null +++ b/pkg/client-go/model_error_authenticator_assurance_level_not_satisfied.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorAuthenticatorAssuranceLevelNotSatisfied type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorAuthenticatorAssuranceLevelNotSatisfied{} + +// ErrorAuthenticatorAssuranceLevelNotSatisfied struct for ErrorAuthenticatorAssuranceLevelNotSatisfied +type ErrorAuthenticatorAssuranceLevelNotSatisfied struct { + Error *GenericError `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorAuthenticatorAssuranceLevelNotSatisfied ErrorAuthenticatorAssuranceLevelNotSatisfied + +// NewErrorAuthenticatorAssuranceLevelNotSatisfied instantiates a new ErrorAuthenticatorAssuranceLevelNotSatisfied 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 NewErrorAuthenticatorAssuranceLevelNotSatisfied() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + this := ErrorAuthenticatorAssuranceLevelNotSatisfied{} + return &this +} + +// NewErrorAuthenticatorAssuranceLevelNotSatisfiedWithDefaults instantiates a new ErrorAuthenticatorAssuranceLevelNotSatisfied 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 NewErrorAuthenticatorAssuranceLevelNotSatisfiedWithDefaults() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + this := ErrorAuthenticatorAssuranceLevelNotSatisfied{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) SetError(v GenericError) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetRedirectBrowserTo() string { + if o == nil || IsNil(o.RedirectBrowserTo) { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || IsNil(o.RedirectBrowserTo) { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) HasRedirectBrowserTo() bool { + if o != nil && !IsNil(o.RedirectBrowserTo) { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ErrorAuthenticatorAssuranceLevelNotSatisfied) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorAuthenticatorAssuranceLevelNotSatisfied) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.RedirectBrowserTo) { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) UnmarshalJSON(data []byte) (err error) { + varErrorAuthenticatorAssuranceLevelNotSatisfied := _ErrorAuthenticatorAssuranceLevelNotSatisfied{} + + err = json.Unmarshal(data, &varErrorAuthenticatorAssuranceLevelNotSatisfied) + + if err != nil { + return err + } + + *o = ErrorAuthenticatorAssuranceLevelNotSatisfied(varErrorAuthenticatorAssuranceLevelNotSatisfied) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorAuthenticatorAssuranceLevelNotSatisfied struct { + value *ErrorAuthenticatorAssuranceLevelNotSatisfied + isSet bool +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Get() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + return v.value +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Set(val *ErrorAuthenticatorAssuranceLevelNotSatisfied) { + v.value = val + v.isSet = true +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorAuthenticatorAssuranceLevelNotSatisfied(val *ErrorAuthenticatorAssuranceLevelNotSatisfied) *NullableErrorAuthenticatorAssuranceLevelNotSatisfied { + return &NullableErrorAuthenticatorAssuranceLevelNotSatisfied{value: val, isSet: true} +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_error_browser_location_change_required.go b/pkg/client-go/model_error_browser_location_change_required.go new file mode 100644 index 0000000..b048cf6 --- /dev/null +++ b/pkg/client-go/model_error_browser_location_change_required.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorBrowserLocationChangeRequired type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorBrowserLocationChangeRequired{} + +// ErrorBrowserLocationChangeRequired struct for ErrorBrowserLocationChangeRequired +type ErrorBrowserLocationChangeRequired struct { + Error *ErrorGeneric `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorBrowserLocationChangeRequired ErrorBrowserLocationChangeRequired + +// NewErrorBrowserLocationChangeRequired instantiates a new ErrorBrowserLocationChangeRequired 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 NewErrorBrowserLocationChangeRequired() *ErrorBrowserLocationChangeRequired { + this := ErrorBrowserLocationChangeRequired{} + return &this +} + +// NewErrorBrowserLocationChangeRequiredWithDefaults instantiates a new ErrorBrowserLocationChangeRequired 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 NewErrorBrowserLocationChangeRequiredWithDefaults() *ErrorBrowserLocationChangeRequired { + this := ErrorBrowserLocationChangeRequired{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorBrowserLocationChangeRequired) GetError() ErrorGeneric { + if o == nil || IsNil(o.Error) { + var ret ErrorGeneric + 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. +func (o *ErrorBrowserLocationChangeRequired) GetErrorOk() (*ErrorGeneric, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorBrowserLocationChangeRequired) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given ErrorGeneric and assigns it to the Error field. +func (o *ErrorBrowserLocationChangeRequired) SetError(v ErrorGeneric) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ErrorBrowserLocationChangeRequired) GetRedirectBrowserTo() string { + if o == nil || IsNil(o.RedirectBrowserTo) { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorBrowserLocationChangeRequired) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || IsNil(o.RedirectBrowserTo) { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ErrorBrowserLocationChangeRequired) HasRedirectBrowserTo() bool { + if o != nil && !IsNil(o.RedirectBrowserTo) { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ErrorBrowserLocationChangeRequired) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ErrorBrowserLocationChangeRequired) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorBrowserLocationChangeRequired) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.RedirectBrowserTo) { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorBrowserLocationChangeRequired) UnmarshalJSON(data []byte) (err error) { + varErrorBrowserLocationChangeRequired := _ErrorBrowserLocationChangeRequired{} + + err = json.Unmarshal(data, &varErrorBrowserLocationChangeRequired) + + if err != nil { + return err + } + + *o = ErrorBrowserLocationChangeRequired(varErrorBrowserLocationChangeRequired) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorBrowserLocationChangeRequired struct { + value *ErrorBrowserLocationChangeRequired + isSet bool +} + +func (v NullableErrorBrowserLocationChangeRequired) Get() *ErrorBrowserLocationChangeRequired { + return v.value +} + +func (v *NullableErrorBrowserLocationChangeRequired) Set(val *ErrorBrowserLocationChangeRequired) { + v.value = val + v.isSet = true +} + +func (v NullableErrorBrowserLocationChangeRequired) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorBrowserLocationChangeRequired) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorBrowserLocationChangeRequired(val *ErrorBrowserLocationChangeRequired) *NullableErrorBrowserLocationChangeRequired { + return &NullableErrorBrowserLocationChangeRequired{value: val, isSet: true} +} + +func (v NullableErrorBrowserLocationChangeRequired) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorBrowserLocationChangeRequired) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_error_flow_replaced.go b/pkg/client-go/model_error_flow_replaced.go new file mode 100644 index 0000000..bf6d84d --- /dev/null +++ b/pkg/client-go/model_error_flow_replaced.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorFlowReplaced type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorFlowReplaced{} + +// ErrorFlowReplaced Is sent when a flow is replaced by a different flow of the same class +type ErrorFlowReplaced struct { + Error *GenericError `json:"error,omitempty"` + // The flow ID that should be used for the new flow as it contains the correct messages. + UseFlowId *string `json:"use_flow_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorFlowReplaced ErrorFlowReplaced + +// NewErrorFlowReplaced instantiates a new ErrorFlowReplaced 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 NewErrorFlowReplaced() *ErrorFlowReplaced { + this := ErrorFlowReplaced{} + return &this +} + +// NewErrorFlowReplacedWithDefaults instantiates a new ErrorFlowReplaced 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 NewErrorFlowReplacedWithDefaults() *ErrorFlowReplaced { + this := ErrorFlowReplaced{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorFlowReplaced) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *ErrorFlowReplaced) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorFlowReplaced) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *ErrorFlowReplaced) SetError(v GenericError) { + o.Error = &v +} + +// GetUseFlowId returns the UseFlowId field value if set, zero value otherwise. +func (o *ErrorFlowReplaced) GetUseFlowId() string { + if o == nil || IsNil(o.UseFlowId) { + var ret string + return ret + } + return *o.UseFlowId +} + +// GetUseFlowIdOk returns a tuple with the UseFlowId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorFlowReplaced) GetUseFlowIdOk() (*string, bool) { + if o == nil || IsNil(o.UseFlowId) { + return nil, false + } + return o.UseFlowId, true +} + +// HasUseFlowId returns a boolean if a field has been set. +func (o *ErrorFlowReplaced) HasUseFlowId() bool { + if o != nil && !IsNil(o.UseFlowId) { + return true + } + + return false +} + +// SetUseFlowId gets a reference to the given string and assigns it to the UseFlowId field. +func (o *ErrorFlowReplaced) SetUseFlowId(v string) { + o.UseFlowId = &v +} + +func (o ErrorFlowReplaced) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorFlowReplaced) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.UseFlowId) { + toSerialize["use_flow_id"] = o.UseFlowId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorFlowReplaced) UnmarshalJSON(data []byte) (err error) { + varErrorFlowReplaced := _ErrorFlowReplaced{} + + err = json.Unmarshal(data, &varErrorFlowReplaced) + + if err != nil { + return err + } + + *o = ErrorFlowReplaced(varErrorFlowReplaced) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "use_flow_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorFlowReplaced struct { + value *ErrorFlowReplaced + isSet bool +} + +func (v NullableErrorFlowReplaced) Get() *ErrorFlowReplaced { + return v.value +} + +func (v *NullableErrorFlowReplaced) Set(val *ErrorFlowReplaced) { + v.value = val + v.isSet = true +} + +func (v NullableErrorFlowReplaced) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorFlowReplaced) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorFlowReplaced(val *ErrorFlowReplaced) *NullableErrorFlowReplaced { + return &NullableErrorFlowReplaced{value: val, isSet: true} +} + +func (v NullableErrorFlowReplaced) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorFlowReplaced) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_error_generic.go b/pkg/client-go/model_error_generic.go new file mode 100644 index 0000000..3484d04 --- /dev/null +++ b/pkg/client-go/model_error_generic.go @@ -0,0 +1,167 @@ +/* +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" + "fmt" +) + +// checks if the ErrorGeneric type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorGeneric{} + +// ErrorGeneric The standard Ory JSON API error format. +type ErrorGeneric struct { + Error GenericError `json:"error"` + AdditionalProperties map[string]interface{} +} + +type _ErrorGeneric ErrorGeneric + +// NewErrorGeneric instantiates a new ErrorGeneric 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 NewErrorGeneric(error_ GenericError) *ErrorGeneric { + this := ErrorGeneric{} + this.Error = error_ + return &this +} + +// NewErrorGenericWithDefaults instantiates a new ErrorGeneric 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 NewErrorGenericWithDefaults() *ErrorGeneric { + this := ErrorGeneric{} + return &this +} + +// GetError returns the Error field value +func (o *ErrorGeneric) GetError() GenericError { + if o == nil { + var ret GenericError + return ret + } + + return o.Error +} + +// GetErrorOk returns a tuple with the Error field value +// and a boolean to check if the value has been set. +func (o *ErrorGeneric) GetErrorOk() (*GenericError, bool) { + if o == nil { + return nil, false + } + return &o.Error, true +} + +// SetError sets field value +func (o *ErrorGeneric) SetError(v GenericError) { + o.Error = v +} + +func (o ErrorGeneric) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorGeneric) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["error"] = o.Error + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorGeneric) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "error", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varErrorGeneric := _ErrorGeneric{} + + err = json.Unmarshal(data, &varErrorGeneric) + + if err != nil { + return err + } + + *o = ErrorGeneric(varErrorGeneric) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorGeneric struct { + value *ErrorGeneric + isSet bool +} + +func (v NullableErrorGeneric) Get() *ErrorGeneric { + return v.value +} + +func (v *NullableErrorGeneric) Set(val *ErrorGeneric) { + v.value = val + v.isSet = true +} + +func (v NullableErrorGeneric) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorGeneric) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorGeneric(val *ErrorGeneric) *NullableErrorGeneric { + return &NullableErrorGeneric{value: val, isSet: true} +} + +func (v NullableErrorGeneric) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorGeneric) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_flow_error.go b/pkg/client-go/model_flow_error.go new file mode 100644 index 0000000..8e755ea --- /dev/null +++ b/pkg/client-go/model_flow_error.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" + "time" +) + +// checks if the FlowError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &FlowError{} + +// FlowError struct for FlowError +type FlowError struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + Error map[string]interface{} `json:"error,omitempty"` + // ID of the error container. + Id string `json:"id"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _FlowError FlowError + +// NewFlowError instantiates a new FlowError 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 NewFlowError(id string) *FlowError { + this := FlowError{} + this.Id = id + return &this +} + +// NewFlowErrorWithDefaults instantiates a new FlowError 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 NewFlowErrorWithDefaults() *FlowError { + this := FlowError{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *FlowError) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FlowError) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *FlowError) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *FlowError) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *FlowError) GetError() map[string]interface{} { + if o == nil || IsNil(o.Error) { + var ret map[string]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. +func (o *FlowError) GetErrorOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Error) { + return map[string]interface{}{}, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *FlowError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given map[string]interface{} and assigns it to the Error field. +func (o *FlowError) SetError(v map[string]interface{}) { + o.Error = v +} + +// GetId returns the Id field value +func (o *FlowError) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *FlowError) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *FlowError) SetId(v string) { + o.Id = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *FlowError) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FlowError) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *FlowError) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *FlowError) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +func (o FlowError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o FlowError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["id"] = o.Id + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *FlowError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varFlowError := _FlowError{} + + err = json.Unmarshal(data, &varFlowError) + + if err != nil { + return err + } + + *o = FlowError(varFlowError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "error") + delete(additionalProperties, "id") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableFlowError struct { + value *FlowError + isSet bool +} + +func (v NullableFlowError) Get() *FlowError { + return v.value +} + +func (v *NullableFlowError) Set(val *FlowError) { + v.value = val + v.isSet = true +} + +func (v NullableFlowError) IsSet() bool { + return v.isSet +} + +func (v *NullableFlowError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFlowError(val *FlowError) *NullableFlowError { + return &NullableFlowError{value: val, isSet: true} +} + +func (v NullableFlowError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFlowError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_generic_error.go b/pkg/client-go/model_generic_error.go new file mode 100644 index 0000000..1931ebe --- /dev/null +++ b/pkg/client-go/model_generic_error.go @@ -0,0 +1,434 @@ +/* +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" + "fmt" +) + +// checks if the GenericError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GenericError{} + +// GenericError struct for GenericError +type GenericError struct { + // The status code + Code *int64 `json:"code,omitempty"` + // Debug information This field is often not exposed to protect against leaking sensitive information. + Debug *string `json:"debug,omitempty"` + // Further error details + Details map[string]interface{} `json:"details,omitempty"` + // The error ID Useful when trying to identify various errors in application logic. + Id *string `json:"id,omitempty"` + // Error message The error's message. + Message string `json:"message"` + // A human-readable reason for the error + Reason *string `json:"reason,omitempty"` + // The request ID The request ID is often exposed internally in order to trace errors across service architectures. This is often a UUID. + Request *string `json:"request,omitempty"` + // The status description + Status *string `json:"status,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _GenericError GenericError + +// NewGenericError instantiates a new GenericError 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 NewGenericError(message string) *GenericError { + this := GenericError{} + this.Message = message + return &this +} + +// NewGenericErrorWithDefaults instantiates a new GenericError 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 NewGenericErrorWithDefaults() *GenericError { + this := GenericError{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *GenericError) GetCode() int64 { + if o == nil || IsNil(o.Code) { + var ret int64 + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetCodeOk() (*int64, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *GenericError) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given int64 and assigns it to the Code field. +func (o *GenericError) SetCode(v int64) { + o.Code = &v +} + +// GetDebug returns the Debug field value if set, zero value otherwise. +func (o *GenericError) GetDebug() string { + if o == nil || IsNil(o.Debug) { + var ret string + return ret + } + return *o.Debug +} + +// GetDebugOk returns a tuple with the Debug field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDebugOk() (*string, bool) { + if o == nil || IsNil(o.Debug) { + return nil, false + } + return o.Debug, true +} + +// HasDebug returns a boolean if a field has been set. +func (o *GenericError) HasDebug() bool { + if o != nil && !IsNil(o.Debug) { + return true + } + + return false +} + +// SetDebug gets a reference to the given string and assigns it to the Debug field. +func (o *GenericError) SetDebug(v string) { + o.Debug = &v +} + +// GetDetails returns the Details field value if set, zero value otherwise. +func (o *GenericError) GetDetails() map[string]interface{} { + if o == nil || IsNil(o.Details) { + var ret map[string]interface{} + return ret + } + return o.Details +} + +// GetDetailsOk returns a tuple with the Details field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDetailsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Details) { + return map[string]interface{}{}, false + } + return o.Details, true +} + +// HasDetails returns a boolean if a field has been set. +func (o *GenericError) HasDetails() bool { + if o != nil && !IsNil(o.Details) { + return true + } + + return false +} + +// SetDetails gets a reference to the given map[string]interface{} and assigns it to the Details field. +func (o *GenericError) SetDetails(v map[string]interface{}) { + o.Details = v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *GenericError) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *GenericError) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *GenericError) SetId(v string) { + o.Id = &v +} + +// GetMessage returns the Message field value +func (o *GenericError) GetMessage() string { + if o == nil { + var ret string + return ret + } + + return o.Message +} + +// GetMessageOk returns a tuple with the Message field value +// and a boolean to check if the value has been set. +func (o *GenericError) GetMessageOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Message, true +} + +// SetMessage sets field value +func (o *GenericError) SetMessage(v string) { + o.Message = v +} + +// GetReason returns the Reason field value if set, zero value otherwise. +func (o *GenericError) GetReason() string { + if o == nil || IsNil(o.Reason) { + var ret string + return ret + } + return *o.Reason +} + +// GetReasonOk returns a tuple with the Reason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetReasonOk() (*string, bool) { + if o == nil || IsNil(o.Reason) { + return nil, false + } + return o.Reason, true +} + +// HasReason returns a boolean if a field has been set. +func (o *GenericError) HasReason() bool { + if o != nil && !IsNil(o.Reason) { + return true + } + + return false +} + +// SetReason gets a reference to the given string and assigns it to the Reason field. +func (o *GenericError) SetReason(v string) { + o.Reason = &v +} + +// GetRequest returns the Request field value if set, zero value otherwise. +func (o *GenericError) GetRequest() string { + if o == nil || IsNil(o.Request) { + var ret string + return ret + } + return *o.Request +} + +// GetRequestOk returns a tuple with the Request field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetRequestOk() (*string, bool) { + if o == nil || IsNil(o.Request) { + return nil, false + } + return o.Request, true +} + +// HasRequest returns a boolean if a field has been set. +func (o *GenericError) HasRequest() bool { + if o != nil && !IsNil(o.Request) { + return true + } + + return false +} + +// SetRequest gets a reference to the given string and assigns it to the Request field. +func (o *GenericError) SetRequest(v string) { + o.Request = &v +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *GenericError) GetStatus() string { + if o == nil || IsNil(o.Status) { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetStatusOk() (*string, bool) { + if o == nil || IsNil(o.Status) { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *GenericError) HasStatus() bool { + if o != nil && !IsNil(o.Status) { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *GenericError) SetStatus(v string) { + o.Status = &v +} + +func (o GenericError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GenericError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.Debug) { + toSerialize["debug"] = o.Debug + } + if !IsNil(o.Details) { + toSerialize["details"] = o.Details + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + toSerialize["message"] = o.Message + if !IsNil(o.Reason) { + toSerialize["reason"] = o.Reason + } + if !IsNil(o.Request) { + toSerialize["request"] = o.Request + } + if !IsNil(o.Status) { + toSerialize["status"] = o.Status + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *GenericError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "message", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varGenericError := _GenericError{} + + err = json.Unmarshal(data, &varGenericError) + + if err != nil { + return err + } + + *o = GenericError(varGenericError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "debug") + delete(additionalProperties, "details") + delete(additionalProperties, "id") + delete(additionalProperties, "message") + delete(additionalProperties, "reason") + delete(additionalProperties, "request") + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableGenericError struct { + value *GenericError + isSet bool +} + +func (v NullableGenericError) Get() *GenericError { + return v.value +} + +func (v *NullableGenericError) Set(val *GenericError) { + v.value = val + v.isSet = true +} + +func (v NullableGenericError) IsSet() bool { + return v.isSet +} + +func (v *NullableGenericError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGenericError(val *GenericError) *NullableGenericError { + return &NullableGenericError{value: val, isSet: true} +} + +func (v NullableGenericError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGenericError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_get_version_200_response.go b/pkg/client-go/model_get_version_200_response.go new file mode 100644 index 0000000..a60de02 --- /dev/null +++ b/pkg/client-go/model_get_version_200_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the GetVersion200Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GetVersion200Response{} + +// GetVersion200Response struct for GetVersion200Response +type GetVersion200Response struct { + // The version of Ory Kratos. + Version string `json:"version"` + AdditionalProperties map[string]interface{} +} + +type _GetVersion200Response GetVersion200Response + +// NewGetVersion200Response instantiates a new GetVersion200Response 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 NewGetVersion200Response(version string) *GetVersion200Response { + this := GetVersion200Response{} + this.Version = version + return &this +} + +// NewGetVersion200ResponseWithDefaults instantiates a new GetVersion200Response 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 NewGetVersion200ResponseWithDefaults() *GetVersion200Response { + this := GetVersion200Response{} + return &this +} + +// GetVersion returns the Version field value +func (o *GetVersion200Response) GetVersion() string { + if o == nil { + var ret string + return ret + } + + return o.Version +} + +// GetVersionOk returns a tuple with the Version field value +// and a boolean to check if the value has been set. +func (o *GetVersion200Response) GetVersionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Version, true +} + +// SetVersion sets field value +func (o *GetVersion200Response) SetVersion(v string) { + o.Version = v +} + +func (o GetVersion200Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GetVersion200Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["version"] = o.Version + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *GetVersion200Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "version", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varGetVersion200Response := _GetVersion200Response{} + + err = json.Unmarshal(data, &varGetVersion200Response) + + if err != nil { + return err + } + + *o = GetVersion200Response(varGetVersion200Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableGetVersion200Response struct { + value *GetVersion200Response + isSet bool +} + +func (v NullableGetVersion200Response) Get() *GetVersion200Response { + return v.value +} + +func (v *NullableGetVersion200Response) Set(val *GetVersion200Response) { + v.value = val + v.isSet = true +} + +func (v NullableGetVersion200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableGetVersion200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGetVersion200Response(val *GetVersion200Response) *NullableGetVersion200Response { + return &NullableGetVersion200Response{value: val, isSet: true} +} + +func (v NullableGetVersion200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGetVersion200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_health_not_ready_status.go b/pkg/client-go/model_health_not_ready_status.go new file mode 100644 index 0000000..5e05db7 --- /dev/null +++ b/pkg/client-go/model_health_not_ready_status.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the HealthNotReadyStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &HealthNotReadyStatus{} + +// HealthNotReadyStatus struct for HealthNotReadyStatus +type HealthNotReadyStatus struct { + // Errors contains a list of errors that caused the not ready status. + Errors *map[string]string `json:"errors,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _HealthNotReadyStatus HealthNotReadyStatus + +// NewHealthNotReadyStatus instantiates a new HealthNotReadyStatus 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 NewHealthNotReadyStatus() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// NewHealthNotReadyStatusWithDefaults instantiates a new HealthNotReadyStatus 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 NewHealthNotReadyStatusWithDefaults() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// GetErrors returns the Errors field value if set, zero value otherwise. +func (o *HealthNotReadyStatus) GetErrors() map[string]string { + if o == nil || IsNil(o.Errors) { + var ret map[string]string + return ret + } + return *o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthNotReadyStatus) GetErrorsOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Errors) { + return nil, false + } + return o.Errors, true +} + +// HasErrors returns a boolean if a field has been set. +func (o *HealthNotReadyStatus) HasErrors() bool { + if o != nil && !IsNil(o.Errors) { + return true + } + + return false +} + +// SetErrors gets a reference to the given map[string]string and assigns it to the Errors field. +func (o *HealthNotReadyStatus) SetErrors(v map[string]string) { + o.Errors = &v +} + +func (o HealthNotReadyStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o HealthNotReadyStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Errors) { + toSerialize["errors"] = o.Errors + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *HealthNotReadyStatus) UnmarshalJSON(data []byte) (err error) { + varHealthNotReadyStatus := _HealthNotReadyStatus{} + + err = json.Unmarshal(data, &varHealthNotReadyStatus) + + if err != nil { + return err + } + + *o = HealthNotReadyStatus(varHealthNotReadyStatus) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "errors") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableHealthNotReadyStatus struct { + value *HealthNotReadyStatus + isSet bool +} + +func (v NullableHealthNotReadyStatus) Get() *HealthNotReadyStatus { + return v.value +} + +func (v *NullableHealthNotReadyStatus) Set(val *HealthNotReadyStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthNotReadyStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthNotReadyStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthNotReadyStatus(val *HealthNotReadyStatus) *NullableHealthNotReadyStatus { + return &NullableHealthNotReadyStatus{value: val, isSet: true} +} + +func (v NullableHealthNotReadyStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthNotReadyStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_health_status.go b/pkg/client-go/model_health_status.go new file mode 100644 index 0000000..c09ccf4 --- /dev/null +++ b/pkg/client-go/model_health_status.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the HealthStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &HealthStatus{} + +// HealthStatus struct for HealthStatus +type HealthStatus struct { + // Status always contains \"ok\". + Status *string `json:"status,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _HealthStatus HealthStatus + +// NewHealthStatus instantiates a new HealthStatus 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 NewHealthStatus() *HealthStatus { + this := HealthStatus{} + return &this +} + +// NewHealthStatusWithDefaults instantiates a new HealthStatus 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 NewHealthStatusWithDefaults() *HealthStatus { + this := HealthStatus{} + return &this +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *HealthStatus) GetStatus() string { + if o == nil || IsNil(o.Status) { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthStatus) GetStatusOk() (*string, bool) { + if o == nil || IsNil(o.Status) { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *HealthStatus) HasStatus() bool { + if o != nil && !IsNil(o.Status) { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *HealthStatus) SetStatus(v string) { + o.Status = &v +} + +func (o HealthStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o HealthStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Status) { + toSerialize["status"] = o.Status + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *HealthStatus) UnmarshalJSON(data []byte) (err error) { + varHealthStatus := _HealthStatus{} + + err = json.Unmarshal(data, &varHealthStatus) + + if err != nil { + return err + } + + *o = HealthStatus(varHealthStatus) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableHealthStatus struct { + value *HealthStatus + isSet bool +} + +func (v NullableHealthStatus) Get() *HealthStatus { + return v.value +} + +func (v *NullableHealthStatus) Set(val *HealthStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthStatus(val *HealthStatus) *NullableHealthStatus { + return &NullableHealthStatus{value: val, isSet: true} +} + +func (v NullableHealthStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity.go b/pkg/client-go/model_identity.go new file mode 100644 index 0000000..f99ffe9 --- /dev/null +++ b/pkg/client-go/model_identity.go @@ -0,0 +1,730 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Identity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Identity{} + +// Identity An [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) represents a (human) user in Ory. +type Identity struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + // Credentials represents all credentials that can be used for authenticating this identity. + Credentials *map[string]IdentityCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // ID is the identity's unique identifier. The Identity ID can not be changed and can not be chosen. This ensures future compatibility and optimization for distributed stores such as CockroachDB. + Id string `json:"id"` + // NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- + MetadataPublic interface{} `json:"metadata_public,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RecoveryAddresses contains all the addresses that can be used to recover an identity. + RecoveryAddresses []RecoveryIdentityAddress `json:"recovery_addresses,omitempty"` + // Region is the Ory Network region this identity is homed in. Set by the multi-region persister; empty on OSS and single-region deployments. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + SchemaId string `json:"schema_id"` + // SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from. format: url + SchemaUrl string `json:"schema_url"` + // State is the identity's state. This value has currently no effect. active StateActive inactive StateInactive + State *string `json:"state,omitempty"` + StateChangedAt *time.Time `json:"state_changed_at,omitempty"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_url`. + Traits interface{} `json:"traits"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // VerifiableAddresses contains all the addresses that can be verified by the user. + VerifiableAddresses []VerifiableIdentityAddress `json:"verifiable_addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Identity Identity + +// NewIdentity instantiates a new Identity 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 NewIdentity(id string, schemaId string, schemaUrl string, traits interface{}) *Identity { + this := Identity{} + this.Id = id + this.SchemaId = schemaId + this.SchemaUrl = schemaUrl + this.Traits = traits + return &this +} + +// NewIdentityWithDefaults instantiates a new Identity 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 NewIdentityWithDefaults() *Identity { + this := Identity{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *Identity) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *Identity) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *Identity) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *Identity) GetCredentials() map[string]IdentityCredentials { + if o == nil || IsNil(o.Credentials) { + var ret map[string]IdentityCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetCredentialsOk() (*map[string]IdentityCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *Identity) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given map[string]IdentityCredentials and assigns it to the Credentials field. +func (o *Identity) SetCredentials(v map[string]IdentityCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *Identity) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *Identity) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *Identity) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetId returns the Id field value +func (o *Identity) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Identity) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Identity) SetId(v string) { + o.Id = v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *Identity) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *Identity) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *Identity) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *Identity) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *Identity) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *Identity) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *Identity) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *Identity) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *Identity) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *Identity) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *Identity) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRecoveryAddresses returns the RecoveryAddresses field value if set, zero value otherwise. +func (o *Identity) GetRecoveryAddresses() []RecoveryIdentityAddress { + if o == nil || IsNil(o.RecoveryAddresses) { + var ret []RecoveryIdentityAddress + return ret + } + return o.RecoveryAddresses +} + +// GetRecoveryAddressesOk returns a tuple with the RecoveryAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetRecoveryAddressesOk() ([]RecoveryIdentityAddress, bool) { + if o == nil || IsNil(o.RecoveryAddresses) { + return nil, false + } + return o.RecoveryAddresses, true +} + +// HasRecoveryAddresses returns a boolean if a field has been set. +func (o *Identity) HasRecoveryAddresses() bool { + if o != nil && !IsNil(o.RecoveryAddresses) { + return true + } + + return false +} + +// SetRecoveryAddresses gets a reference to the given []RecoveryIdentityAddress and assigns it to the RecoveryAddresses field. +func (o *Identity) SetRecoveryAddresses(v []RecoveryIdentityAddress) { + o.RecoveryAddresses = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *Identity) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *Identity) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *Identity) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *Identity) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *Identity) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *Identity) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetSchemaUrl returns the SchemaUrl field value +func (o *Identity) GetSchemaUrl() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaUrl +} + +// GetSchemaUrlOk returns a tuple with the SchemaUrl field value +// and a boolean to check if the value has been set. +func (o *Identity) GetSchemaUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaUrl, true +} + +// SetSchemaUrl sets field value +func (o *Identity) SetSchemaUrl(v string) { + o.SchemaUrl = v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *Identity) GetState() string { + if o == nil || IsNil(o.State) { + var ret string + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetStateOk() (*string, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *Identity) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given string and assigns it to the State field. +func (o *Identity) SetState(v string) { + o.State = &v +} + +// GetStateChangedAt returns the StateChangedAt field value if set, zero value otherwise. +func (o *Identity) GetStateChangedAt() time.Time { + if o == nil || IsNil(o.StateChangedAt) { + var ret time.Time + return ret + } + return *o.StateChangedAt +} + +// GetStateChangedAtOk returns a tuple with the StateChangedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetStateChangedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.StateChangedAt) { + return nil, false + } + return o.StateChangedAt, true +} + +// HasStateChangedAt returns a boolean if a field has been set. +func (o *Identity) HasStateChangedAt() bool { + if o != nil && !IsNil(o.StateChangedAt) { + return true + } + + return false +} + +// SetStateChangedAt gets a reference to the given time.Time and assigns it to the StateChangedAt field. +func (o *Identity) SetStateChangedAt(v time.Time) { + o.StateChangedAt = &v +} + +// GetTraits returns the Traits field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *Identity) GetTraits() interface{} { + if o == nil { + var ret 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. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *Identity) GetTraitsOk() (*interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return nil, false + } + return &o.Traits, true +} + +// SetTraits sets field value +func (o *Identity) SetTraits(v interface{}) { + o.Traits = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *Identity) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *Identity) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *Identity) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetVerifiableAddresses returns the VerifiableAddresses field value if set, zero value otherwise. +func (o *Identity) GetVerifiableAddresses() []VerifiableIdentityAddress { + if o == nil || IsNil(o.VerifiableAddresses) { + var ret []VerifiableIdentityAddress + return ret + } + return o.VerifiableAddresses +} + +// GetVerifiableAddressesOk returns a tuple with the VerifiableAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetVerifiableAddressesOk() ([]VerifiableIdentityAddress, bool) { + if o == nil || IsNil(o.VerifiableAddresses) { + return nil, false + } + return o.VerifiableAddresses, true +} + +// HasVerifiableAddresses returns a boolean if a field has been set. +func (o *Identity) HasVerifiableAddresses() bool { + if o != nil && !IsNil(o.VerifiableAddresses) { + return true + } + + return false +} + +// SetVerifiableAddresses gets a reference to the given []VerifiableIdentityAddress and assigns it to the VerifiableAddresses field. +func (o *Identity) SetVerifiableAddresses(v []VerifiableIdentityAddress) { + o.VerifiableAddresses = v +} + +func (o Identity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Identity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + toSerialize["id"] = o.Id + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.RecoveryAddresses) { + toSerialize["recovery_addresses"] = o.RecoveryAddresses + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + toSerialize["schema_url"] = o.SchemaUrl + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + if !IsNil(o.StateChangedAt) { + toSerialize["state_changed_at"] = o.StateChangedAt + } + if o.Traits != nil { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.VerifiableAddresses) { + toSerialize["verifiable_addresses"] = o.VerifiableAddresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Identity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "schema_id", + "schema_url", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentity := _Identity{} + + err = json.Unmarshal(data, &varIdentity) + + if err != nil { + return err + } + + *o = Identity(varIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "recovery_addresses") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "schema_url") + delete(additionalProperties, "state") + delete(additionalProperties, "state_changed_at") + delete(additionalProperties, "traits") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "verifiable_addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentity struct { + value *Identity + isSet bool +} + +func (v NullableIdentity) Get() *Identity { + return v.value +} + +func (v *NullableIdentity) Set(val *Identity) { + v.value = val + v.isSet = true +} + +func (v NullableIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentity(val *Identity) *NullableIdentity { + return &NullableIdentity{value: val, isSet: true} +} + +func (v NullableIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials.go b/pkg/client-go/model_identity_credentials.go new file mode 100644 index 0000000..9ee0497 --- /dev/null +++ b/pkg/client-go/model_identity_credentials.go @@ -0,0 +1,345 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentials type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentials{} + +// IdentityCredentials Credentials represents a specific credential type +type IdentityCredentials struct { + Config map[string]interface{} `json:"config,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + // Identifiers represent 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // Version refers to the version of the credential. Useful when changing the config schema. + Version *int64 `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentials IdentityCredentials + +// NewIdentityCredentials instantiates a new IdentityCredentials 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 NewIdentityCredentials() *IdentityCredentials { + this := IdentityCredentials{} + return &this +} + +// NewIdentityCredentialsWithDefaults instantiates a new IdentityCredentials 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 NewIdentityCredentialsWithDefaults() *IdentityCredentials { + this := IdentityCredentials{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityCredentials) GetConfig() map[string]interface{} { + if o == nil || IsNil(o.Config) { + var ret map[string]interface{} + return ret + } + return o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetConfigOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Config) { + return map[string]interface{}{}, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityCredentials) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given map[string]interface{} and assigns it to the Config field. +func (o *IdentityCredentials) SetConfig(v map[string]interface{}) { + o.Config = v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *IdentityCredentials) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *IdentityCredentials) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *IdentityCredentials) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetIdentifiers returns the Identifiers field value if set, zero value otherwise. +func (o *IdentityCredentials) GetIdentifiers() []string { + if o == nil || IsNil(o.Identifiers) { + var ret []string + return ret + } + return o.Identifiers +} + +// GetIdentifiersOk returns a tuple with the Identifiers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetIdentifiersOk() ([]string, bool) { + if o == nil || IsNil(o.Identifiers) { + return nil, false + } + return o.Identifiers, true +} + +// HasIdentifiers returns a boolean if a field has been set. +func (o *IdentityCredentials) HasIdentifiers() bool { + if o != nil && !IsNil(o.Identifiers) { + return true + } + + return false +} + +// SetIdentifiers gets a reference to the given []string and assigns it to the Identifiers field. +func (o *IdentityCredentials) SetIdentifiers(v []string) { + o.Identifiers = v +} + +// GetType returns the Type field value if set, zero value otherwise. +func (o *IdentityCredentials) GetType() string { + if o == nil || IsNil(o.Type) { + var ret string + return ret + } + return *o.Type +} + +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetTypeOk() (*string, bool) { + if o == nil || IsNil(o.Type) { + return nil, false + } + return o.Type, true +} + +// HasType returns a boolean if a field has been set. +func (o *IdentityCredentials) HasType() bool { + if o != nil && !IsNil(o.Type) { + return true + } + + return false +} + +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *IdentityCredentials) SetType(v string) { + o.Type = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *IdentityCredentials) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *IdentityCredentials) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *IdentityCredentials) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *IdentityCredentials) GetVersion() int64 { + if o == nil || IsNil(o.Version) { + var ret int64 + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetVersionOk() (*int64, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *IdentityCredentials) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given int64 and assigns it to the Version field. +func (o *IdentityCredentials) SetVersion(v int64) { + o.Version = &v +} + +func (o IdentityCredentials) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentials) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Identifiers) { + toSerialize["identifiers"] = o.Identifiers + } + if !IsNil(o.Type) { + toSerialize["type"] = o.Type + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentials) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentials := _IdentityCredentials{} + + err = json.Unmarshal(data, &varIdentityCredentials) + + if err != nil { + return err + } + + *o = IdentityCredentials(varIdentityCredentials) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + delete(additionalProperties, "created_at") + delete(additionalProperties, "identifiers") + delete(additionalProperties, "type") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentials struct { + value *IdentityCredentials + isSet bool +} + +func (v NullableIdentityCredentials) Get() *IdentityCredentials { + return v.value +} + +func (v *NullableIdentityCredentials) Set(val *IdentityCredentials) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentials) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentials) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentials(val *IdentityCredentials) *NullableIdentityCredentials { + return &NullableIdentityCredentials{value: val, isSet: true} +} + +func (v NullableIdentityCredentials) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentials) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_code.go b/pkg/client-go/model_identity_credentials_code.go new file mode 100644 index 0000000..40e6d9a --- /dev/null +++ b/pkg/client-go/model_identity_credentials_code.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityCredentialsCode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsCode{} + +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code +type IdentityCredentialsCode struct { + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsCode IdentityCredentialsCode + +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode 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 NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode 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 NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || IsNil(o.Addresses) { + var ret []IdentityCredentialsCodeAddress + return ret + } + return o.Addresses +} + +// 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) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || IsNil(o.Addresses) { + return nil, false + } + return o.Addresses, true +} + +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && !IsNil(o.Addresses) { + return true + } + + return false +} + +// 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsCode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Addresses) { + toSerialize["addresses"] = o.Addresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsCode) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsCode := _IdentityCredentialsCode{} + + err = json.Unmarshal(data, &varIdentityCredentialsCode) + + if err != nil { + return err + } + + *o = IdentityCredentialsCode(varIdentityCredentialsCode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode + isSet bool +} + +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { + return v.value +} + +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_code_address.go b/pkg/client-go/model_identity_credentials_code_address.go new file mode 100644 index 0000000..dc6dc73 --- /dev/null +++ b/pkg/client-go/model_identity_credentials_code_address.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityCredentialsCodeAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsCodeAddress{} + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsCodeAddress IdentityCredentialsCodeAddress + +// 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 || IsNil(o.Address) { + 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 || IsNil(o.Address) { + 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 && !IsNil(o.Address) { + 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 || IsNil(o.Channel) { + 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 || IsNil(o.Channel) { + 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 && !IsNil(o.Channel) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsCodeAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Address) { + toSerialize["address"] = o.Address + } + if !IsNil(o.Channel) { + toSerialize["channel"] = o.Channel + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsCodeAddress) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsCodeAddress := _IdentityCredentialsCodeAddress{} + + err = json.Unmarshal(data, &varIdentityCredentialsCodeAddress) + + if err != nil { + return err + } + + *o = IdentityCredentialsCodeAddress(varIdentityCredentialsCodeAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "address") + delete(additionalProperties, "channel") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_identity_credentials_lookup_secret_code.go b/pkg/client-go/model_identity_credentials_lookup_secret_code.go new file mode 100644 index 0000000..cc5dd45 --- /dev/null +++ b/pkg/client-go/model_identity_credentials_lookup_secret_code.go @@ -0,0 +1,193 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentialsLookupSecretCode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsLookupSecretCode{} + +// IdentityCredentialsLookupSecretCode Recovery codes can be used once and are invalidated after use. +type IdentityCredentialsLookupSecretCode struct { + // A recovery code + Code *string `json:"code,omitempty"` + UsedAt *time.Time `json:"used_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsLookupSecretCode IdentityCredentialsLookupSecretCode + +// NewIdentityCredentialsLookupSecretCode instantiates a new IdentityCredentialsLookupSecretCode 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 NewIdentityCredentialsLookupSecretCode() *IdentityCredentialsLookupSecretCode { + this := IdentityCredentialsLookupSecretCode{} + return &this +} + +// NewIdentityCredentialsLookupSecretCodeWithDefaults instantiates a new IdentityCredentialsLookupSecretCode 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 NewIdentityCredentialsLookupSecretCodeWithDefaults() *IdentityCredentialsLookupSecretCode { + this := IdentityCredentialsLookupSecretCode{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *IdentityCredentialsLookupSecretCode) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsLookupSecretCode) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *IdentityCredentialsLookupSecretCode) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *IdentityCredentialsLookupSecretCode) SetCode(v string) { + o.Code = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise. +func (o *IdentityCredentialsLookupSecretCode) GetUsedAt() time.Time { + if o == nil || IsNil(o.UsedAt) { + var ret time.Time + return ret + } + return *o.UsedAt +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsLookupSecretCode) GetUsedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UsedAt) { + return nil, false + } + return o.UsedAt, true +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsLookupSecretCode) HasUsedAt() bool { + if o != nil && !IsNil(o.UsedAt) { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given time.Time and assigns it to the UsedAt field. +func (o *IdentityCredentialsLookupSecretCode) SetUsedAt(v time.Time) { + o.UsedAt = &v +} + +func (o IdentityCredentialsLookupSecretCode) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsLookupSecretCode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.UsedAt) { + toSerialize["used_at"] = o.UsedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsLookupSecretCode) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsLookupSecretCode := _IdentityCredentialsLookupSecretCode{} + + err = json.Unmarshal(data, &varIdentityCredentialsLookupSecretCode) + + if err != nil { + return err + } + + *o = IdentityCredentialsLookupSecretCode(varIdentityCredentialsLookupSecretCode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "used_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsLookupSecretCode struct { + value *IdentityCredentialsLookupSecretCode + isSet bool +} + +func (v NullableIdentityCredentialsLookupSecretCode) Get() *IdentityCredentialsLookupSecretCode { + return v.value +} + +func (v *NullableIdentityCredentialsLookupSecretCode) Set(val *IdentityCredentialsLookupSecretCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsLookupSecretCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsLookupSecretCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsLookupSecretCode(val *IdentityCredentialsLookupSecretCode) *NullableIdentityCredentialsLookupSecretCode { + return &NullableIdentityCredentialsLookupSecretCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsLookupSecretCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsLookupSecretCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_oidc.go b/pkg/client-go/model_identity_credentials_oidc.go new file mode 100644 index 0000000..452cb6c --- /dev/null +++ b/pkg/client-go/model_identity_credentials_oidc.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityCredentialsOidc type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsOidc{} + +// IdentityCredentialsOidc struct for IdentityCredentialsOidc +type IdentityCredentialsOidc struct { + Providers []IdentityCredentialsOidcProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsOidc IdentityCredentialsOidc + +// NewIdentityCredentialsOidc instantiates a new IdentityCredentialsOidc 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 NewIdentityCredentialsOidc() *IdentityCredentialsOidc { + this := IdentityCredentialsOidc{} + return &this +} + +// NewIdentityCredentialsOidcWithDefaults instantiates a new IdentityCredentialsOidc 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 NewIdentityCredentialsOidcWithDefaults() *IdentityCredentialsOidc { + this := IdentityCredentialsOidc{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityCredentialsOidc) GetProviders() []IdentityCredentialsOidcProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityCredentialsOidcProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidc) GetProvidersOk() ([]IdentityCredentialsOidcProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityCredentialsOidc) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityCredentialsOidcProvider and assigns it to the Providers field. +func (o *IdentityCredentialsOidc) SetProviders(v []IdentityCredentialsOidcProvider) { + o.Providers = v +} + +func (o IdentityCredentialsOidc) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsOidc) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsOidc) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsOidc := _IdentityCredentialsOidc{} + + err = json.Unmarshal(data, &varIdentityCredentialsOidc) + + if err != nil { + return err + } + + *o = IdentityCredentialsOidc(varIdentityCredentialsOidc) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsOidc struct { + value *IdentityCredentialsOidc + isSet bool +} + +func (v NullableIdentityCredentialsOidc) Get() *IdentityCredentialsOidc { + return v.value +} + +func (v *NullableIdentityCredentialsOidc) Set(val *IdentityCredentialsOidc) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOidc) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOidc) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOidc(val *IdentityCredentialsOidc) *NullableIdentityCredentialsOidc { + return &NullableIdentityCredentialsOidc{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOidc) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOidc) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_oidc_provider.go b/pkg/client-go/model_identity_credentials_oidc_provider.go new file mode 100644 index 0000000..ce1d28b --- /dev/null +++ b/pkg/client-go/model_identity_credentials_oidc_provider.go @@ -0,0 +1,376 @@ +/* +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" +) + +// checks if the IdentityCredentialsOidcProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsOidcProvider{} + +// IdentityCredentialsOidcProvider struct for IdentityCredentialsOidcProvider +type IdentityCredentialsOidcProvider struct { + InitialAccessToken *string `json:"initial_access_token,omitempty"` + InitialIdToken *string `json:"initial_id_token,omitempty"` + InitialRefreshToken *string `json:"initial_refresh_token,omitempty"` + Organization *string `json:"organization,omitempty"` + Provider *string `json:"provider,omitempty"` + Subject *string `json:"subject,omitempty"` + UseAutoLink *bool `json:"use_auto_link,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsOidcProvider IdentityCredentialsOidcProvider + +// NewIdentityCredentialsOidcProvider instantiates a new IdentityCredentialsOidcProvider 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 NewIdentityCredentialsOidcProvider() *IdentityCredentialsOidcProvider { + this := IdentityCredentialsOidcProvider{} + return &this +} + +// NewIdentityCredentialsOidcProviderWithDefaults instantiates a new IdentityCredentialsOidcProvider 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 NewIdentityCredentialsOidcProviderWithDefaults() *IdentityCredentialsOidcProvider { + this := IdentityCredentialsOidcProvider{} + return &this +} + +// GetInitialAccessToken returns the InitialAccessToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialAccessToken() string { + if o == nil || IsNil(o.InitialAccessToken) { + var ret string + return ret + } + return *o.InitialAccessToken +} + +// GetInitialAccessTokenOk returns a tuple with the InitialAccessToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialAccessTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialAccessToken) { + return nil, false + } + return o.InitialAccessToken, true +} + +// HasInitialAccessToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialAccessToken() bool { + if o != nil && !IsNil(o.InitialAccessToken) { + return true + } + + return false +} + +// SetInitialAccessToken gets a reference to the given string and assigns it to the InitialAccessToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialAccessToken(v string) { + o.InitialAccessToken = &v +} + +// GetInitialIdToken returns the InitialIdToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialIdToken() string { + if o == nil || IsNil(o.InitialIdToken) { + var ret string + return ret + } + return *o.InitialIdToken +} + +// GetInitialIdTokenOk returns a tuple with the InitialIdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialIdToken) { + return nil, false + } + return o.InitialIdToken, true +} + +// HasInitialIdToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialIdToken() bool { + if o != nil && !IsNil(o.InitialIdToken) { + return true + } + + return false +} + +// SetInitialIdToken gets a reference to the given string and assigns it to the InitialIdToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialIdToken(v string) { + o.InitialIdToken = &v +} + +// GetInitialRefreshToken returns the InitialRefreshToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialRefreshToken() string { + if o == nil || IsNil(o.InitialRefreshToken) { + var ret string + return ret + } + return *o.InitialRefreshToken +} + +// GetInitialRefreshTokenOk returns a tuple with the InitialRefreshToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialRefreshTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialRefreshToken) { + return nil, false + } + return o.InitialRefreshToken, true +} + +// HasInitialRefreshToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialRefreshToken() bool { + if o != nil && !IsNil(o.InitialRefreshToken) { + return true + } + + return false +} + +// SetInitialRefreshToken gets a reference to the given string and assigns it to the InitialRefreshToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialRefreshToken(v string) { + o.InitialRefreshToken = &v +} + +// GetOrganization returns the Organization field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization) { + var ret string + return ret + } + return *o.Organization +} + +// GetOrganizationOk returns a tuple with the Organization field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetOrganizationOk() (*string, bool) { + if o == nil || IsNil(o.Organization) { + return nil, false + } + return o.Organization, true +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasOrganization() bool { + if o != nil && !IsNil(o.Organization) { + return true + } + + return false +} + +// SetOrganization gets a reference to the given string and assigns it to the Organization field. +func (o *IdentityCredentialsOidcProvider) SetOrganization(v string) { + o.Organization = &v +} + +// GetProvider returns the Provider field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetProvider() string { + if o == nil || IsNil(o.Provider) { + var ret string + return ret + } + return *o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetProviderOk() (*string, bool) { + if o == nil || IsNil(o.Provider) { + return nil, false + } + return o.Provider, true +} + +// HasProvider returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasProvider() bool { + if o != nil && !IsNil(o.Provider) { + return true + } + + return false +} + +// SetProvider gets a reference to the given string and assigns it to the Provider field. +func (o *IdentityCredentialsOidcProvider) SetProvider(v string) { + o.Provider = &v +} + +// GetSubject returns the Subject field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetSubject() string { + if o == nil || IsNil(o.Subject) { + var ret string + return ret + } + return *o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetSubjectOk() (*string, bool) { + if o == nil || IsNil(o.Subject) { + return nil, false + } + return o.Subject, true +} + +// HasSubject returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasSubject() bool { + if o != nil && !IsNil(o.Subject) { + return true + } + + return false +} + +// SetSubject gets a reference to the given string and assigns it to the Subject field. +func (o *IdentityCredentialsOidcProvider) SetSubject(v string) { + o.Subject = &v +} + +// GetUseAutoLink returns the UseAutoLink field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetUseAutoLink() bool { + if o == nil || IsNil(o.UseAutoLink) { + var ret bool + return ret + } + return *o.UseAutoLink +} + +// GetUseAutoLinkOk returns a tuple with the UseAutoLink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetUseAutoLinkOk() (*bool, bool) { + if o == nil || IsNil(o.UseAutoLink) { + return nil, false + } + return o.UseAutoLink, true +} + +// HasUseAutoLink returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasUseAutoLink() bool { + if o != nil && !IsNil(o.UseAutoLink) { + return true + } + + return false +} + +// SetUseAutoLink gets a reference to the given bool and assigns it to the UseAutoLink field. +func (o *IdentityCredentialsOidcProvider) SetUseAutoLink(v bool) { + o.UseAutoLink = &v +} + +func (o IdentityCredentialsOidcProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsOidcProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.InitialAccessToken) { + toSerialize["initial_access_token"] = o.InitialAccessToken + } + if !IsNil(o.InitialIdToken) { + toSerialize["initial_id_token"] = o.InitialIdToken + } + if !IsNil(o.InitialRefreshToken) { + toSerialize["initial_refresh_token"] = o.InitialRefreshToken + } + if !IsNil(o.Organization) { + toSerialize["organization"] = o.Organization + } + if !IsNil(o.Provider) { + toSerialize["provider"] = o.Provider + } + if !IsNil(o.Subject) { + toSerialize["subject"] = o.Subject + } + if !IsNil(o.UseAutoLink) { + toSerialize["use_auto_link"] = o.UseAutoLink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsOidcProvider) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsOidcProvider := _IdentityCredentialsOidcProvider{} + + err = json.Unmarshal(data, &varIdentityCredentialsOidcProvider) + + if err != nil { + return err + } + + *o = IdentityCredentialsOidcProvider(varIdentityCredentialsOidcProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "initial_access_token") + delete(additionalProperties, "initial_id_token") + delete(additionalProperties, "initial_refresh_token") + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + delete(additionalProperties, "use_auto_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsOidcProvider struct { + value *IdentityCredentialsOidcProvider + isSet bool +} + +func (v NullableIdentityCredentialsOidcProvider) Get() *IdentityCredentialsOidcProvider { + return v.value +} + +func (v *NullableIdentityCredentialsOidcProvider) Set(val *IdentityCredentialsOidcProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOidcProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOidcProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOidcProvider(val *IdentityCredentialsOidcProvider) *NullableIdentityCredentialsOidcProvider { + return &NullableIdentityCredentialsOidcProvider{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOidcProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOidcProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_password.go b/pkg/client-go/model_identity_credentials_password.go new file mode 100644 index 0000000..a8e0830 --- /dev/null +++ b/pkg/client-go/model_identity_credentials_password.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the IdentityCredentialsPassword type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsPassword{} + +// IdentityCredentialsPassword struct for IdentityCredentialsPassword +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"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsPassword IdentityCredentialsPassword + +// NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword 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 NewIdentityCredentialsPassword() *IdentityCredentialsPassword { + this := IdentityCredentialsPassword{} + return &this +} + +// NewIdentityCredentialsPasswordWithDefaults instantiates a new IdentityCredentialsPassword 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 NewIdentityCredentialsPasswordWithDefaults() *IdentityCredentialsPassword { + this := IdentityCredentialsPassword{} + return &this +} + +// GetHashedPassword returns the HashedPassword field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetHashedPassword() string { + if o == nil || IsNil(o.HashedPassword) { + var ret string + return ret + } + return *o.HashedPassword +} + +// GetHashedPasswordOk returns a tuple with the HashedPassword field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetHashedPasswordOk() (*string, bool) { + if o == nil || IsNil(o.HashedPassword) { + return nil, false + } + return o.HashedPassword, true +} + +// HasHashedPassword returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasHashedPassword() bool { + if o != nil && !IsNil(o.HashedPassword) { + return true + } + + return false +} + +// SetHashedPassword gets a reference to the given string and assigns it to the HashedPassword field. +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 || IsNil(o.UsePasswordMigrationHook) { + 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 || IsNil(o.UsePasswordMigrationHook) { + 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 && !IsNil(o.UsePasswordMigrationHook) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsPassword) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.HashedPassword) { + toSerialize["hashed_password"] = o.HashedPassword + } + if !IsNil(o.UsePasswordMigrationHook) { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsPassword) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsPassword := _IdentityCredentialsPassword{} + + err = json.Unmarshal(data, &varIdentityCredentialsPassword) + + if err != nil { + return err + } + + *o = IdentityCredentialsPassword(varIdentityCredentialsPassword) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "hashed_password") + delete(additionalProperties, "use_password_migration_hook") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsPassword struct { + value *IdentityCredentialsPassword + isSet bool +} + +func (v NullableIdentityCredentialsPassword) Get() *IdentityCredentialsPassword { + return v.value +} + +func (v *NullableIdentityCredentialsPassword) Set(val *IdentityCredentialsPassword) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsPassword) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsPassword) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsPassword(val *IdentityCredentialsPassword) *NullableIdentityCredentialsPassword { + return &NullableIdentityCredentialsPassword{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsPassword) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsPassword) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_web_authn.go b/pkg/client-go/model_identity_credentials_web_authn.go new file mode 100644 index 0000000..978c36e --- /dev/null +++ b/pkg/client-go/model_identity_credentials_web_authn.go @@ -0,0 +1,488 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentialsWebAuthn type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthn{} + +// IdentityCredentialsWebAuthn struct for IdentityCredentialsWebAuthn +type IdentityCredentialsWebAuthn struct { + AddedAt *time.Time `json:"added_at,omitempty"` + Attestation *IdentityCredentialsWebAuthnAttestation `json:"attestation,omitempty"` + AttestationType *string `json:"attestation_type,omitempty"` + Authenticator *IdentityCredentialsWebAuthnAuthenticator `json:"authenticator,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + Flags *IdentityCredentialsWebAuthnFlags `json:"flags,omitempty"` + Id []int32 `json:"id,omitempty"` + IsPasswordless *bool `json:"is_passwordless,omitempty"` + PublicKey []int32 `json:"public_key,omitempty"` + Transport []string `json:"transport,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthn IdentityCredentialsWebAuthn + +// NewIdentityCredentialsWebAuthn instantiates a new IdentityCredentialsWebAuthn 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 NewIdentityCredentialsWebAuthn() *IdentityCredentialsWebAuthn { + this := IdentityCredentialsWebAuthn{} + return &this +} + +// NewIdentityCredentialsWebAuthnWithDefaults instantiates a new IdentityCredentialsWebAuthn 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 NewIdentityCredentialsWebAuthnWithDefaults() *IdentityCredentialsWebAuthn { + this := IdentityCredentialsWebAuthn{} + return &this +} + +// GetAddedAt returns the AddedAt field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAddedAt() time.Time { + if o == nil || IsNil(o.AddedAt) { + var ret time.Time + return ret + } + return *o.AddedAt +} + +// GetAddedAtOk returns a tuple with the AddedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAddedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.AddedAt) { + return nil, false + } + return o.AddedAt, true +} + +// HasAddedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAddedAt() bool { + if o != nil && !IsNil(o.AddedAt) { + return true + } + + return false +} + +// SetAddedAt gets a reference to the given time.Time and assigns it to the AddedAt field. +func (o *IdentityCredentialsWebAuthn) SetAddedAt(v time.Time) { + o.AddedAt = &v +} + +// GetAttestation returns the Attestation field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAttestation() IdentityCredentialsWebAuthnAttestation { + if o == nil || IsNil(o.Attestation) { + var ret IdentityCredentialsWebAuthnAttestation + return ret + } + return *o.Attestation +} + +// GetAttestationOk returns a tuple with the Attestation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAttestationOk() (*IdentityCredentialsWebAuthnAttestation, bool) { + if o == nil || IsNil(o.Attestation) { + return nil, false + } + return o.Attestation, true +} + +// HasAttestation returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAttestation() bool { + if o != nil && !IsNil(o.Attestation) { + return true + } + + return false +} + +// SetAttestation gets a reference to the given IdentityCredentialsWebAuthnAttestation and assigns it to the Attestation field. +func (o *IdentityCredentialsWebAuthn) SetAttestation(v IdentityCredentialsWebAuthnAttestation) { + o.Attestation = &v +} + +// GetAttestationType returns the AttestationType field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAttestationType() string { + if o == nil || IsNil(o.AttestationType) { + var ret string + return ret + } + return *o.AttestationType +} + +// GetAttestationTypeOk returns a tuple with the AttestationType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAttestationTypeOk() (*string, bool) { + if o == nil || IsNil(o.AttestationType) { + return nil, false + } + return o.AttestationType, true +} + +// HasAttestationType returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAttestationType() bool { + if o != nil && !IsNil(o.AttestationType) { + return true + } + + return false +} + +// SetAttestationType gets a reference to the given string and assigns it to the AttestationType field. +func (o *IdentityCredentialsWebAuthn) SetAttestationType(v string) { + o.AttestationType = &v +} + +// GetAuthenticator returns the Authenticator field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAuthenticator() IdentityCredentialsWebAuthnAuthenticator { + if o == nil || IsNil(o.Authenticator) { + var ret IdentityCredentialsWebAuthnAuthenticator + return ret + } + return *o.Authenticator +} + +// GetAuthenticatorOk returns a tuple with the Authenticator field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAuthenticatorOk() (*IdentityCredentialsWebAuthnAuthenticator, bool) { + if o == nil || IsNil(o.Authenticator) { + return nil, false + } + return o.Authenticator, true +} + +// HasAuthenticator returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAuthenticator() bool { + if o != nil && !IsNil(o.Authenticator) { + return true + } + + return false +} + +// SetAuthenticator gets a reference to the given IdentityCredentialsWebAuthnAuthenticator and assigns it to the Authenticator field. +func (o *IdentityCredentialsWebAuthn) SetAuthenticator(v IdentityCredentialsWebAuthnAuthenticator) { + o.Authenticator = &v +} + +// GetDisplayName returns the DisplayName field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetDisplayName() string { + if o == nil || IsNil(o.DisplayName) { + var ret string + return ret + } + return *o.DisplayName +} + +// GetDisplayNameOk returns a tuple with the DisplayName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetDisplayNameOk() (*string, bool) { + if o == nil || IsNil(o.DisplayName) { + return nil, false + } + return o.DisplayName, true +} + +// HasDisplayName returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasDisplayName() bool { + if o != nil && !IsNil(o.DisplayName) { + return true + } + + return false +} + +// SetDisplayName gets a reference to the given string and assigns it to the DisplayName field. +func (o *IdentityCredentialsWebAuthn) SetDisplayName(v string) { + o.DisplayName = &v +} + +// GetFlags returns the Flags field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetFlags() IdentityCredentialsWebAuthnFlags { + if o == nil || IsNil(o.Flags) { + var ret IdentityCredentialsWebAuthnFlags + return ret + } + return *o.Flags +} + +// GetFlagsOk returns a tuple with the Flags field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetFlagsOk() (*IdentityCredentialsWebAuthnFlags, bool) { + if o == nil || IsNil(o.Flags) { + return nil, false + } + return o.Flags, true +} + +// HasFlags returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasFlags() bool { + if o != nil && !IsNil(o.Flags) { + return true + } + + return false +} + +// SetFlags gets a reference to the given IdentityCredentialsWebAuthnFlags and assigns it to the Flags field. +func (o *IdentityCredentialsWebAuthn) SetFlags(v IdentityCredentialsWebAuthnFlags) { + o.Flags = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetId() []int32 { + if o == nil || IsNil(o.Id) { + var ret []int32 + return ret + } + return o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetIdOk() ([]int32, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given []int32 and assigns it to the Id field. +func (o *IdentityCredentialsWebAuthn) SetId(v []int32) { + o.Id = v +} + +// GetIsPasswordless returns the IsPasswordless field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetIsPasswordless() bool { + if o == nil || IsNil(o.IsPasswordless) { + var ret bool + return ret + } + return *o.IsPasswordless +} + +// GetIsPasswordlessOk returns a tuple with the IsPasswordless field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetIsPasswordlessOk() (*bool, bool) { + if o == nil || IsNil(o.IsPasswordless) { + return nil, false + } + return o.IsPasswordless, true +} + +// HasIsPasswordless returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasIsPasswordless() bool { + if o != nil && !IsNil(o.IsPasswordless) { + return true + } + + return false +} + +// SetIsPasswordless gets a reference to the given bool and assigns it to the IsPasswordless field. +func (o *IdentityCredentialsWebAuthn) SetIsPasswordless(v bool) { + o.IsPasswordless = &v +} + +// GetPublicKey returns the PublicKey field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetPublicKey() []int32 { + if o == nil || IsNil(o.PublicKey) { + var ret []int32 + return ret + } + return o.PublicKey +} + +// GetPublicKeyOk returns a tuple with the PublicKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetPublicKeyOk() ([]int32, bool) { + if o == nil || IsNil(o.PublicKey) { + return nil, false + } + return o.PublicKey, true +} + +// HasPublicKey returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasPublicKey() bool { + if o != nil && !IsNil(o.PublicKey) { + return true + } + + return false +} + +// SetPublicKey gets a reference to the given []int32 and assigns it to the PublicKey field. +func (o *IdentityCredentialsWebAuthn) SetPublicKey(v []int32) { + o.PublicKey = v +} + +// GetTransport returns the Transport field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetTransport() []string { + if o == nil || IsNil(o.Transport) { + var ret []string + return ret + } + return o.Transport +} + +// GetTransportOk returns a tuple with the Transport field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetTransportOk() ([]string, bool) { + if o == nil || IsNil(o.Transport) { + return nil, false + } + return o.Transport, true +} + +// HasTransport returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasTransport() bool { + if o != nil && !IsNil(o.Transport) { + return true + } + + return false +} + +// SetTransport gets a reference to the given []string and assigns it to the Transport field. +func (o *IdentityCredentialsWebAuthn) SetTransport(v []string) { + o.Transport = v +} + +func (o IdentityCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthn) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AddedAt) { + toSerialize["added_at"] = o.AddedAt + } + if !IsNil(o.Attestation) { + toSerialize["attestation"] = o.Attestation + } + if !IsNil(o.AttestationType) { + toSerialize["attestation_type"] = o.AttestationType + } + if !IsNil(o.Authenticator) { + toSerialize["authenticator"] = o.Authenticator + } + if !IsNil(o.DisplayName) { + toSerialize["display_name"] = o.DisplayName + } + if !IsNil(o.Flags) { + toSerialize["flags"] = o.Flags + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.IsPasswordless) { + toSerialize["is_passwordless"] = o.IsPasswordless + } + if !IsNil(o.PublicKey) { + toSerialize["public_key"] = o.PublicKey + } + if !IsNil(o.Transport) { + toSerialize["transport"] = o.Transport + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthn) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthn := _IdentityCredentialsWebAuthn{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthn) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthn(varIdentityCredentialsWebAuthn) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "added_at") + delete(additionalProperties, "attestation") + delete(additionalProperties, "attestation_type") + delete(additionalProperties, "authenticator") + delete(additionalProperties, "display_name") + delete(additionalProperties, "flags") + delete(additionalProperties, "id") + delete(additionalProperties, "is_passwordless") + delete(additionalProperties, "public_key") + delete(additionalProperties, "transport") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthn struct { + value *IdentityCredentialsWebAuthn + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthn) Get() *IdentityCredentialsWebAuthn { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthn) Set(val *IdentityCredentialsWebAuthn) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthn) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthn) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthn(val *IdentityCredentialsWebAuthn) *NullableIdentityCredentialsWebAuthn { + return &NullableIdentityCredentialsWebAuthn{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthn) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_web_authn_attestation.go b/pkg/client-go/model_identity_credentials_web_authn_attestation.go new file mode 100644 index 0000000..2aa83d0 --- /dev/null +++ b/pkg/client-go/model_identity_credentials_web_authn_attestation.go @@ -0,0 +1,302 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnAttestation{} + +// IdentityCredentialsWebAuthnAttestation struct for IdentityCredentialsWebAuthnAttestation +type IdentityCredentialsWebAuthnAttestation struct { + AuthenticatorData []int32 `json:"authenticator_data,omitempty"` + ClientDataJSON []int32 `json:"client_dataJSON,omitempty"` + ClientDataHash []int32 `json:"client_data_hash,omitempty"` + Object []int32 `json:"object,omitempty"` + PublicKeyAlgorithm *int64 `json:"public_key_algorithm,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnAttestation IdentityCredentialsWebAuthnAttestation + +// NewIdentityCredentialsWebAuthnAttestation instantiates a new IdentityCredentialsWebAuthnAttestation 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 NewIdentityCredentialsWebAuthnAttestation() *IdentityCredentialsWebAuthnAttestation { + this := IdentityCredentialsWebAuthnAttestation{} + return &this +} + +// NewIdentityCredentialsWebAuthnAttestationWithDefaults instantiates a new IdentityCredentialsWebAuthnAttestation 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 NewIdentityCredentialsWebAuthnAttestationWithDefaults() *IdentityCredentialsWebAuthnAttestation { + this := IdentityCredentialsWebAuthnAttestation{} + return &this +} + +// GetAuthenticatorData returns the AuthenticatorData field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetAuthenticatorData() []int32 { + if o == nil || IsNil(o.AuthenticatorData) { + var ret []int32 + return ret + } + return o.AuthenticatorData +} + +// GetAuthenticatorDataOk returns a tuple with the AuthenticatorData field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetAuthenticatorDataOk() ([]int32, bool) { + if o == nil || IsNil(o.AuthenticatorData) { + return nil, false + } + return o.AuthenticatorData, true +} + +// HasAuthenticatorData returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasAuthenticatorData() bool { + if o != nil && !IsNil(o.AuthenticatorData) { + return true + } + + return false +} + +// SetAuthenticatorData gets a reference to the given []int32 and assigns it to the AuthenticatorData field. +func (o *IdentityCredentialsWebAuthnAttestation) SetAuthenticatorData(v []int32) { + o.AuthenticatorData = v +} + +// GetClientDataJSON returns the ClientDataJSON field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataJSON() []int32 { + if o == nil || IsNil(o.ClientDataJSON) { + var ret []int32 + return ret + } + return o.ClientDataJSON +} + +// GetClientDataJSONOk returns a tuple with the ClientDataJSON field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataJSONOk() ([]int32, bool) { + if o == nil || IsNil(o.ClientDataJSON) { + return nil, false + } + return o.ClientDataJSON, true +} + +// HasClientDataJSON returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasClientDataJSON() bool { + if o != nil && !IsNil(o.ClientDataJSON) { + return true + } + + return false +} + +// SetClientDataJSON gets a reference to the given []int32 and assigns it to the ClientDataJSON field. +func (o *IdentityCredentialsWebAuthnAttestation) SetClientDataJSON(v []int32) { + o.ClientDataJSON = v +} + +// GetClientDataHash returns the ClientDataHash field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataHash() []int32 { + if o == nil || IsNil(o.ClientDataHash) { + var ret []int32 + return ret + } + return o.ClientDataHash +} + +// GetClientDataHashOk returns a tuple with the ClientDataHash field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataHashOk() ([]int32, bool) { + if o == nil || IsNil(o.ClientDataHash) { + return nil, false + } + return o.ClientDataHash, true +} + +// HasClientDataHash returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasClientDataHash() bool { + if o != nil && !IsNil(o.ClientDataHash) { + return true + } + + return false +} + +// SetClientDataHash gets a reference to the given []int32 and assigns it to the ClientDataHash field. +func (o *IdentityCredentialsWebAuthnAttestation) SetClientDataHash(v []int32) { + o.ClientDataHash = v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetObject() []int32 { + if o == nil || IsNil(o.Object) { + var ret []int32 + return ret + } + return o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetObjectOk() ([]int32, bool) { + if o == nil || IsNil(o.Object) { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasObject() bool { + if o != nil && !IsNil(o.Object) { + return true + } + + return false +} + +// SetObject gets a reference to the given []int32 and assigns it to the Object field. +func (o *IdentityCredentialsWebAuthnAttestation) SetObject(v []int32) { + o.Object = v +} + +// GetPublicKeyAlgorithm returns the PublicKeyAlgorithm field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetPublicKeyAlgorithm() int64 { + if o == nil || IsNil(o.PublicKeyAlgorithm) { + var ret int64 + return ret + } + return *o.PublicKeyAlgorithm +} + +// GetPublicKeyAlgorithmOk returns a tuple with the PublicKeyAlgorithm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetPublicKeyAlgorithmOk() (*int64, bool) { + if o == nil || IsNil(o.PublicKeyAlgorithm) { + return nil, false + } + return o.PublicKeyAlgorithm, true +} + +// HasPublicKeyAlgorithm returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasPublicKeyAlgorithm() bool { + if o != nil && !IsNil(o.PublicKeyAlgorithm) { + return true + } + + return false +} + +// SetPublicKeyAlgorithm gets a reference to the given int64 and assigns it to the PublicKeyAlgorithm field. +func (o *IdentityCredentialsWebAuthnAttestation) SetPublicKeyAlgorithm(v int64) { + o.PublicKeyAlgorithm = &v +} + +func (o IdentityCredentialsWebAuthnAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AuthenticatorData) { + toSerialize["authenticator_data"] = o.AuthenticatorData + } + if !IsNil(o.ClientDataJSON) { + toSerialize["client_dataJSON"] = o.ClientDataJSON + } + if !IsNil(o.ClientDataHash) { + toSerialize["client_data_hash"] = o.ClientDataHash + } + if !IsNil(o.Object) { + toSerialize["object"] = o.Object + } + if !IsNil(o.PublicKeyAlgorithm) { + toSerialize["public_key_algorithm"] = o.PublicKeyAlgorithm + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnAttestation) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnAttestation := _IdentityCredentialsWebAuthnAttestation{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnAttestation) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnAttestation(varIdentityCredentialsWebAuthnAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "authenticator_data") + delete(additionalProperties, "client_dataJSON") + delete(additionalProperties, "client_data_hash") + delete(additionalProperties, "object") + delete(additionalProperties, "public_key_algorithm") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnAttestation struct { + value *IdentityCredentialsWebAuthnAttestation + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) Get() *IdentityCredentialsWebAuthnAttestation { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) Set(val *IdentityCredentialsWebAuthnAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnAttestation(val *IdentityCredentialsWebAuthnAttestation) *NullableIdentityCredentialsWebAuthnAttestation { + return &NullableIdentityCredentialsWebAuthnAttestation{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_web_authn_authenticator.go b/pkg/client-go/model_identity_credentials_web_authn_authenticator.go new file mode 100644 index 0000000..b7ee96f --- /dev/null +++ b/pkg/client-go/model_identity_credentials_web_authn_authenticator.go @@ -0,0 +1,228 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnAuthenticator type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnAuthenticator{} + +// IdentityCredentialsWebAuthnAuthenticator struct for IdentityCredentialsWebAuthnAuthenticator +type IdentityCredentialsWebAuthnAuthenticator struct { + Aaguid []int32 `json:"aaguid,omitempty"` + CloneWarning *bool `json:"clone_warning,omitempty"` + SignCount *int32 `json:"sign_count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnAuthenticator IdentityCredentialsWebAuthnAuthenticator + +// NewIdentityCredentialsWebAuthnAuthenticator instantiates a new IdentityCredentialsWebAuthnAuthenticator 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 NewIdentityCredentialsWebAuthnAuthenticator() *IdentityCredentialsWebAuthnAuthenticator { + this := IdentityCredentialsWebAuthnAuthenticator{} + return &this +} + +// NewIdentityCredentialsWebAuthnAuthenticatorWithDefaults instantiates a new IdentityCredentialsWebAuthnAuthenticator 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 NewIdentityCredentialsWebAuthnAuthenticatorWithDefaults() *IdentityCredentialsWebAuthnAuthenticator { + this := IdentityCredentialsWebAuthnAuthenticator{} + return &this +} + +// GetAaguid returns the Aaguid field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetAaguid() []int32 { + if o == nil || IsNil(o.Aaguid) { + var ret []int32 + return ret + } + return o.Aaguid +} + +// GetAaguidOk returns a tuple with the Aaguid field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetAaguidOk() ([]int32, bool) { + if o == nil || IsNil(o.Aaguid) { + return nil, false + } + return o.Aaguid, true +} + +// HasAaguid returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasAaguid() bool { + if o != nil && !IsNil(o.Aaguid) { + return true + } + + return false +} + +// SetAaguid gets a reference to the given []int32 and assigns it to the Aaguid field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetAaguid(v []int32) { + o.Aaguid = v +} + +// GetCloneWarning returns the CloneWarning field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetCloneWarning() bool { + if o == nil || IsNil(o.CloneWarning) { + var ret bool + return ret + } + return *o.CloneWarning +} + +// GetCloneWarningOk returns a tuple with the CloneWarning field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetCloneWarningOk() (*bool, bool) { + if o == nil || IsNil(o.CloneWarning) { + return nil, false + } + return o.CloneWarning, true +} + +// HasCloneWarning returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasCloneWarning() bool { + if o != nil && !IsNil(o.CloneWarning) { + return true + } + + return false +} + +// SetCloneWarning gets a reference to the given bool and assigns it to the CloneWarning field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetCloneWarning(v bool) { + o.CloneWarning = &v +} + +// GetSignCount returns the SignCount field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetSignCount() int32 { + if o == nil || IsNil(o.SignCount) { + var ret int32 + return ret + } + return *o.SignCount +} + +// GetSignCountOk returns a tuple with the SignCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetSignCountOk() (*int32, bool) { + if o == nil || IsNil(o.SignCount) { + return nil, false + } + return o.SignCount, true +} + +// HasSignCount returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasSignCount() bool { + if o != nil && !IsNil(o.SignCount) { + return true + } + + return false +} + +// SetSignCount gets a reference to the given int32 and assigns it to the SignCount field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetSignCount(v int32) { + o.SignCount = &v +} + +func (o IdentityCredentialsWebAuthnAuthenticator) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnAuthenticator) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Aaguid) { + toSerialize["aaguid"] = o.Aaguid + } + if !IsNil(o.CloneWarning) { + toSerialize["clone_warning"] = o.CloneWarning + } + if !IsNil(o.SignCount) { + toSerialize["sign_count"] = o.SignCount + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnAuthenticator) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnAuthenticator := _IdentityCredentialsWebAuthnAuthenticator{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnAuthenticator) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnAuthenticator(varIdentityCredentialsWebAuthnAuthenticator) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "aaguid") + delete(additionalProperties, "clone_warning") + delete(additionalProperties, "sign_count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnAuthenticator struct { + value *IdentityCredentialsWebAuthnAuthenticator + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) Get() *IdentityCredentialsWebAuthnAuthenticator { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) Set(val *IdentityCredentialsWebAuthnAuthenticator) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnAuthenticator(val *IdentityCredentialsWebAuthnAuthenticator) *NullableIdentityCredentialsWebAuthnAuthenticator { + return &NullableIdentityCredentialsWebAuthnAuthenticator{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_credentials_web_authn_flags.go b/pkg/client-go/model_identity_credentials_web_authn_flags.go new file mode 100644 index 0000000..3100f0a --- /dev/null +++ b/pkg/client-go/model_identity_credentials_web_authn_flags.go @@ -0,0 +1,265 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnFlags type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnFlags{} + +// IdentityCredentialsWebAuthnFlags struct for IdentityCredentialsWebAuthnFlags +type IdentityCredentialsWebAuthnFlags struct { + BackupEligible *bool `json:"backup_eligible,omitempty"` + BackupState *bool `json:"backup_state,omitempty"` + UserPresent *bool `json:"user_present,omitempty"` + UserVerified *bool `json:"user_verified,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnFlags IdentityCredentialsWebAuthnFlags + +// NewIdentityCredentialsWebAuthnFlags instantiates a new IdentityCredentialsWebAuthnFlags 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 NewIdentityCredentialsWebAuthnFlags() *IdentityCredentialsWebAuthnFlags { + this := IdentityCredentialsWebAuthnFlags{} + return &this +} + +// NewIdentityCredentialsWebAuthnFlagsWithDefaults instantiates a new IdentityCredentialsWebAuthnFlags 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 NewIdentityCredentialsWebAuthnFlagsWithDefaults() *IdentityCredentialsWebAuthnFlags { + this := IdentityCredentialsWebAuthnFlags{} + return &this +} + +// GetBackupEligible returns the BackupEligible field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupEligible() bool { + if o == nil || IsNil(o.BackupEligible) { + var ret bool + return ret + } + return *o.BackupEligible +} + +// GetBackupEligibleOk returns a tuple with the BackupEligible field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupEligibleOk() (*bool, bool) { + if o == nil || IsNil(o.BackupEligible) { + return nil, false + } + return o.BackupEligible, true +} + +// HasBackupEligible returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasBackupEligible() bool { + if o != nil && !IsNil(o.BackupEligible) { + return true + } + + return false +} + +// SetBackupEligible gets a reference to the given bool and assigns it to the BackupEligible field. +func (o *IdentityCredentialsWebAuthnFlags) SetBackupEligible(v bool) { + o.BackupEligible = &v +} + +// GetBackupState returns the BackupState field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupState() bool { + if o == nil || IsNil(o.BackupState) { + var ret bool + return ret + } + return *o.BackupState +} + +// GetBackupStateOk returns a tuple with the BackupState field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupStateOk() (*bool, bool) { + if o == nil || IsNil(o.BackupState) { + return nil, false + } + return o.BackupState, true +} + +// HasBackupState returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasBackupState() bool { + if o != nil && !IsNil(o.BackupState) { + return true + } + + return false +} + +// SetBackupState gets a reference to the given bool and assigns it to the BackupState field. +func (o *IdentityCredentialsWebAuthnFlags) SetBackupState(v bool) { + o.BackupState = &v +} + +// GetUserPresent returns the UserPresent field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetUserPresent() bool { + if o == nil || IsNil(o.UserPresent) { + var ret bool + return ret + } + return *o.UserPresent +} + +// GetUserPresentOk returns a tuple with the UserPresent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetUserPresentOk() (*bool, bool) { + if o == nil || IsNil(o.UserPresent) { + return nil, false + } + return o.UserPresent, true +} + +// HasUserPresent returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasUserPresent() bool { + if o != nil && !IsNil(o.UserPresent) { + return true + } + + return false +} + +// SetUserPresent gets a reference to the given bool and assigns it to the UserPresent field. +func (o *IdentityCredentialsWebAuthnFlags) SetUserPresent(v bool) { + o.UserPresent = &v +} + +// GetUserVerified returns the UserVerified field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetUserVerified() bool { + if o == nil || IsNil(o.UserVerified) { + var ret bool + return ret + } + return *o.UserVerified +} + +// GetUserVerifiedOk returns a tuple with the UserVerified field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetUserVerifiedOk() (*bool, bool) { + if o == nil || IsNil(o.UserVerified) { + return nil, false + } + return o.UserVerified, true +} + +// HasUserVerified returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasUserVerified() bool { + if o != nil && !IsNil(o.UserVerified) { + return true + } + + return false +} + +// SetUserVerified gets a reference to the given bool and assigns it to the UserVerified field. +func (o *IdentityCredentialsWebAuthnFlags) SetUserVerified(v bool) { + o.UserVerified = &v +} + +func (o IdentityCredentialsWebAuthnFlags) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnFlags) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.BackupEligible) { + toSerialize["backup_eligible"] = o.BackupEligible + } + if !IsNil(o.BackupState) { + toSerialize["backup_state"] = o.BackupState + } + if !IsNil(o.UserPresent) { + toSerialize["user_present"] = o.UserPresent + } + if !IsNil(o.UserVerified) { + toSerialize["user_verified"] = o.UserVerified + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnFlags) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnFlags := _IdentityCredentialsWebAuthnFlags{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnFlags) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnFlags(varIdentityCredentialsWebAuthnFlags) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "backup_eligible") + delete(additionalProperties, "backup_state") + delete(additionalProperties, "user_present") + delete(additionalProperties, "user_verified") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnFlags struct { + value *IdentityCredentialsWebAuthnFlags + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnFlags) Get() *IdentityCredentialsWebAuthnFlags { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) Set(val *IdentityCredentialsWebAuthnFlags) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnFlags) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnFlags(val *IdentityCredentialsWebAuthnFlags) *NullableIdentityCredentialsWebAuthnFlags { + return &NullableIdentityCredentialsWebAuthnFlags{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnFlags) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_patch.go b/pkg/client-go/model_identity_patch.go new file mode 100644 index 0000000..1dc2eb4 --- /dev/null +++ b/pkg/client-go/model_identity_patch.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityPatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityPatch{} + +// IdentityPatch Payload for patching an identity +type IdentityPatch struct { + Create *CreateIdentityBody `json:"create,omitempty"` + // The ID of this patch. The patch ID is optional. If specified, the ID will be returned in the response, so consumers of this API can correlate the response with the patch. + PatchId *string `json:"patch_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityPatch IdentityPatch + +// NewIdentityPatch instantiates a new IdentityPatch 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 NewIdentityPatch() *IdentityPatch { + this := IdentityPatch{} + return &this +} + +// NewIdentityPatchWithDefaults instantiates a new IdentityPatch 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 NewIdentityPatchWithDefaults() *IdentityPatch { + this := IdentityPatch{} + return &this +} + +// GetCreate returns the Create field value if set, zero value otherwise. +func (o *IdentityPatch) GetCreate() CreateIdentityBody { + if o == nil || IsNil(o.Create) { + var ret CreateIdentityBody + return ret + } + return *o.Create +} + +// GetCreateOk returns a tuple with the Create field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatch) GetCreateOk() (*CreateIdentityBody, bool) { + if o == nil || IsNil(o.Create) { + return nil, false + } + return o.Create, true +} + +// HasCreate returns a boolean if a field has been set. +func (o *IdentityPatch) HasCreate() bool { + if o != nil && !IsNil(o.Create) { + return true + } + + return false +} + +// SetCreate gets a reference to the given CreateIdentityBody and assigns it to the Create field. +func (o *IdentityPatch) SetCreate(v CreateIdentityBody) { + o.Create = &v +} + +// GetPatchId returns the PatchId field value if set, zero value otherwise. +func (o *IdentityPatch) GetPatchId() string { + if o == nil || IsNil(o.PatchId) { + var ret string + return ret + } + return *o.PatchId +} + +// GetPatchIdOk returns a tuple with the PatchId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatch) GetPatchIdOk() (*string, bool) { + if o == nil || IsNil(o.PatchId) { + return nil, false + } + return o.PatchId, true +} + +// HasPatchId returns a boolean if a field has been set. +func (o *IdentityPatch) HasPatchId() bool { + if o != nil && !IsNil(o.PatchId) { + return true + } + + return false +} + +// SetPatchId gets a reference to the given string and assigns it to the PatchId field. +func (o *IdentityPatch) SetPatchId(v string) { + o.PatchId = &v +} + +func (o IdentityPatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityPatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Create) { + toSerialize["create"] = o.Create + } + if !IsNil(o.PatchId) { + toSerialize["patch_id"] = o.PatchId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityPatch) UnmarshalJSON(data []byte) (err error) { + varIdentityPatch := _IdentityPatch{} + + err = json.Unmarshal(data, &varIdentityPatch) + + if err != nil { + return err + } + + *o = IdentityPatch(varIdentityPatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "create") + delete(additionalProperties, "patch_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityPatch struct { + value *IdentityPatch + isSet bool +} + +func (v NullableIdentityPatch) Get() *IdentityPatch { + return v.value +} + +func (v *NullableIdentityPatch) Set(val *IdentityPatch) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityPatch) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityPatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityPatch(val *IdentityPatch) *NullableIdentityPatch { + return &NullableIdentityPatch{value: val, isSet: true} +} + +func (v NullableIdentityPatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityPatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_patch_response.go b/pkg/client-go/model_identity_patch_response.go new file mode 100644 index 0000000..4ad5104 --- /dev/null +++ b/pkg/client-go/model_identity_patch_response.go @@ -0,0 +1,270 @@ +/* +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" +) + +// checks if the IdentityPatchResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityPatchResponse{} + +// IdentityPatchResponse Response for a single identity patch +type IdentityPatchResponse struct { + // The action for this specific patch create ActionCreate Create this identity. error ActionError Error indicates that the patch failed. + Action *string `json:"action,omitempty"` + // From https://go.dev/wiki/CodeReviewComments#receiver-type: > Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer. > If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, > prefer a pointer receiver, as it will make the intention clearer to the reader. > Don’t mix receiver types. Choose either pointers or struct types for all available methods. + 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. + PatchId *string `json:"patch_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityPatchResponse IdentityPatchResponse + +// NewIdentityPatchResponse instantiates a new IdentityPatchResponse 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 NewIdentityPatchResponse() *IdentityPatchResponse { + this := IdentityPatchResponse{} + return &this +} + +// NewIdentityPatchResponseWithDefaults instantiates a new IdentityPatchResponse 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 NewIdentityPatchResponseWithDefaults() *IdentityPatchResponse { + this := IdentityPatchResponse{} + return &this +} + +// GetAction returns the Action field value if set, zero value otherwise. +func (o *IdentityPatchResponse) GetAction() string { + if o == nil || IsNil(o.Action) { + var ret string + return ret + } + return *o.Action +} + +// GetActionOk returns a tuple with the Action field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetActionOk() (*string, bool) { + if o == nil || IsNil(o.Action) { + return nil, false + } + return o.Action, true +} + +// HasAction returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasAction() bool { + if o != nil && !IsNil(o.Action) { + return true + } + + return false +} + +// SetAction gets a reference to the given string and assigns it to the Action field. +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 || IsNil(o.Error) { + 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 && !IsNil(o.Error) { + 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 || IsNil(o.Identity) { + var ret string + return ret + } + return *o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetIdentityOk() (*string, bool) { + if o == nil || IsNil(o.Identity) { + return nil, false + } + return o.Identity, true +} + +// HasIdentity returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasIdentity() bool { + if o != nil && !IsNil(o.Identity) { + return true + } + + return false +} + +// SetIdentity gets a reference to the given string and assigns it to the Identity field. +func (o *IdentityPatchResponse) SetIdentity(v string) { + o.Identity = &v +} + +// GetPatchId returns the PatchId field value if set, zero value otherwise. +func (o *IdentityPatchResponse) GetPatchId() string { + if o == nil || IsNil(o.PatchId) { + var ret string + return ret + } + return *o.PatchId +} + +// GetPatchIdOk returns a tuple with the PatchId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetPatchIdOk() (*string, bool) { + if o == nil || IsNil(o.PatchId) { + return nil, false + } + return o.PatchId, true +} + +// HasPatchId returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasPatchId() bool { + if o != nil && !IsNil(o.PatchId) { + return true + } + + return false +} + +// SetPatchId gets a reference to the given string and assigns it to the PatchId field. +func (o *IdentityPatchResponse) SetPatchId(v string) { + o.PatchId = &v +} + +func (o IdentityPatchResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityPatchResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Action) { + toSerialize["action"] = o.Action + } + if o.Error != nil { + toSerialize["error"] = o.Error + } + if !IsNil(o.Identity) { + toSerialize["identity"] = o.Identity + } + if !IsNil(o.PatchId) { + toSerialize["patch_id"] = o.PatchId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityPatchResponse) UnmarshalJSON(data []byte) (err error) { + varIdentityPatchResponse := _IdentityPatchResponse{} + + err = json.Unmarshal(data, &varIdentityPatchResponse) + + if err != nil { + return err + } + + *o = IdentityPatchResponse(varIdentityPatchResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "error") + delete(additionalProperties, "identity") + delete(additionalProperties, "patch_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityPatchResponse struct { + value *IdentityPatchResponse + isSet bool +} + +func (v NullableIdentityPatchResponse) Get() *IdentityPatchResponse { + return v.value +} + +func (v *NullableIdentityPatchResponse) Set(val *IdentityPatchResponse) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityPatchResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityPatchResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityPatchResponse(val *IdentityPatchResponse) *NullableIdentityPatchResponse { + return &NullableIdentityPatchResponse{value: val, isSet: true} +} + +func (v NullableIdentityPatchResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityPatchResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_schema_container.go b/pkg/client-go/model_identity_schema_container.go new file mode 100644 index 0000000..b3e4cf3 --- /dev/null +++ b/pkg/client-go/model_identity_schema_container.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the IdentitySchemaContainer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentitySchemaContainer{} + +// IdentitySchemaContainer An Identity JSON Schema Container +type IdentitySchemaContainer struct { + // The ID of the Identity JSON Schema + Id string `json:"id"` + // The actual Identity JSON Schema + Schema map[string]interface{} `json:"schema"` + AdditionalProperties map[string]interface{} +} + +type _IdentitySchemaContainer IdentitySchemaContainer + +// NewIdentitySchemaContainer instantiates a new IdentitySchemaContainer 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 NewIdentitySchemaContainer(id string, schema map[string]interface{}) *IdentitySchemaContainer { + this := IdentitySchemaContainer{} + this.Id = id + this.Schema = schema + return &this +} + +// NewIdentitySchemaContainerWithDefaults instantiates a new IdentitySchemaContainer 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 NewIdentitySchemaContainerWithDefaults() *IdentitySchemaContainer { + this := IdentitySchemaContainer{} + return &this +} + +// GetId returns the Id field value +func (o *IdentitySchemaContainer) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *IdentitySchemaContainer) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *IdentitySchemaContainer) SetId(v string) { + o.Id = v +} + +// GetSchema returns the Schema field value +func (o *IdentitySchemaContainer) GetSchema() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Schema +} + +// GetSchemaOk returns a tuple with the Schema field value +// and a boolean to check if the value has been set. +func (o *IdentitySchemaContainer) GetSchemaOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Schema, true +} + +// SetSchema sets field value +func (o *IdentitySchemaContainer) SetSchema(v map[string]interface{}) { + o.Schema = v +} + +func (o IdentitySchemaContainer) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentitySchemaContainer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["schema"] = o.Schema + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentitySchemaContainer) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "schema", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentitySchemaContainer := _IdentitySchemaContainer{} + + err = json.Unmarshal(data, &varIdentitySchemaContainer) + + if err != nil { + return err + } + + *o = IdentitySchemaContainer(varIdentitySchemaContainer) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "schema") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentitySchemaContainer struct { + value *IdentitySchemaContainer + isSet bool +} + +func (v NullableIdentitySchemaContainer) Get() *IdentitySchemaContainer { + return v.value +} + +func (v *NullableIdentitySchemaContainer) Set(val *IdentitySchemaContainer) { + v.value = val + v.isSet = true +} + +func (v NullableIdentitySchemaContainer) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentitySchemaContainer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentitySchemaContainer(val *IdentitySchemaContainer) *NullableIdentitySchemaContainer { + return &NullableIdentitySchemaContainer{value: val, isSet: true} +} + +func (v NullableIdentitySchemaContainer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentitySchemaContainer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials.go b/pkg/client-go/model_identity_with_credentials.go new file mode 100644 index 0000000..9d188ef --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials.go @@ -0,0 +1,376 @@ +/* +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" +) + +// checks if the IdentityWithCredentials type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentials{} + +// IdentityWithCredentials Create Identity and Import Credentials +type IdentityWithCredentials struct { + LookupSecret *AdminIdentityImportCredentialsLookupSecret `json:"lookup_secret,omitempty"` + Oidc *IdentityWithCredentialsOidc `json:"oidc,omitempty"` + Passkey *IdentityWithCredentialsPasskey `json:"passkey,omitempty"` + Password *IdentityWithCredentialsPassword `json:"password,omitempty"` + Saml *IdentityWithCredentialsSaml `json:"saml,omitempty"` + Totp *IdentityWithCredentialsTotp `json:"totp,omitempty"` + Webauthn *IdentityWithCredentialsWebAuthn `json:"webauthn,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentials IdentityWithCredentials + +// NewIdentityWithCredentials instantiates a new IdentityWithCredentials 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 NewIdentityWithCredentials() *IdentityWithCredentials { + this := IdentityWithCredentials{} + return &this +} + +// NewIdentityWithCredentialsWithDefaults instantiates a new IdentityWithCredentials 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 NewIdentityWithCredentialsWithDefaults() *IdentityWithCredentials { + this := IdentityWithCredentials{} + return &this +} + +// GetLookupSecret returns the LookupSecret field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetLookupSecret() AdminIdentityImportCredentialsLookupSecret { + if o == nil || IsNil(o.LookupSecret) { + var ret AdminIdentityImportCredentialsLookupSecret + return ret + } + return *o.LookupSecret +} + +// GetLookupSecretOk returns a tuple with the LookupSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetLookupSecretOk() (*AdminIdentityImportCredentialsLookupSecret, bool) { + if o == nil || IsNil(o.LookupSecret) { + return nil, false + } + return o.LookupSecret, true +} + +// HasLookupSecret returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasLookupSecret() bool { + if o != nil && !IsNil(o.LookupSecret) { + return true + } + + return false +} + +// SetLookupSecret gets a reference to the given AdminIdentityImportCredentialsLookupSecret and assigns it to the LookupSecret field. +func (o *IdentityWithCredentials) SetLookupSecret(v AdminIdentityImportCredentialsLookupSecret) { + o.LookupSecret = &v +} + +// GetOidc returns the Oidc field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetOidc() IdentityWithCredentialsOidc { + if o == nil || IsNil(o.Oidc) { + var ret IdentityWithCredentialsOidc + return ret + } + return *o.Oidc +} + +// GetOidcOk returns a tuple with the Oidc field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetOidcOk() (*IdentityWithCredentialsOidc, bool) { + if o == nil || IsNil(o.Oidc) { + return nil, false + } + return o.Oidc, true +} + +// HasOidc returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasOidc() bool { + if o != nil && !IsNil(o.Oidc) { + return true + } + + return false +} + +// SetOidc gets a reference to the given IdentityWithCredentialsOidc and assigns it to the Oidc field. +func (o *IdentityWithCredentials) SetOidc(v IdentityWithCredentialsOidc) { + o.Oidc = &v +} + +// GetPasskey returns the Passkey field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetPasskey() IdentityWithCredentialsPasskey { + if o == nil || IsNil(o.Passkey) { + var ret IdentityWithCredentialsPasskey + return ret + } + return *o.Passkey +} + +// GetPasskeyOk returns a tuple with the Passkey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetPasskeyOk() (*IdentityWithCredentialsPasskey, bool) { + if o == nil || IsNil(o.Passkey) { + return nil, false + } + return o.Passkey, true +} + +// HasPasskey returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasPasskey() bool { + if o != nil && !IsNil(o.Passkey) { + return true + } + + return false +} + +// SetPasskey gets a reference to the given IdentityWithCredentialsPasskey and assigns it to the Passkey field. +func (o *IdentityWithCredentials) SetPasskey(v IdentityWithCredentialsPasskey) { + o.Passkey = &v +} + +// GetPassword returns the Password field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetPassword() IdentityWithCredentialsPassword { + if o == nil || IsNil(o.Password) { + var ret IdentityWithCredentialsPassword + return ret + } + return *o.Password +} + +// GetPasswordOk returns a tuple with the Password field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetPasswordOk() (*IdentityWithCredentialsPassword, bool) { + if o == nil || IsNil(o.Password) { + return nil, false + } + return o.Password, true +} + +// HasPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasPassword() bool { + if o != nil && !IsNil(o.Password) { + return true + } + + return false +} + +// SetPassword gets a reference to the given IdentityWithCredentialsPassword and assigns it to the Password field. +func (o *IdentityWithCredentials) SetPassword(v IdentityWithCredentialsPassword) { + o.Password = &v +} + +// GetSaml returns the Saml field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetSaml() IdentityWithCredentialsSaml { + if o == nil || IsNil(o.Saml) { + var ret IdentityWithCredentialsSaml + return ret + } + return *o.Saml +} + +// GetSamlOk returns a tuple with the Saml field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetSamlOk() (*IdentityWithCredentialsSaml, bool) { + if o == nil || IsNil(o.Saml) { + return nil, false + } + return o.Saml, true +} + +// HasSaml returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasSaml() bool { + if o != nil && !IsNil(o.Saml) { + return true + } + + return false +} + +// SetSaml gets a reference to the given IdentityWithCredentialsSaml and assigns it to the Saml field. +func (o *IdentityWithCredentials) SetSaml(v IdentityWithCredentialsSaml) { + o.Saml = &v +} + +// GetTotp returns the Totp field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetTotp() IdentityWithCredentialsTotp { + if o == nil || IsNil(o.Totp) { + var ret IdentityWithCredentialsTotp + return ret + } + return *o.Totp +} + +// GetTotpOk returns a tuple with the Totp field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetTotpOk() (*IdentityWithCredentialsTotp, bool) { + if o == nil || IsNil(o.Totp) { + return nil, false + } + return o.Totp, true +} + +// HasTotp returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasTotp() bool { + if o != nil && !IsNil(o.Totp) { + return true + } + + return false +} + +// SetTotp gets a reference to the given IdentityWithCredentialsTotp and assigns it to the Totp field. +func (o *IdentityWithCredentials) SetTotp(v IdentityWithCredentialsTotp) { + o.Totp = &v +} + +// GetWebauthn returns the Webauthn field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetWebauthn() IdentityWithCredentialsWebAuthn { + if o == nil || IsNil(o.Webauthn) { + var ret IdentityWithCredentialsWebAuthn + return ret + } + return *o.Webauthn +} + +// GetWebauthnOk returns a tuple with the Webauthn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetWebauthnOk() (*IdentityWithCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Webauthn) { + return nil, false + } + return o.Webauthn, true +} + +// HasWebauthn returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasWebauthn() bool { + if o != nil && !IsNil(o.Webauthn) { + return true + } + + return false +} + +// SetWebauthn gets a reference to the given IdentityWithCredentialsWebAuthn and assigns it to the Webauthn field. +func (o *IdentityWithCredentials) SetWebauthn(v IdentityWithCredentialsWebAuthn) { + o.Webauthn = &v +} + +func (o IdentityWithCredentials) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentials) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.LookupSecret) { + toSerialize["lookup_secret"] = o.LookupSecret + } + if !IsNil(o.Oidc) { + toSerialize["oidc"] = o.Oidc + } + if !IsNil(o.Passkey) { + toSerialize["passkey"] = o.Passkey + } + if !IsNil(o.Password) { + toSerialize["password"] = o.Password + } + if !IsNil(o.Saml) { + toSerialize["saml"] = o.Saml + } + if !IsNil(o.Totp) { + toSerialize["totp"] = o.Totp + } + if !IsNil(o.Webauthn) { + toSerialize["webauthn"] = o.Webauthn + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentials) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentials := _IdentityWithCredentials{} + + err = json.Unmarshal(data, &varIdentityWithCredentials) + + if err != nil { + return err + } + + *o = IdentityWithCredentials(varIdentityWithCredentials) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "lookup_secret") + delete(additionalProperties, "oidc") + delete(additionalProperties, "passkey") + delete(additionalProperties, "password") + delete(additionalProperties, "saml") + delete(additionalProperties, "totp") + delete(additionalProperties, "webauthn") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentials struct { + value *IdentityWithCredentials + isSet bool +} + +func (v NullableIdentityWithCredentials) Get() *IdentityWithCredentials { + return v.value +} + +func (v *NullableIdentityWithCredentials) Set(val *IdentityWithCredentials) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentials) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentials) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentials(val *IdentityWithCredentials) *NullableIdentityWithCredentials { + return &NullableIdentityWithCredentials{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentials) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentials) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_oidc.go b/pkg/client-go/model_identity_with_credentials_oidc.go new file mode 100644 index 0000000..307b9ee --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_oidc.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsOidc type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidc{} + +// IdentityWithCredentialsOidc Create Identity and Import Social Sign In Credentials +type IdentityWithCredentialsOidc struct { + Config *IdentityWithCredentialsOidcConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidc IdentityWithCredentialsOidc + +// NewIdentityWithCredentialsOidc instantiates a new IdentityWithCredentialsOidc 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 NewIdentityWithCredentialsOidc() *IdentityWithCredentialsOidc { + this := IdentityWithCredentialsOidc{} + return &this +} + +// NewIdentityWithCredentialsOidcWithDefaults instantiates a new IdentityWithCredentialsOidc 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 NewIdentityWithCredentialsOidcWithDefaults() *IdentityWithCredentialsOidc { + this := IdentityWithCredentialsOidc{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidc) GetConfig() IdentityWithCredentialsOidcConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsOidcConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidc) GetConfigOk() (*IdentityWithCredentialsOidcConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidc) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsOidcConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsOidc) SetConfig(v IdentityWithCredentialsOidcConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsOidc) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidc) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidc) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsOidc := _IdentityWithCredentialsOidc{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidc) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidc(varIdentityWithCredentialsOidc) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidc struct { + value *IdentityWithCredentialsOidc + isSet bool +} + +func (v NullableIdentityWithCredentialsOidc) Get() *IdentityWithCredentialsOidc { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidc) Set(val *IdentityWithCredentialsOidc) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidc) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidc) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidc(val *IdentityWithCredentialsOidc) *NullableIdentityWithCredentialsOidc { + return &NullableIdentityWithCredentialsOidc{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidc) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidc) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_oidc_config.go b/pkg/client-go/model_identity_with_credentials_oidc_config.go new file mode 100644 index 0000000..ffe6f89 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_oidc_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsOidcConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidcConfig{} + +// IdentityWithCredentialsOidcConfig struct for IdentityWithCredentialsOidcConfig +type IdentityWithCredentialsOidcConfig struct { + // A list of OpenID Connect Providers + Providers []IdentityWithCredentialsOidcConfigProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidcConfig IdentityWithCredentialsOidcConfig + +// NewIdentityWithCredentialsOidcConfig instantiates a new IdentityWithCredentialsOidcConfig 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 NewIdentityWithCredentialsOidcConfig() *IdentityWithCredentialsOidcConfig { + this := IdentityWithCredentialsOidcConfig{} + return &this +} + +// NewIdentityWithCredentialsOidcConfigWithDefaults instantiates a new IdentityWithCredentialsOidcConfig 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 NewIdentityWithCredentialsOidcConfigWithDefaults() *IdentityWithCredentialsOidcConfig { + this := IdentityWithCredentialsOidcConfig{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidcConfig) GetProviders() []IdentityWithCredentialsOidcConfigProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityWithCredentialsOidcConfigProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfig) GetProvidersOk() ([]IdentityWithCredentialsOidcConfigProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfig) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityWithCredentialsOidcConfigProvider and assigns it to the Providers field. +func (o *IdentityWithCredentialsOidcConfig) SetProviders(v []IdentityWithCredentialsOidcConfigProvider) { + o.Providers = v +} + +func (o IdentityWithCredentialsOidcConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidcConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidcConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsOidcConfig := _IdentityWithCredentialsOidcConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidcConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidcConfig(varIdentityWithCredentialsOidcConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidcConfig struct { + value *IdentityWithCredentialsOidcConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsOidcConfig) Get() *IdentityWithCredentialsOidcConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidcConfig) Set(val *IdentityWithCredentialsOidcConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidcConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidcConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidcConfig(val *IdentityWithCredentialsOidcConfig) *NullableIdentityWithCredentialsOidcConfig { + return &NullableIdentityWithCredentialsOidcConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidcConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidcConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_oidc_config_provider.go b/pkg/client-go/model_identity_with_credentials_oidc_config_provider.go new file mode 100644 index 0000000..ec9f565 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_oidc_config_provider.go @@ -0,0 +1,284 @@ +/* +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" + "fmt" +) + +// checks if the IdentityWithCredentialsOidcConfigProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidcConfigProvider{} + +// IdentityWithCredentialsOidcConfigProvider Create Identity and Import Social Sign In Credentials Configuration +type IdentityWithCredentialsOidcConfigProvider struct { + Organization NullableString `json:"organization,omitempty"` + // The OpenID Connect provider to link the subject to. Usually something like `google` or `github`. + Provider string `json:"provider"` + // The subject (`sub`) of the OpenID Connect connection. Usually the `sub` field of the ID Token. + Subject string `json:"subject"` + // If set, this credential allows the user to sign in using the OpenID Connect provider without setting the subject first. + UseAutoLink *bool `json:"use_auto_link,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidcConfigProvider IdentityWithCredentialsOidcConfigProvider + +// NewIdentityWithCredentialsOidcConfigProvider instantiates a new IdentityWithCredentialsOidcConfigProvider 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 NewIdentityWithCredentialsOidcConfigProvider(provider string, subject string) *IdentityWithCredentialsOidcConfigProvider { + this := IdentityWithCredentialsOidcConfigProvider{} + this.Provider = provider + this.Subject = subject + return &this +} + +// NewIdentityWithCredentialsOidcConfigProviderWithDefaults instantiates a new IdentityWithCredentialsOidcConfigProvider 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 NewIdentityWithCredentialsOidcConfigProviderWithDefaults() *IdentityWithCredentialsOidcConfigProvider { + this := IdentityWithCredentialsOidcConfigProvider{} + return &this +} + +// GetOrganization returns the Organization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityWithCredentialsOidcConfigProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization.Get()) { + var ret string + return ret + } + return *o.Organization.Get() +} + +// GetOrganizationOk returns a tuple with the Organization 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 *IdentityWithCredentialsOidcConfigProvider) GetOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Organization.Get(), o.Organization.IsSet() +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) HasOrganization() bool { + if o != nil && o.Organization.IsSet() { + return true + } + + return false +} + +// SetOrganization gets a reference to the given NullableString and assigns it to the Organization field. +func (o *IdentityWithCredentialsOidcConfigProvider) SetOrganization(v string) { + o.Organization.Set(&v) +} + +// SetOrganizationNil sets the value for Organization to be an explicit nil +func (o *IdentityWithCredentialsOidcConfigProvider) SetOrganizationNil() { + o.Organization.Set(nil) +} + +// UnsetOrganization ensures that no value is present for Organization, not even an explicit nil +func (o *IdentityWithCredentialsOidcConfigProvider) UnsetOrganization() { + o.Organization.Unset() +} + +// GetProvider returns the Provider field value +func (o *IdentityWithCredentialsOidcConfigProvider) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *IdentityWithCredentialsOidcConfigProvider) SetProvider(v string) { + o.Provider = v +} + +// GetSubject returns the Subject field value +func (o *IdentityWithCredentialsOidcConfigProvider) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *IdentityWithCredentialsOidcConfigProvider) SetSubject(v string) { + o.Subject = v +} + +// GetUseAutoLink returns the UseAutoLink field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidcConfigProvider) GetUseAutoLink() bool { + if o == nil || IsNil(o.UseAutoLink) { + var ret bool + return ret + } + return *o.UseAutoLink +} + +// GetUseAutoLinkOk returns a tuple with the UseAutoLink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetUseAutoLinkOk() (*bool, bool) { + if o == nil || IsNil(o.UseAutoLink) { + return nil, false + } + return o.UseAutoLink, true +} + +// HasUseAutoLink returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) HasUseAutoLink() bool { + if o != nil && !IsNil(o.UseAutoLink) { + return true + } + + return false +} + +// SetUseAutoLink gets a reference to the given bool and assigns it to the UseAutoLink field. +func (o *IdentityWithCredentialsOidcConfigProvider) SetUseAutoLink(v bool) { + o.UseAutoLink = &v +} + +func (o IdentityWithCredentialsOidcConfigProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidcConfigProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Organization.IsSet() { + toSerialize["organization"] = o.Organization.Get() + } + toSerialize["provider"] = o.Provider + toSerialize["subject"] = o.Subject + if !IsNil(o.UseAutoLink) { + toSerialize["use_auto_link"] = o.UseAutoLink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidcConfigProvider) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider", + "subject", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentityWithCredentialsOidcConfigProvider := _IdentityWithCredentialsOidcConfigProvider{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidcConfigProvider) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidcConfigProvider(varIdentityWithCredentialsOidcConfigProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + delete(additionalProperties, "use_auto_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidcConfigProvider struct { + value *IdentityWithCredentialsOidcConfigProvider + isSet bool +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) Get() *IdentityWithCredentialsOidcConfigProvider { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) Set(val *IdentityWithCredentialsOidcConfigProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidcConfigProvider(val *IdentityWithCredentialsOidcConfigProvider) *NullableIdentityWithCredentialsOidcConfigProvider { + return &NullableIdentityWithCredentialsOidcConfigProvider{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_passkey.go b/pkg/client-go/model_identity_with_credentials_passkey.go new file mode 100644 index 0000000..2e8bf99 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_passkey.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasskey type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasskey{} + +// IdentityWithCredentialsPasskey Create Identity and Import Passkey Credentials +type IdentityWithCredentialsPasskey struct { + Config *IdentityWithCredentialsPasskeyConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasskey IdentityWithCredentialsPasskey + +// NewIdentityWithCredentialsPasskey instantiates a new IdentityWithCredentialsPasskey 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 NewIdentityWithCredentialsPasskey() *IdentityWithCredentialsPasskey { + this := IdentityWithCredentialsPasskey{} + return &this +} + +// NewIdentityWithCredentialsPasskeyWithDefaults instantiates a new IdentityWithCredentialsPasskey 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 NewIdentityWithCredentialsPasskeyWithDefaults() *IdentityWithCredentialsPasskey { + this := IdentityWithCredentialsPasskey{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskey) GetConfig() IdentityWithCredentialsPasskeyConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsPasskeyConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskey) GetConfigOk() (*IdentityWithCredentialsPasskeyConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskey) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsPasskeyConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsPasskey) SetConfig(v IdentityWithCredentialsPasskeyConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsPasskey) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasskey) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasskey) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasskey := _IdentityWithCredentialsPasskey{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasskey) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasskey(varIdentityWithCredentialsPasskey) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasskey struct { + value *IdentityWithCredentialsPasskey + isSet bool +} + +func (v NullableIdentityWithCredentialsPasskey) Get() *IdentityWithCredentialsPasskey { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasskey) Set(val *IdentityWithCredentialsPasskey) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasskey) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasskey) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasskey(val *IdentityWithCredentialsPasskey) *NullableIdentityWithCredentialsPasskey { + return &NullableIdentityWithCredentialsPasskey{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasskey) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasskey) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_passkey_config.go b/pkg/client-go/model_identity_with_credentials_passkey_config.go new file mode 100644 index 0000000..a3de891 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_passkey_config.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasskeyConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasskeyConfig{} + +// IdentityWithCredentialsPasskeyConfig Create Identity and Import Passkey Credentials Configuration +type IdentityWithCredentialsPasskeyConfig struct { + Credentials []IdentityCredentialsWebAuthn `json:"credentials,omitempty"` + // UserHandle is the user handle of the webauthn credential. + UserHandle []int32 `json:"user_handle,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasskeyConfig IdentityWithCredentialsPasskeyConfig + +// NewIdentityWithCredentialsPasskeyConfig instantiates a new IdentityWithCredentialsPasskeyConfig 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 NewIdentityWithCredentialsPasskeyConfig() *IdentityWithCredentialsPasskeyConfig { + this := IdentityWithCredentialsPasskeyConfig{} + return &this +} + +// NewIdentityWithCredentialsPasskeyConfigWithDefaults instantiates a new IdentityWithCredentialsPasskeyConfig 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 NewIdentityWithCredentialsPasskeyConfigWithDefaults() *IdentityWithCredentialsPasskeyConfig { + this := IdentityWithCredentialsPasskeyConfig{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskeyConfig) GetCredentials() []IdentityCredentialsWebAuthn { + if o == nil || IsNil(o.Credentials) { + var ret []IdentityCredentialsWebAuthn + return ret + } + return o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskeyConfig) GetCredentialsOk() ([]IdentityCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskeyConfig) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given []IdentityCredentialsWebAuthn and assigns it to the Credentials field. +func (o *IdentityWithCredentialsPasskeyConfig) SetCredentials(v []IdentityCredentialsWebAuthn) { + o.Credentials = v +} + +// GetUserHandle returns the UserHandle field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskeyConfig) GetUserHandle() []int32 { + if o == nil || IsNil(o.UserHandle) { + var ret []int32 + return ret + } + return o.UserHandle +} + +// GetUserHandleOk returns a tuple with the UserHandle field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskeyConfig) GetUserHandleOk() ([]int32, bool) { + if o == nil || IsNil(o.UserHandle) { + return nil, false + } + return o.UserHandle, true +} + +// HasUserHandle returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskeyConfig) HasUserHandle() bool { + if o != nil && !IsNil(o.UserHandle) { + return true + } + + return false +} + +// SetUserHandle gets a reference to the given []int32 and assigns it to the UserHandle field. +func (o *IdentityWithCredentialsPasskeyConfig) SetUserHandle(v []int32) { + o.UserHandle = v +} + +func (o IdentityWithCredentialsPasskeyConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasskeyConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.UserHandle) { + toSerialize["user_handle"] = o.UserHandle + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasskeyConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasskeyConfig := _IdentityWithCredentialsPasskeyConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasskeyConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasskeyConfig(varIdentityWithCredentialsPasskeyConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "user_handle") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasskeyConfig struct { + value *IdentityWithCredentialsPasskeyConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) Get() *IdentityWithCredentialsPasskeyConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) Set(val *IdentityWithCredentialsPasskeyConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasskeyConfig(val *IdentityWithCredentialsPasskeyConfig) *NullableIdentityWithCredentialsPasskeyConfig { + return &NullableIdentityWithCredentialsPasskeyConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_password.go b/pkg/client-go/model_identity_with_credentials_password.go new file mode 100644 index 0000000..adc4b65 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_password.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPassword type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPassword{} + +// IdentityWithCredentialsPassword Create Identity and Import Password Credentials +type IdentityWithCredentialsPassword struct { + Config *IdentityWithCredentialsPasswordConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPassword IdentityWithCredentialsPassword + +// NewIdentityWithCredentialsPassword instantiates a new IdentityWithCredentialsPassword 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 NewIdentityWithCredentialsPassword() *IdentityWithCredentialsPassword { + this := IdentityWithCredentialsPassword{} + return &this +} + +// NewIdentityWithCredentialsPasswordWithDefaults instantiates a new IdentityWithCredentialsPassword 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 NewIdentityWithCredentialsPasswordWithDefaults() *IdentityWithCredentialsPassword { + this := IdentityWithCredentialsPassword{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPassword) GetConfig() IdentityWithCredentialsPasswordConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsPasswordConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPassword) GetConfigOk() (*IdentityWithCredentialsPasswordConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPassword) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsPasswordConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsPassword) SetConfig(v IdentityWithCredentialsPasswordConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsPassword) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPassword) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPassword) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPassword := _IdentityWithCredentialsPassword{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPassword) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPassword(varIdentityWithCredentialsPassword) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPassword struct { + value *IdentityWithCredentialsPassword + isSet bool +} + +func (v NullableIdentityWithCredentialsPassword) Get() *IdentityWithCredentialsPassword { + return v.value +} + +func (v *NullableIdentityWithCredentialsPassword) Set(val *IdentityWithCredentialsPassword) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPassword) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPassword) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPassword(val *IdentityWithCredentialsPassword) *NullableIdentityWithCredentialsPassword { + return &NullableIdentityWithCredentialsPassword{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPassword) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPassword) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_password_config.go b/pkg/client-go/model_identity_with_credentials_password_config.go new file mode 100644 index 0000000..26ee07b --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_password_config.go @@ -0,0 +1,231 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasswordConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasswordConfig{} + +// IdentityWithCredentialsPasswordConfig Create Identity and Import Password Credentials Configuration +type IdentityWithCredentialsPasswordConfig struct { + // The hashed password in [PHC format](https://www.ory.com/docs/kratos/manage-identities/import-user-accounts-identities#hashed-passwords) + 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"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasswordConfig IdentityWithCredentialsPasswordConfig + +// NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig 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 NewIdentityWithCredentialsPasswordConfig() *IdentityWithCredentialsPasswordConfig { + this := IdentityWithCredentialsPasswordConfig{} + return &this +} + +// NewIdentityWithCredentialsPasswordConfigWithDefaults instantiates a new IdentityWithCredentialsPasswordConfig 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 NewIdentityWithCredentialsPasswordConfigWithDefaults() *IdentityWithCredentialsPasswordConfig { + this := IdentityWithCredentialsPasswordConfig{} + return &this +} + +// GetHashedPassword returns the HashedPassword field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetHashedPassword() string { + if o == nil || IsNil(o.HashedPassword) { + var ret string + return ret + } + return *o.HashedPassword +} + +// GetHashedPasswordOk returns a tuple with the HashedPassword field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetHashedPasswordOk() (*string, bool) { + if o == nil || IsNil(o.HashedPassword) { + return nil, false + } + return o.HashedPassword, true +} + +// HasHashedPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasHashedPassword() bool { + if o != nil && !IsNil(o.HashedPassword) { + return true + } + + return false +} + +// SetHashedPassword gets a reference to the given string and assigns it to the HashedPassword field. +func (o *IdentityWithCredentialsPasswordConfig) SetHashedPassword(v string) { + o.HashedPassword = &v +} + +// GetPassword returns the Password field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetPassword() string { + if o == nil || IsNil(o.Password) { + var ret string + return ret + } + return *o.Password +} + +// GetPasswordOk returns a tuple with the Password field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetPasswordOk() (*string, bool) { + if o == nil || IsNil(o.Password) { + return nil, false + } + return o.Password, true +} + +// HasPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasPassword() bool { + if o != nil && !IsNil(o.Password) { + return true + } + + return false +} + +// SetPassword gets a reference to the given string and assigns it to the Password field. +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 || IsNil(o.UsePasswordMigrationHook) { + 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 || IsNil(o.UsePasswordMigrationHook) { + 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 && !IsNil(o.UsePasswordMigrationHook) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasswordConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.HashedPassword) { + toSerialize["hashed_password"] = o.HashedPassword + } + if !IsNil(o.Password) { + toSerialize["password"] = o.Password + } + if !IsNil(o.UsePasswordMigrationHook) { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasswordConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasswordConfig := _IdentityWithCredentialsPasswordConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasswordConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasswordConfig(varIdentityWithCredentialsPasswordConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "hashed_password") + delete(additionalProperties, "password") + delete(additionalProperties, "use_password_migration_hook") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasswordConfig struct { + value *IdentityWithCredentialsPasswordConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsPasswordConfig) Get() *IdentityWithCredentialsPasswordConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) Set(val *IdentityWithCredentialsPasswordConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasswordConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasswordConfig(val *IdentityWithCredentialsPasswordConfig) *NullableIdentityWithCredentialsPasswordConfig { + return &NullableIdentityWithCredentialsPasswordConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_saml.go b/pkg/client-go/model_identity_with_credentials_saml.go new file mode 100644 index 0000000..5047d6b --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_saml.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsSaml type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSaml{} + +// IdentityWithCredentialsSaml Payload to import SAML credentials +type IdentityWithCredentialsSaml struct { + Config *IdentityWithCredentialsSamlConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSaml IdentityWithCredentialsSaml + +// NewIdentityWithCredentialsSaml instantiates a new IdentityWithCredentialsSaml 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 NewIdentityWithCredentialsSaml() *IdentityWithCredentialsSaml { + this := IdentityWithCredentialsSaml{} + return &this +} + +// NewIdentityWithCredentialsSamlWithDefaults instantiates a new IdentityWithCredentialsSaml 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 NewIdentityWithCredentialsSamlWithDefaults() *IdentityWithCredentialsSaml { + this := IdentityWithCredentialsSaml{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsSaml) GetConfig() IdentityWithCredentialsSamlConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsSamlConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSaml) GetConfigOk() (*IdentityWithCredentialsSamlConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSaml) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsSamlConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsSaml) SetConfig(v IdentityWithCredentialsSamlConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsSaml) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSaml) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSaml) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsSaml := _IdentityWithCredentialsSaml{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSaml) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSaml(varIdentityWithCredentialsSaml) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSaml struct { + value *IdentityWithCredentialsSaml + isSet bool +} + +func (v NullableIdentityWithCredentialsSaml) Get() *IdentityWithCredentialsSaml { + return v.value +} + +func (v *NullableIdentityWithCredentialsSaml) Set(val *IdentityWithCredentialsSaml) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSaml) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSaml) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSaml(val *IdentityWithCredentialsSaml) *NullableIdentityWithCredentialsSaml { + return &NullableIdentityWithCredentialsSaml{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSaml) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSaml) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_saml_config.go b/pkg/client-go/model_identity_with_credentials_saml_config.go new file mode 100644 index 0000000..5a06532 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_saml_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsSamlConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSamlConfig{} + +// IdentityWithCredentialsSamlConfig Payload of SAML providers +type IdentityWithCredentialsSamlConfig struct { + // A list of SAML Providers + Providers []IdentityWithCredentialsSamlConfigProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSamlConfig IdentityWithCredentialsSamlConfig + +// NewIdentityWithCredentialsSamlConfig instantiates a new IdentityWithCredentialsSamlConfig 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 NewIdentityWithCredentialsSamlConfig() *IdentityWithCredentialsSamlConfig { + this := IdentityWithCredentialsSamlConfig{} + return &this +} + +// NewIdentityWithCredentialsSamlConfigWithDefaults instantiates a new IdentityWithCredentialsSamlConfig 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 NewIdentityWithCredentialsSamlConfigWithDefaults() *IdentityWithCredentialsSamlConfig { + this := IdentityWithCredentialsSamlConfig{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityWithCredentialsSamlConfig) GetProviders() []IdentityWithCredentialsSamlConfigProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityWithCredentialsSamlConfigProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfig) GetProvidersOk() ([]IdentityWithCredentialsSamlConfigProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSamlConfig) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityWithCredentialsSamlConfigProvider and assigns it to the Providers field. +func (o *IdentityWithCredentialsSamlConfig) SetProviders(v []IdentityWithCredentialsSamlConfigProvider) { + o.Providers = v +} + +func (o IdentityWithCredentialsSamlConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSamlConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSamlConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsSamlConfig := _IdentityWithCredentialsSamlConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSamlConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSamlConfig(varIdentityWithCredentialsSamlConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSamlConfig struct { + value *IdentityWithCredentialsSamlConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsSamlConfig) Get() *IdentityWithCredentialsSamlConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsSamlConfig) Set(val *IdentityWithCredentialsSamlConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSamlConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSamlConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSamlConfig(val *IdentityWithCredentialsSamlConfig) *NullableIdentityWithCredentialsSamlConfig { + return &NullableIdentityWithCredentialsSamlConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSamlConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSamlConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_saml_config_provider.go b/pkg/client-go/model_identity_with_credentials_saml_config_provider.go new file mode 100644 index 0000000..4262c83 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_saml_config_provider.go @@ -0,0 +1,246 @@ +/* +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" + "fmt" +) + +// checks if the IdentityWithCredentialsSamlConfigProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSamlConfigProvider{} + +// IdentityWithCredentialsSamlConfigProvider Payload of specific SAML provider +type IdentityWithCredentialsSamlConfigProvider struct { + Organization NullableString `json:"organization,omitempty"` + // The SAML provider to link the subject to. + Provider string `json:"provider"` + // The unique subject of the SAML connection. This value must be immutable at the source. + Subject string `json:"subject"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSamlConfigProvider IdentityWithCredentialsSamlConfigProvider + +// NewIdentityWithCredentialsSamlConfigProvider instantiates a new IdentityWithCredentialsSamlConfigProvider 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 NewIdentityWithCredentialsSamlConfigProvider(provider string, subject string) *IdentityWithCredentialsSamlConfigProvider { + this := IdentityWithCredentialsSamlConfigProvider{} + this.Provider = provider + this.Subject = subject + return &this +} + +// NewIdentityWithCredentialsSamlConfigProviderWithDefaults instantiates a new IdentityWithCredentialsSamlConfigProvider 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 NewIdentityWithCredentialsSamlConfigProviderWithDefaults() *IdentityWithCredentialsSamlConfigProvider { + this := IdentityWithCredentialsSamlConfigProvider{} + return &this +} + +// GetOrganization returns the Organization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityWithCredentialsSamlConfigProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization.Get()) { + var ret string + return ret + } + return *o.Organization.Get() +} + +// GetOrganizationOk returns a tuple with the Organization 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 *IdentityWithCredentialsSamlConfigProvider) GetOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Organization.Get(), o.Organization.IsSet() +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) HasOrganization() bool { + if o != nil && o.Organization.IsSet() { + return true + } + + return false +} + +// SetOrganization gets a reference to the given NullableString and assigns it to the Organization field. +func (o *IdentityWithCredentialsSamlConfigProvider) SetOrganization(v string) { + o.Organization.Set(&v) +} + +// SetOrganizationNil sets the value for Organization to be an explicit nil +func (o *IdentityWithCredentialsSamlConfigProvider) SetOrganizationNil() { + o.Organization.Set(nil) +} + +// UnsetOrganization ensures that no value is present for Organization, not even an explicit nil +func (o *IdentityWithCredentialsSamlConfigProvider) UnsetOrganization() { + o.Organization.Unset() +} + +// GetProvider returns the Provider field value +func (o *IdentityWithCredentialsSamlConfigProvider) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *IdentityWithCredentialsSamlConfigProvider) SetProvider(v string) { + o.Provider = v +} + +// GetSubject returns the Subject field value +func (o *IdentityWithCredentialsSamlConfigProvider) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *IdentityWithCredentialsSamlConfigProvider) SetSubject(v string) { + o.Subject = v +} + +func (o IdentityWithCredentialsSamlConfigProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSamlConfigProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Organization.IsSet() { + toSerialize["organization"] = o.Organization.Get() + } + toSerialize["provider"] = o.Provider + toSerialize["subject"] = o.Subject + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSamlConfigProvider) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider", + "subject", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentityWithCredentialsSamlConfigProvider := _IdentityWithCredentialsSamlConfigProvider{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSamlConfigProvider) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSamlConfigProvider(varIdentityWithCredentialsSamlConfigProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSamlConfigProvider struct { + value *IdentityWithCredentialsSamlConfigProvider + isSet bool +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) Get() *IdentityWithCredentialsSamlConfigProvider { + return v.value +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) Set(val *IdentityWithCredentialsSamlConfigProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSamlConfigProvider(val *IdentityWithCredentialsSamlConfigProvider) *NullableIdentityWithCredentialsSamlConfigProvider { + return &NullableIdentityWithCredentialsSamlConfigProvider{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_totp.go b/pkg/client-go/model_identity_with_credentials_totp.go new file mode 100644 index 0000000..94da1eb --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_totp.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsTotp type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsTotp{} + +// IdentityWithCredentialsTotp Create Identity and Import TOTP 2FA Credentials +type IdentityWithCredentialsTotp struct { + Config *IdentityWithCredentialsTotpConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsTotp IdentityWithCredentialsTotp + +// NewIdentityWithCredentialsTotp instantiates a new IdentityWithCredentialsTotp 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 NewIdentityWithCredentialsTotp() *IdentityWithCredentialsTotp { + this := IdentityWithCredentialsTotp{} + return &this +} + +// NewIdentityWithCredentialsTotpWithDefaults instantiates a new IdentityWithCredentialsTotp 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 NewIdentityWithCredentialsTotpWithDefaults() *IdentityWithCredentialsTotp { + this := IdentityWithCredentialsTotp{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsTotp) GetConfig() IdentityWithCredentialsTotpConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsTotpConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsTotp) GetConfigOk() (*IdentityWithCredentialsTotpConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsTotp) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsTotpConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsTotp) SetConfig(v IdentityWithCredentialsTotpConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsTotp) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsTotp) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsTotp) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsTotp := _IdentityWithCredentialsTotp{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsTotp) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsTotp(varIdentityWithCredentialsTotp) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsTotp struct { + value *IdentityWithCredentialsTotp + isSet bool +} + +func (v NullableIdentityWithCredentialsTotp) Get() *IdentityWithCredentialsTotp { + return v.value +} + +func (v *NullableIdentityWithCredentialsTotp) Set(val *IdentityWithCredentialsTotp) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsTotp) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsTotp) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsTotp(val *IdentityWithCredentialsTotp) *NullableIdentityWithCredentialsTotp { + return &NullableIdentityWithCredentialsTotp{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsTotp) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsTotp) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_totp_config.go b/pkg/client-go/model_identity_with_credentials_totp_config.go new file mode 100644 index 0000000..d84498d --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_totp_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsTotpConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsTotpConfig{} + +// IdentityWithCredentialsTotpConfig Create Identity and Import TOTP 2FA Credentials Configuration +type IdentityWithCredentialsTotpConfig struct { + // TOTPURL is the TOTP URL For more details see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + TotpUrl *string `json:"totp_url,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsTotpConfig IdentityWithCredentialsTotpConfig + +// NewIdentityWithCredentialsTotpConfig instantiates a new IdentityWithCredentialsTotpConfig 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 NewIdentityWithCredentialsTotpConfig() *IdentityWithCredentialsTotpConfig { + this := IdentityWithCredentialsTotpConfig{} + return &this +} + +// NewIdentityWithCredentialsTotpConfigWithDefaults instantiates a new IdentityWithCredentialsTotpConfig 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 NewIdentityWithCredentialsTotpConfigWithDefaults() *IdentityWithCredentialsTotpConfig { + this := IdentityWithCredentialsTotpConfig{} + return &this +} + +// GetTotpUrl returns the TotpUrl field value if set, zero value otherwise. +func (o *IdentityWithCredentialsTotpConfig) GetTotpUrl() string { + if o == nil || IsNil(o.TotpUrl) { + var ret string + return ret + } + return *o.TotpUrl +} + +// GetTotpUrlOk returns a tuple with the TotpUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsTotpConfig) GetTotpUrlOk() (*string, bool) { + if o == nil || IsNil(o.TotpUrl) { + return nil, false + } + return o.TotpUrl, true +} + +// HasTotpUrl returns a boolean if a field has been set. +func (o *IdentityWithCredentialsTotpConfig) HasTotpUrl() bool { + if o != nil && !IsNil(o.TotpUrl) { + return true + } + + return false +} + +// SetTotpUrl gets a reference to the given string and assigns it to the TotpUrl field. +func (o *IdentityWithCredentialsTotpConfig) SetTotpUrl(v string) { + o.TotpUrl = &v +} + +func (o IdentityWithCredentialsTotpConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsTotpConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.TotpUrl) { + toSerialize["totp_url"] = o.TotpUrl + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsTotpConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsTotpConfig := _IdentityWithCredentialsTotpConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsTotpConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsTotpConfig(varIdentityWithCredentialsTotpConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "totp_url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsTotpConfig struct { + value *IdentityWithCredentialsTotpConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsTotpConfig) Get() *IdentityWithCredentialsTotpConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsTotpConfig) Set(val *IdentityWithCredentialsTotpConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsTotpConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsTotpConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsTotpConfig(val *IdentityWithCredentialsTotpConfig) *NullableIdentityWithCredentialsTotpConfig { + return &NullableIdentityWithCredentialsTotpConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsTotpConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsTotpConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_web_authn.go b/pkg/client-go/model_identity_with_credentials_web_authn.go new file mode 100644 index 0000000..6f4a70d --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_web_authn.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsWebAuthn type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsWebAuthn{} + +// IdentityWithCredentialsWebAuthn Create Identity and Import WebAuthn Credentials +type IdentityWithCredentialsWebAuthn struct { + Config *IdentityWithCredentialsWebAuthnConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsWebAuthn IdentityWithCredentialsWebAuthn + +// NewIdentityWithCredentialsWebAuthn instantiates a new IdentityWithCredentialsWebAuthn 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 NewIdentityWithCredentialsWebAuthn() *IdentityWithCredentialsWebAuthn { + this := IdentityWithCredentialsWebAuthn{} + return &this +} + +// NewIdentityWithCredentialsWebAuthnWithDefaults instantiates a new IdentityWithCredentialsWebAuthn 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 NewIdentityWithCredentialsWebAuthnWithDefaults() *IdentityWithCredentialsWebAuthn { + this := IdentityWithCredentialsWebAuthn{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthn) GetConfig() IdentityWithCredentialsWebAuthnConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsWebAuthnConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthn) GetConfigOk() (*IdentityWithCredentialsWebAuthnConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthn) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsWebAuthnConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsWebAuthn) SetConfig(v IdentityWithCredentialsWebAuthnConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsWebAuthn) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsWebAuthn) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsWebAuthn := _IdentityWithCredentialsWebAuthn{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsWebAuthn) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsWebAuthn(varIdentityWithCredentialsWebAuthn) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsWebAuthn struct { + value *IdentityWithCredentialsWebAuthn + isSet bool +} + +func (v NullableIdentityWithCredentialsWebAuthn) Get() *IdentityWithCredentialsWebAuthn { + return v.value +} + +func (v *NullableIdentityWithCredentialsWebAuthn) Set(val *IdentityWithCredentialsWebAuthn) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsWebAuthn) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsWebAuthn) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsWebAuthn(val *IdentityWithCredentialsWebAuthn) *NullableIdentityWithCredentialsWebAuthn { + return &NullableIdentityWithCredentialsWebAuthn{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsWebAuthn) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_identity_with_credentials_web_authn_config.go b/pkg/client-go/model_identity_with_credentials_web_authn_config.go new file mode 100644 index 0000000..0a61870 --- /dev/null +++ b/pkg/client-go/model_identity_with_credentials_web_authn_config.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsWebAuthnConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsWebAuthnConfig{} + +// IdentityWithCredentialsWebAuthnConfig Create Identity and Import WebAuthn Credentials Configuration +type IdentityWithCredentialsWebAuthnConfig struct { + Credentials []IdentityCredentialsWebAuthn `json:"credentials,omitempty"` + // UserHandle is the user handle of the webauthn credential. + UserHandle []int32 `json:"user_handle,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsWebAuthnConfig IdentityWithCredentialsWebAuthnConfig + +// NewIdentityWithCredentialsWebAuthnConfig instantiates a new IdentityWithCredentialsWebAuthnConfig 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 NewIdentityWithCredentialsWebAuthnConfig() *IdentityWithCredentialsWebAuthnConfig { + this := IdentityWithCredentialsWebAuthnConfig{} + return &this +} + +// NewIdentityWithCredentialsWebAuthnConfigWithDefaults instantiates a new IdentityWithCredentialsWebAuthnConfig 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 NewIdentityWithCredentialsWebAuthnConfigWithDefaults() *IdentityWithCredentialsWebAuthnConfig { + this := IdentityWithCredentialsWebAuthnConfig{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthnConfig) GetCredentials() []IdentityCredentialsWebAuthn { + if o == nil || IsNil(o.Credentials) { + var ret []IdentityCredentialsWebAuthn + return ret + } + return o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) GetCredentialsOk() ([]IdentityCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given []IdentityCredentialsWebAuthn and assigns it to the Credentials field. +func (o *IdentityWithCredentialsWebAuthnConfig) SetCredentials(v []IdentityCredentialsWebAuthn) { + o.Credentials = v +} + +// GetUserHandle returns the UserHandle field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthnConfig) GetUserHandle() []int32 { + if o == nil || IsNil(o.UserHandle) { + var ret []int32 + return ret + } + return o.UserHandle +} + +// GetUserHandleOk returns a tuple with the UserHandle field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) GetUserHandleOk() ([]int32, bool) { + if o == nil || IsNil(o.UserHandle) { + return nil, false + } + return o.UserHandle, true +} + +// HasUserHandle returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) HasUserHandle() bool { + if o != nil && !IsNil(o.UserHandle) { + return true + } + + return false +} + +// SetUserHandle gets a reference to the given []int32 and assigns it to the UserHandle field. +func (o *IdentityWithCredentialsWebAuthnConfig) SetUserHandle(v []int32) { + o.UserHandle = v +} + +func (o IdentityWithCredentialsWebAuthnConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsWebAuthnConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.UserHandle) { + toSerialize["user_handle"] = o.UserHandle + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsWebAuthnConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsWebAuthnConfig := _IdentityWithCredentialsWebAuthnConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsWebAuthnConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsWebAuthnConfig(varIdentityWithCredentialsWebAuthnConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "user_handle") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsWebAuthnConfig struct { + value *IdentityWithCredentialsWebAuthnConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) Get() *IdentityWithCredentialsWebAuthnConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) Set(val *IdentityWithCredentialsWebAuthnConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsWebAuthnConfig(val *IdentityWithCredentialsWebAuthnConfig) *NullableIdentityWithCredentialsWebAuthnConfig { + return &NullableIdentityWithCredentialsWebAuthnConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_is_alive_200_response.go b/pkg/client-go/model_is_alive_200_response.go new file mode 100644 index 0000000..59a8ab5 --- /dev/null +++ b/pkg/client-go/model_is_alive_200_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the IsAlive200Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IsAlive200Response{} + +// IsAlive200Response struct for IsAlive200Response +type IsAlive200Response struct { + // Always \"ok\". + Status string `json:"status"` + AdditionalProperties map[string]interface{} +} + +type _IsAlive200Response IsAlive200Response + +// NewIsAlive200Response instantiates a new IsAlive200Response 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 NewIsAlive200Response(status string) *IsAlive200Response { + this := IsAlive200Response{} + this.Status = status + return &this +} + +// NewIsAlive200ResponseWithDefaults instantiates a new IsAlive200Response 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 NewIsAlive200ResponseWithDefaults() *IsAlive200Response { + this := IsAlive200Response{} + return &this +} + +// GetStatus returns the Status field value +func (o *IsAlive200Response) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *IsAlive200Response) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *IsAlive200Response) SetStatus(v string) { + o.Status = v +} + +func (o IsAlive200Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IsAlive200Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["status"] = o.Status + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IsAlive200Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "status", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIsAlive200Response := _IsAlive200Response{} + + err = json.Unmarshal(data, &varIsAlive200Response) + + if err != nil { + return err + } + + *o = IsAlive200Response(varIsAlive200Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIsAlive200Response struct { + value *IsAlive200Response + isSet bool +} + +func (v NullableIsAlive200Response) Get() *IsAlive200Response { + return v.value +} + +func (v *NullableIsAlive200Response) Set(val *IsAlive200Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsAlive200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsAlive200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsAlive200Response(val *IsAlive200Response) *NullableIsAlive200Response { + return &NullableIsAlive200Response{value: val, isSet: true} +} + +func (v NullableIsAlive200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsAlive200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_is_ready_503_response.go b/pkg/client-go/model_is_ready_503_response.go new file mode 100644 index 0000000..ed05af1 --- /dev/null +++ b/pkg/client-go/model_is_ready_503_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the IsReady503Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IsReady503Response{} + +// IsReady503Response struct for IsReady503Response +type IsReady503Response struct { + // Errors contains a list of errors that caused the not ready status. + Errors map[string]string `json:"errors"` + AdditionalProperties map[string]interface{} +} + +type _IsReady503Response IsReady503Response + +// NewIsReady503Response instantiates a new IsReady503Response 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 NewIsReady503Response(errors map[string]string) *IsReady503Response { + this := IsReady503Response{} + this.Errors = errors + return &this +} + +// NewIsReady503ResponseWithDefaults instantiates a new IsReady503Response 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 NewIsReady503ResponseWithDefaults() *IsReady503Response { + this := IsReady503Response{} + return &this +} + +// GetErrors returns the Errors field value +func (o *IsReady503Response) GetErrors() map[string]string { + if o == nil { + var ret map[string]string + return ret + } + + return o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value +// and a boolean to check if the value has been set. +func (o *IsReady503Response) GetErrorsOk() (*map[string]string, bool) { + if o == nil { + return nil, false + } + return &o.Errors, true +} + +// SetErrors sets field value +func (o *IsReady503Response) SetErrors(v map[string]string) { + o.Errors = v +} + +func (o IsReady503Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IsReady503Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["errors"] = o.Errors + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IsReady503Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "errors", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIsReady503Response := _IsReady503Response{} + + err = json.Unmarshal(data, &varIsReady503Response) + + if err != nil { + return err + } + + *o = IsReady503Response(varIsReady503Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "errors") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIsReady503Response struct { + value *IsReady503Response + isSet bool +} + +func (v NullableIsReady503Response) Get() *IsReady503Response { + return v.value +} + +func (v *NullableIsReady503Response) Set(val *IsReady503Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsReady503Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsReady503Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsReady503Response(val *IsReady503Response) *NullableIsReady503Response { + return &NullableIsReady503Response{value: val, isSet: true} +} + +func (v NullableIsReady503Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsReady503Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_json_patch.go b/pkg/client-go/model_json_patch.go new file mode 100644 index 0000000..111265f --- /dev/null +++ b/pkg/client-go/model_json_patch.go @@ -0,0 +1,275 @@ +/* +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" + "fmt" +) + +// checks if the JsonPatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &JsonPatch{} + +// JsonPatch A JSONPatch document as defined by RFC 6902 +type JsonPatch struct { + // This field is used together with operation \"move\" and uses JSON Pointer notation. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + From *string `json:"from,omitempty"` + // The operation to be performed. One of \"add\", \"remove\", \"replace\", \"move\", \"copy\", or \"test\". + Op string `json:"op"` + // The path to the target path. Uses JSON pointer notation. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + Path string `json:"path"` + // The value to be used within the operations. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + Value interface{} `json:"value,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _JsonPatch JsonPatch + +// NewJsonPatch instantiates a new JsonPatch 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 NewJsonPatch(op string, path string) *JsonPatch { + this := JsonPatch{} + this.Op = op + this.Path = path + return &this +} + +// NewJsonPatchWithDefaults instantiates a new JsonPatch 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 NewJsonPatchWithDefaults() *JsonPatch { + this := JsonPatch{} + return &this +} + +// GetFrom returns the From field value if set, zero value otherwise. +func (o *JsonPatch) GetFrom() string { + if o == nil || IsNil(o.From) { + var ret string + return ret + } + return *o.From +} + +// GetFromOk returns a tuple with the From field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetFromOk() (*string, bool) { + if o == nil || IsNil(o.From) { + return nil, false + } + return o.From, true +} + +// HasFrom returns a boolean if a field has been set. +func (o *JsonPatch) HasFrom() bool { + if o != nil && !IsNil(o.From) { + return true + } + + return false +} + +// SetFrom gets a reference to the given string and assigns it to the From field. +func (o *JsonPatch) SetFrom(v string) { + o.From = &v +} + +// GetOp returns the Op field value +func (o *JsonPatch) GetOp() string { + if o == nil { + var ret string + return ret + } + + return o.Op +} + +// GetOpOk returns a tuple with the Op field value +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetOpOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Op, true +} + +// SetOp sets field value +func (o *JsonPatch) SetOp(v string) { + o.Op = v +} + +// GetPath returns the Path field value +func (o *JsonPatch) GetPath() string { + if o == nil { + var ret string + return ret + } + + return o.Path +} + +// GetPathOk returns a tuple with the Path field value +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetPathOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Path, true +} + +// SetPath sets field value +func (o *JsonPatch) SetPath(v string) { + o.Path = v +} + +// GetValue returns the Value field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *JsonPatch) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value 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 *JsonPatch) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *JsonPatch) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given interface{} and assigns it to the Value field. +func (o *JsonPatch) SetValue(v interface{}) { + o.Value = v +} + +func (o JsonPatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o JsonPatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.From) { + toSerialize["from"] = o.From + } + toSerialize["op"] = o.Op + toSerialize["path"] = o.Path + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *JsonPatch) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "op", + "path", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varJsonPatch := _JsonPatch{} + + err = json.Unmarshal(data, &varJsonPatch) + + if err != nil { + return err + } + + *o = JsonPatch(varJsonPatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "from") + delete(additionalProperties, "op") + delete(additionalProperties, "path") + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableJsonPatch struct { + value *JsonPatch + isSet bool +} + +func (v NullableJsonPatch) Get() *JsonPatch { + return v.value +} + +func (v *NullableJsonPatch) Set(val *JsonPatch) { + v.value = val + v.isSet = true +} + +func (v NullableJsonPatch) IsSet() bool { + return v.isSet +} + +func (v *NullableJsonPatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableJsonPatch(val *JsonPatch) *NullableJsonPatch { + return &NullableJsonPatch{value: val, isSet: true} +} + +func (v NullableJsonPatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableJsonPatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_key_state.go b/pkg/client-go/model_key_state.go new file mode 100644 index 0000000..b8ea1c2 --- /dev/null +++ b/pkg/client-go/model_key_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// KeyState Only a \"confirmed\" key can be used to authenticate. A \"locked\" key was disabled after too many wrong-PIN attempts; rotating its pin_secret unlocks it. +type KeyState string + +// List of KeyState +const ( + KEYSTATE_INITIAL KeyState = "initial" + KEYSTATE_CONFIRMED KeyState = "confirmed" + KEYSTATE_LOCKED KeyState = "locked" +) + +// All allowed values of KeyState enum +var AllowedKeyStateEnumValues = []KeyState{ + "initial", + "confirmed", + "locked", +} + +func (v *KeyState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := KeyState(value) + for _, existing := range AllowedKeyStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid KeyState", value) +} + +// NewKeyStateFromValue returns a pointer to a valid KeyState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewKeyStateFromValue(v string) (*KeyState, error) { + ev := KeyState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for KeyState: valid values are %v", v, AllowedKeyStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v KeyState) IsValid() bool { + for _, existing := range AllowedKeyStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to KeyState value +func (v KeyState) Ptr() *KeyState { + return &v +} + +type NullableKeyState struct { + value *KeyState + isSet bool +} + +func (v NullableKeyState) Get() *KeyState { + return v.value +} + +func (v *NullableKeyState) Set(val *KeyState) { + v.value = val + v.isSet = true +} + +func (v NullableKeyState) IsSet() bool { + return v.isSet +} + +func (v *NullableKeyState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableKeyState(val *KeyState) *NullableKeyState { + return &NullableKeyState{value: val, isSet: true} +} + +func (v NullableKeyState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableKeyState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow.go b/pkg/client-go/model_login_flow.go new file mode 100644 index 0000000..d71db3c --- /dev/null +++ b/pkg/client-go/model_login_flow.go @@ -0,0 +1,853 @@ +/* +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" + "fmt" + "time" +) + +// checks if the LoginFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlow{} + +// 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // 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"` + // ID represents the flow's unique ID. When performing the login flow, this represents the id in the login UI's query parameter: http:///?flow= + Id string `json:"id"` + // IdentitySchema optionally holds the ID of the identity schema that is used for this flow. This value can be set by the user when creating the flow and should be retained when the flow is saved or converted to another flow. + IdentitySchema *string `json:"identity_schema,omitempty"` + // IssuedAt is the time (UTC) when the flow started. + IssuedAt time.Time `json:"issued_at"` + // Ory OAuth 2.0 Login Challenge. This value is set using the `login_challenge` query parameter of the registration and login endpoints. If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + Oauth2LoginChallenge *string `json:"oauth2_login_challenge,omitempty"` + Oauth2LoginRequest *OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // Refresh stores whether this login flow should enforce re-authentication. + Refresh *bool `json:"refresh,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + RequestedAal *AuthenticatorAssuranceLevel `json:"requested_aal,omitempty"` + // ReturnTo contains the requested return_to URL. + ReturnTo *string `json:"return_to,omitempty"` + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. + 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"` + TestContext *LoginFlowTestContext `json:"test_context,omitempty"` + // 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"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlow LoginFlow + +// NewLoginFlow instantiates a new LoginFlow 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 NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { + this := LoginFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewLoginFlowWithDefaults instantiates a new LoginFlow 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 NewLoginFlowWithDefaults() *LoginFlow { + this := LoginFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *LoginFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *LoginFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *LoginFlow) SetActive(v string) { + o.Active = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *LoginFlow) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *LoginFlow) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *LoginFlow) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *LoginFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *LoginFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *LoginFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *LoginFlow) SetId(v string) { + o.Id = v +} + +// GetIdentitySchema returns the IdentitySchema field value if set, zero value otherwise. +func (o *LoginFlow) GetIdentitySchema() string { + if o == nil || IsNil(o.IdentitySchema) { + var ret string + return ret + } + return *o.IdentitySchema +} + +// GetIdentitySchemaOk returns a tuple with the IdentitySchema field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIdentitySchemaOk() (*string, bool) { + if o == nil || IsNil(o.IdentitySchema) { + return nil, false + } + return o.IdentitySchema, true +} + +// HasIdentitySchema returns a boolean if a field has been set. +func (o *LoginFlow) HasIdentitySchema() bool { + if o != nil && !IsNil(o.IdentitySchema) { + return true + } + + return false +} + +// SetIdentitySchema gets a reference to the given string and assigns it to the IdentitySchema field. +func (o *LoginFlow) SetIdentitySchema(v string) { + o.IdentitySchema = &v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *LoginFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *LoginFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOauth2LoginChallenge returns the Oauth2LoginChallenge field value if set, zero value otherwise. +func (o *LoginFlow) GetOauth2LoginChallenge() string { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + var ret string + return ret + } + return *o.Oauth2LoginChallenge +} + +// GetOauth2LoginChallengeOk returns a tuple with the Oauth2LoginChallenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetOauth2LoginChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + return nil, false + } + return o.Oauth2LoginChallenge, true +} + +// HasOauth2LoginChallenge returns a boolean if a field has been set. +func (o *LoginFlow) HasOauth2LoginChallenge() bool { + if o != nil && !IsNil(o.Oauth2LoginChallenge) { + return true + } + + return false +} + +// SetOauth2LoginChallenge gets a reference to the given string and assigns it to the Oauth2LoginChallenge field. +func (o *LoginFlow) SetOauth2LoginChallenge(v string) { + o.Oauth2LoginChallenge = &v +} + +// GetOauth2LoginRequest returns the Oauth2LoginRequest field value if set, zero value otherwise. +func (o *LoginFlow) GetOauth2LoginRequest() OAuth2LoginRequest { + if o == nil || IsNil(o.Oauth2LoginRequest) { + var ret OAuth2LoginRequest + return ret + } + return *o.Oauth2LoginRequest +} + +// GetOauth2LoginRequestOk returns a tuple with the Oauth2LoginRequest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetOauth2LoginRequestOk() (*OAuth2LoginRequest, bool) { + if o == nil || IsNil(o.Oauth2LoginRequest) { + return nil, false + } + return o.Oauth2LoginRequest, true +} + +// HasOauth2LoginRequest returns a boolean if a field has been set. +func (o *LoginFlow) HasOauth2LoginRequest() bool { + if o != nil && !IsNil(o.Oauth2LoginRequest) { + return true + } + + return false +} + +// SetOauth2LoginRequest gets a reference to the given OAuth2LoginRequest and assigns it to the Oauth2LoginRequest field. +func (o *LoginFlow) SetOauth2LoginRequest(v OAuth2LoginRequest) { + o.Oauth2LoginRequest = &v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *LoginFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *LoginFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *LoginFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *LoginFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *LoginFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *LoginFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRefresh returns the Refresh field value if set, zero value otherwise. +func (o *LoginFlow) GetRefresh() bool { + if o == nil || IsNil(o.Refresh) { + var ret bool + return ret + } + return *o.Refresh +} + +// GetRefreshOk returns a tuple with the Refresh field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRefreshOk() (*bool, bool) { + if o == nil || IsNil(o.Refresh) { + return nil, false + } + return o.Refresh, true +} + +// HasRefresh returns a boolean if a field has been set. +func (o *LoginFlow) HasRefresh() bool { + if o != nil && !IsNil(o.Refresh) { + return true + } + + return false +} + +// SetRefresh gets a reference to the given bool and assigns it to the Refresh field. +func (o *LoginFlow) SetRefresh(v bool) { + o.Refresh = &v +} + +// GetRequestUrl returns the RequestUrl field value +func (o *LoginFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *LoginFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetRequestedAal returns the RequestedAal field value if set, zero value otherwise. +func (o *LoginFlow) GetRequestedAal() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.RequestedAal) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.RequestedAal +} + +// GetRequestedAalOk returns a tuple with the RequestedAal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRequestedAalOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.RequestedAal) { + return nil, false + } + return o.RequestedAal, true +} + +// HasRequestedAal returns a boolean if a field has been set. +func (o *LoginFlow) HasRequestedAal() bool { + if o != nil && !IsNil(o.RequestedAal) { + return true + } + + return false +} + +// SetRequestedAal gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the RequestedAal field. +func (o *LoginFlow) SetRequestedAal(v AuthenticatorAssuranceLevel) { + o.RequestedAal = &v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *LoginFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *LoginFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *LoginFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetSessionTokenExchangeCode returns the SessionTokenExchangeCode field value if set, zero value otherwise. +func (o *LoginFlow) GetSessionTokenExchangeCode() string { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + var ret string + return ret + } + return *o.SessionTokenExchangeCode +} + +// GetSessionTokenExchangeCodeOk returns a tuple with the SessionTokenExchangeCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetSessionTokenExchangeCodeOk() (*string, bool) { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + return nil, false + } + return o.SessionTokenExchangeCode, true +} + +// HasSessionTokenExchangeCode returns a boolean if a field has been set. +func (o *LoginFlow) HasSessionTokenExchangeCode() bool { + if o != nil && !IsNil(o.SessionTokenExchangeCode) { + return true + } + + return false +} + +// SetSessionTokenExchangeCode gets a reference to the given string and assigns it to the SessionTokenExchangeCode field. +func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { + o.SessionTokenExchangeCode = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + +// GetTestContext returns the TestContext field value if set, zero value otherwise. +func (o *LoginFlow) GetTestContext() LoginFlowTestContext { + if o == nil || IsNil(o.TestContext) { + var ret LoginFlowTestContext + return ret + } + return *o.TestContext +} + +// GetTestContextOk returns a tuple with the TestContext field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTestContextOk() (*LoginFlowTestContext, bool) { + if o == nil || IsNil(o.TestContext) { + return nil, false + } + return o.TestContext, true +} + +// HasTestContext returns a boolean if a field has been set. +func (o *LoginFlow) HasTestContext() bool { + if o != nil && !IsNil(o.TestContext) { + return true + } + + return false +} + +// SetTestContext gets a reference to the given LoginFlowTestContext and assigns it to the TestContext field. +func (o *LoginFlow) SetTestContext(v LoginFlowTestContext) { + o.TestContext = &v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *LoginFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *LoginFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *LoginFlow) SetUi(v UiContainer) { + o.Ui = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *LoginFlow) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *LoginFlow) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *LoginFlow) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +func (o LoginFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + if !IsNil(o.IdentitySchema) { + toSerialize["identity_schema"] = o.IdentitySchema + } + toSerialize["issued_at"] = o.IssuedAt + if !IsNil(o.Oauth2LoginChallenge) { + toSerialize["oauth2_login_challenge"] = o.Oauth2LoginChallenge + } + if !IsNil(o.Oauth2LoginRequest) { + toSerialize["oauth2_login_request"] = o.Oauth2LoginRequest + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.Refresh) { + toSerialize["refresh"] = o.Refresh + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.RequestedAal) { + toSerialize["requested_aal"] = o.RequestedAal + } + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if !IsNil(o.SessionTokenExchangeCode) { + toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TestContext) { + toSerialize["test_context"] = o.TestContext + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLoginFlow := _LoginFlow{} + + err = json.Unmarshal(data, &varLoginFlow) + + if err != nil { + return err + } + + *o = LoginFlow(varLoginFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "created_at") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity_schema") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "oauth2_login_challenge") + delete(additionalProperties, "oauth2_login_request") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "refresh") + delete(additionalProperties, "request_url") + delete(additionalProperties, "requested_aal") + delete(additionalProperties, "return_to") + delete(additionalProperties, "session_token_exchange_code") + delete(additionalProperties, "state") + delete(additionalProperties, "test_context") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlow struct { + value *LoginFlow + isSet bool +} + +func (v NullableLoginFlow) Get() *LoginFlow { + return v.value +} + +func (v *NullableLoginFlow) Set(val *LoginFlow) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlow(val *LoginFlow) *NullableLoginFlow { + return &NullableLoginFlow{value: val, isSet: true} +} + +func (v NullableLoginFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow_state.go b/pkg/client-go/model_login_flow_state.go new file mode 100644 index 0000000..b5c2a1a --- /dev/null +++ b/pkg/client-go/model_login_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +// All allowed values of LoginFlowState enum +var AllowedLoginFlowStateEnumValues = []LoginFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range AllowedLoginFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// NewLoginFlowStateFromValue returns a pointer to a valid LoginFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewLoginFlowStateFromValue(v string) (*LoginFlowState, error) { + ev := LoginFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for LoginFlowState: valid values are %v", v, AllowedLoginFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v LoginFlowState) IsValid() bool { + for _, existing := range AllowedLoginFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to loginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow_test_context.go b/pkg/client-go/model_login_flow_test_context.go new file mode 100644 index 0000000..30d4079 --- /dev/null +++ b/pkg/client-go/model_login_flow_test_context.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the LoginFlowTestContext type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestContext{} + +// LoginFlowTestContext Admin-test extension of a login flow. Populated only for flows created by the admin test endpoint; included in the flow's API response so the admin UI can render the pre-scoped provider and (once captured) the debug round-trip result. +type LoginFlowTestContext struct { + DebugPayload *LoginFlowTestDebugPayload `json:"debug_payload,omitempty"` + // The ID of the OIDC provider this test flow targets. + ProviderId string `json:"provider_id"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestContext LoginFlowTestContext + +// NewLoginFlowTestContext instantiates a new LoginFlowTestContext 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 NewLoginFlowTestContext(providerId string) *LoginFlowTestContext { + this := LoginFlowTestContext{} + this.ProviderId = providerId + return &this +} + +// NewLoginFlowTestContextWithDefaults instantiates a new LoginFlowTestContext 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 NewLoginFlowTestContextWithDefaults() *LoginFlowTestContext { + this := LoginFlowTestContext{} + return &this +} + +// GetDebugPayload returns the DebugPayload field value if set, zero value otherwise. +func (o *LoginFlowTestContext) GetDebugPayload() LoginFlowTestDebugPayload { + if o == nil || IsNil(o.DebugPayload) { + var ret LoginFlowTestDebugPayload + return ret + } + return *o.DebugPayload +} + +// GetDebugPayloadOk returns a tuple with the DebugPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestContext) GetDebugPayloadOk() (*LoginFlowTestDebugPayload, bool) { + if o == nil || IsNil(o.DebugPayload) { + return nil, false + } + return o.DebugPayload, true +} + +// HasDebugPayload returns a boolean if a field has been set. +func (o *LoginFlowTestContext) HasDebugPayload() bool { + if o != nil && !IsNil(o.DebugPayload) { + return true + } + + return false +} + +// SetDebugPayload gets a reference to the given LoginFlowTestDebugPayload and assigns it to the DebugPayload field. +func (o *LoginFlowTestContext) SetDebugPayload(v LoginFlowTestDebugPayload) { + o.DebugPayload = &v +} + +// GetProviderId returns the ProviderId field value +func (o *LoginFlowTestContext) GetProviderId() string { + if o == nil { + var ret string + return ret + } + + return o.ProviderId +} + +// GetProviderIdOk returns a tuple with the ProviderId field value +// and a boolean to check if the value has been set. +func (o *LoginFlowTestContext) GetProviderIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ProviderId, true +} + +// SetProviderId sets field value +func (o *LoginFlowTestContext) SetProviderId(v string) { + o.ProviderId = v +} + +func (o LoginFlowTestContext) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestContext) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DebugPayload) { + toSerialize["debug_payload"] = o.DebugPayload + } + toSerialize["provider_id"] = o.ProviderId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestContext) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLoginFlowTestContext := _LoginFlowTestContext{} + + err = json.Unmarshal(data, &varLoginFlowTestContext) + + if err != nil { + return err + } + + *o = LoginFlowTestContext(varLoginFlowTestContext) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "debug_payload") + delete(additionalProperties, "provider_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestContext struct { + value *LoginFlowTestContext + isSet bool +} + +func (v NullableLoginFlowTestContext) Get() *LoginFlowTestContext { + return v.value +} + +func (v *NullableLoginFlowTestContext) Set(val *LoginFlowTestContext) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestContext) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestContext) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestContext(val *LoginFlowTestContext) *NullableLoginFlowTestContext { + return &NullableLoginFlowTestContext{value: val, isSet: true} +} + +func (v NullableLoginFlowTestContext) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestContext) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow_test_debug_payload.go b/pkg/client-go/model_login_flow_test_debug_payload.go new file mode 100644 index 0000000..3756c2f --- /dev/null +++ b/pkg/client-go/model_login_flow_test_debug_payload.go @@ -0,0 +1,420 @@ +/* +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" +) + +// checks if the LoginFlowTestDebugPayload type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestDebugPayload{} + +// LoginFlowTestDebugPayload Contains the parsed claims, the Jsonnet mapper input and output, and any schema validation errors. Bearer tokens (id_token, access_token, refresh_token) are intentionally excluded to limit the blast radius of the debug payload leaking through audit logs or admin browsers. +type LoginFlowTestDebugPayload struct { + Error *LoginFlowTestStepError `json:"error,omitempty"` + // Claims extracted from the ID token. + IdTokenClaims map[string]interface{} `json:"id_token_claims,omitempty"` + // Input JSON that was fed into the Jsonnet mapper. + JsonnetInput map[string]interface{} `json:"jsonnet_input,omitempty"` + // URL of the Jsonnet mapper that was executed on the claims. + JsonnetMapperUrl *string `json:"jsonnet_mapper_url,omitempty"` + // Output JSON returned by the Jsonnet mapper. + JsonnetOutput map[string]interface{} `json:"jsonnet_output,omitempty"` + // Anything the Jsonnet mapper wrote to standard error. + JsonnetStderr *string `json:"jsonnet_stderr,omitempty"` + // Identity-schema validation errors produced from the mapped traits. + SchemaValidationErrors []LoginFlowTestSchemaValidationError `json:"schema_validation_errors,omitempty"` + // Claims returned from the provider's userinfo endpoint, if any. + Userinfo map[string]interface{} `json:"userinfo,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestDebugPayload LoginFlowTestDebugPayload + +// NewLoginFlowTestDebugPayload instantiates a new LoginFlowTestDebugPayload 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 NewLoginFlowTestDebugPayload() *LoginFlowTestDebugPayload { + this := LoginFlowTestDebugPayload{} + return &this +} + +// NewLoginFlowTestDebugPayloadWithDefaults instantiates a new LoginFlowTestDebugPayload 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 NewLoginFlowTestDebugPayloadWithDefaults() *LoginFlowTestDebugPayload { + this := LoginFlowTestDebugPayload{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetError() LoginFlowTestStepError { + if o == nil || IsNil(o.Error) { + var ret LoginFlowTestStepError + 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. +func (o *LoginFlowTestDebugPayload) GetErrorOk() (*LoginFlowTestStepError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given LoginFlowTestStepError and assigns it to the Error field. +func (o *LoginFlowTestDebugPayload) SetError(v LoginFlowTestStepError) { + o.Error = &v +} + +// GetIdTokenClaims returns the IdTokenClaims field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetIdTokenClaims() map[string]interface{} { + if o == nil || IsNil(o.IdTokenClaims) { + var ret map[string]interface{} + return ret + } + return o.IdTokenClaims +} + +// GetIdTokenClaimsOk returns a tuple with the IdTokenClaims field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetIdTokenClaimsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.IdTokenClaims) { + return map[string]interface{}{}, false + } + return o.IdTokenClaims, true +} + +// HasIdTokenClaims returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasIdTokenClaims() bool { + if o != nil && !IsNil(o.IdTokenClaims) { + return true + } + + return false +} + +// SetIdTokenClaims gets a reference to the given map[string]interface{} and assigns it to the IdTokenClaims field. +func (o *LoginFlowTestDebugPayload) SetIdTokenClaims(v map[string]interface{}) { + o.IdTokenClaims = v +} + +// GetJsonnetInput returns the JsonnetInput field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetInput() map[string]interface{} { + if o == nil || IsNil(o.JsonnetInput) { + var ret map[string]interface{} + return ret + } + return o.JsonnetInput +} + +// GetJsonnetInputOk returns a tuple with the JsonnetInput field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetInputOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.JsonnetInput) { + return map[string]interface{}{}, false + } + return o.JsonnetInput, true +} + +// HasJsonnetInput returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetInput() bool { + if o != nil && !IsNil(o.JsonnetInput) { + return true + } + + return false +} + +// SetJsonnetInput gets a reference to the given map[string]interface{} and assigns it to the JsonnetInput field. +func (o *LoginFlowTestDebugPayload) SetJsonnetInput(v map[string]interface{}) { + o.JsonnetInput = v +} + +// GetJsonnetMapperUrl returns the JsonnetMapperUrl field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetMapperUrl() string { + if o == nil || IsNil(o.JsonnetMapperUrl) { + var ret string + return ret + } + return *o.JsonnetMapperUrl +} + +// GetJsonnetMapperUrlOk returns a tuple with the JsonnetMapperUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetMapperUrlOk() (*string, bool) { + if o == nil || IsNil(o.JsonnetMapperUrl) { + return nil, false + } + return o.JsonnetMapperUrl, true +} + +// HasJsonnetMapperUrl returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetMapperUrl() bool { + if o != nil && !IsNil(o.JsonnetMapperUrl) { + return true + } + + return false +} + +// SetJsonnetMapperUrl gets a reference to the given string and assigns it to the JsonnetMapperUrl field. +func (o *LoginFlowTestDebugPayload) SetJsonnetMapperUrl(v string) { + o.JsonnetMapperUrl = &v +} + +// GetJsonnetOutput returns the JsonnetOutput field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetOutput() map[string]interface{} { + if o == nil || IsNil(o.JsonnetOutput) { + var ret map[string]interface{} + return ret + } + return o.JsonnetOutput +} + +// GetJsonnetOutputOk returns a tuple with the JsonnetOutput field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetOutputOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.JsonnetOutput) { + return map[string]interface{}{}, false + } + return o.JsonnetOutput, true +} + +// HasJsonnetOutput returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetOutput() bool { + if o != nil && !IsNil(o.JsonnetOutput) { + return true + } + + return false +} + +// SetJsonnetOutput gets a reference to the given map[string]interface{} and assigns it to the JsonnetOutput field. +func (o *LoginFlowTestDebugPayload) SetJsonnetOutput(v map[string]interface{}) { + o.JsonnetOutput = v +} + +// GetJsonnetStderr returns the JsonnetStderr field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetStderr() string { + if o == nil || IsNil(o.JsonnetStderr) { + var ret string + return ret + } + return *o.JsonnetStderr +} + +// GetJsonnetStderrOk returns a tuple with the JsonnetStderr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetStderrOk() (*string, bool) { + if o == nil || IsNil(o.JsonnetStderr) { + return nil, false + } + return o.JsonnetStderr, true +} + +// HasJsonnetStderr returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetStderr() bool { + if o != nil && !IsNil(o.JsonnetStderr) { + return true + } + + return false +} + +// SetJsonnetStderr gets a reference to the given string and assigns it to the JsonnetStderr field. +func (o *LoginFlowTestDebugPayload) SetJsonnetStderr(v string) { + o.JsonnetStderr = &v +} + +// GetSchemaValidationErrors returns the SchemaValidationErrors field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetSchemaValidationErrors() []LoginFlowTestSchemaValidationError { + if o == nil || IsNil(o.SchemaValidationErrors) { + var ret []LoginFlowTestSchemaValidationError + return ret + } + return o.SchemaValidationErrors +} + +// GetSchemaValidationErrorsOk returns a tuple with the SchemaValidationErrors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetSchemaValidationErrorsOk() ([]LoginFlowTestSchemaValidationError, bool) { + if o == nil || IsNil(o.SchemaValidationErrors) { + return nil, false + } + return o.SchemaValidationErrors, true +} + +// HasSchemaValidationErrors returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasSchemaValidationErrors() bool { + if o != nil && !IsNil(o.SchemaValidationErrors) { + return true + } + + return false +} + +// SetSchemaValidationErrors gets a reference to the given []LoginFlowTestSchemaValidationError and assigns it to the SchemaValidationErrors field. +func (o *LoginFlowTestDebugPayload) SetSchemaValidationErrors(v []LoginFlowTestSchemaValidationError) { + o.SchemaValidationErrors = v +} + +// GetUserinfo returns the Userinfo field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetUserinfo() map[string]interface{} { + if o == nil || IsNil(o.Userinfo) { + var ret map[string]interface{} + return ret + } + return o.Userinfo +} + +// GetUserinfoOk returns a tuple with the Userinfo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetUserinfoOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Userinfo) { + return map[string]interface{}{}, false + } + return o.Userinfo, true +} + +// HasUserinfo returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasUserinfo() bool { + if o != nil && !IsNil(o.Userinfo) { + return true + } + + return false +} + +// SetUserinfo gets a reference to the given map[string]interface{} and assigns it to the Userinfo field. +func (o *LoginFlowTestDebugPayload) SetUserinfo(v map[string]interface{}) { + o.Userinfo = v +} + +func (o LoginFlowTestDebugPayload) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestDebugPayload) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.IdTokenClaims) { + toSerialize["id_token_claims"] = o.IdTokenClaims + } + if !IsNil(o.JsonnetInput) { + toSerialize["jsonnet_input"] = o.JsonnetInput + } + if !IsNil(o.JsonnetMapperUrl) { + toSerialize["jsonnet_mapper_url"] = o.JsonnetMapperUrl + } + if !IsNil(o.JsonnetOutput) { + toSerialize["jsonnet_output"] = o.JsonnetOutput + } + if !IsNil(o.JsonnetStderr) { + toSerialize["jsonnet_stderr"] = o.JsonnetStderr + } + if !IsNil(o.SchemaValidationErrors) { + toSerialize["schema_validation_errors"] = o.SchemaValidationErrors + } + if !IsNil(o.Userinfo) { + toSerialize["userinfo"] = o.Userinfo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestDebugPayload) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestDebugPayload := _LoginFlowTestDebugPayload{} + + err = json.Unmarshal(data, &varLoginFlowTestDebugPayload) + + if err != nil { + return err + } + + *o = LoginFlowTestDebugPayload(varLoginFlowTestDebugPayload) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "id_token_claims") + delete(additionalProperties, "jsonnet_input") + delete(additionalProperties, "jsonnet_mapper_url") + delete(additionalProperties, "jsonnet_output") + delete(additionalProperties, "jsonnet_stderr") + delete(additionalProperties, "schema_validation_errors") + delete(additionalProperties, "userinfo") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestDebugPayload struct { + value *LoginFlowTestDebugPayload + isSet bool +} + +func (v NullableLoginFlowTestDebugPayload) Get() *LoginFlowTestDebugPayload { + return v.value +} + +func (v *NullableLoginFlowTestDebugPayload) Set(val *LoginFlowTestDebugPayload) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestDebugPayload) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestDebugPayload) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestDebugPayload(val *LoginFlowTestDebugPayload) *NullableLoginFlowTestDebugPayload { + return &NullableLoginFlowTestDebugPayload{value: val, isSet: true} +} + +func (v NullableLoginFlowTestDebugPayload) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestDebugPayload) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow_test_schema_validation_error.go b/pkg/client-go/model_login_flow_test_schema_validation_error.go new file mode 100644 index 0000000..a505ca2 --- /dev/null +++ b/pkg/client-go/model_login_flow_test_schema_validation_error.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the LoginFlowTestSchemaValidationError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestSchemaValidationError{} + +// LoginFlowTestSchemaValidationError One identity-schema validation failure recorded while evaluating the traits produced by the Jsonnet mapper. +type LoginFlowTestSchemaValidationError struct { + // Human-readable description of the validation failure. + Message *string `json:"message,omitempty"` + // JSON pointer to the field that failed validation. + Path *string `json:"path,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestSchemaValidationError LoginFlowTestSchemaValidationError + +// NewLoginFlowTestSchemaValidationError instantiates a new LoginFlowTestSchemaValidationError 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 NewLoginFlowTestSchemaValidationError() *LoginFlowTestSchemaValidationError { + this := LoginFlowTestSchemaValidationError{} + return &this +} + +// NewLoginFlowTestSchemaValidationErrorWithDefaults instantiates a new LoginFlowTestSchemaValidationError 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 NewLoginFlowTestSchemaValidationErrorWithDefaults() *LoginFlowTestSchemaValidationError { + this := LoginFlowTestSchemaValidationError{} + return &this +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *LoginFlowTestSchemaValidationError) GetMessage() string { + if o == nil || IsNil(o.Message) { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestSchemaValidationError) GetMessageOk() (*string, bool) { + if o == nil || IsNil(o.Message) { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *LoginFlowTestSchemaValidationError) HasMessage() bool { + if o != nil && !IsNil(o.Message) { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *LoginFlowTestSchemaValidationError) SetMessage(v string) { + o.Message = &v +} + +// GetPath returns the Path field value if set, zero value otherwise. +func (o *LoginFlowTestSchemaValidationError) GetPath() string { + if o == nil || IsNil(o.Path) { + var ret string + return ret + } + return *o.Path +} + +// GetPathOk returns a tuple with the Path field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestSchemaValidationError) GetPathOk() (*string, bool) { + if o == nil || IsNil(o.Path) { + return nil, false + } + return o.Path, true +} + +// HasPath returns a boolean if a field has been set. +func (o *LoginFlowTestSchemaValidationError) HasPath() bool { + if o != nil && !IsNil(o.Path) { + return true + } + + return false +} + +// SetPath gets a reference to the given string and assigns it to the Path field. +func (o *LoginFlowTestSchemaValidationError) SetPath(v string) { + o.Path = &v +} + +func (o LoginFlowTestSchemaValidationError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestSchemaValidationError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Message) { + toSerialize["message"] = o.Message + } + if !IsNil(o.Path) { + toSerialize["path"] = o.Path + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestSchemaValidationError) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestSchemaValidationError := _LoginFlowTestSchemaValidationError{} + + err = json.Unmarshal(data, &varLoginFlowTestSchemaValidationError) + + if err != nil { + return err + } + + *o = LoginFlowTestSchemaValidationError(varLoginFlowTestSchemaValidationError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "message") + delete(additionalProperties, "path") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestSchemaValidationError struct { + value *LoginFlowTestSchemaValidationError + isSet bool +} + +func (v NullableLoginFlowTestSchemaValidationError) Get() *LoginFlowTestSchemaValidationError { + return v.value +} + +func (v *NullableLoginFlowTestSchemaValidationError) Set(val *LoginFlowTestSchemaValidationError) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestSchemaValidationError) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestSchemaValidationError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestSchemaValidationError(val *LoginFlowTestSchemaValidationError) *NullableLoginFlowTestSchemaValidationError { + return &NullableLoginFlowTestSchemaValidationError{value: val, isSet: true} +} + +func (v NullableLoginFlowTestSchemaValidationError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestSchemaValidationError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_login_flow_test_step_error.go b/pkg/client-go/model_login_flow_test_step_error.go new file mode 100644 index 0000000..5604020 --- /dev/null +++ b/pkg/client-go/model_login_flow_test_step_error.go @@ -0,0 +1,231 @@ +/* +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" +) + +// checks if the LoginFlowTestStepError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestStepError{} + +// LoginFlowTestStepError Populated when any step (token exchange, claims decode, Jsonnet evaluation, schema validation) cannot complete. +type LoginFlowTestStepError struct { + // Human-readable message describing the failure. + Message *string `json:"message,omitempty"` + // Short classification of the failure cause (for example, \"access_denied\" or \"traits_invalid\"). + Reason *string `json:"reason,omitempty"` + // Machine-readable identifier of the failed step (for example, \"token_exchange\" or \"schema_validate\"). + Step *string `json:"step,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestStepError LoginFlowTestStepError + +// NewLoginFlowTestStepError instantiates a new LoginFlowTestStepError 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 NewLoginFlowTestStepError() *LoginFlowTestStepError { + this := LoginFlowTestStepError{} + return &this +} + +// NewLoginFlowTestStepErrorWithDefaults instantiates a new LoginFlowTestStepError 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 NewLoginFlowTestStepErrorWithDefaults() *LoginFlowTestStepError { + this := LoginFlowTestStepError{} + return &this +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetMessage() string { + if o == nil || IsNil(o.Message) { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetMessageOk() (*string, bool) { + if o == nil || IsNil(o.Message) { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasMessage() bool { + if o != nil && !IsNil(o.Message) { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *LoginFlowTestStepError) SetMessage(v string) { + o.Message = &v +} + +// GetReason returns the Reason field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetReason() string { + if o == nil || IsNil(o.Reason) { + var ret string + return ret + } + return *o.Reason +} + +// GetReasonOk returns a tuple with the Reason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetReasonOk() (*string, bool) { + if o == nil || IsNil(o.Reason) { + return nil, false + } + return o.Reason, true +} + +// HasReason returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasReason() bool { + if o != nil && !IsNil(o.Reason) { + return true + } + + return false +} + +// SetReason gets a reference to the given string and assigns it to the Reason field. +func (o *LoginFlowTestStepError) SetReason(v string) { + o.Reason = &v +} + +// GetStep returns the Step field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetStep() string { + if o == nil || IsNil(o.Step) { + var ret string + return ret + } + return *o.Step +} + +// GetStepOk returns a tuple with the Step field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetStepOk() (*string, bool) { + if o == nil || IsNil(o.Step) { + return nil, false + } + return o.Step, true +} + +// HasStep returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasStep() bool { + if o != nil && !IsNil(o.Step) { + return true + } + + return false +} + +// SetStep gets a reference to the given string and assigns it to the Step field. +func (o *LoginFlowTestStepError) SetStep(v string) { + o.Step = &v +} + +func (o LoginFlowTestStepError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestStepError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Message) { + toSerialize["message"] = o.Message + } + if !IsNil(o.Reason) { + toSerialize["reason"] = o.Reason + } + if !IsNil(o.Step) { + toSerialize["step"] = o.Step + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestStepError) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestStepError := _LoginFlowTestStepError{} + + err = json.Unmarshal(data, &varLoginFlowTestStepError) + + if err != nil { + return err + } + + *o = LoginFlowTestStepError(varLoginFlowTestStepError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "message") + delete(additionalProperties, "reason") + delete(additionalProperties, "step") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestStepError struct { + value *LoginFlowTestStepError + isSet bool +} + +func (v NullableLoginFlowTestStepError) Get() *LoginFlowTestStepError { + return v.value +} + +func (v *NullableLoginFlowTestStepError) Set(val *LoginFlowTestStepError) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestStepError) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestStepError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestStepError(val *LoginFlowTestStepError) *NullableLoginFlowTestStepError { + return &NullableLoginFlowTestStepError{value: val, isSet: true} +} + +func (v NullableLoginFlowTestStepError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestStepError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_logout_flow.go b/pkg/client-go/model_logout_flow.go new file mode 100644 index 0000000..8823e51 --- /dev/null +++ b/pkg/client-go/model_logout_flow.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the LogoutFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LogoutFlow{} + +// LogoutFlow Logout Flow +type LogoutFlow struct { + // LogoutToken can be used to perform logout using AJAX. + LogoutToken string `json:"logout_token"` + // LogoutURL can be opened in a browser to sign the user out. format: uri + LogoutUrl string `json:"logout_url"` + AdditionalProperties map[string]interface{} +} + +type _LogoutFlow LogoutFlow + +// NewLogoutFlow instantiates a new LogoutFlow 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 NewLogoutFlow(logoutToken string, logoutUrl string) *LogoutFlow { + this := LogoutFlow{} + this.LogoutToken = logoutToken + this.LogoutUrl = logoutUrl + return &this +} + +// NewLogoutFlowWithDefaults instantiates a new LogoutFlow 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 NewLogoutFlowWithDefaults() *LogoutFlow { + this := LogoutFlow{} + return &this +} + +// GetLogoutToken returns the LogoutToken field value +func (o *LogoutFlow) GetLogoutToken() string { + if o == nil { + var ret string + return ret + } + + return o.LogoutToken +} + +// GetLogoutTokenOk returns a tuple with the LogoutToken field value +// and a boolean to check if the value has been set. +func (o *LogoutFlow) GetLogoutTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LogoutToken, true +} + +// SetLogoutToken sets field value +func (o *LogoutFlow) SetLogoutToken(v string) { + o.LogoutToken = v +} + +// GetLogoutUrl returns the LogoutUrl field value +func (o *LogoutFlow) GetLogoutUrl() string { + if o == nil { + var ret string + return ret + } + + return o.LogoutUrl +} + +// GetLogoutUrlOk returns a tuple with the LogoutUrl field value +// and a boolean to check if the value has been set. +func (o *LogoutFlow) GetLogoutUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LogoutUrl, true +} + +// SetLogoutUrl sets field value +func (o *LogoutFlow) SetLogoutUrl(v string) { + o.LogoutUrl = v +} + +func (o LogoutFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LogoutFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["logout_token"] = o.LogoutToken + toSerialize["logout_url"] = o.LogoutUrl + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LogoutFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "logout_token", + "logout_url", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLogoutFlow := _LogoutFlow{} + + err = json.Unmarshal(data, &varLogoutFlow) + + if err != nil { + return err + } + + *o = LogoutFlow(varLogoutFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "logout_token") + delete(additionalProperties, "logout_url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLogoutFlow struct { + value *LogoutFlow + isSet bool +} + +func (v NullableLogoutFlow) Get() *LogoutFlow { + return v.value +} + +func (v *NullableLogoutFlow) Set(val *LogoutFlow) { + v.value = val + v.isSet = true +} + +func (v NullableLogoutFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableLogoutFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLogoutFlow(val *LogoutFlow) *NullableLogoutFlow { + return &NullableLogoutFlow{value: val, isSet: true} +} + +func (v NullableLogoutFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLogoutFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_manage_sessions_body.go b/pkg/client-go/model_manage_sessions_body.go new file mode 100644 index 0000000..2af0e88 --- /dev/null +++ b/pkg/client-go/model_manage_sessions_body.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the ManageSessionsBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ManageSessionsBody{} + +// ManageSessionsBody Body for the bulk session management endpoint. Exactly one of `identities` or `sessions` must be provided. To operate on every session in the network, pass `identities: [\"*\"]` — the wildcard must appear alone, never mixed with explicit IDs. +type ManageSessionsBody struct { + // Action to perform on the matching sessions. disable ManageSessionsActionDisable delete ManageSessionsActionDelete + Action string `json:"action"` + // Identity IDs whose sessions should be disabled or deleted, or `[\"*\"]` to operate on every session in the network. Mutually exclusive with `sessions`. + Identities []string `json:"identities,omitempty"` + // Session IDs to disable or delete. Mutually exclusive with `identities`. The wildcard `[\"*\"]` is not accepted in this field — pass `identities: [\"*\"]` to scope the operation to every session in the network. + Sessions []string `json:"sessions,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ManageSessionsBody ManageSessionsBody + +// NewManageSessionsBody instantiates a new ManageSessionsBody 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 NewManageSessionsBody(action string) *ManageSessionsBody { + this := ManageSessionsBody{} + this.Action = action + return &this +} + +// NewManageSessionsBodyWithDefaults instantiates a new ManageSessionsBody 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 NewManageSessionsBodyWithDefaults() *ManageSessionsBody { + this := ManageSessionsBody{} + return &this +} + +// GetAction returns the Action field value +func (o *ManageSessionsBody) 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 *ManageSessionsBody) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ManageSessionsBody) SetAction(v string) { + o.Action = v +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *ManageSessionsBody) GetIdentities() []string { + if o == nil || IsNil(o.Identities) { + var ret []string + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsBody) GetIdentitiesOk() ([]string, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *ManageSessionsBody) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []string and assigns it to the Identities field. +func (o *ManageSessionsBody) SetIdentities(v []string) { + o.Identities = v +} + +// GetSessions returns the Sessions field value if set, zero value otherwise. +func (o *ManageSessionsBody) GetSessions() []string { + if o == nil || IsNil(o.Sessions) { + var ret []string + return ret + } + return o.Sessions +} + +// GetSessionsOk returns a tuple with the Sessions field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsBody) GetSessionsOk() ([]string, bool) { + if o == nil || IsNil(o.Sessions) { + return nil, false + } + return o.Sessions, true +} + +// HasSessions returns a boolean if a field has been set. +func (o *ManageSessionsBody) HasSessions() bool { + if o != nil && !IsNil(o.Sessions) { + return true + } + + return false +} + +// SetSessions gets a reference to the given []string and assigns it to the Sessions field. +func (o *ManageSessionsBody) SetSessions(v []string) { + o.Sessions = v +} + +func (o ManageSessionsBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ManageSessionsBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + if !IsNil(o.Sessions) { + toSerialize["sessions"] = o.Sessions + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ManageSessionsBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varManageSessionsBody := _ManageSessionsBody{} + + err = json.Unmarshal(data, &varManageSessionsBody) + + if err != nil { + return err + } + + *o = ManageSessionsBody(varManageSessionsBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "identities") + delete(additionalProperties, "sessions") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableManageSessionsBody struct { + value *ManageSessionsBody + isSet bool +} + +func (v NullableManageSessionsBody) Get() *ManageSessionsBody { + return v.value +} + +func (v *NullableManageSessionsBody) Set(val *ManageSessionsBody) { + v.value = val + v.isSet = true +} + +func (v NullableManageSessionsBody) IsSet() bool { + return v.isSet +} + +func (v *NullableManageSessionsBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableManageSessionsBody(val *ManageSessionsBody) *NullableManageSessionsBody { + return &NullableManageSessionsBody{value: val, isSet: true} +} + +func (v NullableManageSessionsBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableManageSessionsBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_manage_sessions_response.go b/pkg/client-go/model_manage_sessions_response.go new file mode 100644 index 0000000..698ec2c --- /dev/null +++ b/pkg/client-go/model_manage_sessions_response.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the ManageSessionsResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ManageSessionsResponse{} + +// ManageSessionsResponse Response body for the bulk session management endpoint. Reports how many rows the call processed and, for the wildcard variant, whether the network still has matching rows left. Explicit-IDs requests always return `more: false`. Wildcard callers drain the network by re-issuing the same request while `more` is `true`. +type ManageSessionsResponse struct { + // True when the call reached the per-call batch limit and additional matching rows may remain. Always false for explicit-IDs requests. + More *bool `json:"more,omitempty"` + // Number of sessions processed in this call. For `disable`, counts only sessions that were active before the call (already-inactive sessions are skipped). For `delete`, counts every matching row removed. + Processed *int64 `json:"processed,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ManageSessionsResponse ManageSessionsResponse + +// NewManageSessionsResponse instantiates a new ManageSessionsResponse 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 NewManageSessionsResponse() *ManageSessionsResponse { + this := ManageSessionsResponse{} + return &this +} + +// NewManageSessionsResponseWithDefaults instantiates a new ManageSessionsResponse 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 NewManageSessionsResponseWithDefaults() *ManageSessionsResponse { + this := ManageSessionsResponse{} + return &this +} + +// GetMore returns the More field value if set, zero value otherwise. +func (o *ManageSessionsResponse) GetMore() bool { + if o == nil || IsNil(o.More) { + var ret bool + return ret + } + return *o.More +} + +// GetMoreOk returns a tuple with the More field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsResponse) GetMoreOk() (*bool, bool) { + if o == nil || IsNil(o.More) { + return nil, false + } + return o.More, true +} + +// HasMore returns a boolean if a field has been set. +func (o *ManageSessionsResponse) HasMore() bool { + if o != nil && !IsNil(o.More) { + return true + } + + return false +} + +// SetMore gets a reference to the given bool and assigns it to the More field. +func (o *ManageSessionsResponse) SetMore(v bool) { + o.More = &v +} + +// GetProcessed returns the Processed field value if set, zero value otherwise. +func (o *ManageSessionsResponse) GetProcessed() int64 { + if o == nil || IsNil(o.Processed) { + var ret int64 + return ret + } + return *o.Processed +} + +// GetProcessedOk returns a tuple with the Processed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsResponse) GetProcessedOk() (*int64, bool) { + if o == nil || IsNil(o.Processed) { + return nil, false + } + return o.Processed, true +} + +// HasProcessed returns a boolean if a field has been set. +func (o *ManageSessionsResponse) HasProcessed() bool { + if o != nil && !IsNil(o.Processed) { + return true + } + + return false +} + +// SetProcessed gets a reference to the given int64 and assigns it to the Processed field. +func (o *ManageSessionsResponse) SetProcessed(v int64) { + o.Processed = &v +} + +func (o ManageSessionsResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ManageSessionsResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.More) { + toSerialize["more"] = o.More + } + if !IsNil(o.Processed) { + toSerialize["processed"] = o.Processed + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ManageSessionsResponse) UnmarshalJSON(data []byte) (err error) { + varManageSessionsResponse := _ManageSessionsResponse{} + + err = json.Unmarshal(data, &varManageSessionsResponse) + + if err != nil { + return err + } + + *o = ManageSessionsResponse(varManageSessionsResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "more") + delete(additionalProperties, "processed") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableManageSessionsResponse struct { + value *ManageSessionsResponse + isSet bool +} + +func (v NullableManageSessionsResponse) Get() *ManageSessionsResponse { + return v.value +} + +func (v *NullableManageSessionsResponse) Set(val *ManageSessionsResponse) { + v.value = val + v.isSet = true +} + +func (v NullableManageSessionsResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableManageSessionsResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableManageSessionsResponse(val *ManageSessionsResponse) *NullableManageSessionsResponse { + return &NullableManageSessionsResponse{value: val, isSet: true} +} + +func (v NullableManageSessionsResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableManageSessionsResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_message.go b/pkg/client-go/model_message.go new file mode 100644 index 0000000..5dbfffd --- /dev/null +++ b/pkg/client-go/model_message.go @@ -0,0 +1,507 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Message type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Message{} + +// Message struct for Message +type Message struct { + Body string `json:"body"` + Channel *string `json:"channel,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at"` + // Dispatches store information about the attempts of delivering a message May contain an error if any happened, or just the `success` state. + Dispatches []MessageDispatch `json:"dispatches,omitempty"` + Id string `json:"id"` + Recipient string `json:"recipient"` + SendCount int64 `json:"send_count"` + Status CourierMessageStatus `json:"status"` + Subject string `json:"subject"` + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid verifiable_address_changed TypeVerifiableAddressChanged authenticator_key_added TypeAuthenticatorKeyAdded + TemplateType string `json:"template_type"` + Type CourierMessageType `json:"type"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at"` + AdditionalProperties map[string]interface{} +} + +type _Message Message + +// NewMessage instantiates a new Message 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 NewMessage(body string, createdAt time.Time, id string, recipient string, sendCount int64, status CourierMessageStatus, subject string, templateType string, type_ CourierMessageType, updatedAt time.Time) *Message { + this := Message{} + this.Body = body + this.CreatedAt = createdAt + this.Id = id + this.Recipient = recipient + this.SendCount = sendCount + this.Status = status + this.Subject = subject + this.TemplateType = templateType + this.Type = type_ + this.UpdatedAt = updatedAt + return &this +} + +// NewMessageWithDefaults instantiates a new Message 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 NewMessageWithDefaults() *Message { + this := Message{} + return &this +} + +// GetBody returns the Body field value +func (o *Message) GetBody() string { + if o == nil { + var ret string + return ret + } + + return o.Body +} + +// GetBodyOk returns a tuple with the Body field value +// and a boolean to check if the value has been set. +func (o *Message) GetBodyOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Body, true +} + +// SetBody sets field value +func (o *Message) SetBody(v string) { + o.Body = v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *Message) GetChannel() string { + if o == nil || IsNil(o.Channel) { + 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 *Message) GetChannelOk() (*string, bool) { + if o == nil || IsNil(o.Channel) { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *Message) HasChannel() bool { + if o != nil && !IsNil(o.Channel) { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *Message) SetChannel(v string) { + o.Channel = &v +} + +// GetCreatedAt returns the CreatedAt field value +func (o *Message) GetCreatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value +// and a boolean to check if the value has been set. +func (o *Message) GetCreatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.CreatedAt, true +} + +// SetCreatedAt sets field value +func (o *Message) SetCreatedAt(v time.Time) { + o.CreatedAt = v +} + +// GetDispatches returns the Dispatches field value if set, zero value otherwise. +func (o *Message) GetDispatches() []MessageDispatch { + if o == nil || IsNil(o.Dispatches) { + var ret []MessageDispatch + return ret + } + return o.Dispatches +} + +// GetDispatchesOk returns a tuple with the Dispatches field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Message) GetDispatchesOk() ([]MessageDispatch, bool) { + if o == nil || IsNil(o.Dispatches) { + return nil, false + } + return o.Dispatches, true +} + +// HasDispatches returns a boolean if a field has been set. +func (o *Message) HasDispatches() bool { + if o != nil && !IsNil(o.Dispatches) { + return true + } + + return false +} + +// SetDispatches gets a reference to the given []MessageDispatch and assigns it to the Dispatches field. +func (o *Message) SetDispatches(v []MessageDispatch) { + o.Dispatches = v +} + +// GetId returns the Id field value +func (o *Message) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Message) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Message) SetId(v string) { + o.Id = v +} + +// GetRecipient returns the Recipient field value +func (o *Message) GetRecipient() string { + if o == nil { + var ret string + return ret + } + + return o.Recipient +} + +// GetRecipientOk returns a tuple with the Recipient field value +// and a boolean to check if the value has been set. +func (o *Message) GetRecipientOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Recipient, true +} + +// SetRecipient sets field value +func (o *Message) SetRecipient(v string) { + o.Recipient = v +} + +// GetSendCount returns the SendCount field value +func (o *Message) GetSendCount() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.SendCount +} + +// GetSendCountOk returns a tuple with the SendCount field value +// and a boolean to check if the value has been set. +func (o *Message) GetSendCountOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.SendCount, true +} + +// SetSendCount sets field value +func (o *Message) SetSendCount(v int64) { + o.SendCount = v +} + +// GetStatus returns the Status field value +func (o *Message) GetStatus() CourierMessageStatus { + if o == nil { + var ret CourierMessageStatus + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *Message) GetStatusOk() (*CourierMessageStatus, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *Message) SetStatus(v CourierMessageStatus) { + o.Status = v +} + +// GetSubject returns the Subject field value +func (o *Message) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *Message) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *Message) SetSubject(v string) { + o.Subject = v +} + +// GetTemplateType returns the TemplateType field value +func (o *Message) GetTemplateType() string { + if o == nil { + var ret string + return ret + } + + return o.TemplateType +} + +// GetTemplateTypeOk returns a tuple with the TemplateType field value +// and a boolean to check if the value has been set. +func (o *Message) GetTemplateTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.TemplateType, true +} + +// SetTemplateType sets field value +func (o *Message) SetTemplateType(v string) { + o.TemplateType = v +} + +// GetType returns the Type field value +func (o *Message) GetType() CourierMessageType { + if o == nil { + var ret CourierMessageType + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *Message) GetTypeOk() (*CourierMessageType, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *Message) SetType(v CourierMessageType) { + o.Type = v +} + +// GetUpdatedAt returns the UpdatedAt field value +func (o *Message) GetUpdatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value +// and a boolean to check if the value has been set. +func (o *Message) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.UpdatedAt, true +} + +// SetUpdatedAt sets field value +func (o *Message) SetUpdatedAt(v time.Time) { + o.UpdatedAt = v +} + +func (o Message) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Message) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["body"] = o.Body + if !IsNil(o.Channel) { + toSerialize["channel"] = o.Channel + } + toSerialize["created_at"] = o.CreatedAt + if !IsNil(o.Dispatches) { + toSerialize["dispatches"] = o.Dispatches + } + toSerialize["id"] = o.Id + toSerialize["recipient"] = o.Recipient + toSerialize["send_count"] = o.SendCount + toSerialize["status"] = o.Status + toSerialize["subject"] = o.Subject + toSerialize["template_type"] = o.TemplateType + toSerialize["type"] = o.Type + toSerialize["updated_at"] = o.UpdatedAt + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Message) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "body", + "created_at", + "id", + "recipient", + "send_count", + "status", + "subject", + "template_type", + "type", + "updated_at", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varMessage := _Message{} + + err = json.Unmarshal(data, &varMessage) + + if err != nil { + return err + } + + *o = Message(varMessage) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "body") + delete(additionalProperties, "channel") + delete(additionalProperties, "created_at") + delete(additionalProperties, "dispatches") + delete(additionalProperties, "id") + delete(additionalProperties, "recipient") + delete(additionalProperties, "send_count") + delete(additionalProperties, "status") + delete(additionalProperties, "subject") + delete(additionalProperties, "template_type") + delete(additionalProperties, "type") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableMessage struct { + value *Message + isSet bool +} + +func (v NullableMessage) Get() *Message { + return v.value +} + +func (v *NullableMessage) Set(val *Message) { + v.value = val + v.isSet = true +} + +func (v NullableMessage) IsSet() bool { + return v.isSet +} + +func (v *NullableMessage) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMessage(val *Message) *NullableMessage { + return &NullableMessage{value: val, isSet: true} +} + +func (v NullableMessage) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMessage) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_message_dispatch.go b/pkg/client-go/model_message_dispatch.go new file mode 100644 index 0000000..a7a118c --- /dev/null +++ b/pkg/client-go/model_message_dispatch.go @@ -0,0 +1,326 @@ +/* +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" + "fmt" + "time" +) + +// checks if the MessageDispatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &MessageDispatch{} + +// MessageDispatch MessageDispatch represents an attempt of sending a courier message It contains the status of the attempt (failed or successful) and the error if any occured +type MessageDispatch struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at"` + Error map[string]interface{} `json:"error,omitempty"` + // The ID of this message dispatch + Id string `json:"id"` + // The ID of the message being dispatched + MessageId string `json:"message_id"` + // The status of this dispatch Either \"failed\" or \"success\" failed CourierMessageDispatchStatusFailed success CourierMessageDispatchStatusSuccess + Status string `json:"status"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at"` + AdditionalProperties map[string]interface{} +} + +type _MessageDispatch MessageDispatch + +// NewMessageDispatch instantiates a new MessageDispatch 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 NewMessageDispatch(createdAt time.Time, id string, messageId string, status string, updatedAt time.Time) *MessageDispatch { + this := MessageDispatch{} + this.CreatedAt = createdAt + this.Id = id + this.MessageId = messageId + this.Status = status + this.UpdatedAt = updatedAt + return &this +} + +// NewMessageDispatchWithDefaults instantiates a new MessageDispatch 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 NewMessageDispatchWithDefaults() *MessageDispatch { + this := MessageDispatch{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value +func (o *MessageDispatch) GetCreatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetCreatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.CreatedAt, true +} + +// SetCreatedAt sets field value +func (o *MessageDispatch) SetCreatedAt(v time.Time) { + o.CreatedAt = v +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *MessageDispatch) GetError() map[string]interface{} { + if o == nil || IsNil(o.Error) { + var ret map[string]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. +func (o *MessageDispatch) GetErrorOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Error) { + return map[string]interface{}{}, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *MessageDispatch) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given map[string]interface{} and assigns it to the Error field. +func (o *MessageDispatch) SetError(v map[string]interface{}) { + o.Error = v +} + +// GetId returns the Id field value +func (o *MessageDispatch) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *MessageDispatch) SetId(v string) { + o.Id = v +} + +// GetMessageId returns the MessageId field value +func (o *MessageDispatch) GetMessageId() string { + if o == nil { + var ret string + return ret + } + + return o.MessageId +} + +// GetMessageIdOk returns a tuple with the MessageId field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetMessageIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.MessageId, true +} + +// SetMessageId sets field value +func (o *MessageDispatch) SetMessageId(v string) { + o.MessageId = v +} + +// GetStatus returns the Status field value +func (o *MessageDispatch) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *MessageDispatch) SetStatus(v string) { + o.Status = v +} + +// GetUpdatedAt returns the UpdatedAt field value +func (o *MessageDispatch) GetUpdatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.UpdatedAt, true +} + +// SetUpdatedAt sets field value +func (o *MessageDispatch) SetUpdatedAt(v time.Time) { + o.UpdatedAt = v +} + +func (o MessageDispatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o MessageDispatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["created_at"] = o.CreatedAt + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["id"] = o.Id + toSerialize["message_id"] = o.MessageId + toSerialize["status"] = o.Status + toSerialize["updated_at"] = o.UpdatedAt + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *MessageDispatch) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "created_at", + "id", + "message_id", + "status", + "updated_at", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varMessageDispatch := _MessageDispatch{} + + err = json.Unmarshal(data, &varMessageDispatch) + + if err != nil { + return err + } + + *o = MessageDispatch(varMessageDispatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "error") + delete(additionalProperties, "id") + delete(additionalProperties, "message_id") + delete(additionalProperties, "status") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableMessageDispatch struct { + value *MessageDispatch + isSet bool +} + +func (v NullableMessageDispatch) Get() *MessageDispatch { + return v.value +} + +func (v *NullableMessageDispatch) Set(val *MessageDispatch) { + v.value = val + v.isSet = true +} + +func (v NullableMessageDispatch) IsSet() bool { + return v.isSet +} + +func (v *NullableMessageDispatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMessageDispatch(val *MessageDispatch) *NullableMessageDispatch { + return &NullableMessageDispatch{value: val, isSet: true} +} + +func (v NullableMessageDispatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMessageDispatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_needs_privileged_session_error.go b/pkg/client-go/model_needs_privileged_session_error.go new file mode 100644 index 0000000..6b26e35 --- /dev/null +++ b/pkg/client-go/model_needs_privileged_session_error.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the NeedsPrivilegedSessionError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &NeedsPrivilegedSessionError{} + +// NeedsPrivilegedSessionError struct for NeedsPrivilegedSessionError +type NeedsPrivilegedSessionError struct { + Error *GenericError `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo string `json:"redirect_browser_to"` + AdditionalProperties map[string]interface{} +} + +type _NeedsPrivilegedSessionError NeedsPrivilegedSessionError + +// NewNeedsPrivilegedSessionError instantiates a new NeedsPrivilegedSessionError 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 NewNeedsPrivilegedSessionError(redirectBrowserTo string) *NeedsPrivilegedSessionError { + this := NeedsPrivilegedSessionError{} + this.RedirectBrowserTo = redirectBrowserTo + return &this +} + +// NewNeedsPrivilegedSessionErrorWithDefaults instantiates a new NeedsPrivilegedSessionError 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 NewNeedsPrivilegedSessionErrorWithDefaults() *NeedsPrivilegedSessionError { + this := NeedsPrivilegedSessionError{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *NeedsPrivilegedSessionError) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *NeedsPrivilegedSessionError) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *NeedsPrivilegedSessionError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *NeedsPrivilegedSessionError) SetError(v GenericError) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value +func (o *NeedsPrivilegedSessionError) 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 *NeedsPrivilegedSessionError) GetRedirectBrowserToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RedirectBrowserTo, true +} + +// SetRedirectBrowserTo sets field value +func (o *NeedsPrivilegedSessionError) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = v +} + +func (o NeedsPrivilegedSessionError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o NeedsPrivilegedSessionError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *NeedsPrivilegedSessionError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "redirect_browser_to", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varNeedsPrivilegedSessionError := _NeedsPrivilegedSessionError{} + + err = json.Unmarshal(data, &varNeedsPrivilegedSessionError) + + if err != nil { + return err + } + + *o = NeedsPrivilegedSessionError(varNeedsPrivilegedSessionError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableNeedsPrivilegedSessionError struct { + value *NeedsPrivilegedSessionError + isSet bool +} + +func (v NullableNeedsPrivilegedSessionError) Get() *NeedsPrivilegedSessionError { + return v.value +} + +func (v *NullableNeedsPrivilegedSessionError) Set(val *NeedsPrivilegedSessionError) { + v.value = val + v.isSet = true +} + +func (v NullableNeedsPrivilegedSessionError) IsSet() bool { + return v.isSet +} + +func (v *NullableNeedsPrivilegedSessionError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableNeedsPrivilegedSessionError(val *NeedsPrivilegedSessionError) *NullableNeedsPrivilegedSessionError { + return &NullableNeedsPrivilegedSessionError{value: val, isSet: true} +} + +func (v NullableNeedsPrivilegedSessionError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableNeedsPrivilegedSessionError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_o_auth2_client.go b/pkg/client-go/model_o_auth2_client.go new file mode 100644 index 0000000..f731a0e --- /dev/null +++ b/pkg/client-go/model_o_auth2_client.go @@ -0,0 +1,1935 @@ +/* +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" + "time" +) + +// checks if the OAuth2Client type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2Client{} + +// OAuth2Client struct for OAuth2Client +type OAuth2Client struct { + // 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"` + Audience []string `json:"audience,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantAccessTokenLifespan *string `json:"authorization_code_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantIdTokenLifespan *string `json:"authorization_code_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantRefreshTokenLifespan *string `json:"authorization_code_grant_refresh_token_lifespan,omitempty"` + // OpenID Connect Back-Channel Logout Session Required Boolean value specifying whether the RP requires that a sid (session ID) Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. + BackchannelLogoutSessionRequired *bool `json:"backchannel_logout_session_required,omitempty"` + // OpenID Connect Back-Channel Logout URI RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. + BackchannelLogoutUri *string `json:"backchannel_logout_uri,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ClientCredentialsGrantAccessTokenLifespan *string `json:"client_credentials_grant_access_token_lifespan,omitempty"` + // OAuth 2.0 Client ID The ID is immutable. If no ID is provided, a UUID4 will be generated. + ClientId *string `json:"client_id,omitempty"` + // OAuth 2.0 Client Name The human-readable name of the client to be presented to the end-user during authorization. + ClientName *string `json:"client_name,omitempty"` + // OAuth 2.0 Client Secret The secret will be included in the create request as cleartext, and then never again. The secret is kept in hashed format and is not recoverable once lost. + ClientSecret *string `json:"client_secret,omitempty"` + // OAuth 2.0 Client Secret Expires At The field is currently not supported and its value is always 0. + ClientSecretExpiresAt *int64 `json:"client_secret_expires_at,omitempty"` + // OAuth 2.0 Client URI ClientURI is a URL string of a web page providing information about the client. If present, the server SHOULD display this URL to the end-user in a clickable fashion. + ClientUri *string `json:"client_uri,omitempty"` + Contacts []string `json:"contacts,omitempty"` + // OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. + CreatedAt *time.Time `json:"created_at,omitempty"` + // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. + FrontchannelLogoutSessionRequired *bool `json:"frontchannel_logout_session_required,omitempty"` + // OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. + FrontchannelLogoutUri *string `json:"frontchannel_logout_uri,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ImplicitGrantAccessTokenLifespan *string `json:"implicit_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ImplicitGrantIdTokenLifespan *string `json:"implicit_grant_id_token_lifespan,omitempty"` + // OAuth 2.0 Client JSON Web Key Set Client's JSON Web Key Set [JWK] document, passed by value. The semantics of the jwks parameter are the same as the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri parameter, for instance, by native applications that might not have a location to host the contents of the JWK Set. If a Client can use jwks_uri, it MUST NOT use jwks. One significant downside of jwks is that it does not enable key rotation (which jwks_uri does, as described in Section 10 of OpenID Connect Core 1.0 [OpenID.Core]). The jwks_uri and jwks parameters MUST NOT be used together. + Jwks interface{} `json:"jwks,omitempty"` + // OAuth 2.0 Client JSON Web Key Set URL URL for the Client's JSON Web Key Set [JWK] document. If the Client signs requests to the Server, it contains the signing key(s) the Server uses to validate signatures from the Client. The JWK Set MAY also contain the Client's encryption keys(s), which are used by the Server to encrypt responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage. Although some algorithms allow the same key to be used for both signatures and encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match those in the certificate. + JwksUri *string `json:"jwks_uri,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + JwtBearerGrantAccessTokenLifespan *string `json:"jwt_bearer_grant_access_token_lifespan,omitempty"` + // OAuth 2.0 Client Logo URI A URL string referencing the client's logo. + LogoUri *string `json:"logo_uri,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + // OAuth 2.0 Client Owner Owner is a string identifying the owner of the OAuth 2.0 Client. + Owner *string `json:"owner,omitempty"` + // OAuth 2.0 Client Policy URI PolicyURI is a URL string that points to a human-readable privacy policy document that describes how the deployment organization collects, uses, retains, and discloses personal data. + PolicyUri *string `json:"policy_uri,omitempty"` + PostLogoutRedirectUris []string `json:"post_logout_redirect_uris,omitempty"` + RedirectUris []string `json:"redirect_uris,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantAccessTokenLifespan *string `json:"refresh_token_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantIdTokenLifespan *string `json:"refresh_token_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantRefreshTokenLifespan *string `json:"refresh_token_grant_refresh_token_lifespan,omitempty"` + // OpenID Connect Dynamic Client Registration Access Token RegistrationAccessToken can be used to update, get, or delete the OAuth2 Client. It is sent when creating a client using Dynamic Client Registration. + RegistrationAccessToken *string `json:"registration_access_token,omitempty"` + // OpenID Connect Dynamic Client Registration URL RegistrationClientURI is the URL used to update, get, or delete the OAuth2 Client. + RegistrationClientUri *string `json:"registration_client_uri,omitempty"` + // OpenID Connect Request Object Signing Algorithm JWS [JWS] alg algorithm [JWA] that MUST be used for signing Request Objects sent to the OP. All Request Objects from this Client MUST be rejected, if not signed with this algorithm. + RequestObjectSigningAlg *string `json:"request_object_signing_alg,omitempty"` + RequestUris []string `json:"request_uris,omitempty"` + ResponseTypes []string `json:"response_types,omitempty"` + // OAuth 2.0 Client Scope Scope is a string containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. + Scope *string `json:"scope,omitempty"` + // OpenID Connect Sector Identifier URI URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. The URL references a file with a single JSON array of redirect_uri values. + SectorIdentifierUri *string `json:"sector_identifier_uri,omitempty"` + // SkipConsent skips the consent screen for this client. This field can only be set from the admin API. + SkipConsent *bool `json:"skip_consent,omitempty"` + // SkipLogoutConsent skips the logout consent screen for this client. This field can only be set from the admin API. + SkipLogoutConsent *bool `json:"skip_logout_consent,omitempty"` + // OpenID Connect Subject Type The `subject_types_supported` Discovery parameter contains a list of the supported subject_type values for this server. Valid types include `pairwise` and `public`. + SubjectType *string `json:"subject_type,omitempty"` + // OAuth 2.0 Token Endpoint Authentication Method Requested Client Authentication method for the Token Endpoint. The options are: `client_secret_basic`: (default) Send `client_id` and `client_secret` as `application/x-www-form-urlencoded` encoded in the HTTP Authorization header. `client_secret_post`: Send `client_id` and `client_secret` as `application/x-www-form-urlencoded` in the HTTP body. `private_key_jwt`: Use JSON Web Tokens to authenticate the client. `none`: Used for public clients (native apps, mobile apps) which can not have secrets. + TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"` + // OAuth 2.0 Token Endpoint Signing Algorithm Requested Client Authentication signing algorithm for the Token Endpoint. + TokenEndpointAuthSigningAlg *string `json:"token_endpoint_auth_signing_alg,omitempty"` + // OAuth 2.0 Client Terms of Service URI A URL string pointing to a human-readable terms of service document for the client that describes a contractual relationship between the end-user and the client that the end-user accepts when authorizing the client. + TosUri *string `json:"tos_uri,omitempty"` + // OAuth 2.0 Client Last Update Date UpdatedAt returns the timestamp of the last update. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // OpenID Connect Request Userinfo Signed Response Algorithm JWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses. If this is specified, the response will be JWT [JWT] serialized, and signed using JWS. The default, if omitted, is for the UserInfo Response to return the Claims as a UTF-8 encoded JSON object using the application/json content-type. + UserinfoSignedResponseAlg *string `json:"userinfo_signed_response_alg,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2Client OAuth2Client + +// NewOAuth2Client instantiates a new OAuth2Client 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 NewOAuth2Client() *OAuth2Client { + this := OAuth2Client{} + return &this +} + +// NewOAuth2ClientWithDefaults instantiates a new OAuth2Client 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 NewOAuth2ClientWithDefaults() *OAuth2Client { + this := OAuth2Client{} + return &this +} + +// GetAccessTokenStrategy returns the AccessTokenStrategy field value if set, zero value otherwise. +func (o *OAuth2Client) GetAccessTokenStrategy() string { + if o == nil || IsNil(o.AccessTokenStrategy) { + var ret string + return ret + } + return *o.AccessTokenStrategy +} + +// GetAccessTokenStrategyOk returns a tuple with the AccessTokenStrategy field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAccessTokenStrategyOk() (*string, bool) { + if o == nil || IsNil(o.AccessTokenStrategy) { + return nil, false + } + return o.AccessTokenStrategy, true +} + +// HasAccessTokenStrategy returns a boolean if a field has been set. +func (o *OAuth2Client) HasAccessTokenStrategy() bool { + if o != nil && !IsNil(o.AccessTokenStrategy) { + return true + } + + return false +} + +// SetAccessTokenStrategy gets a reference to the given string and assigns it to the AccessTokenStrategy field. +func (o *OAuth2Client) SetAccessTokenStrategy(v string) { + o.AccessTokenStrategy = &v +} + +// GetAllowedCorsOrigins returns the AllowedCorsOrigins field value if set, zero value otherwise. +func (o *OAuth2Client) GetAllowedCorsOrigins() []string { + if o == nil || IsNil(o.AllowedCorsOrigins) { + var ret []string + return ret + } + return o.AllowedCorsOrigins +} + +// GetAllowedCorsOriginsOk returns a tuple with the AllowedCorsOrigins field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAllowedCorsOriginsOk() ([]string, bool) { + if o == nil || IsNil(o.AllowedCorsOrigins) { + return nil, false + } + return o.AllowedCorsOrigins, true +} + +// HasAllowedCorsOrigins returns a boolean if a field has been set. +func (o *OAuth2Client) HasAllowedCorsOrigins() bool { + if o != nil && !IsNil(o.AllowedCorsOrigins) { + return true + } + + return false +} + +// SetAllowedCorsOrigins gets a reference to the given []string and assigns it to the AllowedCorsOrigins field. +func (o *OAuth2Client) SetAllowedCorsOrigins(v []string) { + o.AllowedCorsOrigins = v +} + +// GetAudience returns the Audience field value if set, zero value otherwise. +func (o *OAuth2Client) GetAudience() []string { + if o == nil || IsNil(o.Audience) { + var ret []string + return ret + } + return o.Audience +} + +// GetAudienceOk returns a tuple with the Audience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.Audience) { + return nil, false + } + return o.Audience, true +} + +// HasAudience returns a boolean if a field has been set. +func (o *OAuth2Client) HasAudience() bool { + if o != nil && !IsNil(o.Audience) { + return true + } + + return false +} + +// SetAudience gets a reference to the given []string and assigns it to the Audience field. +func (o *OAuth2Client) SetAudience(v []string) { + o.Audience = v +} + +// GetAuthorizationCodeGrantAccessTokenLifespan returns the AuthorizationCodeGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantAccessTokenLifespan +} + +// GetAuthorizationCodeGrantAccessTokenLifespanOk returns a tuple with the AuthorizationCodeGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantAccessTokenLifespan, true +} + +// HasAuthorizationCodeGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantAccessTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantAccessTokenLifespan(v string) { + o.AuthorizationCodeGrantAccessTokenLifespan = &v +} + +// GetAuthorizationCodeGrantIdTokenLifespan returns the AuthorizationCodeGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantIdTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantIdTokenLifespan +} + +// GetAuthorizationCodeGrantIdTokenLifespanOk returns a tuple with the AuthorizationCodeGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantIdTokenLifespan, true +} + +// HasAuthorizationCodeGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantIdTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantIdTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantIdTokenLifespan(v string) { + o.AuthorizationCodeGrantIdTokenLifespan = &v +} + +// GetAuthorizationCodeGrantRefreshTokenLifespan returns the AuthorizationCodeGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantRefreshTokenLifespan +} + +// GetAuthorizationCodeGrantRefreshTokenLifespanOk returns a tuple with the AuthorizationCodeGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantRefreshTokenLifespan, true +} + +// HasAuthorizationCodeGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantRefreshTokenLifespan(v string) { + o.AuthorizationCodeGrantRefreshTokenLifespan = &v +} + +// GetBackchannelLogoutSessionRequired returns the BackchannelLogoutSessionRequired field value if set, zero value otherwise. +func (o *OAuth2Client) GetBackchannelLogoutSessionRequired() bool { + if o == nil || IsNil(o.BackchannelLogoutSessionRequired) { + var ret bool + return ret + } + return *o.BackchannelLogoutSessionRequired +} + +// GetBackchannelLogoutSessionRequiredOk returns a tuple with the BackchannelLogoutSessionRequired field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetBackchannelLogoutSessionRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.BackchannelLogoutSessionRequired) { + return nil, false + } + return o.BackchannelLogoutSessionRequired, true +} + +// HasBackchannelLogoutSessionRequired returns a boolean if a field has been set. +func (o *OAuth2Client) HasBackchannelLogoutSessionRequired() bool { + if o != nil && !IsNil(o.BackchannelLogoutSessionRequired) { + return true + } + + return false +} + +// SetBackchannelLogoutSessionRequired gets a reference to the given bool and assigns it to the BackchannelLogoutSessionRequired field. +func (o *OAuth2Client) SetBackchannelLogoutSessionRequired(v bool) { + o.BackchannelLogoutSessionRequired = &v +} + +// GetBackchannelLogoutUri returns the BackchannelLogoutUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetBackchannelLogoutUri() string { + if o == nil || IsNil(o.BackchannelLogoutUri) { + var ret string + return ret + } + return *o.BackchannelLogoutUri +} + +// GetBackchannelLogoutUriOk returns a tuple with the BackchannelLogoutUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetBackchannelLogoutUriOk() (*string, bool) { + if o == nil || IsNil(o.BackchannelLogoutUri) { + return nil, false + } + return o.BackchannelLogoutUri, true +} + +// HasBackchannelLogoutUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasBackchannelLogoutUri() bool { + if o != nil && !IsNil(o.BackchannelLogoutUri) { + return true + } + + return false +} + +// SetBackchannelLogoutUri gets a reference to the given string and assigns it to the BackchannelLogoutUri field. +func (o *OAuth2Client) SetBackchannelLogoutUri(v string) { + o.BackchannelLogoutUri = &v +} + +// GetClientCredentialsGrantAccessTokenLifespan returns the ClientCredentialsGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientCredentialsGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.ClientCredentialsGrantAccessTokenLifespan +} + +// GetClientCredentialsGrantAccessTokenLifespanOk returns a tuple with the ClientCredentialsGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientCredentialsGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + return nil, false + } + return o.ClientCredentialsGrantAccessTokenLifespan, true +} + +// HasClientCredentialsGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientCredentialsGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetClientCredentialsGrantAccessTokenLifespan gets a reference to the given string and assigns it to the ClientCredentialsGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetClientCredentialsGrantAccessTokenLifespan(v string) { + o.ClientCredentialsGrantAccessTokenLifespan = &v +} + +// GetClientId returns the ClientId field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientId() string { + if o == nil || IsNil(o.ClientId) { + var ret string + return ret + } + return *o.ClientId +} + +// GetClientIdOk returns a tuple with the ClientId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientId) { + return nil, false + } + return o.ClientId, true +} + +// HasClientId returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientId() bool { + if o != nil && !IsNil(o.ClientId) { + return true + } + + return false +} + +// SetClientId gets a reference to the given string and assigns it to the ClientId field. +func (o *OAuth2Client) SetClientId(v string) { + o.ClientId = &v +} + +// GetClientName returns the ClientName field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientName() string { + if o == nil || IsNil(o.ClientName) { + var ret string + return ret + } + return *o.ClientName +} + +// GetClientNameOk returns a tuple with the ClientName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientNameOk() (*string, bool) { + if o == nil || IsNil(o.ClientName) { + return nil, false + } + return o.ClientName, true +} + +// HasClientName returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientName() bool { + if o != nil && !IsNil(o.ClientName) { + return true + } + + return false +} + +// SetClientName gets a reference to the given string and assigns it to the ClientName field. +func (o *OAuth2Client) SetClientName(v string) { + o.ClientName = &v +} + +// GetClientSecret returns the ClientSecret field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientSecret() string { + if o == nil || IsNil(o.ClientSecret) { + var ret string + return ret + } + return *o.ClientSecret +} + +// GetClientSecretOk returns a tuple with the ClientSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientSecretOk() (*string, bool) { + if o == nil || IsNil(o.ClientSecret) { + return nil, false + } + return o.ClientSecret, true +} + +// HasClientSecret returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientSecret() bool { + if o != nil && !IsNil(o.ClientSecret) { + return true + } + + return false +} + +// SetClientSecret gets a reference to the given string and assigns it to the ClientSecret field. +func (o *OAuth2Client) SetClientSecret(v string) { + o.ClientSecret = &v +} + +// GetClientSecretExpiresAt returns the ClientSecretExpiresAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientSecretExpiresAt() int64 { + if o == nil || IsNil(o.ClientSecretExpiresAt) { + var ret int64 + return ret + } + return *o.ClientSecretExpiresAt +} + +// GetClientSecretExpiresAtOk returns a tuple with the ClientSecretExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientSecretExpiresAtOk() (*int64, bool) { + if o == nil || IsNil(o.ClientSecretExpiresAt) { + return nil, false + } + return o.ClientSecretExpiresAt, true +} + +// HasClientSecretExpiresAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientSecretExpiresAt() bool { + if o != nil && !IsNil(o.ClientSecretExpiresAt) { + return true + } + + return false +} + +// SetClientSecretExpiresAt gets a reference to the given int64 and assigns it to the ClientSecretExpiresAt field. +func (o *OAuth2Client) SetClientSecretExpiresAt(v int64) { + o.ClientSecretExpiresAt = &v +} + +// GetClientUri returns the ClientUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientUri() string { + if o == nil || IsNil(o.ClientUri) { + var ret string + return ret + } + return *o.ClientUri +} + +// GetClientUriOk returns a tuple with the ClientUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientUriOk() (*string, bool) { + if o == nil || IsNil(o.ClientUri) { + return nil, false + } + return o.ClientUri, true +} + +// HasClientUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientUri() bool { + if o != nil && !IsNil(o.ClientUri) { + return true + } + + return false +} + +// SetClientUri gets a reference to the given string and assigns it to the ClientUri field. +func (o *OAuth2Client) SetClientUri(v string) { + o.ClientUri = &v +} + +// GetContacts returns the Contacts field value if set, zero value otherwise. +func (o *OAuth2Client) GetContacts() []string { + if o == nil || IsNil(o.Contacts) { + var ret []string + return ret + } + return o.Contacts +} + +// GetContactsOk returns a tuple with the Contacts field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetContactsOk() ([]string, bool) { + if o == nil || IsNil(o.Contacts) { + return nil, false + } + return o.Contacts, true +} + +// HasContacts returns a boolean if a field has been set. +func (o *OAuth2Client) HasContacts() bool { + if o != nil && !IsNil(o.Contacts) { + return true + } + + return false +} + +// SetContacts gets a reference to the given []string and assigns it to the Contacts field. +func (o *OAuth2Client) SetContacts(v []string) { + o.Contacts = v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *OAuth2Client) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetFrontchannelLogoutSessionRequired returns the FrontchannelLogoutSessionRequired field value if set, zero value otherwise. +func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool { + if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { + var ret bool + return ret + } + return *o.FrontchannelLogoutSessionRequired +} + +// GetFrontchannelLogoutSessionRequiredOk returns a tuple with the FrontchannelLogoutSessionRequired field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetFrontchannelLogoutSessionRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { + return nil, false + } + return o.FrontchannelLogoutSessionRequired, true +} + +// HasFrontchannelLogoutSessionRequired returns a boolean if a field has been set. +func (o *OAuth2Client) HasFrontchannelLogoutSessionRequired() bool { + if o != nil && !IsNil(o.FrontchannelLogoutSessionRequired) { + return true + } + + return false +} + +// SetFrontchannelLogoutSessionRequired gets a reference to the given bool and assigns it to the FrontchannelLogoutSessionRequired field. +func (o *OAuth2Client) SetFrontchannelLogoutSessionRequired(v bool) { + o.FrontchannelLogoutSessionRequired = &v +} + +// GetFrontchannelLogoutUri returns the FrontchannelLogoutUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetFrontchannelLogoutUri() string { + if o == nil || IsNil(o.FrontchannelLogoutUri) { + var ret string + return ret + } + return *o.FrontchannelLogoutUri +} + +// GetFrontchannelLogoutUriOk returns a tuple with the FrontchannelLogoutUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetFrontchannelLogoutUriOk() (*string, bool) { + if o == nil || IsNil(o.FrontchannelLogoutUri) { + return nil, false + } + return o.FrontchannelLogoutUri, true +} + +// HasFrontchannelLogoutUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasFrontchannelLogoutUri() bool { + if o != nil && !IsNil(o.FrontchannelLogoutUri) { + return true + } + + return false +} + +// SetFrontchannelLogoutUri gets a reference to the given string and assigns it to the FrontchannelLogoutUri field. +func (o *OAuth2Client) SetFrontchannelLogoutUri(v string) { + o.FrontchannelLogoutUri = &v +} + +// GetGrantTypes returns the GrantTypes field value if set, zero value otherwise. +func (o *OAuth2Client) GetGrantTypes() []string { + if o == nil || IsNil(o.GrantTypes) { + var ret []string + return ret + } + return o.GrantTypes +} + +// GetGrantTypesOk returns a tuple with the GrantTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetGrantTypesOk() ([]string, bool) { + if o == nil || IsNil(o.GrantTypes) { + return nil, false + } + return o.GrantTypes, true +} + +// HasGrantTypes returns a boolean if a field has been set. +func (o *OAuth2Client) HasGrantTypes() bool { + if o != nil && !IsNil(o.GrantTypes) { + return true + } + + return false +} + +// SetGrantTypes gets a reference to the given []string and assigns it to the GrantTypes field. +func (o *OAuth2Client) SetGrantTypes(v []string) { + o.GrantTypes = v +} + +// GetImplicitGrantAccessTokenLifespan returns the ImplicitGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetImplicitGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.ImplicitGrantAccessTokenLifespan +} + +// GetImplicitGrantAccessTokenLifespanOk returns a tuple with the ImplicitGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetImplicitGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { + return nil, false + } + return o.ImplicitGrantAccessTokenLifespan, true +} + +// HasImplicitGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasImplicitGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.ImplicitGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetImplicitGrantAccessTokenLifespan gets a reference to the given string and assigns it to the ImplicitGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetImplicitGrantAccessTokenLifespan(v string) { + o.ImplicitGrantAccessTokenLifespan = &v +} + +// GetImplicitGrantIdTokenLifespan returns the ImplicitGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetImplicitGrantIdTokenLifespan() string { + if o == nil || IsNil(o.ImplicitGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.ImplicitGrantIdTokenLifespan +} + +// GetImplicitGrantIdTokenLifespanOk returns a tuple with the ImplicitGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetImplicitGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ImplicitGrantIdTokenLifespan) { + return nil, false + } + return o.ImplicitGrantIdTokenLifespan, true +} + +// HasImplicitGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasImplicitGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.ImplicitGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetImplicitGrantIdTokenLifespan gets a reference to the given string and assigns it to the ImplicitGrantIdTokenLifespan field. +func (o *OAuth2Client) SetImplicitGrantIdTokenLifespan(v string) { + o.ImplicitGrantIdTokenLifespan = &v +} + +// GetJwks returns the Jwks field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *OAuth2Client) GetJwks() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Jwks +} + +// GetJwksOk returns a tuple with the Jwks 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 *OAuth2Client) GetJwksOk() (*interface{}, bool) { + if o == nil || IsNil(o.Jwks) { + return nil, false + } + return &o.Jwks, true +} + +// HasJwks returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwks() bool { + if o != nil && !IsNil(o.Jwks) { + return true + } + + return false +} + +// SetJwks gets a reference to the given interface{} and assigns it to the Jwks field. +func (o *OAuth2Client) SetJwks(v interface{}) { + o.Jwks = v +} + +// GetJwksUri returns the JwksUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetJwksUri() string { + if o == nil || IsNil(o.JwksUri) { + var ret string + return ret + } + return *o.JwksUri +} + +// GetJwksUriOk returns a tuple with the JwksUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetJwksUriOk() (*string, bool) { + if o == nil || IsNil(o.JwksUri) { + return nil, false + } + return o.JwksUri, true +} + +// HasJwksUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwksUri() bool { + if o != nil && !IsNil(o.JwksUri) { + return true + } + + return false +} + +// SetJwksUri gets a reference to the given string and assigns it to the JwksUri field. +func (o *OAuth2Client) SetJwksUri(v string) { + o.JwksUri = &v +} + +// GetJwtBearerGrantAccessTokenLifespan returns the JwtBearerGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetJwtBearerGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.JwtBearerGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.JwtBearerGrantAccessTokenLifespan +} + +// GetJwtBearerGrantAccessTokenLifespanOk returns a tuple with the JwtBearerGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetJwtBearerGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.JwtBearerGrantAccessTokenLifespan) { + return nil, false + } + return o.JwtBearerGrantAccessTokenLifespan, true +} + +// HasJwtBearerGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwtBearerGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.JwtBearerGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetJwtBearerGrantAccessTokenLifespan gets a reference to the given string and assigns it to the JwtBearerGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetJwtBearerGrantAccessTokenLifespan(v string) { + o.JwtBearerGrantAccessTokenLifespan = &v +} + +// GetLogoUri returns the LogoUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetLogoUri() string { + if o == nil || IsNil(o.LogoUri) { + var ret string + return ret + } + return *o.LogoUri +} + +// GetLogoUriOk returns a tuple with the LogoUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetLogoUriOk() (*string, bool) { + if o == nil || IsNil(o.LogoUri) { + return nil, false + } + return o.LogoUri, true +} + +// HasLogoUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasLogoUri() bool { + if o != nil && !IsNil(o.LogoUri) { + return true + } + + return false +} + +// SetLogoUri gets a reference to the given string and assigns it to the LogoUri field. +func (o *OAuth2Client) SetLogoUri(v string) { + o.LogoUri = &v +} + +// GetMetadata returns the Metadata field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *OAuth2Client) GetMetadata() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Metadata +} + +// GetMetadataOk returns a tuple with the Metadata 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 *OAuth2Client) GetMetadataOk() (*interface{}, bool) { + if o == nil || IsNil(o.Metadata) { + return nil, false + } + return &o.Metadata, true +} + +// HasMetadata returns a boolean if a field has been set. +func (o *OAuth2Client) HasMetadata() bool { + if o != nil && !IsNil(o.Metadata) { + return true + } + + return false +} + +// SetMetadata gets a reference to the given interface{} and assigns it to the Metadata field. +func (o *OAuth2Client) SetMetadata(v interface{}) { + o.Metadata = v +} + +// GetOwner returns the Owner field value if set, zero value otherwise. +func (o *OAuth2Client) GetOwner() string { + if o == nil || IsNil(o.Owner) { + var ret string + return ret + } + return *o.Owner +} + +// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetOwnerOk() (*string, bool) { + if o == nil || IsNil(o.Owner) { + return nil, false + } + return o.Owner, true +} + +// HasOwner returns a boolean if a field has been set. +func (o *OAuth2Client) HasOwner() bool { + if o != nil && !IsNil(o.Owner) { + return true + } + + return false +} + +// SetOwner gets a reference to the given string and assigns it to the Owner field. +func (o *OAuth2Client) SetOwner(v string) { + o.Owner = &v +} + +// GetPolicyUri returns the PolicyUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetPolicyUri() string { + if o == nil || IsNil(o.PolicyUri) { + var ret string + return ret + } + return *o.PolicyUri +} + +// GetPolicyUriOk returns a tuple with the PolicyUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetPolicyUriOk() (*string, bool) { + if o == nil || IsNil(o.PolicyUri) { + return nil, false + } + return o.PolicyUri, true +} + +// HasPolicyUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasPolicyUri() bool { + if o != nil && !IsNil(o.PolicyUri) { + return true + } + + return false +} + +// SetPolicyUri gets a reference to the given string and assigns it to the PolicyUri field. +func (o *OAuth2Client) SetPolicyUri(v string) { + o.PolicyUri = &v +} + +// GetPostLogoutRedirectUris returns the PostLogoutRedirectUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetPostLogoutRedirectUris() []string { + if o == nil || IsNil(o.PostLogoutRedirectUris) { + var ret []string + return ret + } + return o.PostLogoutRedirectUris +} + +// GetPostLogoutRedirectUrisOk returns a tuple with the PostLogoutRedirectUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetPostLogoutRedirectUrisOk() ([]string, bool) { + if o == nil || IsNil(o.PostLogoutRedirectUris) { + return nil, false + } + return o.PostLogoutRedirectUris, true +} + +// HasPostLogoutRedirectUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasPostLogoutRedirectUris() bool { + if o != nil && !IsNil(o.PostLogoutRedirectUris) { + return true + } + + return false +} + +// SetPostLogoutRedirectUris gets a reference to the given []string and assigns it to the PostLogoutRedirectUris field. +func (o *OAuth2Client) SetPostLogoutRedirectUris(v []string) { + o.PostLogoutRedirectUris = v +} + +// GetRedirectUris returns the RedirectUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetRedirectUris() []string { + if o == nil || IsNil(o.RedirectUris) { + var ret []string + return ret + } + return o.RedirectUris +} + +// GetRedirectUrisOk returns a tuple with the RedirectUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRedirectUrisOk() ([]string, bool) { + if o == nil || IsNil(o.RedirectUris) { + return nil, false + } + return o.RedirectUris, true +} + +// HasRedirectUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasRedirectUris() bool { + if o != nil && !IsNil(o.RedirectUris) { + return true + } + + return false +} + +// SetRedirectUris gets a reference to the given []string and assigns it to the RedirectUris field. +func (o *OAuth2Client) SetRedirectUris(v []string) { + o.RedirectUris = v +} + +// GetRefreshTokenGrantAccessTokenLifespan returns the RefreshTokenGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantAccessTokenLifespan +} + +// GetRefreshTokenGrantAccessTokenLifespanOk returns a tuple with the RefreshTokenGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantAccessTokenLifespan, true +} + +// HasRefreshTokenGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantAccessTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantAccessTokenLifespan(v string) { + o.RefreshTokenGrantAccessTokenLifespan = &v +} + +// GetRefreshTokenGrantIdTokenLifespan returns the RefreshTokenGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantIdTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantIdTokenLifespan +} + +// GetRefreshTokenGrantIdTokenLifespanOk returns a tuple with the RefreshTokenGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantIdTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantIdTokenLifespan, true +} + +// HasRefreshTokenGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantIdTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantIdTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantIdTokenLifespan(v string) { + o.RefreshTokenGrantIdTokenLifespan = &v +} + +// GetRefreshTokenGrantRefreshTokenLifespan returns the RefreshTokenGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantRefreshTokenLifespan +} + +// GetRefreshTokenGrantRefreshTokenLifespanOk returns a tuple with the RefreshTokenGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantRefreshTokenLifespan, true +} + +// HasRefreshTokenGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantRefreshTokenLifespan(v string) { + o.RefreshTokenGrantRefreshTokenLifespan = &v +} + +// GetRegistrationAccessToken returns the RegistrationAccessToken field value if set, zero value otherwise. +func (o *OAuth2Client) GetRegistrationAccessToken() string { + if o == nil || IsNil(o.RegistrationAccessToken) { + var ret string + return ret + } + return *o.RegistrationAccessToken +} + +// GetRegistrationAccessTokenOk returns a tuple with the RegistrationAccessToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRegistrationAccessTokenOk() (*string, bool) { + if o == nil || IsNil(o.RegistrationAccessToken) { + return nil, false + } + return o.RegistrationAccessToken, true +} + +// HasRegistrationAccessToken returns a boolean if a field has been set. +func (o *OAuth2Client) HasRegistrationAccessToken() bool { + if o != nil && !IsNil(o.RegistrationAccessToken) { + return true + } + + return false +} + +// SetRegistrationAccessToken gets a reference to the given string and assigns it to the RegistrationAccessToken field. +func (o *OAuth2Client) SetRegistrationAccessToken(v string) { + o.RegistrationAccessToken = &v +} + +// GetRegistrationClientUri returns the RegistrationClientUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetRegistrationClientUri() string { + if o == nil || IsNil(o.RegistrationClientUri) { + var ret string + return ret + } + return *o.RegistrationClientUri +} + +// GetRegistrationClientUriOk returns a tuple with the RegistrationClientUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRegistrationClientUriOk() (*string, bool) { + if o == nil || IsNil(o.RegistrationClientUri) { + return nil, false + } + return o.RegistrationClientUri, true +} + +// HasRegistrationClientUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasRegistrationClientUri() bool { + if o != nil && !IsNil(o.RegistrationClientUri) { + return true + } + + return false +} + +// SetRegistrationClientUri gets a reference to the given string and assigns it to the RegistrationClientUri field. +func (o *OAuth2Client) SetRegistrationClientUri(v string) { + o.RegistrationClientUri = &v +} + +// GetRequestObjectSigningAlg returns the RequestObjectSigningAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetRequestObjectSigningAlg() string { + if o == nil || IsNil(o.RequestObjectSigningAlg) { + var ret string + return ret + } + return *o.RequestObjectSigningAlg +} + +// GetRequestObjectSigningAlgOk returns a tuple with the RequestObjectSigningAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRequestObjectSigningAlgOk() (*string, bool) { + if o == nil || IsNil(o.RequestObjectSigningAlg) { + return nil, false + } + return o.RequestObjectSigningAlg, true +} + +// HasRequestObjectSigningAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasRequestObjectSigningAlg() bool { + if o != nil && !IsNil(o.RequestObjectSigningAlg) { + return true + } + + return false +} + +// SetRequestObjectSigningAlg gets a reference to the given string and assigns it to the RequestObjectSigningAlg field. +func (o *OAuth2Client) SetRequestObjectSigningAlg(v string) { + o.RequestObjectSigningAlg = &v +} + +// GetRequestUris returns the RequestUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetRequestUris() []string { + if o == nil || IsNil(o.RequestUris) { + var ret []string + return ret + } + return o.RequestUris +} + +// GetRequestUrisOk returns a tuple with the RequestUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRequestUrisOk() ([]string, bool) { + if o == nil || IsNil(o.RequestUris) { + return nil, false + } + return o.RequestUris, true +} + +// HasRequestUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasRequestUris() bool { + if o != nil && !IsNil(o.RequestUris) { + return true + } + + return false +} + +// SetRequestUris gets a reference to the given []string and assigns it to the RequestUris field. +func (o *OAuth2Client) SetRequestUris(v []string) { + o.RequestUris = v +} + +// GetResponseTypes returns the ResponseTypes field value if set, zero value otherwise. +func (o *OAuth2Client) GetResponseTypes() []string { + if o == nil || IsNil(o.ResponseTypes) { + var ret []string + return ret + } + return o.ResponseTypes +} + +// GetResponseTypesOk returns a tuple with the ResponseTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetResponseTypesOk() ([]string, bool) { + if o == nil || IsNil(o.ResponseTypes) { + return nil, false + } + return o.ResponseTypes, true +} + +// HasResponseTypes returns a boolean if a field has been set. +func (o *OAuth2Client) HasResponseTypes() bool { + if o != nil && !IsNil(o.ResponseTypes) { + return true + } + + return false +} + +// SetResponseTypes gets a reference to the given []string and assigns it to the ResponseTypes field. +func (o *OAuth2Client) SetResponseTypes(v []string) { + o.ResponseTypes = v +} + +// GetScope returns the Scope field value if set, zero value otherwise. +func (o *OAuth2Client) GetScope() string { + if o == nil || IsNil(o.Scope) { + var ret string + return ret + } + return *o.Scope +} + +// GetScopeOk returns a tuple with the Scope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetScopeOk() (*string, bool) { + if o == nil || IsNil(o.Scope) { + return nil, false + } + return o.Scope, true +} + +// HasScope returns a boolean if a field has been set. +func (o *OAuth2Client) HasScope() bool { + if o != nil && !IsNil(o.Scope) { + return true + } + + return false +} + +// SetScope gets a reference to the given string and assigns it to the Scope field. +func (o *OAuth2Client) SetScope(v string) { + o.Scope = &v +} + +// GetSectorIdentifierUri returns the SectorIdentifierUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetSectorIdentifierUri() string { + if o == nil || IsNil(o.SectorIdentifierUri) { + var ret string + return ret + } + return *o.SectorIdentifierUri +} + +// GetSectorIdentifierUriOk returns a tuple with the SectorIdentifierUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSectorIdentifierUriOk() (*string, bool) { + if o == nil || IsNil(o.SectorIdentifierUri) { + return nil, false + } + return o.SectorIdentifierUri, true +} + +// HasSectorIdentifierUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasSectorIdentifierUri() bool { + if o != nil && !IsNil(o.SectorIdentifierUri) { + return true + } + + return false +} + +// SetSectorIdentifierUri gets a reference to the given string and assigns it to the SectorIdentifierUri field. +func (o *OAuth2Client) SetSectorIdentifierUri(v string) { + o.SectorIdentifierUri = &v +} + +// GetSkipConsent returns the SkipConsent field value if set, zero value otherwise. +func (o *OAuth2Client) GetSkipConsent() bool { + if o == nil || IsNil(o.SkipConsent) { + var ret bool + return ret + } + return *o.SkipConsent +} + +// GetSkipConsentOk returns a tuple with the SkipConsent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSkipConsentOk() (*bool, bool) { + if o == nil || IsNil(o.SkipConsent) { + return nil, false + } + return o.SkipConsent, true +} + +// HasSkipConsent returns a boolean if a field has been set. +func (o *OAuth2Client) HasSkipConsent() bool { + if o != nil && !IsNil(o.SkipConsent) { + return true + } + + return false +} + +// SetSkipConsent gets a reference to the given bool and assigns it to the SkipConsent field. +func (o *OAuth2Client) SetSkipConsent(v bool) { + o.SkipConsent = &v +} + +// GetSkipLogoutConsent returns the SkipLogoutConsent field value if set, zero value otherwise. +func (o *OAuth2Client) GetSkipLogoutConsent() bool { + if o == nil || IsNil(o.SkipLogoutConsent) { + var ret bool + return ret + } + return *o.SkipLogoutConsent +} + +// GetSkipLogoutConsentOk returns a tuple with the SkipLogoutConsent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSkipLogoutConsentOk() (*bool, bool) { + if o == nil || IsNil(o.SkipLogoutConsent) { + return nil, false + } + return o.SkipLogoutConsent, true +} + +// HasSkipLogoutConsent returns a boolean if a field has been set. +func (o *OAuth2Client) HasSkipLogoutConsent() bool { + if o != nil && !IsNil(o.SkipLogoutConsent) { + return true + } + + return false +} + +// SetSkipLogoutConsent gets a reference to the given bool and assigns it to the SkipLogoutConsent field. +func (o *OAuth2Client) SetSkipLogoutConsent(v bool) { + o.SkipLogoutConsent = &v +} + +// GetSubjectType returns the SubjectType field value if set, zero value otherwise. +func (o *OAuth2Client) GetSubjectType() string { + if o == nil || IsNil(o.SubjectType) { + var ret string + return ret + } + return *o.SubjectType +} + +// GetSubjectTypeOk returns a tuple with the SubjectType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSubjectTypeOk() (*string, bool) { + if o == nil || IsNil(o.SubjectType) { + return nil, false + } + return o.SubjectType, true +} + +// HasSubjectType returns a boolean if a field has been set. +func (o *OAuth2Client) HasSubjectType() bool { + if o != nil && !IsNil(o.SubjectType) { + return true + } + + return false +} + +// SetSubjectType gets a reference to the given string and assigns it to the SubjectType field. +func (o *OAuth2Client) SetSubjectType(v string) { + o.SubjectType = &v +} + +// GetTokenEndpointAuthMethod returns the TokenEndpointAuthMethod field value if set, zero value otherwise. +func (o *OAuth2Client) GetTokenEndpointAuthMethod() string { + if o == nil || IsNil(o.TokenEndpointAuthMethod) { + var ret string + return ret + } + return *o.TokenEndpointAuthMethod +} + +// GetTokenEndpointAuthMethodOk returns a tuple with the TokenEndpointAuthMethod field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTokenEndpointAuthMethodOk() (*string, bool) { + if o == nil || IsNil(o.TokenEndpointAuthMethod) { + return nil, false + } + return o.TokenEndpointAuthMethod, true +} + +// HasTokenEndpointAuthMethod returns a boolean if a field has been set. +func (o *OAuth2Client) HasTokenEndpointAuthMethod() bool { + if o != nil && !IsNil(o.TokenEndpointAuthMethod) { + return true + } + + return false +} + +// SetTokenEndpointAuthMethod gets a reference to the given string and assigns it to the TokenEndpointAuthMethod field. +func (o *OAuth2Client) SetTokenEndpointAuthMethod(v string) { + o.TokenEndpointAuthMethod = &v +} + +// GetTokenEndpointAuthSigningAlg returns the TokenEndpointAuthSigningAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetTokenEndpointAuthSigningAlg() string { + if o == nil || IsNil(o.TokenEndpointAuthSigningAlg) { + var ret string + return ret + } + return *o.TokenEndpointAuthSigningAlg +} + +// GetTokenEndpointAuthSigningAlgOk returns a tuple with the TokenEndpointAuthSigningAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTokenEndpointAuthSigningAlgOk() (*string, bool) { + if o == nil || IsNil(o.TokenEndpointAuthSigningAlg) { + return nil, false + } + return o.TokenEndpointAuthSigningAlg, true +} + +// HasTokenEndpointAuthSigningAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasTokenEndpointAuthSigningAlg() bool { + if o != nil && !IsNil(o.TokenEndpointAuthSigningAlg) { + return true + } + + return false +} + +// SetTokenEndpointAuthSigningAlg gets a reference to the given string and assigns it to the TokenEndpointAuthSigningAlg field. +func (o *OAuth2Client) SetTokenEndpointAuthSigningAlg(v string) { + o.TokenEndpointAuthSigningAlg = &v +} + +// GetTosUri returns the TosUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetTosUri() string { + if o == nil || IsNil(o.TosUri) { + var ret string + return ret + } + return *o.TosUri +} + +// GetTosUriOk returns a tuple with the TosUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTosUriOk() (*string, bool) { + if o == nil || IsNil(o.TosUri) { + return nil, false + } + return o.TosUri, true +} + +// HasTosUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasTosUri() bool { + if o != nil && !IsNil(o.TosUri) { + return true + } + + return false +} + +// SetTosUri gets a reference to the given string and assigns it to the TosUri field. +func (o *OAuth2Client) SetTosUri(v string) { + o.TosUri = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *OAuth2Client) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetUserinfoSignedResponseAlg returns the UserinfoSignedResponseAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetUserinfoSignedResponseAlg() string { + if o == nil || IsNil(o.UserinfoSignedResponseAlg) { + var ret string + return ret + } + return *o.UserinfoSignedResponseAlg +} + +// GetUserinfoSignedResponseAlgOk returns a tuple with the UserinfoSignedResponseAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetUserinfoSignedResponseAlgOk() (*string, bool) { + if o == nil || IsNil(o.UserinfoSignedResponseAlg) { + return nil, false + } + return o.UserinfoSignedResponseAlg, true +} + +// HasUserinfoSignedResponseAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasUserinfoSignedResponseAlg() bool { + if o != nil && !IsNil(o.UserinfoSignedResponseAlg) { + return true + } + + return false +} + +// SetUserinfoSignedResponseAlg gets a reference to the given string and assigns it to the UserinfoSignedResponseAlg field. +func (o *OAuth2Client) SetUserinfoSignedResponseAlg(v string) { + o.UserinfoSignedResponseAlg = &v +} + +func (o OAuth2Client) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2Client) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AccessTokenStrategy) { + toSerialize["access_token_strategy"] = o.AccessTokenStrategy + } + if !IsNil(o.AllowedCorsOrigins) { + toSerialize["allowed_cors_origins"] = o.AllowedCorsOrigins + } + if !IsNil(o.Audience) { + toSerialize["audience"] = o.Audience + } + if !IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + toSerialize["authorization_code_grant_access_token_lifespan"] = o.AuthorizationCodeGrantAccessTokenLifespan + } + if !IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + toSerialize["authorization_code_grant_id_token_lifespan"] = o.AuthorizationCodeGrantIdTokenLifespan + } + if !IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + toSerialize["authorization_code_grant_refresh_token_lifespan"] = o.AuthorizationCodeGrantRefreshTokenLifespan + } + if !IsNil(o.BackchannelLogoutSessionRequired) { + toSerialize["backchannel_logout_session_required"] = o.BackchannelLogoutSessionRequired + } + if !IsNil(o.BackchannelLogoutUri) { + toSerialize["backchannel_logout_uri"] = o.BackchannelLogoutUri + } + if !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + toSerialize["client_credentials_grant_access_token_lifespan"] = o.ClientCredentialsGrantAccessTokenLifespan + } + if !IsNil(o.ClientId) { + toSerialize["client_id"] = o.ClientId + } + if !IsNil(o.ClientName) { + toSerialize["client_name"] = o.ClientName + } + if !IsNil(o.ClientSecret) { + toSerialize["client_secret"] = o.ClientSecret + } + if !IsNil(o.ClientSecretExpiresAt) { + toSerialize["client_secret_expires_at"] = o.ClientSecretExpiresAt + } + if !IsNil(o.ClientUri) { + toSerialize["client_uri"] = o.ClientUri + } + if !IsNil(o.Contacts) { + toSerialize["contacts"] = o.Contacts + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.FrontchannelLogoutSessionRequired) { + toSerialize["frontchannel_logout_session_required"] = o.FrontchannelLogoutSessionRequired + } + if !IsNil(o.FrontchannelLogoutUri) { + toSerialize["frontchannel_logout_uri"] = o.FrontchannelLogoutUri + } + if !IsNil(o.GrantTypes) { + toSerialize["grant_types"] = o.GrantTypes + } + if !IsNil(o.ImplicitGrantAccessTokenLifespan) { + toSerialize["implicit_grant_access_token_lifespan"] = o.ImplicitGrantAccessTokenLifespan + } + if !IsNil(o.ImplicitGrantIdTokenLifespan) { + toSerialize["implicit_grant_id_token_lifespan"] = o.ImplicitGrantIdTokenLifespan + } + if o.Jwks != nil { + toSerialize["jwks"] = o.Jwks + } + if !IsNil(o.JwksUri) { + toSerialize["jwks_uri"] = o.JwksUri + } + if !IsNil(o.JwtBearerGrantAccessTokenLifespan) { + toSerialize["jwt_bearer_grant_access_token_lifespan"] = o.JwtBearerGrantAccessTokenLifespan + } + if !IsNil(o.LogoUri) { + toSerialize["logo_uri"] = o.LogoUri + } + if o.Metadata != nil { + toSerialize["metadata"] = o.Metadata + } + if !IsNil(o.Owner) { + toSerialize["owner"] = o.Owner + } + if !IsNil(o.PolicyUri) { + toSerialize["policy_uri"] = o.PolicyUri + } + if !IsNil(o.PostLogoutRedirectUris) { + toSerialize["post_logout_redirect_uris"] = o.PostLogoutRedirectUris + } + if !IsNil(o.RedirectUris) { + toSerialize["redirect_uris"] = o.RedirectUris + } + if !IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + toSerialize["refresh_token_grant_access_token_lifespan"] = o.RefreshTokenGrantAccessTokenLifespan + } + if !IsNil(o.RefreshTokenGrantIdTokenLifespan) { + toSerialize["refresh_token_grant_id_token_lifespan"] = o.RefreshTokenGrantIdTokenLifespan + } + if !IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + toSerialize["refresh_token_grant_refresh_token_lifespan"] = o.RefreshTokenGrantRefreshTokenLifespan + } + if !IsNil(o.RegistrationAccessToken) { + toSerialize["registration_access_token"] = o.RegistrationAccessToken + } + if !IsNil(o.RegistrationClientUri) { + toSerialize["registration_client_uri"] = o.RegistrationClientUri + } + if !IsNil(o.RequestObjectSigningAlg) { + toSerialize["request_object_signing_alg"] = o.RequestObjectSigningAlg + } + if !IsNil(o.RequestUris) { + toSerialize["request_uris"] = o.RequestUris + } + if !IsNil(o.ResponseTypes) { + toSerialize["response_types"] = o.ResponseTypes + } + if !IsNil(o.Scope) { + toSerialize["scope"] = o.Scope + } + if !IsNil(o.SectorIdentifierUri) { + toSerialize["sector_identifier_uri"] = o.SectorIdentifierUri + } + if !IsNil(o.SkipConsent) { + toSerialize["skip_consent"] = o.SkipConsent + } + if !IsNil(o.SkipLogoutConsent) { + toSerialize["skip_logout_consent"] = o.SkipLogoutConsent + } + if !IsNil(o.SubjectType) { + toSerialize["subject_type"] = o.SubjectType + } + if !IsNil(o.TokenEndpointAuthMethod) { + toSerialize["token_endpoint_auth_method"] = o.TokenEndpointAuthMethod + } + if !IsNil(o.TokenEndpointAuthSigningAlg) { + toSerialize["token_endpoint_auth_signing_alg"] = o.TokenEndpointAuthSigningAlg + } + if !IsNil(o.TosUri) { + toSerialize["tos_uri"] = o.TosUri + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.UserinfoSignedResponseAlg) { + toSerialize["userinfo_signed_response_alg"] = o.UserinfoSignedResponseAlg + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2Client) UnmarshalJSON(data []byte) (err error) { + varOAuth2Client := _OAuth2Client{} + + err = json.Unmarshal(data, &varOAuth2Client) + + if err != nil { + return err + } + + *o = OAuth2Client(varOAuth2Client) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "access_token_strategy") + delete(additionalProperties, "allowed_cors_origins") + delete(additionalProperties, "audience") + delete(additionalProperties, "authorization_code_grant_access_token_lifespan") + delete(additionalProperties, "authorization_code_grant_id_token_lifespan") + delete(additionalProperties, "authorization_code_grant_refresh_token_lifespan") + delete(additionalProperties, "backchannel_logout_session_required") + delete(additionalProperties, "backchannel_logout_uri") + delete(additionalProperties, "client_credentials_grant_access_token_lifespan") + delete(additionalProperties, "client_id") + delete(additionalProperties, "client_name") + delete(additionalProperties, "client_secret") + delete(additionalProperties, "client_secret_expires_at") + delete(additionalProperties, "client_uri") + delete(additionalProperties, "contacts") + delete(additionalProperties, "created_at") + delete(additionalProperties, "frontchannel_logout_session_required") + delete(additionalProperties, "frontchannel_logout_uri") + delete(additionalProperties, "grant_types") + delete(additionalProperties, "implicit_grant_access_token_lifespan") + delete(additionalProperties, "implicit_grant_id_token_lifespan") + delete(additionalProperties, "jwks") + delete(additionalProperties, "jwks_uri") + delete(additionalProperties, "jwt_bearer_grant_access_token_lifespan") + delete(additionalProperties, "logo_uri") + delete(additionalProperties, "metadata") + delete(additionalProperties, "owner") + delete(additionalProperties, "policy_uri") + delete(additionalProperties, "post_logout_redirect_uris") + delete(additionalProperties, "redirect_uris") + delete(additionalProperties, "refresh_token_grant_access_token_lifespan") + delete(additionalProperties, "refresh_token_grant_id_token_lifespan") + delete(additionalProperties, "refresh_token_grant_refresh_token_lifespan") + delete(additionalProperties, "registration_access_token") + delete(additionalProperties, "registration_client_uri") + delete(additionalProperties, "request_object_signing_alg") + delete(additionalProperties, "request_uris") + delete(additionalProperties, "response_types") + delete(additionalProperties, "scope") + delete(additionalProperties, "sector_identifier_uri") + delete(additionalProperties, "skip_consent") + delete(additionalProperties, "skip_logout_consent") + delete(additionalProperties, "subject_type") + delete(additionalProperties, "token_endpoint_auth_method") + delete(additionalProperties, "token_endpoint_auth_signing_alg") + delete(additionalProperties, "tos_uri") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "userinfo_signed_response_alg") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2Client struct { + value *OAuth2Client + isSet bool +} + +func (v NullableOAuth2Client) Get() *OAuth2Client { + return v.value +} + +func (v *NullableOAuth2Client) Set(val *OAuth2Client) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2Client) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2Client) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2Client(val *OAuth2Client) *NullableOAuth2Client { + return &NullableOAuth2Client{value: val, isSet: true} +} + +func (v NullableOAuth2Client) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2Client) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_o_auth2_consent_request_open_id_connect_context.go b/pkg/client-go/model_o_auth2_consent_request_open_id_connect_context.go new file mode 100644 index 0000000..038f235 --- /dev/null +++ b/pkg/client-go/model_o_auth2_consent_request_open_id_connect_context.go @@ -0,0 +1,307 @@ +/* +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" +) + +// checks if the OAuth2ConsentRequestOpenIDConnectContext type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2ConsentRequestOpenIDConnectContext{} + +// OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext +type OAuth2ConsentRequestOpenIDConnectContext struct { + // 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. + Display *string `json:"display,omitempty"` + // IDTokenHintClaims are the claims of the ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. + IdTokenHintClaims map[string]interface{} `json:"id_token_hint_claims,omitempty"` + // LoginHint hints about the login identifier the End-User might use to log in (if necessary). This hint can be used by an RP if it first asks the End-User for their e-mail address (or other identifier) and then wants to pass that value as a hint to the discovered authorization service. This value MAY also be a phone number in the format specified for the phone_number Claim. The use of this parameter is optional. + LoginHint *string `json:"login_hint,omitempty"` + // UILocales is the End-User'id preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value \\\"fr-CA fr en\\\" represents a preference for French as spoken in Canada, then French (without a region designation), followed by English (without a region designation). An error SHOULD NOT result if some or all of the requested locales are not supported by the OpenID Provider. + UiLocales []string `json:"ui_locales,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext + +// NewOAuth2ConsentRequestOpenIDConnectContext instantiates a new OAuth2ConsentRequestOpenIDConnectContext 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 NewOAuth2ConsentRequestOpenIDConnectContext() *OAuth2ConsentRequestOpenIDConnectContext { + this := OAuth2ConsentRequestOpenIDConnectContext{} + return &this +} + +// NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults instantiates a new OAuth2ConsentRequestOpenIDConnectContext 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 NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults() *OAuth2ConsentRequestOpenIDConnectContext { + this := OAuth2ConsentRequestOpenIDConnectContext{} + return &this +} + +// GetAcrValues returns the AcrValues field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValues() []string { + if o == nil || IsNil(o.AcrValues) { + var ret []string + return ret + } + return o.AcrValues +} + +// GetAcrValuesOk returns a tuple with the AcrValues field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValuesOk() ([]string, bool) { + if o == nil || IsNil(o.AcrValues) { + return nil, false + } + return o.AcrValues, true +} + +// HasAcrValues returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasAcrValues() bool { + if o != nil && !IsNil(o.AcrValues) { + return true + } + + return false +} + +// SetAcrValues gets a reference to the given []string and assigns it to the AcrValues field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetAcrValues(v []string) { + o.AcrValues = v +} + +// GetDisplay returns the Display field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetDisplay() string { + if o == nil || IsNil(o.Display) { + var ret string + return ret + } + return *o.Display +} + +// GetDisplayOk returns a tuple with the Display field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetDisplayOk() (*string, bool) { + if o == nil || IsNil(o.Display) { + return nil, false + } + return o.Display, true +} + +// HasDisplay returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasDisplay() bool { + if o != nil && !IsNil(o.Display) { + return true + } + + return false +} + +// SetDisplay gets a reference to the given string and assigns it to the Display field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetDisplay(v string) { + o.Display = &v +} + +// GetIdTokenHintClaims returns the IdTokenHintClaims field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetIdTokenHintClaims() map[string]interface{} { + if o == nil || IsNil(o.IdTokenHintClaims) { + var ret map[string]interface{} + return ret + } + return o.IdTokenHintClaims +} + +// GetIdTokenHintClaimsOk returns a tuple with the IdTokenHintClaims field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetIdTokenHintClaimsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.IdTokenHintClaims) { + return map[string]interface{}{}, false + } + return o.IdTokenHintClaims, true +} + +// HasIdTokenHintClaims returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasIdTokenHintClaims() bool { + if o != nil && !IsNil(o.IdTokenHintClaims) { + return true + } + + return false +} + +// SetIdTokenHintClaims gets a reference to the given map[string]interface{} and assigns it to the IdTokenHintClaims field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetIdTokenHintClaims(v map[string]interface{}) { + o.IdTokenHintClaims = v +} + +// GetLoginHint returns the LoginHint field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetLoginHint() string { + if o == nil || IsNil(o.LoginHint) { + var ret string + return ret + } + return *o.LoginHint +} + +// GetLoginHintOk returns a tuple with the LoginHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetLoginHintOk() (*string, bool) { + if o == nil || IsNil(o.LoginHint) { + return nil, false + } + return o.LoginHint, true +} + +// HasLoginHint returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasLoginHint() bool { + if o != nil && !IsNil(o.LoginHint) { + return true + } + + return false +} + +// SetLoginHint gets a reference to the given string and assigns it to the LoginHint field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetLoginHint(v string) { + o.LoginHint = &v +} + +// GetUiLocales returns the UiLocales field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetUiLocales() []string { + if o == nil || IsNil(o.UiLocales) { + var ret []string + return ret + } + return o.UiLocales +} + +// GetUiLocalesOk returns a tuple with the UiLocales field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetUiLocalesOk() ([]string, bool) { + if o == nil || IsNil(o.UiLocales) { + return nil, false + } + return o.UiLocales, true +} + +// HasUiLocales returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasUiLocales() bool { + if o != nil && !IsNil(o.UiLocales) { + return true + } + + return false +} + +// SetUiLocales gets a reference to the given []string and assigns it to the UiLocales field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetUiLocales(v []string) { + o.UiLocales = v +} + +func (o OAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2ConsentRequestOpenIDConnectContext) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AcrValues) { + toSerialize["acr_values"] = o.AcrValues + } + if !IsNil(o.Display) { + toSerialize["display"] = o.Display + } + if !IsNil(o.IdTokenHintClaims) { + toSerialize["id_token_hint_claims"] = o.IdTokenHintClaims + } + if !IsNil(o.LoginHint) { + toSerialize["login_hint"] = o.LoginHint + } + if !IsNil(o.UiLocales) { + toSerialize["ui_locales"] = o.UiLocales + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2ConsentRequestOpenIDConnectContext) UnmarshalJSON(data []byte) (err error) { + varOAuth2ConsentRequestOpenIDConnectContext := _OAuth2ConsentRequestOpenIDConnectContext{} + + err = json.Unmarshal(data, &varOAuth2ConsentRequestOpenIDConnectContext) + + if err != nil { + return err + } + + *o = OAuth2ConsentRequestOpenIDConnectContext(varOAuth2ConsentRequestOpenIDConnectContext) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "acr_values") + delete(additionalProperties, "display") + delete(additionalProperties, "id_token_hint_claims") + delete(additionalProperties, "login_hint") + delete(additionalProperties, "ui_locales") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2ConsentRequestOpenIDConnectContext struct { + value *OAuth2ConsentRequestOpenIDConnectContext + isSet bool +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) Get() *OAuth2ConsentRequestOpenIDConnectContext { + return v.value +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) Set(val *OAuth2ConsentRequestOpenIDConnectContext) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2ConsentRequestOpenIDConnectContext(val *OAuth2ConsentRequestOpenIDConnectContext) *NullableOAuth2ConsentRequestOpenIDConnectContext { + return &NullableOAuth2ConsentRequestOpenIDConnectContext{value: val, isSet: true} +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_o_auth2_login_request.go b/pkg/client-go/model_o_auth2_login_request.go new file mode 100644 index 0000000..ab4b9b6 --- /dev/null +++ b/pkg/client-go/model_o_auth2_login_request.go @@ -0,0 +1,455 @@ +/* +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" +) + +// checks if the OAuth2LoginRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2LoginRequest{} + +// OAuth2LoginRequest OAuth2LoginRequest struct for OAuth2LoginRequest +type OAuth2LoginRequest struct { + // 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"` + OidcContext *OAuth2ConsentRequestOpenIDConnectContext `json:"oidc_context,omitempty"` + // RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` + // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \\\"sid\\\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. + SessionId *string `json:"session_id,omitempty"` + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. This feature allows you to update / set session information. + Skip *bool `json:"skip,omitempty"` + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. If this value is set and `skip` is true, you MUST include this subject type when accepting the login request, or the request will fail. + Subject *string `json:"subject,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2LoginRequest OAuth2LoginRequest + +// NewOAuth2LoginRequest instantiates a new OAuth2LoginRequest 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 NewOAuth2LoginRequest() *OAuth2LoginRequest { + this := OAuth2LoginRequest{} + return &this +} + +// NewOAuth2LoginRequestWithDefaults instantiates a new OAuth2LoginRequest 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 NewOAuth2LoginRequestWithDefaults() *OAuth2LoginRequest { + this := OAuth2LoginRequest{} + return &this +} + +// GetChallenge returns the Challenge field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetChallenge() string { + if o == nil || IsNil(o.Challenge) { + var ret string + return ret + } + return *o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Challenge) { + return nil, false + } + return o.Challenge, true +} + +// HasChallenge returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasChallenge() bool { + if o != nil && !IsNil(o.Challenge) { + return true + } + + return false +} + +// SetChallenge gets a reference to the given string and assigns it to the Challenge field. +func (o *OAuth2LoginRequest) SetChallenge(v string) { + o.Challenge = &v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *OAuth2LoginRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetOidcContext returns the OidcContext field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetOidcContext() OAuth2ConsentRequestOpenIDConnectContext { + if o == nil || IsNil(o.OidcContext) { + var ret OAuth2ConsentRequestOpenIDConnectContext + return ret + } + return *o.OidcContext +} + +// GetOidcContextOk returns a tuple with the OidcContext field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetOidcContextOk() (*OAuth2ConsentRequestOpenIDConnectContext, bool) { + if o == nil || IsNil(o.OidcContext) { + return nil, false + } + return o.OidcContext, true +} + +// HasOidcContext returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasOidcContext() bool { + if o != nil && !IsNil(o.OidcContext) { + return true + } + + return false +} + +// SetOidcContext gets a reference to the given OAuth2ConsentRequestOpenIDConnectContext and assigns it to the OidcContext field. +func (o *OAuth2LoginRequest) SetOidcContext(v OAuth2ConsentRequestOpenIDConnectContext) { + o.OidcContext = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *OAuth2LoginRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *OAuth2LoginRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *OAuth2LoginRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +// GetSessionId returns the SessionId field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSessionId() string { + if o == nil || IsNil(o.SessionId) { + var ret string + return ret + } + return *o.SessionId +} + +// GetSessionIdOk returns a tuple with the SessionId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSessionIdOk() (*string, bool) { + if o == nil || IsNil(o.SessionId) { + return nil, false + } + return o.SessionId, true +} + +// HasSessionId returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSessionId() bool { + if o != nil && !IsNil(o.SessionId) { + return true + } + + return false +} + +// SetSessionId gets a reference to the given string and assigns it to the SessionId field. +func (o *OAuth2LoginRequest) SetSessionId(v string) { + o.SessionId = &v +} + +// GetSkip returns the Skip field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSkip() bool { + if o == nil || IsNil(o.Skip) { + var ret bool + return ret + } + return *o.Skip +} + +// GetSkipOk returns a tuple with the Skip field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSkipOk() (*bool, bool) { + if o == nil || IsNil(o.Skip) { + return nil, false + } + return o.Skip, true +} + +// HasSkip returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSkip() bool { + if o != nil && !IsNil(o.Skip) { + return true + } + + return false +} + +// SetSkip gets a reference to the given bool and assigns it to the Skip field. +func (o *OAuth2LoginRequest) SetSkip(v bool) { + o.Skip = &v +} + +// GetSubject returns the Subject field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSubject() string { + if o == nil || IsNil(o.Subject) { + var ret string + return ret + } + return *o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSubjectOk() (*string, bool) { + if o == nil || IsNil(o.Subject) { + return nil, false + } + return o.Subject, true +} + +// HasSubject returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSubject() bool { + if o != nil && !IsNil(o.Subject) { + return true + } + + return false +} + +// SetSubject gets a reference to the given string and assigns it to the Subject field. +func (o *OAuth2LoginRequest) SetSubject(v string) { + o.Subject = &v +} + +func (o OAuth2LoginRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2LoginRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Challenge) { + toSerialize["challenge"] = o.Challenge + } + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.OidcContext) { + toSerialize["oidc_context"] = o.OidcContext + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + if !IsNil(o.SessionId) { + toSerialize["session_id"] = o.SessionId + } + if !IsNil(o.Skip) { + toSerialize["skip"] = o.Skip + } + if !IsNil(o.Subject) { + toSerialize["subject"] = o.Subject + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2LoginRequest) UnmarshalJSON(data []byte) (err error) { + varOAuth2LoginRequest := _OAuth2LoginRequest{} + + err = json.Unmarshal(data, &varOAuth2LoginRequest) + + if err != nil { + return err + } + + *o = OAuth2LoginRequest(varOAuth2LoginRequest) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "challenge") + delete(additionalProperties, "client") + delete(additionalProperties, "oidc_context") + delete(additionalProperties, "request_url") + delete(additionalProperties, "requested_access_token_audience") + delete(additionalProperties, "requested_scope") + delete(additionalProperties, "session_id") + delete(additionalProperties, "skip") + delete(additionalProperties, "subject") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2LoginRequest struct { + value *OAuth2LoginRequest + isSet bool +} + +func (v NullableOAuth2LoginRequest) Get() *OAuth2LoginRequest { + return v.value +} + +func (v *NullableOAuth2LoginRequest) Set(val *OAuth2LoginRequest) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2LoginRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2LoginRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2LoginRequest(val *OAuth2LoginRequest) *NullableOAuth2LoginRequest { + return &NullableOAuth2LoginRequest{value: val, isSet: true} +} + +func (v NullableOAuth2LoginRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2LoginRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_patch_identities_body.go b/pkg/client-go/model_patch_identities_body.go new file mode 100644 index 0000000..251da77 --- /dev/null +++ b/pkg/client-go/model_patch_identities_body.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the PatchIdentitiesBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PatchIdentitiesBody{} + +// PatchIdentitiesBody Patch Identities Body +type PatchIdentitiesBody struct { + // Identities holds the list of patches to apply required + Identities []IdentityPatch `json:"identities,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _PatchIdentitiesBody PatchIdentitiesBody + +// NewPatchIdentitiesBody instantiates a new PatchIdentitiesBody 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 NewPatchIdentitiesBody() *PatchIdentitiesBody { + this := PatchIdentitiesBody{} + return &this +} + +// NewPatchIdentitiesBodyWithDefaults instantiates a new PatchIdentitiesBody 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 NewPatchIdentitiesBodyWithDefaults() *PatchIdentitiesBody { + this := PatchIdentitiesBody{} + return &this +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *PatchIdentitiesBody) GetIdentities() []IdentityPatch { + if o == nil || IsNil(o.Identities) { + var ret []IdentityPatch + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PatchIdentitiesBody) GetIdentitiesOk() ([]IdentityPatch, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *PatchIdentitiesBody) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []IdentityPatch and assigns it to the Identities field. +func (o *PatchIdentitiesBody) SetIdentities(v []IdentityPatch) { + o.Identities = v +} + +func (o PatchIdentitiesBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PatchIdentitiesBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PatchIdentitiesBody) UnmarshalJSON(data []byte) (err error) { + varPatchIdentitiesBody := _PatchIdentitiesBody{} + + err = json.Unmarshal(data, &varPatchIdentitiesBody) + + if err != nil { + return err + } + + *o = PatchIdentitiesBody(varPatchIdentitiesBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "identities") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePatchIdentitiesBody struct { + value *PatchIdentitiesBody + isSet bool +} + +func (v NullablePatchIdentitiesBody) Get() *PatchIdentitiesBody { + return v.value +} + +func (v *NullablePatchIdentitiesBody) Set(val *PatchIdentitiesBody) { + v.value = val + v.isSet = true +} + +func (v NullablePatchIdentitiesBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePatchIdentitiesBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePatchIdentitiesBody(val *PatchIdentitiesBody) *NullablePatchIdentitiesBody { + return &NullablePatchIdentitiesBody{value: val, isSet: true} +} + +func (v NullablePatchIdentitiesBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePatchIdentitiesBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_perform_native_logout_body.go b/pkg/client-go/model_perform_native_logout_body.go new file mode 100644 index 0000000..d3a97b4 --- /dev/null +++ b/pkg/client-go/model_perform_native_logout_body.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the PerformNativeLogoutBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PerformNativeLogoutBody{} + +// PerformNativeLogoutBody Perform Native Logout Request Body +type PerformNativeLogoutBody struct { + // The Session Token Invalidate this session token. + SessionToken string `json:"session_token"` + AdditionalProperties map[string]interface{} +} + +type _PerformNativeLogoutBody PerformNativeLogoutBody + +// NewPerformNativeLogoutBody instantiates a new PerformNativeLogoutBody 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 NewPerformNativeLogoutBody(sessionToken string) *PerformNativeLogoutBody { + this := PerformNativeLogoutBody{} + this.SessionToken = sessionToken + return &this +} + +// NewPerformNativeLogoutBodyWithDefaults instantiates a new PerformNativeLogoutBody 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 NewPerformNativeLogoutBodyWithDefaults() *PerformNativeLogoutBody { + this := PerformNativeLogoutBody{} + return &this +} + +// GetSessionToken returns the SessionToken field value +func (o *PerformNativeLogoutBody) GetSessionToken() string { + if o == nil { + var ret string + return ret + } + + return o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value +// and a boolean to check if the value has been set. +func (o *PerformNativeLogoutBody) GetSessionTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SessionToken, true +} + +// SetSessionToken sets field value +func (o *PerformNativeLogoutBody) SetSessionToken(v string) { + o.SessionToken = v +} + +func (o PerformNativeLogoutBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PerformNativeLogoutBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["session_token"] = o.SessionToken + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PerformNativeLogoutBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session_token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varPerformNativeLogoutBody := _PerformNativeLogoutBody{} + + err = json.Unmarshal(data, &varPerformNativeLogoutBody) + + if err != nil { + return err + } + + *o = PerformNativeLogoutBody(varPerformNativeLogoutBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePerformNativeLogoutBody struct { + value *PerformNativeLogoutBody + isSet bool +} + +func (v NullablePerformNativeLogoutBody) Get() *PerformNativeLogoutBody { + return v.value +} + +func (v *NullablePerformNativeLogoutBody) Set(val *PerformNativeLogoutBody) { + v.value = val + v.isSet = true +} + +func (v NullablePerformNativeLogoutBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePerformNativeLogoutBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePerformNativeLogoutBody(val *PerformNativeLogoutBody) *NullablePerformNativeLogoutBody { + return &NullablePerformNativeLogoutBody{value: val, isSet: true} +} + +func (v NullablePerformNativeLogoutBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePerformNativeLogoutBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_pin_config.go b/pkg/client-go/model_pin_config.go new file mode 100644 index 0000000..6e16b00 --- /dev/null +++ b/pkg/client-go/model_pin_config.go @@ -0,0 +1,270 @@ +/* +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" + "time" +) + +// checks if the PINConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PINConfig{} + +// PINConfig The pin_secret field holds only the at-rest ciphertext; the plaintext exists transiently in server memory during verification and is invalidated once the key locks. +type PINConfig struct { + // When the pin_secret was first issued. + CreatedAt *time.Time `json:"created_at,omitempty"` + // The number of consecutive wrong-PIN attempts so far; the key locks when it reaches the configured maximum (pin_max_attempts, default 5). + FailedAttempts *int32 `json:"failed_attempts,omitempty"` + // The at-rest ciphertext of the pin_secret. It never leaves the server and is cleared once the key locks. + PinSecret *string `json:"pin_secret,omitempty"` + // When the pin_secret was last rotated. Omitted if the secret was never rotated. + RotatedAt *time.Time `json:"rotated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _PINConfig PINConfig + +// NewPINConfig instantiates a new PINConfig 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 NewPINConfig() *PINConfig { + this := PINConfig{} + return &this +} + +// NewPINConfigWithDefaults instantiates a new PINConfig 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 NewPINConfigWithDefaults() *PINConfig { + this := PINConfig{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *PINConfig) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *PINConfig) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *PINConfig) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetFailedAttempts returns the FailedAttempts field value if set, zero value otherwise. +func (o *PINConfig) GetFailedAttempts() int32 { + if o == nil || IsNil(o.FailedAttempts) { + var ret int32 + return ret + } + return *o.FailedAttempts +} + +// GetFailedAttemptsOk returns a tuple with the FailedAttempts field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetFailedAttemptsOk() (*int32, bool) { + if o == nil || IsNil(o.FailedAttempts) { + return nil, false + } + return o.FailedAttempts, true +} + +// HasFailedAttempts returns a boolean if a field has been set. +func (o *PINConfig) HasFailedAttempts() bool { + if o != nil && !IsNil(o.FailedAttempts) { + return true + } + + return false +} + +// SetFailedAttempts gets a reference to the given int32 and assigns it to the FailedAttempts field. +func (o *PINConfig) SetFailedAttempts(v int32) { + o.FailedAttempts = &v +} + +// GetPinSecret returns the PinSecret field value if set, zero value otherwise. +func (o *PINConfig) GetPinSecret() string { + if o == nil || IsNil(o.PinSecret) { + var ret string + return ret + } + return *o.PinSecret +} + +// GetPinSecretOk returns a tuple with the PinSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetPinSecretOk() (*string, bool) { + if o == nil || IsNil(o.PinSecret) { + return nil, false + } + return o.PinSecret, true +} + +// HasPinSecret returns a boolean if a field has been set. +func (o *PINConfig) HasPinSecret() bool { + if o != nil && !IsNil(o.PinSecret) { + return true + } + + return false +} + +// SetPinSecret gets a reference to the given string and assigns it to the PinSecret field. +func (o *PINConfig) SetPinSecret(v string) { + o.PinSecret = &v +} + +// GetRotatedAt returns the RotatedAt field value if set, zero value otherwise. +func (o *PINConfig) GetRotatedAt() time.Time { + if o == nil || IsNil(o.RotatedAt) { + var ret time.Time + return ret + } + return *o.RotatedAt +} + +// GetRotatedAtOk returns a tuple with the RotatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetRotatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.RotatedAt) { + return nil, false + } + return o.RotatedAt, true +} + +// HasRotatedAt returns a boolean if a field has been set. +func (o *PINConfig) HasRotatedAt() bool { + if o != nil && !IsNil(o.RotatedAt) { + return true + } + + return false +} + +// SetRotatedAt gets a reference to the given time.Time and assigns it to the RotatedAt field. +func (o *PINConfig) SetRotatedAt(v time.Time) { + o.RotatedAt = &v +} + +func (o PINConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PINConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.FailedAttempts) { + toSerialize["failed_attempts"] = o.FailedAttempts + } + if !IsNil(o.PinSecret) { + toSerialize["pin_secret"] = o.PinSecret + } + if !IsNil(o.RotatedAt) { + toSerialize["rotated_at"] = o.RotatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PINConfig) UnmarshalJSON(data []byte) (err error) { + varPINConfig := _PINConfig{} + + err = json.Unmarshal(data, &varPINConfig) + + if err != nil { + return err + } + + *o = PINConfig(varPINConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "failed_attempts") + delete(additionalProperties, "pin_secret") + delete(additionalProperties, "rotated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePINConfig struct { + value *PINConfig + isSet bool +} + +func (v NullablePINConfig) Get() *PINConfig { + return v.value +} + +func (v *NullablePINConfig) Set(val *PINConfig) { + v.value = val + v.isSet = true +} + +func (v NullablePINConfig) IsSet() bool { + return v.isSet +} + +func (v *NullablePINConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePINConfig(val *PINConfig) *NullablePINConfig { + return &NullablePINConfig{value: val, isSet: true} +} + +func (v NullablePINConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePINConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_provider.go b/pkg/client-go/model_provider.go new file mode 100644 index 0000000..aa4eb6e --- /dev/null +++ b/pkg/client-go/model_provider.go @@ -0,0 +1,383 @@ +/* +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" +) + +// checks if the Provider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Provider{} + +// Provider struct for Provider +type Provider struct { + // The RP's client identifier, issued by the IdP. + ClientId *string `json:"client_id,omitempty"` + // A full path of the IdP config file. + ConfigUrl *string `json:"config_url,omitempty"` + // By specifying one of domain_hints values provided by the accounts endpoints, the FedCM dialog selectively shows the specified account. + DomainHint *string `json:"domain_hint,omitempty"` + // Array of strings that specifies the user information (\"name\", \" email\", \"picture\") that RP needs IdP to share with them. Note: Field API is supported by Chrome 132 and later. + Fields []string `json:"fields,omitempty"` + // By specifying one of login_hints values provided by the accounts endpoints, the FedCM dialog selectively shows the specified account. + LoginHint *string `json:"login_hint,omitempty"` + // A random string to ensure the response is issued for this specific request. Prevents replay attacks. + Nonce *string `json:"nonce,omitempty"` + // Custom object that allows to specify additional key-value parameters: scope: A string value containing additional permissions that RP needs to request, for example \" drive.readonly calendar.readonly\" nonce: A random string to ensure the response is issued for this specific request. Prevents replay attacks. Other custom key-value parameters. Note: parameters is supported from Chrome 132. + Parameters *map[string]string `json:"parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Provider Provider + +// NewProvider instantiates a new Provider 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 NewProvider() *Provider { + this := Provider{} + return &this +} + +// NewProviderWithDefaults instantiates a new Provider 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 NewProviderWithDefaults() *Provider { + this := Provider{} + return &this +} + +// GetClientId returns the ClientId field value if set, zero value otherwise. +func (o *Provider) GetClientId() string { + if o == nil || IsNil(o.ClientId) { + var ret string + return ret + } + return *o.ClientId +} + +// GetClientIdOk returns a tuple with the ClientId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetClientIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientId) { + return nil, false + } + return o.ClientId, true +} + +// HasClientId returns a boolean if a field has been set. +func (o *Provider) HasClientId() bool { + if o != nil && !IsNil(o.ClientId) { + return true + } + + return false +} + +// SetClientId gets a reference to the given string and assigns it to the ClientId field. +func (o *Provider) SetClientId(v string) { + o.ClientId = &v +} + +// GetConfigUrl returns the ConfigUrl field value if set, zero value otherwise. +func (o *Provider) GetConfigUrl() string { + if o == nil || IsNil(o.ConfigUrl) { + var ret string + return ret + } + return *o.ConfigUrl +} + +// GetConfigUrlOk returns a tuple with the ConfigUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetConfigUrlOk() (*string, bool) { + if o == nil || IsNil(o.ConfigUrl) { + return nil, false + } + return o.ConfigUrl, true +} + +// HasConfigUrl returns a boolean if a field has been set. +func (o *Provider) HasConfigUrl() bool { + if o != nil && !IsNil(o.ConfigUrl) { + return true + } + + return false +} + +// SetConfigUrl gets a reference to the given string and assigns it to the ConfigUrl field. +func (o *Provider) SetConfigUrl(v string) { + o.ConfigUrl = &v +} + +// GetDomainHint returns the DomainHint field value if set, zero value otherwise. +func (o *Provider) GetDomainHint() string { + if o == nil || IsNil(o.DomainHint) { + var ret string + return ret + } + return *o.DomainHint +} + +// GetDomainHintOk returns a tuple with the DomainHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetDomainHintOk() (*string, bool) { + if o == nil || IsNil(o.DomainHint) { + return nil, false + } + return o.DomainHint, true +} + +// HasDomainHint returns a boolean if a field has been set. +func (o *Provider) HasDomainHint() bool { + if o != nil && !IsNil(o.DomainHint) { + return true + } + + return false +} + +// SetDomainHint gets a reference to the given string and assigns it to the DomainHint field. +func (o *Provider) SetDomainHint(v string) { + o.DomainHint = &v +} + +// GetFields returns the Fields field value if set, zero value otherwise. +func (o *Provider) GetFields() []string { + if o == nil || IsNil(o.Fields) { + var ret []string + return ret + } + return o.Fields +} + +// GetFieldsOk returns a tuple with the Fields field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetFieldsOk() ([]string, bool) { + if o == nil || IsNil(o.Fields) { + return nil, false + } + return o.Fields, true +} + +// HasFields returns a boolean if a field has been set. +func (o *Provider) HasFields() bool { + if o != nil && !IsNil(o.Fields) { + return true + } + + return false +} + +// SetFields gets a reference to the given []string and assigns it to the Fields field. +func (o *Provider) SetFields(v []string) { + o.Fields = v +} + +// GetLoginHint returns the LoginHint field value if set, zero value otherwise. +func (o *Provider) GetLoginHint() string { + if o == nil || IsNil(o.LoginHint) { + var ret string + return ret + } + return *o.LoginHint +} + +// GetLoginHintOk returns a tuple with the LoginHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetLoginHintOk() (*string, bool) { + if o == nil || IsNil(o.LoginHint) { + return nil, false + } + return o.LoginHint, true +} + +// HasLoginHint returns a boolean if a field has been set. +func (o *Provider) HasLoginHint() bool { + if o != nil && !IsNil(o.LoginHint) { + return true + } + + return false +} + +// SetLoginHint gets a reference to the given string and assigns it to the LoginHint field. +func (o *Provider) SetLoginHint(v string) { + o.LoginHint = &v +} + +// GetNonce returns the Nonce field value if set, zero value otherwise. +func (o *Provider) GetNonce() string { + if o == nil || IsNil(o.Nonce) { + var ret string + return ret + } + return *o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetNonceOk() (*string, bool) { + if o == nil || IsNil(o.Nonce) { + return nil, false + } + return o.Nonce, true +} + +// HasNonce returns a boolean if a field has been set. +func (o *Provider) HasNonce() bool { + if o != nil && !IsNil(o.Nonce) { + return true + } + + return false +} + +// SetNonce gets a reference to the given string and assigns it to the Nonce field. +func (o *Provider) SetNonce(v string) { + o.Nonce = &v +} + +// GetParameters returns the Parameters field value if set, zero value otherwise. +func (o *Provider) GetParameters() map[string]string { + if o == nil || IsNil(o.Parameters) { + var ret map[string]string + return ret + } + return *o.Parameters +} + +// GetParametersOk returns a tuple with the Parameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetParametersOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Parameters) { + return nil, false + } + return o.Parameters, true +} + +// HasParameters returns a boolean if a field has been set. +func (o *Provider) HasParameters() bool { + if o != nil && !IsNil(o.Parameters) { + return true + } + + return false +} + +// SetParameters gets a reference to the given map[string]string and assigns it to the Parameters field. +func (o *Provider) SetParameters(v map[string]string) { + o.Parameters = &v +} + +func (o Provider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Provider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ClientId) { + toSerialize["client_id"] = o.ClientId + } + if !IsNil(o.ConfigUrl) { + toSerialize["config_url"] = o.ConfigUrl + } + if !IsNil(o.DomainHint) { + toSerialize["domain_hint"] = o.DomainHint + } + if !IsNil(o.Fields) { + toSerialize["fields"] = o.Fields + } + if !IsNil(o.LoginHint) { + toSerialize["login_hint"] = o.LoginHint + } + if !IsNil(o.Nonce) { + toSerialize["nonce"] = o.Nonce + } + if !IsNil(o.Parameters) { + toSerialize["parameters"] = o.Parameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Provider) UnmarshalJSON(data []byte) (err error) { + varProvider := _Provider{} + + err = json.Unmarshal(data, &varProvider) + + if err != nil { + return err + } + + *o = Provider(varProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "client_id") + delete(additionalProperties, "config_url") + delete(additionalProperties, "domain_hint") + delete(additionalProperties, "fields") + delete(additionalProperties, "login_hint") + delete(additionalProperties, "nonce") + delete(additionalProperties, "parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableProvider struct { + value *Provider + isSet bool +} + +func (v NullableProvider) Get() *Provider { + return v.value +} + +func (v *NullableProvider) Set(val *Provider) { + v.value = val + v.isSet = true +} + +func (v NullableProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableProvider(val *Provider) *NullableProvider { + return &NullableProvider{value: val, isSet: true} +} + +func (v NullableProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_recovery_code_for_identity.go b/pkg/client-go/model_recovery_code_for_identity.go new file mode 100644 index 0000000..1c4f9ba --- /dev/null +++ b/pkg/client-go/model_recovery_code_for_identity.go @@ -0,0 +1,237 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryCodeForIdentity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryCodeForIdentity{} + +// RecoveryCodeForIdentity Used when an administrator creates a recovery code for an identity. +type RecoveryCodeForIdentity struct { + // Expires At is the timestamp of when the recovery flow expires The timestamp when the recovery code expires. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // RecoveryCode is the code that can be used to recover the account + RecoveryCode string `json:"recovery_code"` + // RecoveryLink with flow This link opens the recovery UI with an empty `code` field. + RecoveryLink string `json:"recovery_link"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryCodeForIdentity RecoveryCodeForIdentity + +// NewRecoveryCodeForIdentity instantiates a new RecoveryCodeForIdentity 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 NewRecoveryCodeForIdentity(recoveryCode string, recoveryLink string) *RecoveryCodeForIdentity { + this := RecoveryCodeForIdentity{} + this.RecoveryCode = recoveryCode + this.RecoveryLink = recoveryLink + return &this +} + +// NewRecoveryCodeForIdentityWithDefaults instantiates a new RecoveryCodeForIdentity 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 NewRecoveryCodeForIdentityWithDefaults() *RecoveryCodeForIdentity { + this := RecoveryCodeForIdentity{} + return &this +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *RecoveryCodeForIdentity) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *RecoveryCodeForIdentity) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *RecoveryCodeForIdentity) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetRecoveryCode returns the RecoveryCode field value +func (o *RecoveryCodeForIdentity) GetRecoveryCode() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryCode +} + +// GetRecoveryCodeOk returns a tuple with the RecoveryCode field value +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetRecoveryCodeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryCode, true +} + +// SetRecoveryCode sets field value +func (o *RecoveryCodeForIdentity) SetRecoveryCode(v string) { + o.RecoveryCode = v +} + +// GetRecoveryLink returns the RecoveryLink field value +func (o *RecoveryCodeForIdentity) GetRecoveryLink() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryLink +} + +// GetRecoveryLinkOk returns a tuple with the RecoveryLink field value +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetRecoveryLinkOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryLink, true +} + +// SetRecoveryLink sets field value +func (o *RecoveryCodeForIdentity) SetRecoveryLink(v string) { + o.RecoveryLink = v +} + +func (o RecoveryCodeForIdentity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryCodeForIdentity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["recovery_code"] = o.RecoveryCode + toSerialize["recovery_link"] = o.RecoveryLink + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryCodeForIdentity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "recovery_code", + "recovery_link", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryCodeForIdentity := _RecoveryCodeForIdentity{} + + err = json.Unmarshal(data, &varRecoveryCodeForIdentity) + + if err != nil { + return err + } + + *o = RecoveryCodeForIdentity(varRecoveryCodeForIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_at") + delete(additionalProperties, "recovery_code") + delete(additionalProperties, "recovery_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryCodeForIdentity struct { + value *RecoveryCodeForIdentity + isSet bool +} + +func (v NullableRecoveryCodeForIdentity) Get() *RecoveryCodeForIdentity { + return v.value +} + +func (v *NullableRecoveryCodeForIdentity) Set(val *RecoveryCodeForIdentity) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryCodeForIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryCodeForIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryCodeForIdentity(val *RecoveryCodeForIdentity) *NullableRecoveryCodeForIdentity { + return &NullableRecoveryCodeForIdentity{value: val, isSet: true} +} + +func (v NullableRecoveryCodeForIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryCodeForIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_recovery_flow.go b/pkg/client-go/model_recovery_flow.go new file mode 100644 index 0000000..4440920 --- /dev/null +++ b/pkg/client-go/model_recovery_flow.go @@ -0,0 +1,504 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryFlow{} + +// RecoveryFlow This request is used when an identity wants to recover their account. We recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery) +type RecoveryFlow struct { + // Active, if set, contains the recovery method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // Contains possible actions that could follow this flow + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting, a new request has to be initiated. + ExpiresAt time.Time `json:"expires_at"` + // ID represents the request's unique ID. When performing the recovery flow, this represents the id in the recovery ui's query parameter: http://?request= + Id string `json:"id"` + // IssuedAt is the time (UTC) when the request occurred. + IssuedAt time.Time `json:"issued_at"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryFlow RecoveryFlow + +// NewRecoveryFlow instantiates a new RecoveryFlow 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 NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { + this := RecoveryFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewRecoveryFlowWithDefaults instantiates a new RecoveryFlow 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 NewRecoveryFlowWithDefaults() *RecoveryFlow { + this := RecoveryFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *RecoveryFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *RecoveryFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *RecoveryFlow) SetActive(v string) { + o.Active = &v +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *RecoveryFlow) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *RecoveryFlow) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *RecoveryFlow) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *RecoveryFlow) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *RecoveryFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *RecoveryFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *RecoveryFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *RecoveryFlow) SetId(v string) { + o.Id = v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *RecoveryFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *RecoveryFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetRequestUrl returns the RequestUrl field value +func (o *RecoveryFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *RecoveryFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *RecoveryFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *RecoveryFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *RecoveryFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *RecoveryFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *RecoveryFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *RecoveryFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o RecoveryFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + toSerialize["issued_at"] = o.IssuedAt + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryFlow := _RecoveryFlow{} + + err = json.Unmarshal(data, &varRecoveryFlow) + + if err != nil { + return err + } + + *o = RecoveryFlow(varRecoveryFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "continue_with") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryFlow struct { + value *RecoveryFlow + isSet bool +} + +func (v NullableRecoveryFlow) Get() *RecoveryFlow { + return v.value +} + +func (v *NullableRecoveryFlow) Set(val *RecoveryFlow) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryFlow(val *RecoveryFlow) *NullableRecoveryFlow { + return &NullableRecoveryFlow{value: val, isSet: true} +} + +func (v NullableRecoveryFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_recovery_flow_state.go b/pkg/client-go/model_recovery_flow_state.go new file mode 100644 index 0000000..1b52ba6 --- /dev/null +++ b/pkg/client-go/model_recovery_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + RECOVERYFLOWSTATE_CHOOSE_METHOD RecoveryFlowState = "choose_method" + RECOVERYFLOWSTATE_SENT_EMAIL RecoveryFlowState = "sent_email" + RECOVERYFLOWSTATE_PASSED_CHALLENGE RecoveryFlowState = "passed_challenge" +) + +// All allowed values of RecoveryFlowState enum +var AllowedRecoveryFlowStateEnumValues = []RecoveryFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *RecoveryFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RecoveryFlowState(value) + for _, existing := range AllowedRecoveryFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RecoveryFlowState", value) +} + +// NewRecoveryFlowStateFromValue returns a pointer to a valid RecoveryFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewRecoveryFlowStateFromValue(v string) (*RecoveryFlowState, error) { + ev := RecoveryFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for RecoveryFlowState: valid values are %v", v, AllowedRecoveryFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v RecoveryFlowState) IsValid() bool { + for _, existing := range AllowedRecoveryFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to recoveryFlowState value +func (v RecoveryFlowState) Ptr() *RecoveryFlowState { + return &v +} + +type NullableRecoveryFlowState struct { + value *RecoveryFlowState + isSet bool +} + +func (v NullableRecoveryFlowState) Get() *RecoveryFlowState { + return v.value +} + +func (v *NullableRecoveryFlowState) Set(val *RecoveryFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryFlowState(val *RecoveryFlowState) *NullableRecoveryFlowState { + return &NullableRecoveryFlowState{value: val, isSet: true} +} + +func (v NullableRecoveryFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_recovery_identity_address.go b/pkg/client-go/model_recovery_identity_address.go new file mode 100644 index 0000000..3bae00d --- /dev/null +++ b/pkg/client-go/model_recovery_identity_address.go @@ -0,0 +1,358 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryIdentityAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryIdentityAddress{} + +// RecoveryIdentityAddress struct for RecoveryIdentityAddress +type RecoveryIdentityAddress struct { + BreakGlassForOrganization NullableString `json:"break_glass_for_organization,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + Id *string `json:"id,omitempty"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Value string `json:"value"` + Via string `json:"via"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryIdentityAddress RecoveryIdentityAddress + +// NewRecoveryIdentityAddress instantiates a new RecoveryIdentityAddress 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 NewRecoveryIdentityAddress(value string, via string) *RecoveryIdentityAddress { + this := RecoveryIdentityAddress{} + this.Value = value + this.Via = via + return &this +} + +// NewRecoveryIdentityAddressWithDefaults instantiates a new RecoveryIdentityAddress 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 NewRecoveryIdentityAddressWithDefaults() *RecoveryIdentityAddress { + this := RecoveryIdentityAddress{} + return &this +} + +// GetBreakGlassForOrganization returns the BreakGlassForOrganization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *RecoveryIdentityAddress) GetBreakGlassForOrganization() string { + if o == nil || IsNil(o.BreakGlassForOrganization.Get()) { + var ret string + return ret + } + return *o.BreakGlassForOrganization.Get() +} + +// GetBreakGlassForOrganizationOk returns a tuple with the BreakGlassForOrganization 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 *RecoveryIdentityAddress) GetBreakGlassForOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.BreakGlassForOrganization.Get(), o.BreakGlassForOrganization.IsSet() +} + +// HasBreakGlassForOrganization returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasBreakGlassForOrganization() bool { + if o != nil && o.BreakGlassForOrganization.IsSet() { + return true + } + + return false +} + +// SetBreakGlassForOrganization gets a reference to the given NullableString and assigns it to the BreakGlassForOrganization field. +func (o *RecoveryIdentityAddress) SetBreakGlassForOrganization(v string) { + o.BreakGlassForOrganization.Set(&v) +} + +// SetBreakGlassForOrganizationNil sets the value for BreakGlassForOrganization to be an explicit nil +func (o *RecoveryIdentityAddress) SetBreakGlassForOrganizationNil() { + o.BreakGlassForOrganization.Set(nil) +} + +// UnsetBreakGlassForOrganization ensures that no value is present for BreakGlassForOrganization, not even an explicit nil +func (o *RecoveryIdentityAddress) UnsetBreakGlassForOrganization() { + o.BreakGlassForOrganization.Unset() +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *RecoveryIdentityAddress) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *RecoveryIdentityAddress) SetId(v string) { + o.Id = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *RecoveryIdentityAddress) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetValue returns the Value field value +func (o *RecoveryIdentityAddress) GetValue() string { + if o == nil { + var ret string + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetValueOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *RecoveryIdentityAddress) SetValue(v string) { + o.Value = v +} + +// GetVia returns the Via field value +func (o *RecoveryIdentityAddress) GetVia() string { + if o == nil { + var ret string + return ret + } + + return o.Via +} + +// GetViaOk returns a tuple with the Via field value +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetViaOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Via, true +} + +// SetVia sets field value +func (o *RecoveryIdentityAddress) SetVia(v string) { + o.Via = v +} + +func (o RecoveryIdentityAddress) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryIdentityAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.BreakGlassForOrganization.IsSet() { + toSerialize["break_glass_for_organization"] = o.BreakGlassForOrganization.Get() + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["value"] = o.Value + toSerialize["via"] = o.Via + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryIdentityAddress) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "value", + "via", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryIdentityAddress := _RecoveryIdentityAddress{} + + err = json.Unmarshal(data, &varRecoveryIdentityAddress) + + if err != nil { + return err + } + + *o = RecoveryIdentityAddress(varRecoveryIdentityAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "break_glass_for_organization") + delete(additionalProperties, "created_at") + delete(additionalProperties, "id") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "value") + delete(additionalProperties, "via") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryIdentityAddress struct { + value *RecoveryIdentityAddress + isSet bool +} + +func (v NullableRecoveryIdentityAddress) Get() *RecoveryIdentityAddress { + return v.value +} + +func (v *NullableRecoveryIdentityAddress) Set(val *RecoveryIdentityAddress) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryIdentityAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryIdentityAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryIdentityAddress(val *RecoveryIdentityAddress) *NullableRecoveryIdentityAddress { + return &NullableRecoveryIdentityAddress{value: val, isSet: true} +} + +func (v NullableRecoveryIdentityAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryIdentityAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_recovery_link_for_identity.go b/pkg/client-go/model_recovery_link_for_identity.go new file mode 100644 index 0000000..60c0143 --- /dev/null +++ b/pkg/client-go/model_recovery_link_for_identity.go @@ -0,0 +1,207 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryLinkForIdentity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryLinkForIdentity{} + +// RecoveryLinkForIdentity Used when an administrator creates a recovery link for an identity. +type RecoveryLinkForIdentity struct { + // Recovery Link Expires At The timestamp when the recovery link expires. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Recovery Link This link can be used to recover the account. + RecoveryLink string `json:"recovery_link"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryLinkForIdentity RecoveryLinkForIdentity + +// NewRecoveryLinkForIdentity instantiates a new RecoveryLinkForIdentity 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 NewRecoveryLinkForIdentity(recoveryLink string) *RecoveryLinkForIdentity { + this := RecoveryLinkForIdentity{} + this.RecoveryLink = recoveryLink + return &this +} + +// NewRecoveryLinkForIdentityWithDefaults instantiates a new RecoveryLinkForIdentity 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 NewRecoveryLinkForIdentityWithDefaults() *RecoveryLinkForIdentity { + this := RecoveryLinkForIdentity{} + return &this +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *RecoveryLinkForIdentity) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryLinkForIdentity) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *RecoveryLinkForIdentity) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *RecoveryLinkForIdentity) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetRecoveryLink returns the RecoveryLink field value +func (o *RecoveryLinkForIdentity) GetRecoveryLink() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryLink +} + +// GetRecoveryLinkOk returns a tuple with the RecoveryLink field value +// and a boolean to check if the value has been set. +func (o *RecoveryLinkForIdentity) GetRecoveryLinkOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryLink, true +} + +// SetRecoveryLink sets field value +func (o *RecoveryLinkForIdentity) SetRecoveryLink(v string) { + o.RecoveryLink = v +} + +func (o RecoveryLinkForIdentity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryLinkForIdentity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["recovery_link"] = o.RecoveryLink + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryLinkForIdentity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "recovery_link", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryLinkForIdentity := _RecoveryLinkForIdentity{} + + err = json.Unmarshal(data, &varRecoveryLinkForIdentity) + + if err != nil { + return err + } + + *o = RecoveryLinkForIdentity(varRecoveryLinkForIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_at") + delete(additionalProperties, "recovery_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryLinkForIdentity struct { + value *RecoveryLinkForIdentity + isSet bool +} + +func (v NullableRecoveryLinkForIdentity) Get() *RecoveryLinkForIdentity { + return v.value +} + +func (v *NullableRecoveryLinkForIdentity) Set(val *RecoveryLinkForIdentity) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryLinkForIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryLinkForIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryLinkForIdentity(val *RecoveryLinkForIdentity) *NullableRecoveryLinkForIdentity { + return &NullableRecoveryLinkForIdentity{value: val, isSet: true} +} + +func (v NullableRecoveryLinkForIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryLinkForIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_registration_flow.go b/pkg/client-go/model_registration_flow.go new file mode 100644 index 0000000..b2976e1 --- /dev/null +++ b/pkg/client-go/model_registration_flow.go @@ -0,0 +1,665 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RegistrationFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RegistrationFlow{} + +// 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // ID represents the flow's unique ID. When performing the registration flow, this represents the id in the registration ui's query parameter: http:///?flow= + Id string `json:"id"` + // IdentitySchema optionally holds the ID of the identity schema that is used for this flow. This value can be set by the user when creating the flow and should be retained when the flow is saved or converted to another flow. + IdentitySchema *string `json:"identity_schema,omitempty"` + // IssuedAt is the time (UTC) when the flow occurred. + IssuedAt time.Time `json:"issued_at"` + // Ory OAuth 2.0 Login Challenge. This value is set using the `login_challenge` query parameter of the registration and login endpoints. If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + Oauth2LoginChallenge *string `json:"oauth2_login_challenge,omitempty"` + Oauth2LoginRequest *OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + ReturnTo *string `json:"return_to,omitempty"` + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. + SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: 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. + State interface{} `json:"state"` + // TransientPayload is used to pass data from the registration to a webhook + 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"` + AdditionalProperties map[string]interface{} +} + +type _RegistrationFlow RegistrationFlow + +// NewRegistrationFlow instantiates a new RegistrationFlow 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 NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { + this := RegistrationFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewRegistrationFlowWithDefaults instantiates a new RegistrationFlow 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 NewRegistrationFlowWithDefaults() *RegistrationFlow { + this := RegistrationFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *RegistrationFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *RegistrationFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *RegistrationFlow) SetActive(v string) { + o.Active = &v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *RegistrationFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *RegistrationFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *RegistrationFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *RegistrationFlow) SetId(v string) { + o.Id = v +} + +// GetIdentitySchema returns the IdentitySchema field value if set, zero value otherwise. +func (o *RegistrationFlow) GetIdentitySchema() string { + if o == nil || IsNil(o.IdentitySchema) { + var ret string + return ret + } + return *o.IdentitySchema +} + +// GetIdentitySchemaOk returns a tuple with the IdentitySchema field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIdentitySchemaOk() (*string, bool) { + if o == nil || IsNil(o.IdentitySchema) { + return nil, false + } + return o.IdentitySchema, true +} + +// HasIdentitySchema returns a boolean if a field has been set. +func (o *RegistrationFlow) HasIdentitySchema() bool { + if o != nil && !IsNil(o.IdentitySchema) { + return true + } + + return false +} + +// SetIdentitySchema gets a reference to the given string and assigns it to the IdentitySchema field. +func (o *RegistrationFlow) SetIdentitySchema(v string) { + o.IdentitySchema = &v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *RegistrationFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *RegistrationFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOauth2LoginChallenge returns the Oauth2LoginChallenge field value if set, zero value otherwise. +func (o *RegistrationFlow) GetOauth2LoginChallenge() string { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + var ret string + return ret + } + return *o.Oauth2LoginChallenge +} + +// GetOauth2LoginChallengeOk returns a tuple with the Oauth2LoginChallenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetOauth2LoginChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + return nil, false + } + return o.Oauth2LoginChallenge, true +} + +// HasOauth2LoginChallenge returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOauth2LoginChallenge() bool { + if o != nil && !IsNil(o.Oauth2LoginChallenge) { + return true + } + + return false +} + +// SetOauth2LoginChallenge gets a reference to the given string and assigns it to the Oauth2LoginChallenge field. +func (o *RegistrationFlow) SetOauth2LoginChallenge(v string) { + o.Oauth2LoginChallenge = &v +} + +// GetOauth2LoginRequest returns the Oauth2LoginRequest field value if set, zero value otherwise. +func (o *RegistrationFlow) GetOauth2LoginRequest() OAuth2LoginRequest { + if o == nil || IsNil(o.Oauth2LoginRequest) { + var ret OAuth2LoginRequest + return ret + } + return *o.Oauth2LoginRequest +} + +// GetOauth2LoginRequestOk returns a tuple with the Oauth2LoginRequest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetOauth2LoginRequestOk() (*OAuth2LoginRequest, bool) { + if o == nil || IsNil(o.Oauth2LoginRequest) { + return nil, false + } + return o.Oauth2LoginRequest, true +} + +// HasOauth2LoginRequest returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOauth2LoginRequest() bool { + if o != nil && !IsNil(o.Oauth2LoginRequest) { + return true + } + + return false +} + +// SetOauth2LoginRequest gets a reference to the given OAuth2LoginRequest and assigns it to the Oauth2LoginRequest field. +func (o *RegistrationFlow) SetOauth2LoginRequest(v OAuth2LoginRequest) { + o.Oauth2LoginRequest = &v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *RegistrationFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *RegistrationFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *RegistrationFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *RegistrationFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *RegistrationFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRequestUrl returns the RequestUrl field value +func (o *RegistrationFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *RegistrationFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *RegistrationFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *RegistrationFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *RegistrationFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetSessionTokenExchangeCode returns the SessionTokenExchangeCode field value if set, zero value otherwise. +func (o *RegistrationFlow) GetSessionTokenExchangeCode() string { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + var ret string + return ret + } + return *o.SessionTokenExchangeCode +} + +// GetSessionTokenExchangeCodeOk returns a tuple with the SessionTokenExchangeCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetSessionTokenExchangeCodeOk() (*string, bool) { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + return nil, false + } + return o.SessionTokenExchangeCode, true +} + +// HasSessionTokenExchangeCode returns a boolean if a field has been set. +func (o *RegistrationFlow) HasSessionTokenExchangeCode() bool { + if o != nil && !IsNil(o.SessionTokenExchangeCode) { + return true + } + + return false +} + +// SetSessionTokenExchangeCode gets a reference to the given string and assigns it to the SessionTokenExchangeCode field. +func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { + o.SessionTokenExchangeCode = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *RegistrationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RegistrationFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RegistrationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetType returns the Type field value +func (o *RegistrationFlow) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *RegistrationFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *RegistrationFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *RegistrationFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o RegistrationFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RegistrationFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + if !IsNil(o.IdentitySchema) { + toSerialize["identity_schema"] = o.IdentitySchema + } + toSerialize["issued_at"] = o.IssuedAt + if !IsNil(o.Oauth2LoginChallenge) { + toSerialize["oauth2_login_challenge"] = o.Oauth2LoginChallenge + } + if !IsNil(o.Oauth2LoginRequest) { + toSerialize["oauth2_login_request"] = o.Oauth2LoginRequest + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if !IsNil(o.SessionTokenExchangeCode) { + toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RegistrationFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRegistrationFlow := _RegistrationFlow{} + + err = json.Unmarshal(data, &varRegistrationFlow) + + if err != nil { + return err + } + + *o = RegistrationFlow(varRegistrationFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity_schema") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "oauth2_login_challenge") + delete(additionalProperties, "oauth2_login_request") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "session_token_exchange_code") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRegistrationFlow struct { + value *RegistrationFlow + isSet bool +} + +func (v NullableRegistrationFlow) Get() *RegistrationFlow { + return v.value +} + +func (v *NullableRegistrationFlow) Set(val *RegistrationFlow) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlow(val *RegistrationFlow) *NullableRegistrationFlow { + return &NullableRegistrationFlow{value: val, isSet: true} +} + +func (v NullableRegistrationFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_registration_flow_state.go b/pkg/client-go/model_registration_flow_state.go new file mode 100644 index 0000000..2211c6a --- /dev/null +++ b/pkg/client-go/model_registration_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +// All allowed values of RegistrationFlowState enum +var AllowedRegistrationFlowStateEnumValues = []RegistrationFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range AllowedRegistrationFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// NewRegistrationFlowStateFromValue returns a pointer to a valid RegistrationFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewRegistrationFlowStateFromValue(v string) (*RegistrationFlowState, error) { + ev := RegistrationFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for RegistrationFlowState: valid values are %v", v, AllowedRegistrationFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v RegistrationFlowState) IsValid() bool { + for _, existing := range AllowedRegistrationFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to registrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_self_service_flow_expired_error.go b/pkg/client-go/model_self_service_flow_expired_error.go new file mode 100644 index 0000000..9878b0f --- /dev/null +++ b/pkg/client-go/model_self_service_flow_expired_error.go @@ -0,0 +1,269 @@ +/* +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" + "time" +) + +// checks if the SelfServiceFlowExpiredError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SelfServiceFlowExpiredError{} + +// SelfServiceFlowExpiredError Is sent when a flow is expired +type SelfServiceFlowExpiredError struct { + Error *GenericError `json:"error,omitempty"` + // When the flow has expired + ExpiredAt *time.Time `json:"expired_at,omitempty"` + // A Duration represents the elapsed time between two instants as an int64 nanosecond count. The representation limits the largest representable duration to approximately 290 years. + Since *int64 `json:"since,omitempty"` + // The flow ID that should be used for the new flow as it contains the correct messages. + UseFlowId *string `json:"use_flow_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SelfServiceFlowExpiredError SelfServiceFlowExpiredError + +// NewSelfServiceFlowExpiredError instantiates a new SelfServiceFlowExpiredError 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 NewSelfServiceFlowExpiredError() *SelfServiceFlowExpiredError { + this := SelfServiceFlowExpiredError{} + return &this +} + +// NewSelfServiceFlowExpiredErrorWithDefaults instantiates a new SelfServiceFlowExpiredError 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 NewSelfServiceFlowExpiredErrorWithDefaults() *SelfServiceFlowExpiredError { + this := SelfServiceFlowExpiredError{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *SelfServiceFlowExpiredError) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *SelfServiceFlowExpiredError) SetError(v GenericError) { + o.Error = &v +} + +// GetExpiredAt returns the ExpiredAt field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetExpiredAt() time.Time { + if o == nil || IsNil(o.ExpiredAt) { + var ret time.Time + return ret + } + return *o.ExpiredAt +} + +// GetExpiredAtOk returns a tuple with the ExpiredAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetExpiredAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiredAt) { + return nil, false + } + return o.ExpiredAt, true +} + +// HasExpiredAt returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasExpiredAt() bool { + if o != nil && !IsNil(o.ExpiredAt) { + return true + } + + return false +} + +// SetExpiredAt gets a reference to the given time.Time and assigns it to the ExpiredAt field. +func (o *SelfServiceFlowExpiredError) SetExpiredAt(v time.Time) { + o.ExpiredAt = &v +} + +// GetSince returns the Since field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetSince() int64 { + if o == nil || IsNil(o.Since) { + var ret int64 + return ret + } + return *o.Since +} + +// GetSinceOk returns a tuple with the Since field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetSinceOk() (*int64, bool) { + if o == nil || IsNil(o.Since) { + return nil, false + } + return o.Since, true +} + +// HasSince returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasSince() bool { + if o != nil && !IsNil(o.Since) { + return true + } + + return false +} + +// SetSince gets a reference to the given int64 and assigns it to the Since field. +func (o *SelfServiceFlowExpiredError) SetSince(v int64) { + o.Since = &v +} + +// GetUseFlowId returns the UseFlowId field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetUseFlowId() string { + if o == nil || IsNil(o.UseFlowId) { + var ret string + return ret + } + return *o.UseFlowId +} + +// GetUseFlowIdOk returns a tuple with the UseFlowId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetUseFlowIdOk() (*string, bool) { + if o == nil || IsNil(o.UseFlowId) { + return nil, false + } + return o.UseFlowId, true +} + +// HasUseFlowId returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasUseFlowId() bool { + if o != nil && !IsNil(o.UseFlowId) { + return true + } + + return false +} + +// SetUseFlowId gets a reference to the given string and assigns it to the UseFlowId field. +func (o *SelfServiceFlowExpiredError) SetUseFlowId(v string) { + o.UseFlowId = &v +} + +func (o SelfServiceFlowExpiredError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SelfServiceFlowExpiredError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.ExpiredAt) { + toSerialize["expired_at"] = o.ExpiredAt + } + if !IsNil(o.Since) { + toSerialize["since"] = o.Since + } + if !IsNil(o.UseFlowId) { + toSerialize["use_flow_id"] = o.UseFlowId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SelfServiceFlowExpiredError) UnmarshalJSON(data []byte) (err error) { + varSelfServiceFlowExpiredError := _SelfServiceFlowExpiredError{} + + err = json.Unmarshal(data, &varSelfServiceFlowExpiredError) + + if err != nil { + return err + } + + *o = SelfServiceFlowExpiredError(varSelfServiceFlowExpiredError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "expired_at") + delete(additionalProperties, "since") + delete(additionalProperties, "use_flow_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSelfServiceFlowExpiredError struct { + value *SelfServiceFlowExpiredError + isSet bool +} + +func (v NullableSelfServiceFlowExpiredError) Get() *SelfServiceFlowExpiredError { + return v.value +} + +func (v *NullableSelfServiceFlowExpiredError) Set(val *SelfServiceFlowExpiredError) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceFlowExpiredError) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceFlowExpiredError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceFlowExpiredError(val *SelfServiceFlowExpiredError) *NullableSelfServiceFlowExpiredError { + return &NullableSelfServiceFlowExpiredError{value: val, isSet: true} +} + +func (v NullableSelfServiceFlowExpiredError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceFlowExpiredError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_session.go b/pkg/client-go/model_session.go new file mode 100644 index 0000000..1228e15 --- /dev/null +++ b/pkg/client-go/model_session.go @@ -0,0 +1,509 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Session type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Session{} + +// Session A Session +type Session struct { + // Active state. If false the session is no longer active. + Active *bool `json:"active,omitempty"` + // The Session Authentication Timestamp When this session was authenticated at. If multi-factor authentication was used this is the time when the last factor was authenticated (e.g. the TOTP code challenge was completed). + AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"` + // A list of authenticators which were used to authenticate the session. + AuthenticationMethods []SessionAuthenticationMethod `json:"authentication_methods,omitempty"` + AuthenticatorAssuranceLevel *AuthenticatorAssuranceLevel `json:"authenticator_assurance_level,omitempty"` + // Devices has history of all endpoints where the session was used + Devices []SessionDevice `json:"devices,omitempty"` + // The Session Expiry When this session expires at. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Session ID + Id string `json:"id"` + Identity *Identity `json:"identity,omitempty"` + // The Session Issuance Timestamp When this session was issued at. Usually equal or close to `authenticated_at`. + IssuedAt *time.Time `json:"issued_at,omitempty"` + // Tokenized is the tokenized (e.g. JWT) version of the session. It is only set when the `tokenize_as` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized *string `json:"tokenized,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Session Session + +// NewSession instantiates a new Session 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 NewSession(id string) *Session { + this := Session{} + this.Id = id + return &this +} + +// NewSessionWithDefaults instantiates a new Session 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 NewSessionWithDefaults() *Session { + this := Session{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *Session) GetActive() bool { + if o == nil || IsNil(o.Active) { + var ret bool + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetActiveOk() (*bool, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *Session) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given bool and assigns it to the Active field. +func (o *Session) SetActive(v bool) { + o.Active = &v +} + +// GetAuthenticatedAt returns the AuthenticatedAt field value if set, zero value otherwise. +func (o *Session) GetAuthenticatedAt() time.Time { + if o == nil || IsNil(o.AuthenticatedAt) { + var ret time.Time + return ret + } + return *o.AuthenticatedAt +} + +// GetAuthenticatedAtOk returns a tuple with the AuthenticatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.AuthenticatedAt) { + return nil, false + } + return o.AuthenticatedAt, true +} + +// HasAuthenticatedAt returns a boolean if a field has been set. +func (o *Session) HasAuthenticatedAt() bool { + if o != nil && !IsNil(o.AuthenticatedAt) { + return true + } + + return false +} + +// SetAuthenticatedAt gets a reference to the given time.Time and assigns it to the AuthenticatedAt field. +func (o *Session) SetAuthenticatedAt(v time.Time) { + o.AuthenticatedAt = &v +} + +// GetAuthenticationMethods returns the AuthenticationMethods field value if set, zero value otherwise. +func (o *Session) GetAuthenticationMethods() []SessionAuthenticationMethod { + if o == nil || IsNil(o.AuthenticationMethods) { + var ret []SessionAuthenticationMethod + return ret + } + return o.AuthenticationMethods +} + +// GetAuthenticationMethodsOk returns a tuple with the AuthenticationMethods field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticationMethodsOk() ([]SessionAuthenticationMethod, bool) { + if o == nil || IsNil(o.AuthenticationMethods) { + return nil, false + } + return o.AuthenticationMethods, true +} + +// HasAuthenticationMethods returns a boolean if a field has been set. +func (o *Session) HasAuthenticationMethods() bool { + if o != nil && !IsNil(o.AuthenticationMethods) { + return true + } + + return false +} + +// SetAuthenticationMethods gets a reference to the given []SessionAuthenticationMethod and assigns it to the AuthenticationMethods field. +func (o *Session) SetAuthenticationMethods(v []SessionAuthenticationMethod) { + o.AuthenticationMethods = v +} + +// GetAuthenticatorAssuranceLevel returns the AuthenticatorAssuranceLevel field value if set, zero value otherwise. +func (o *Session) GetAuthenticatorAssuranceLevel() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.AuthenticatorAssuranceLevel) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.AuthenticatorAssuranceLevel +} + +// GetAuthenticatorAssuranceLevelOk returns a tuple with the AuthenticatorAssuranceLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticatorAssuranceLevelOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.AuthenticatorAssuranceLevel) { + return nil, false + } + return o.AuthenticatorAssuranceLevel, true +} + +// HasAuthenticatorAssuranceLevel returns a boolean if a field has been set. +func (o *Session) HasAuthenticatorAssuranceLevel() bool { + if o != nil && !IsNil(o.AuthenticatorAssuranceLevel) { + return true + } + + return false +} + +// SetAuthenticatorAssuranceLevel gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the AuthenticatorAssuranceLevel field. +func (o *Session) SetAuthenticatorAssuranceLevel(v AuthenticatorAssuranceLevel) { + o.AuthenticatorAssuranceLevel = &v +} + +// GetDevices returns the Devices field value if set, zero value otherwise. +func (o *Session) GetDevices() []SessionDevice { + if o == nil || IsNil(o.Devices) { + var ret []SessionDevice + return ret + } + return o.Devices +} + +// GetDevicesOk returns a tuple with the Devices field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetDevicesOk() ([]SessionDevice, bool) { + if o == nil || IsNil(o.Devices) { + return nil, false + } + return o.Devices, true +} + +// HasDevices returns a boolean if a field has been set. +func (o *Session) HasDevices() bool { + if o != nil && !IsNil(o.Devices) { + return true + } + + return false +} + +// SetDevices gets a reference to the given []SessionDevice and assigns it to the Devices field. +func (o *Session) SetDevices(v []SessionDevice) { + o.Devices = v +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *Session) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *Session) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *Session) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetId returns the Id field value +func (o *Session) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Session) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Session) SetId(v string) { + o.Id = v +} + +// GetIdentity returns the Identity field value if set, zero value otherwise. +func (o *Session) GetIdentity() Identity { + if o == nil || IsNil(o.Identity) { + var ret Identity + return ret + } + return *o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetIdentityOk() (*Identity, bool) { + if o == nil || IsNil(o.Identity) { + return nil, false + } + return o.Identity, true +} + +// HasIdentity returns a boolean if a field has been set. +func (o *Session) HasIdentity() bool { + if o != nil && !IsNil(o.Identity) { + return true + } + + return false +} + +// SetIdentity gets a reference to the given Identity and assigns it to the Identity field. +func (o *Session) SetIdentity(v Identity) { + o.Identity = &v +} + +// GetIssuedAt returns the IssuedAt field value if set, zero value otherwise. +func (o *Session) GetIssuedAt() time.Time { + if o == nil || IsNil(o.IssuedAt) { + var ret time.Time + return ret + } + return *o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetIssuedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.IssuedAt) { + return nil, false + } + return o.IssuedAt, true +} + +// HasIssuedAt returns a boolean if a field has been set. +func (o *Session) HasIssuedAt() bool { + if o != nil && !IsNil(o.IssuedAt) { + return true + } + + return false +} + +// SetIssuedAt gets a reference to the given time.Time and assigns it to the IssuedAt field. +func (o *Session) SetIssuedAt(v time.Time) { + o.IssuedAt = &v +} + +// GetTokenized returns the Tokenized field value if set, zero value otherwise. +func (o *Session) GetTokenized() string { + if o == nil || IsNil(o.Tokenized) { + var ret string + return ret + } + return *o.Tokenized +} + +// GetTokenizedOk returns a tuple with the Tokenized field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetTokenizedOk() (*string, bool) { + if o == nil || IsNil(o.Tokenized) { + return nil, false + } + return o.Tokenized, true +} + +// HasTokenized returns a boolean if a field has been set. +func (o *Session) HasTokenized() bool { + if o != nil && !IsNil(o.Tokenized) { + return true + } + + return false +} + +// SetTokenized gets a reference to the given string and assigns it to the Tokenized field. +func (o *Session) SetTokenized(v string) { + o.Tokenized = &v +} + +func (o Session) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Session) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.AuthenticatedAt) { + toSerialize["authenticated_at"] = o.AuthenticatedAt + } + if !IsNil(o.AuthenticationMethods) { + toSerialize["authentication_methods"] = o.AuthenticationMethods + } + if !IsNil(o.AuthenticatorAssuranceLevel) { + toSerialize["authenticator_assurance_level"] = o.AuthenticatorAssuranceLevel + } + if !IsNil(o.Devices) { + toSerialize["devices"] = o.Devices + } + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["id"] = o.Id + if !IsNil(o.Identity) { + toSerialize["identity"] = o.Identity + } + if !IsNil(o.IssuedAt) { + toSerialize["issued_at"] = o.IssuedAt + } + if !IsNil(o.Tokenized) { + toSerialize["tokenized"] = o.Tokenized + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Session) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSession := _Session{} + + err = json.Unmarshal(data, &varSession) + + if err != nil { + return err + } + + *o = Session(varSession) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "authenticated_at") + delete(additionalProperties, "authentication_methods") + delete(additionalProperties, "authenticator_assurance_level") + delete(additionalProperties, "devices") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "tokenized") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSession struct { + value *Session + isSet bool +} + +func (v NullableSession) Get() *Session { + return v.value +} + +func (v *NullableSession) Set(val *Session) { + v.value = val + v.isSet = true +} + +func (v NullableSession) IsSet() bool { + return v.isSet +} + +func (v *NullableSession) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSession(val *Session) *NullableSession { + return &NullableSession{value: val, isSet: true} +} + +func (v NullableSession) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSession) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_session_authentication_method.go b/pkg/client-go/model_session_authentication_method.go new file mode 100644 index 0000000..a72b735 --- /dev/null +++ b/pkg/client-go/model_session_authentication_method.go @@ -0,0 +1,383 @@ +/* +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" + "time" +) + +// checks if the SessionAuthenticationMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SessionAuthenticationMethod{} + +// SessionAuthenticationMethod A singular authenticator used during authentication / login. +type SessionAuthenticationMethod struct { + Aal *AuthenticatorAssuranceLevel `json:"aal,omitempty"` + // When the authentication challenge was completed. + CompletedAt *time.Time `json:"completed_at,omitempty"` + // The method used in this authenticator. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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 + Method *string `json:"method,omitempty"` + // The Organization id used for authentication + Organization *string `json:"organization,omitempty"` + // OIDC or SAML provider id used for authentication + Provider *string `json:"provider,omitempty"` + // UpstreamACR is the `acr` claim reported by the upstream OIDC provider, if any. Populated only for OIDC login methods when the upstream ID token contained an `acr` claim. + UpstreamAcr *string `json:"upstream_acr,omitempty"` + // UpstreamAMR is the `amr` claim reported by the upstream OIDC provider, if any. Populated only for OIDC login methods when the upstream ID token contained an `amr` claim. + UpstreamAmr []string `json:"upstream_amr,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SessionAuthenticationMethod SessionAuthenticationMethod + +// NewSessionAuthenticationMethod instantiates a new SessionAuthenticationMethod 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 NewSessionAuthenticationMethod() *SessionAuthenticationMethod { + this := SessionAuthenticationMethod{} + return &this +} + +// NewSessionAuthenticationMethodWithDefaults instantiates a new SessionAuthenticationMethod 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 NewSessionAuthenticationMethodWithDefaults() *SessionAuthenticationMethod { + this := SessionAuthenticationMethod{} + return &this +} + +// GetAal returns the Aal field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetAal() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.Aal) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.Aal +} + +// GetAalOk returns a tuple with the Aal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetAalOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.Aal) { + return nil, false + } + return o.Aal, true +} + +// HasAal returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasAal() bool { + if o != nil && !IsNil(o.Aal) { + return true + } + + return false +} + +// SetAal gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the Aal field. +func (o *SessionAuthenticationMethod) SetAal(v AuthenticatorAssuranceLevel) { + o.Aal = &v +} + +// GetCompletedAt returns the CompletedAt field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetCompletedAt() time.Time { + if o == nil || IsNil(o.CompletedAt) { + var ret time.Time + return ret + } + return *o.CompletedAt +} + +// GetCompletedAtOk returns a tuple with the CompletedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetCompletedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CompletedAt) { + return nil, false + } + return o.CompletedAt, true +} + +// HasCompletedAt returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasCompletedAt() bool { + if o != nil && !IsNil(o.CompletedAt) { + return true + } + + return false +} + +// SetCompletedAt gets a reference to the given time.Time and assigns it to the CompletedAt field. +func (o *SessionAuthenticationMethod) SetCompletedAt(v time.Time) { + o.CompletedAt = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetMethod() string { + if o == nil || IsNil(o.Method) { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetMethodOk() (*string, bool) { + if o == nil || IsNil(o.Method) { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasMethod() bool { + if o != nil && !IsNil(o.Method) { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *SessionAuthenticationMethod) SetMethod(v string) { + o.Method = &v +} + +// GetOrganization returns the Organization field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetOrganization() string { + if o == nil || IsNil(o.Organization) { + var ret string + return ret + } + return *o.Organization +} + +// GetOrganizationOk returns a tuple with the Organization field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetOrganizationOk() (*string, bool) { + if o == nil || IsNil(o.Organization) { + return nil, false + } + return o.Organization, true +} + +// HasOrganization returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasOrganization() bool { + if o != nil && !IsNil(o.Organization) { + return true + } + + return false +} + +// SetOrganization gets a reference to the given string and assigns it to the Organization field. +func (o *SessionAuthenticationMethod) SetOrganization(v string) { + o.Organization = &v +} + +// GetProvider returns the Provider field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetProvider() string { + if o == nil || IsNil(o.Provider) { + var ret string + return ret + } + return *o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetProviderOk() (*string, bool) { + if o == nil || IsNil(o.Provider) { + return nil, false + } + return o.Provider, true +} + +// HasProvider returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasProvider() bool { + if o != nil && !IsNil(o.Provider) { + return true + } + + return false +} + +// SetProvider gets a reference to the given string and assigns it to the Provider field. +func (o *SessionAuthenticationMethod) SetProvider(v string) { + o.Provider = &v +} + +// GetUpstreamAcr returns the UpstreamAcr field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetUpstreamAcr() string { + if o == nil || IsNil(o.UpstreamAcr) { + var ret string + return ret + } + return *o.UpstreamAcr +} + +// GetUpstreamAcrOk returns a tuple with the UpstreamAcr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetUpstreamAcrOk() (*string, bool) { + if o == nil || IsNil(o.UpstreamAcr) { + return nil, false + } + return o.UpstreamAcr, true +} + +// HasUpstreamAcr returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasUpstreamAcr() bool { + if o != nil && !IsNil(o.UpstreamAcr) { + return true + } + + return false +} + +// SetUpstreamAcr gets a reference to the given string and assigns it to the UpstreamAcr field. +func (o *SessionAuthenticationMethod) SetUpstreamAcr(v string) { + o.UpstreamAcr = &v +} + +// GetUpstreamAmr returns the UpstreamAmr field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetUpstreamAmr() []string { + if o == nil || IsNil(o.UpstreamAmr) { + var ret []string + return ret + } + return o.UpstreamAmr +} + +// GetUpstreamAmrOk returns a tuple with the UpstreamAmr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetUpstreamAmrOk() ([]string, bool) { + if o == nil || IsNil(o.UpstreamAmr) { + return nil, false + } + return o.UpstreamAmr, true +} + +// HasUpstreamAmr returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasUpstreamAmr() bool { + if o != nil && !IsNil(o.UpstreamAmr) { + return true + } + + return false +} + +// SetUpstreamAmr gets a reference to the given []string and assigns it to the UpstreamAmr field. +func (o *SessionAuthenticationMethod) SetUpstreamAmr(v []string) { + o.UpstreamAmr = v +} + +func (o SessionAuthenticationMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SessionAuthenticationMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Aal) { + toSerialize["aal"] = o.Aal + } + if !IsNil(o.CompletedAt) { + toSerialize["completed_at"] = o.CompletedAt + } + if !IsNil(o.Method) { + toSerialize["method"] = o.Method + } + if !IsNil(o.Organization) { + toSerialize["organization"] = o.Organization + } + if !IsNil(o.Provider) { + toSerialize["provider"] = o.Provider + } + if !IsNil(o.UpstreamAcr) { + toSerialize["upstream_acr"] = o.UpstreamAcr + } + if !IsNil(o.UpstreamAmr) { + toSerialize["upstream_amr"] = o.UpstreamAmr + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SessionAuthenticationMethod) UnmarshalJSON(data []byte) (err error) { + varSessionAuthenticationMethod := _SessionAuthenticationMethod{} + + err = json.Unmarshal(data, &varSessionAuthenticationMethod) + + if err != nil { + return err + } + + *o = SessionAuthenticationMethod(varSessionAuthenticationMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "aal") + delete(additionalProperties, "completed_at") + delete(additionalProperties, "method") + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "upstream_acr") + delete(additionalProperties, "upstream_amr") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSessionAuthenticationMethod struct { + value *SessionAuthenticationMethod + isSet bool +} + +func (v NullableSessionAuthenticationMethod) Get() *SessionAuthenticationMethod { + return v.value +} + +func (v *NullableSessionAuthenticationMethod) Set(val *SessionAuthenticationMethod) { + v.value = val + v.isSet = true +} + +func (v NullableSessionAuthenticationMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableSessionAuthenticationMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSessionAuthenticationMethod(val *SessionAuthenticationMethod) *NullableSessionAuthenticationMethod { + return &NullableSessionAuthenticationMethod{value: val, isSet: true} +} + +func (v NullableSessionAuthenticationMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSessionAuthenticationMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_session_device.go b/pkg/client-go/model_session_device.go new file mode 100644 index 0000000..3370aed --- /dev/null +++ b/pkg/client-go/model_session_device.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" +) + +// checks if the SessionDevice type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SessionDevice{} + +// SessionDevice Device corresponding to a Session +type SessionDevice struct { + // Device record ID + Id string `json:"id"` + // IPAddress of the client + IpAddress *string `json:"ip_address,omitempty"` + // Geo Location corresponding to the IP Address + Location *string `json:"location,omitempty"` + // UserAgent of the client + UserAgent *string `json:"user_agent,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SessionDevice SessionDevice + +// NewSessionDevice instantiates a new SessionDevice 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 NewSessionDevice(id string) *SessionDevice { + this := SessionDevice{} + this.Id = id + return &this +} + +// NewSessionDeviceWithDefaults instantiates a new SessionDevice 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 NewSessionDeviceWithDefaults() *SessionDevice { + this := SessionDevice{} + return &this +} + +// GetId returns the Id field value +func (o *SessionDevice) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *SessionDevice) SetId(v string) { + o.Id = v +} + +// GetIpAddress returns the IpAddress field value if set, zero value otherwise. +func (o *SessionDevice) GetIpAddress() string { + if o == nil || IsNil(o.IpAddress) { + var ret string + return ret + } + return *o.IpAddress +} + +// GetIpAddressOk returns a tuple with the IpAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetIpAddressOk() (*string, bool) { + if o == nil || IsNil(o.IpAddress) { + return nil, false + } + return o.IpAddress, true +} + +// HasIpAddress returns a boolean if a field has been set. +func (o *SessionDevice) HasIpAddress() bool { + if o != nil && !IsNil(o.IpAddress) { + return true + } + + return false +} + +// SetIpAddress gets a reference to the given string and assigns it to the IpAddress field. +func (o *SessionDevice) SetIpAddress(v string) { + o.IpAddress = &v +} + +// GetLocation returns the Location field value if set, zero value otherwise. +func (o *SessionDevice) GetLocation() string { + if o == nil || IsNil(o.Location) { + var ret string + return ret + } + return *o.Location +} + +// GetLocationOk returns a tuple with the Location field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetLocationOk() (*string, bool) { + if o == nil || IsNil(o.Location) { + return nil, false + } + return o.Location, true +} + +// HasLocation returns a boolean if a field has been set. +func (o *SessionDevice) HasLocation() bool { + if o != nil && !IsNil(o.Location) { + return true + } + + return false +} + +// SetLocation gets a reference to the given string and assigns it to the Location field. +func (o *SessionDevice) SetLocation(v string) { + o.Location = &v +} + +// GetUserAgent returns the UserAgent field value if set, zero value otherwise. +func (o *SessionDevice) GetUserAgent() string { + if o == nil || IsNil(o.UserAgent) { + var ret string + return ret + } + return *o.UserAgent +} + +// GetUserAgentOk returns a tuple with the UserAgent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetUserAgentOk() (*string, bool) { + if o == nil || IsNil(o.UserAgent) { + return nil, false + } + return o.UserAgent, true +} + +// HasUserAgent returns a boolean if a field has been set. +func (o *SessionDevice) HasUserAgent() bool { + if o != nil && !IsNil(o.UserAgent) { + return true + } + + return false +} + +// SetUserAgent gets a reference to the given string and assigns it to the UserAgent field. +func (o *SessionDevice) SetUserAgent(v string) { + o.UserAgent = &v +} + +func (o SessionDevice) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SessionDevice) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.IpAddress) { + toSerialize["ip_address"] = o.IpAddress + } + if !IsNil(o.Location) { + toSerialize["location"] = o.Location + } + if !IsNil(o.UserAgent) { + toSerialize["user_agent"] = o.UserAgent + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SessionDevice) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSessionDevice := _SessionDevice{} + + err = json.Unmarshal(data, &varSessionDevice) + + if err != nil { + return err + } + + *o = SessionDevice(varSessionDevice) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "ip_address") + delete(additionalProperties, "location") + delete(additionalProperties, "user_agent") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSessionDevice struct { + value *SessionDevice + isSet bool +} + +func (v NullableSessionDevice) Get() *SessionDevice { + return v.value +} + +func (v *NullableSessionDevice) Set(val *SessionDevice) { + v.value = val + v.isSet = true +} + +func (v NullableSessionDevice) IsSet() bool { + return v.isSet +} + +func (v *NullableSessionDevice) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSessionDevice(val *SessionDevice) *NullableSessionDevice { + return &NullableSessionDevice{value: val, isSet: true} +} + +func (v NullableSessionDevice) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSessionDevice) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_settings_flow.go b/pkg/client-go/model_settings_flow.go new file mode 100644 index 0000000..5767aee --- /dev/null +++ b/pkg/client-go/model_settings_flow.go @@ -0,0 +1,581 @@ +/* +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" + "fmt" + "time" +) + +// checks if the SettingsFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SettingsFlow{} + +// SettingsFlow This flow is used when an identity wants to update settings (e.g. profile data, passwords, ...) in a selfservice manner. We recommend reading the [User Settings Documentation](../self-service/flows/user-settings) +type SettingsFlow struct { + // Active, if set, contains the registration method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // 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. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting, a new flow has to be initiated. + ExpiresAt time.Time `json:"expires_at"` + // ID represents the flow's unique ID. When performing the settings flow, this represents the id in the settings ui's query parameter: http://?flow= + Id string `json:"id"` + Identity Identity `json:"identity"` + // IssuedAt is the time (UTC) when the flow occurred. + IssuedAt time.Time `json:"issued_at"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _SettingsFlow SettingsFlow + +// NewSettingsFlow instantiates a new SettingsFlow 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 NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { + this := SettingsFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.Identity = identity + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewSettingsFlowWithDefaults instantiates a new SettingsFlow 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 NewSettingsFlowWithDefaults() *SettingsFlow { + this := SettingsFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *SettingsFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *SettingsFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *SettingsFlow) SetActive(v string) { + o.Active = &v +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SettingsFlow) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *SettingsFlow) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SettingsFlow) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SettingsFlow) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *SettingsFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *SettingsFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *SettingsFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *SettingsFlow) SetId(v string) { + o.Id = v +} + +// GetIdentity returns the Identity field value +func (o *SettingsFlow) GetIdentity() Identity { + if o == nil { + var ret Identity + return ret + } + + return o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIdentityOk() (*Identity, bool) { + if o == nil { + return nil, false + } + return &o.Identity, true +} + +// SetIdentity sets field value +func (o *SettingsFlow) SetIdentity(v Identity) { + o.Identity = v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *SettingsFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *SettingsFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *SettingsFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *SettingsFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *SettingsFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *SettingsFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *SettingsFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *SettingsFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRequestUrl returns the RequestUrl field value +func (o *SettingsFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *SettingsFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *SettingsFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *SettingsFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *SettingsFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *SettingsFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *SettingsFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *SettingsFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o SettingsFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SettingsFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + toSerialize["identity"] = o.Identity + toSerialize["issued_at"] = o.IssuedAt + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SettingsFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "identity", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSettingsFlow := _SettingsFlow{} + + err = json.Unmarshal(data, &varSettingsFlow) + + if err != nil { + return err + } + + *o = SettingsFlow(varSettingsFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "continue_with") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSettingsFlow struct { + value *SettingsFlow + isSet bool +} + +func (v NullableSettingsFlow) Get() *SettingsFlow { + return v.value +} + +func (v *NullableSettingsFlow) Set(val *SettingsFlow) { + v.value = val + v.isSet = true +} + +func (v NullableSettingsFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableSettingsFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSettingsFlow(val *SettingsFlow) *NullableSettingsFlow { + return &NullableSettingsFlow{value: val, isSet: true} +} + +func (v NullableSettingsFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSettingsFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_settings_flow_state.go b/pkg/client-go/model_settings_flow_state.go new file mode 100644 index 0000000..47817bc --- /dev/null +++ b/pkg/client-go/model_settings_flow_state.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// 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 +const ( + SETTINGSFLOWSTATE_SHOW_FORM SettingsFlowState = "show_form" + SETTINGSFLOWSTATE_SUCCESS SettingsFlowState = "success" +) + +// All allowed values of SettingsFlowState enum +var AllowedSettingsFlowStateEnumValues = []SettingsFlowState{ + "show_form", + "success", +} + +func (v *SettingsFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SettingsFlowState(value) + for _, existing := range AllowedSettingsFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SettingsFlowState", value) +} + +// NewSettingsFlowStateFromValue returns a pointer to a valid SettingsFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewSettingsFlowStateFromValue(v string) (*SettingsFlowState, error) { + ev := SettingsFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for SettingsFlowState: valid values are %v", v, AllowedSettingsFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v SettingsFlowState) IsValid() bool { + for _, existing := range AllowedSettingsFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to settingsFlowState value +func (v SettingsFlowState) Ptr() *SettingsFlowState { + return &v +} + +type NullableSettingsFlowState struct { + value *SettingsFlowState + isSet bool +} + +func (v NullableSettingsFlowState) Get() *SettingsFlowState { + return v.value +} + +func (v *NullableSettingsFlowState) Set(val *SettingsFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSettingsFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSettingsFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSettingsFlowState(val *SettingsFlowState) *NullableSettingsFlowState { + return &NullableSettingsFlowState{value: val, isSet: true} +} + +func (v NullableSettingsFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSettingsFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_successful_code_exchange_response.go b/pkg/client-go/model_successful_code_exchange_response.go new file mode 100644 index 0000000..e1bbaa2 --- /dev/null +++ b/pkg/client-go/model_successful_code_exchange_response.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulCodeExchangeResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulCodeExchangeResponse{} + +// SuccessfulCodeExchangeResponse The Response for Registration Flows via API +type SuccessfulCodeExchangeResponse struct { + 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulCodeExchangeResponse SuccessfulCodeExchangeResponse + +// NewSuccessfulCodeExchangeResponse instantiates a new SuccessfulCodeExchangeResponse 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 NewSuccessfulCodeExchangeResponse(session Session) *SuccessfulCodeExchangeResponse { + this := SuccessfulCodeExchangeResponse{} + this.Session = session + return &this +} + +// NewSuccessfulCodeExchangeResponseWithDefaults instantiates a new SuccessfulCodeExchangeResponse 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 NewSuccessfulCodeExchangeResponseWithDefaults() *SuccessfulCodeExchangeResponse { + this := SuccessfulCodeExchangeResponse{} + return &this +} + +// GetSession returns the Session field value +func (o *SuccessfulCodeExchangeResponse) GetSession() Session { + if o == nil { + var ret Session + return ret + } + + return o.Session +} + +// GetSessionOk returns a tuple with the Session field value +// and a boolean to check if the value has been set. +func (o *SuccessfulCodeExchangeResponse) GetSessionOk() (*Session, bool) { + if o == nil { + return nil, false + } + return &o.Session, true +} + +// SetSession sets field value +func (o *SuccessfulCodeExchangeResponse) SetSession(v Session) { + o.Session = v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulCodeExchangeResponse) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulCodeExchangeResponse) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulCodeExchangeResponse) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulCodeExchangeResponse) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulCodeExchangeResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulCodeExchangeResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["session"] = o.Session + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulCodeExchangeResponse) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulCodeExchangeResponse := _SuccessfulCodeExchangeResponse{} + + err = json.Unmarshal(data, &varSuccessfulCodeExchangeResponse) + + if err != nil { + return err + } + + *o = SuccessfulCodeExchangeResponse(varSuccessfulCodeExchangeResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulCodeExchangeResponse struct { + value *SuccessfulCodeExchangeResponse + isSet bool +} + +func (v NullableSuccessfulCodeExchangeResponse) Get() *SuccessfulCodeExchangeResponse { + return v.value +} + +func (v *NullableSuccessfulCodeExchangeResponse) Set(val *SuccessfulCodeExchangeResponse) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulCodeExchangeResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulCodeExchangeResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulCodeExchangeResponse(val *SuccessfulCodeExchangeResponse) *NullableSuccessfulCodeExchangeResponse { + return &NullableSuccessfulCodeExchangeResponse{value: val, isSet: true} +} + +func (v NullableSuccessfulCodeExchangeResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulCodeExchangeResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_successful_native_login.go b/pkg/client-go/model_successful_native_login.go new file mode 100644 index 0000000..05bf6b7 --- /dev/null +++ b/pkg/client-go/model_successful_native_login.go @@ -0,0 +1,243 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulNativeLogin type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulNativeLogin{} + +// SuccessfulNativeLogin The Response for Login Flows via API +type SuccessfulNativeLogin struct { + // 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulNativeLogin SuccessfulNativeLogin + +// NewSuccessfulNativeLogin instantiates a new SuccessfulNativeLogin 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 NewSuccessfulNativeLogin(session Session) *SuccessfulNativeLogin { + this := SuccessfulNativeLogin{} + this.Session = session + return &this +} + +// NewSuccessfulNativeLoginWithDefaults instantiates a new SuccessfulNativeLogin 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 NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { + this := SuccessfulNativeLogin{} + return &this +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 || IsNil(o.ContinueWith) { + 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 && !IsNil(o.ContinueWith) { + 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 { + var ret Session + return ret + } + + return o.Session +} + +// GetSessionOk returns a tuple with the Session field value +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetSessionOk() (*Session, bool) { + if o == nil { + return nil, false + } + return &o.Session, true +} + +// SetSession sets field value +func (o *SuccessfulNativeLogin) SetSession(v Session) { + o.Session = v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulNativeLogin) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulNativeLogin) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["session"] = o.Session + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulNativeLogin) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulNativeLogin := _SuccessfulNativeLogin{} + + err = json.Unmarshal(data, &varSuccessfulNativeLogin) + + if err != nil { + return err + } + + *o = SuccessfulNativeLogin(varSuccessfulNativeLogin) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "continue_with") + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulNativeLogin struct { + value *SuccessfulNativeLogin + isSet bool +} + +func (v NullableSuccessfulNativeLogin) Get() *SuccessfulNativeLogin { + return v.value +} + +func (v *NullableSuccessfulNativeLogin) Set(val *SuccessfulNativeLogin) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulNativeLogin) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulNativeLogin) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulNativeLogin(val *SuccessfulNativeLogin) *NullableSuccessfulNativeLogin { + return &NullableSuccessfulNativeLogin{value: val, isSet: true} +} + +func (v NullableSuccessfulNativeLogin) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulNativeLogin) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_successful_native_registration.go b/pkg/client-go/model_successful_native_registration.go new file mode 100644 index 0000000..d12b45a --- /dev/null +++ b/pkg/client-go/model_successful_native_registration.go @@ -0,0 +1,280 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulNativeRegistration type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulNativeRegistration{} + +// SuccessfulNativeRegistration The Response for Registration Flows via API +type SuccessfulNativeRegistration struct { + // 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"` + Identity Identity `json:"identity"` + Session *Session `json:"session,omitempty"` + // The Session Token This field is only set when the session hook is configured as a post-registration hook. 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulNativeRegistration SuccessfulNativeRegistration + +// NewSuccessfulNativeRegistration instantiates a new SuccessfulNativeRegistration 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 NewSuccessfulNativeRegistration(identity Identity) *SuccessfulNativeRegistration { + this := SuccessfulNativeRegistration{} + this.Identity = identity + return &this +} + +// NewSuccessfulNativeRegistrationWithDefaults instantiates a new SuccessfulNativeRegistration 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 NewSuccessfulNativeRegistrationWithDefaults() *SuccessfulNativeRegistration { + this := SuccessfulNativeRegistration{} + return &this +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *SuccessfulNativeRegistration) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeRegistration) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetIdentity returns the Identity field value +func (o *SuccessfulNativeRegistration) GetIdentity() Identity { + if o == nil { + var ret Identity + return ret + } + + return o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetIdentityOk() (*Identity, bool) { + if o == nil { + return nil, false + } + return &o.Identity, true +} + +// SetIdentity sets field value +func (o *SuccessfulNativeRegistration) SetIdentity(v Identity) { + o.Identity = v +} + +// GetSession returns the Session field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetSession() Session { + if o == nil || IsNil(o.Session) { + var ret Session + return ret + } + return *o.Session +} + +// GetSessionOk returns a tuple with the Session field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetSessionOk() (*Session, bool) { + if o == nil || IsNil(o.Session) { + return nil, false + } + return o.Session, true +} + +// HasSession returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasSession() bool { + if o != nil && !IsNil(o.Session) { + return true + } + + return false +} + +// SetSession gets a reference to the given Session and assigns it to the Session field. +func (o *SuccessfulNativeRegistration) SetSession(v Session) { + o.Session = &v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulNativeRegistration) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulNativeRegistration) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulNativeRegistration) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["identity"] = o.Identity + if !IsNil(o.Session) { + toSerialize["session"] = o.Session + } + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulNativeRegistration) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulNativeRegistration := _SuccessfulNativeRegistration{} + + err = json.Unmarshal(data, &varSuccessfulNativeRegistration) + + if err != nil { + return err + } + + *o = SuccessfulNativeRegistration(varSuccessfulNativeRegistration) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "continue_with") + delete(additionalProperties, "identity") + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulNativeRegistration struct { + value *SuccessfulNativeRegistration + isSet bool +} + +func (v NullableSuccessfulNativeRegistration) Get() *SuccessfulNativeRegistration { + return v.value +} + +func (v *NullableSuccessfulNativeRegistration) Set(val *SuccessfulNativeRegistration) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulNativeRegistration) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulNativeRegistration) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulNativeRegistration(val *SuccessfulNativeRegistration) *NullableSuccessfulNativeRegistration { + return &NullableSuccessfulNativeRegistration{value: val, isSet: true} +} + +func (v NullableSuccessfulNativeRegistration) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulNativeRegistration) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_token_pagination.go b/pkg/client-go/model_token_pagination.go new file mode 100644 index 0000000..d51af6c --- /dev/null +++ b/pkg/client-go/model_token_pagination.go @@ -0,0 +1,197 @@ +/* +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" +) + +// checks if the TokenPagination type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TokenPagination{} + +// TokenPagination struct for TokenPagination +type TokenPagination struct { + // Items per page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + PageSize *int64 `json:"page_size,omitempty"` + // Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + PageToken *string `json:"page_token,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _TokenPagination TokenPagination + +// NewTokenPagination instantiates a new TokenPagination 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 NewTokenPagination() *TokenPagination { + this := TokenPagination{} + var pageSize int64 = 250 + this.PageSize = &pageSize + return &this +} + +// NewTokenPaginationWithDefaults instantiates a new TokenPagination 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 NewTokenPaginationWithDefaults() *TokenPagination { + this := TokenPagination{} + var pageSize int64 = 250 + this.PageSize = &pageSize + return &this +} + +// GetPageSize returns the PageSize field value if set, zero value otherwise. +func (o *TokenPagination) GetPageSize() int64 { + if o == nil || IsNil(o.PageSize) { + var ret int64 + return ret + } + return *o.PageSize +} + +// GetPageSizeOk returns a tuple with the PageSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPagination) GetPageSizeOk() (*int64, bool) { + if o == nil || IsNil(o.PageSize) { + return nil, false + } + return o.PageSize, true +} + +// HasPageSize returns a boolean if a field has been set. +func (o *TokenPagination) HasPageSize() bool { + if o != nil && !IsNil(o.PageSize) { + return true + } + + return false +} + +// SetPageSize gets a reference to the given int64 and assigns it to the PageSize field. +func (o *TokenPagination) SetPageSize(v int64) { + o.PageSize = &v +} + +// GetPageToken returns the PageToken field value if set, zero value otherwise. +func (o *TokenPagination) GetPageToken() string { + if o == nil || IsNil(o.PageToken) { + var ret string + return ret + } + return *o.PageToken +} + +// GetPageTokenOk returns a tuple with the PageToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPagination) GetPageTokenOk() (*string, bool) { + if o == nil || IsNil(o.PageToken) { + return nil, false + } + return o.PageToken, true +} + +// HasPageToken returns a boolean if a field has been set. +func (o *TokenPagination) HasPageToken() bool { + if o != nil && !IsNil(o.PageToken) { + return true + } + + return false +} + +// SetPageToken gets a reference to the given string and assigns it to the PageToken field. +func (o *TokenPagination) SetPageToken(v string) { + o.PageToken = &v +} + +func (o TokenPagination) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TokenPagination) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.PageSize) { + toSerialize["page_size"] = o.PageSize + } + if !IsNil(o.PageToken) { + toSerialize["page_token"] = o.PageToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *TokenPagination) UnmarshalJSON(data []byte) (err error) { + varTokenPagination := _TokenPagination{} + + err = json.Unmarshal(data, &varTokenPagination) + + if err != nil { + return err + } + + *o = TokenPagination(varTokenPagination) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "page_size") + delete(additionalProperties, "page_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableTokenPagination struct { + value *TokenPagination + isSet bool +} + +func (v NullableTokenPagination) Get() *TokenPagination { + return v.value +} + +func (v *NullableTokenPagination) Set(val *TokenPagination) { + v.value = val + v.isSet = true +} + +func (v NullableTokenPagination) IsSet() bool { + return v.isSet +} + +func (v *NullableTokenPagination) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTokenPagination(val *TokenPagination) *NullableTokenPagination { + return &NullableTokenPagination{value: val, isSet: true} +} + +func (v NullableTokenPagination) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTokenPagination) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_token_pagination_headers.go b/pkg/client-go/model_token_pagination_headers.go new file mode 100644 index 0000000..0bf5689 --- /dev/null +++ b/pkg/client-go/model_token_pagination_headers.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the TokenPaginationHeaders type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TokenPaginationHeaders{} + +// TokenPaginationHeaders struct for TokenPaginationHeaders +type TokenPaginationHeaders struct { + // The link header contains pagination links. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). in: header + Link *string `json:"link,omitempty"` + // The total number of clients. in: header + XTotalCount *string `json:"x-total-count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _TokenPaginationHeaders TokenPaginationHeaders + +// NewTokenPaginationHeaders instantiates a new TokenPaginationHeaders 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 NewTokenPaginationHeaders() *TokenPaginationHeaders { + this := TokenPaginationHeaders{} + return &this +} + +// NewTokenPaginationHeadersWithDefaults instantiates a new TokenPaginationHeaders 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 NewTokenPaginationHeadersWithDefaults() *TokenPaginationHeaders { + this := TokenPaginationHeaders{} + return &this +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *TokenPaginationHeaders) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPaginationHeaders) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *TokenPaginationHeaders) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *TokenPaginationHeaders) SetLink(v string) { + o.Link = &v +} + +// GetXTotalCount returns the XTotalCount field value if set, zero value otherwise. +func (o *TokenPaginationHeaders) GetXTotalCount() string { + if o == nil || IsNil(o.XTotalCount) { + var ret string + return ret + } + return *o.XTotalCount +} + +// GetXTotalCountOk returns a tuple with the XTotalCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPaginationHeaders) GetXTotalCountOk() (*string, bool) { + if o == nil || IsNil(o.XTotalCount) { + return nil, false + } + return o.XTotalCount, true +} + +// HasXTotalCount returns a boolean if a field has been set. +func (o *TokenPaginationHeaders) HasXTotalCount() bool { + if o != nil && !IsNil(o.XTotalCount) { + return true + } + + return false +} + +// SetXTotalCount gets a reference to the given string and assigns it to the XTotalCount field. +func (o *TokenPaginationHeaders) SetXTotalCount(v string) { + o.XTotalCount = &v +} + +func (o TokenPaginationHeaders) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TokenPaginationHeaders) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + if !IsNil(o.XTotalCount) { + toSerialize["x-total-count"] = o.XTotalCount + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *TokenPaginationHeaders) UnmarshalJSON(data []byte) (err error) { + varTokenPaginationHeaders := _TokenPaginationHeaders{} + + err = json.Unmarshal(data, &varTokenPaginationHeaders) + + if err != nil { + return err + } + + *o = TokenPaginationHeaders(varTokenPaginationHeaders) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "link") + delete(additionalProperties, "x-total-count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableTokenPaginationHeaders struct { + value *TokenPaginationHeaders + isSet bool +} + +func (v NullableTokenPaginationHeaders) Get() *TokenPaginationHeaders { + return v.value +} + +func (v *NullableTokenPaginationHeaders) Set(val *TokenPaginationHeaders) { + v.value = val + v.isSet = true +} + +func (v NullableTokenPaginationHeaders) IsSet() bool { + return v.isSet +} + +func (v *NullableTokenPaginationHeaders) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTokenPaginationHeaders(val *TokenPaginationHeaders) *NullableTokenPaginationHeaders { + return &NullableTokenPaginationHeaders{value: val, isSet: true} +} + +func (v NullableTokenPaginationHeaders) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTokenPaginationHeaders) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_container.go b/pkg/client-go/model_ui_container.go new file mode 100644 index 0000000..25e0ab5 --- /dev/null +++ b/pkg/client-go/model_ui_container.go @@ -0,0 +1,264 @@ +/* +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" + "fmt" +) + +// checks if the UiContainer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiContainer{} + +// UiContainer Container represents a HTML Form. The container can work with both HTTP Form and JSON requests +type UiContainer struct { + // Action should be used as the form action URL `
`. + Action string `json:"action"` + Messages []UiText `json:"messages,omitempty"` + // Method is the form method (e.g. POST) + Method string `json:"method"` + Nodes []UiNode `json:"nodes"` + AdditionalProperties map[string]interface{} +} + +type _UiContainer UiContainer + +// NewUiContainer instantiates a new UiContainer 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 NewUiContainer(action string, method string, nodes []UiNode) *UiContainer { + this := UiContainer{} + this.Action = action + this.Method = method + this.Nodes = nodes + return &this +} + +// NewUiContainerWithDefaults instantiates a new UiContainer 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 NewUiContainerWithDefaults() *UiContainer { + this := UiContainer{} + return &this +} + +// GetAction returns the Action field value +func (o *UiContainer) 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 *UiContainer) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *UiContainer) SetAction(v string) { + o.Action = v +} + +// GetMessages returns the Messages field value if set, zero value otherwise. +func (o *UiContainer) GetMessages() []UiText { + if o == nil || IsNil(o.Messages) { + var ret []UiText + return ret + } + return o.Messages +} + +// GetMessagesOk returns a tuple with the Messages field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiContainer) GetMessagesOk() ([]UiText, bool) { + if o == nil || IsNil(o.Messages) { + return nil, false + } + return o.Messages, true +} + +// HasMessages returns a boolean if a field has been set. +func (o *UiContainer) HasMessages() bool { + if o != nil && !IsNil(o.Messages) { + return true + } + + return false +} + +// SetMessages gets a reference to the given []UiText and assigns it to the Messages field. +func (o *UiContainer) SetMessages(v []UiText) { + o.Messages = v +} + +// GetMethod returns the Method field value +func (o *UiContainer) 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 *UiContainer) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UiContainer) SetMethod(v string) { + o.Method = v +} + +// GetNodes returns the Nodes field value +func (o *UiContainer) GetNodes() []UiNode { + if o == nil { + var ret []UiNode + return ret + } + + return o.Nodes +} + +// GetNodesOk returns a tuple with the Nodes field value +// and a boolean to check if the value has been set. +func (o *UiContainer) GetNodesOk() ([]UiNode, bool) { + if o == nil { + return nil, false + } + return o.Nodes, true +} + +// SetNodes sets field value +func (o *UiContainer) SetNodes(v []UiNode) { + o.Nodes = v +} + +func (o UiContainer) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiContainer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + if !IsNil(o.Messages) { + toSerialize["messages"] = o.Messages + } + toSerialize["method"] = o.Method + toSerialize["nodes"] = o.Nodes + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiContainer) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "method", + "nodes", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiContainer := _UiContainer{} + + err = json.Unmarshal(data, &varUiContainer) + + if err != nil { + return err + } + + *o = UiContainer(varUiContainer) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "messages") + delete(additionalProperties, "method") + delete(additionalProperties, "nodes") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiContainer struct { + value *UiContainer + isSet bool +} + +func (v NullableUiContainer) Get() *UiContainer { + return v.value +} + +func (v *NullableUiContainer) Set(val *UiContainer) { + v.value = val + v.isSet = true +} + +func (v NullableUiContainer) IsSet() bool { + return v.isSet +} + +func (v *NullableUiContainer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiContainer(val *UiContainer) *NullableUiContainer { + return &NullableUiContainer{value: val, isSet: true} +} + +func (v NullableUiContainer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiContainer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node.go b/pkg/client-go/model_ui_node.go new file mode 100644 index 0000000..ffa5f78 --- /dev/null +++ b/pkg/client-go/model_ui_node.go @@ -0,0 +1,285 @@ +/* +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" + "fmt" +) + +// checks if the UiNode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNode{} + +// 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 passkey PasskeyGroup identifier_first IdentifierFirstGroup captcha CaptchaGroup saml SAMLGroup deviceauthn DeviceAuthnGroup + Group string `json:"group"` + Messages []UiText `json:"messages"` + Meta UiNodeMeta `json:"meta"` + // The node's type text Text input Input img Image a Anchor script Script div Division + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiNode UiNode + +// NewUiNode instantiates a new UiNode 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 NewUiNode(attributes UiNodeAttributes, group string, messages []UiText, meta UiNodeMeta, type_ string) *UiNode { + this := UiNode{} + this.Attributes = attributes + this.Group = group + this.Messages = messages + this.Meta = meta + this.Type = type_ + return &this +} + +// NewUiNodeWithDefaults instantiates a new UiNode 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 NewUiNodeWithDefaults() *UiNode { + this := UiNode{} + return &this +} + +// GetAttributes returns the Attributes field value +func (o *UiNode) GetAttributes() UiNodeAttributes { + if o == nil { + var ret UiNodeAttributes + return ret + } + + return o.Attributes +} + +// GetAttributesOk returns a tuple with the Attributes field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetAttributesOk() (*UiNodeAttributes, bool) { + if o == nil { + return nil, false + } + return &o.Attributes, true +} + +// SetAttributes sets field value +func (o *UiNode) SetAttributes(v UiNodeAttributes) { + o.Attributes = v +} + +// GetGroup returns the Group field value +func (o *UiNode) GetGroup() string { + if o == nil { + var ret string + return ret + } + + return o.Group +} + +// GetGroupOk returns a tuple with the Group field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetGroupOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Group, true +} + +// SetGroup sets field value +func (o *UiNode) SetGroup(v string) { + o.Group = v +} + +// GetMessages returns the Messages field value +func (o *UiNode) GetMessages() []UiText { + if o == nil { + var ret []UiText + return ret + } + + return o.Messages +} + +// GetMessagesOk returns a tuple with the Messages field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetMessagesOk() ([]UiText, bool) { + if o == nil { + return nil, false + } + return o.Messages, true +} + +// SetMessages sets field value +func (o *UiNode) SetMessages(v []UiText) { + o.Messages = v +} + +// GetMeta returns the Meta field value +func (o *UiNode) GetMeta() UiNodeMeta { + if o == nil { + var ret UiNodeMeta + return ret + } + + return o.Meta +} + +// GetMetaOk returns a tuple with the Meta field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetMetaOk() (*UiNodeMeta, bool) { + if o == nil { + return nil, false + } + return &o.Meta, true +} + +// SetMeta sets field value +func (o *UiNode) SetMeta(v UiNodeMeta) { + o.Meta = v +} + +// GetType returns the Type field value +func (o *UiNode) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNode) SetType(v string) { + o.Type = v +} + +func (o UiNode) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["attributes"] = o.Attributes + toSerialize["group"] = o.Group + toSerialize["messages"] = o.Messages + toSerialize["meta"] = o.Meta + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNode) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "attributes", + "group", + "messages", + "meta", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNode := _UiNode{} + + err = json.Unmarshal(data, &varUiNode) + + if err != nil { + return err + } + + *o = UiNode(varUiNode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attributes") + delete(additionalProperties, "group") + delete(additionalProperties, "messages") + delete(additionalProperties, "meta") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNode struct { + value *UiNode + isSet bool +} + +func (v NullableUiNode) Get() *UiNode { + return v.value +} + +func (v *NullableUiNode) Set(val *UiNode) { + v.value = val + v.isSet = true +} + +func (v NullableUiNode) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNode(val *UiNode) *NullableUiNode { + return &NullableUiNode{value: val, isSet: true} +} + +func (v NullableUiNode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_anchor_attributes.go b/pkg/client-go/model_ui_node_anchor_attributes.go new file mode 100644 index 0000000..4b9da73 --- /dev/null +++ b/pkg/client-go/model_ui_node_anchor_attributes.go @@ -0,0 +1,257 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeAnchorAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeAnchorAttributes{} + +// UiNodeAnchorAttributes struct for UiNodeAnchorAttributes +type UiNodeAnchorAttributes struct { + // The link's href (destination) URL. format: uri + 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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + Title UiText `json:"title"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeAnchorAttributes UiNodeAnchorAttributes + +// NewUiNodeAnchorAttributes instantiates a new UiNodeAnchorAttributes 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 NewUiNodeAnchorAttributes(href string, id string, nodeType string, title UiText) *UiNodeAnchorAttributes { + this := UiNodeAnchorAttributes{} + this.Href = href + this.Id = id + this.NodeType = nodeType + this.Title = title + return &this +} + +// NewUiNodeAnchorAttributesWithDefaults instantiates a new UiNodeAnchorAttributes 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 NewUiNodeAnchorAttributesWithDefaults() *UiNodeAnchorAttributes { + this := UiNodeAnchorAttributes{} + return &this +} + +// GetHref returns the Href field value +func (o *UiNodeAnchorAttributes) GetHref() string { + if o == nil { + var ret string + return ret + } + + return o.Href +} + +// GetHrefOk returns a tuple with the Href field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetHrefOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Href, true +} + +// SetHref sets field value +func (o *UiNodeAnchorAttributes) SetHref(v string) { + o.Href = v +} + +// GetId returns the Id field value +func (o *UiNodeAnchorAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeAnchorAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeAnchorAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeAnchorAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetTitle returns the Title field value +func (o *UiNodeAnchorAttributes) GetTitle() UiText { + if o == nil { + var ret UiText + return ret + } + + return o.Title +} + +// GetTitleOk returns a tuple with the Title field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetTitleOk() (*UiText, bool) { + if o == nil { + return nil, false + } + return &o.Title, true +} + +// SetTitle sets field value +func (o *UiNodeAnchorAttributes) SetTitle(v UiText) { + o.Title = v +} + +func (o UiNodeAnchorAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeAnchorAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["href"] = o.Href + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["title"] = o.Title + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeAnchorAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "href", + "id", + "node_type", + "title", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeAnchorAttributes := _UiNodeAnchorAttributes{} + + err = json.Unmarshal(data, &varUiNodeAnchorAttributes) + + if err != nil { + return err + } + + *o = UiNodeAnchorAttributes(varUiNodeAnchorAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "href") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "title") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeAnchorAttributes struct { + value *UiNodeAnchorAttributes + isSet bool +} + +func (v NullableUiNodeAnchorAttributes) Get() *UiNodeAnchorAttributes { + return v.value +} + +func (v *NullableUiNodeAnchorAttributes) Set(val *UiNodeAnchorAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeAnchorAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeAnchorAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeAnchorAttributes(val *UiNodeAnchorAttributes) *NullableUiNodeAnchorAttributes { + return &NullableUiNodeAnchorAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeAnchorAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeAnchorAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_attributes.go b/pkg/client-go/model_ui_node_attributes.go new file mode 100644 index 0000000..4dfcdcc --- /dev/null +++ b/pkg/client-go/model_ui_node_attributes.go @@ -0,0 +1,354 @@ +/* +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" + "fmt" +) + +// UiNodeAttributes - struct for UiNodeAttributes +type UiNodeAttributes struct { + UiNodeAnchorAttributes *UiNodeAnchorAttributes + UiNodeDivisionAttributes *UiNodeDivisionAttributes + UiNodeImageAttributes *UiNodeImageAttributes + UiNodeInputAttributes *UiNodeInputAttributes + UiNodeScriptAttributes *UiNodeScriptAttributes + UiNodeTextAttributes *UiNodeTextAttributes +} + +// UiNodeAnchorAttributesAsUiNodeAttributes is a convenience function that returns UiNodeAnchorAttributes wrapped in UiNodeAttributes +func UiNodeAnchorAttributesAsUiNodeAttributes(v *UiNodeAnchorAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeAnchorAttributes: v, + } +} + +// UiNodeDivisionAttributesAsUiNodeAttributes is a convenience function that returns UiNodeDivisionAttributes wrapped in UiNodeAttributes +func UiNodeDivisionAttributesAsUiNodeAttributes(v *UiNodeDivisionAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeDivisionAttributes: v, + } +} + +// UiNodeImageAttributesAsUiNodeAttributes is a convenience function that returns UiNodeImageAttributes wrapped in UiNodeAttributes +func UiNodeImageAttributesAsUiNodeAttributes(v *UiNodeImageAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeImageAttributes: v, + } +} + +// UiNodeInputAttributesAsUiNodeAttributes is a convenience function that returns UiNodeInputAttributes wrapped in UiNodeAttributes +func UiNodeInputAttributesAsUiNodeAttributes(v *UiNodeInputAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeInputAttributes: v, + } +} + +// UiNodeScriptAttributesAsUiNodeAttributes is a convenience function that returns UiNodeScriptAttributes wrapped in UiNodeAttributes +func UiNodeScriptAttributesAsUiNodeAttributes(v *UiNodeScriptAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeScriptAttributes: v, + } +} + +// UiNodeTextAttributesAsUiNodeAttributes is a convenience function that returns UiNodeTextAttributes wrapped in UiNodeAttributes +func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeTextAttributes: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'div' + if jsonDict["node_type"] == "div" { + // try to unmarshal JSON data into UiNodeDivisionAttributes + err = json.Unmarshal(data, &dst.UiNodeDivisionAttributes) + if err == nil { + return nil // data stored in dst.UiNodeDivisionAttributes, return on the first match + } else { + dst.UiNodeDivisionAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeDivisionAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeImageAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // 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 'uiNodeDivisionAttributes' + if jsonDict["node_type"] == "uiNodeDivisionAttributes" { + // try to unmarshal JSON data into UiNodeDivisionAttributes + err = json.Unmarshal(data, &dst.UiNodeDivisionAttributes) + if err == nil { + return nil // data stored in dst.UiNodeDivisionAttributes, return on the first match + } else { + dst.UiNodeDivisionAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeDivisionAttributes: %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 { + dst.UiNodeImageAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UiNodeAttributes) MarshalJSON() ([]byte, error) { + if src.UiNodeAnchorAttributes != nil { + return json.Marshal(&src.UiNodeAnchorAttributes) + } + + if src.UiNodeDivisionAttributes != nil { + return json.Marshal(&src.UiNodeDivisionAttributes) + } + + if src.UiNodeImageAttributes != nil { + return json.Marshal(&src.UiNodeImageAttributes) + } + + if src.UiNodeInputAttributes != nil { + return json.Marshal(&src.UiNodeInputAttributes) + } + + if src.UiNodeScriptAttributes != nil { + return json.Marshal(&src.UiNodeScriptAttributes) + } + + if src.UiNodeTextAttributes != nil { + return json.Marshal(&src.UiNodeTextAttributes) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UiNodeAttributes) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UiNodeAnchorAttributes != nil { + return obj.UiNodeAnchorAttributes + } + + if obj.UiNodeDivisionAttributes != nil { + return obj.UiNodeDivisionAttributes + } + + if obj.UiNodeImageAttributes != nil { + return obj.UiNodeImageAttributes + } + + if obj.UiNodeInputAttributes != nil { + return obj.UiNodeInputAttributes + } + + if obj.UiNodeScriptAttributes != nil { + return obj.UiNodeScriptAttributes + } + + if obj.UiNodeTextAttributes != nil { + return obj.UiNodeTextAttributes + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UiNodeAttributes) GetActualInstanceValue() interface{} { + if obj.UiNodeAnchorAttributes != nil { + return *obj.UiNodeAnchorAttributes + } + + if obj.UiNodeDivisionAttributes != nil { + return *obj.UiNodeDivisionAttributes + } + + if obj.UiNodeImageAttributes != nil { + return *obj.UiNodeImageAttributes + } + + if obj.UiNodeInputAttributes != nil { + return *obj.UiNodeInputAttributes + } + + if obj.UiNodeScriptAttributes != nil { + return *obj.UiNodeScriptAttributes + } + + if obj.UiNodeTextAttributes != nil { + return *obj.UiNodeTextAttributes + } + + // all schemas are nil + return nil +} + +type NullableUiNodeAttributes struct { + value *UiNodeAttributes + isSet bool +} + +func (v NullableUiNodeAttributes) Get() *UiNodeAttributes { + return v.value +} + +func (v *NullableUiNodeAttributes) Set(val *UiNodeAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeAttributes(val *UiNodeAttributes) *NullableUiNodeAttributes { + return &NullableUiNodeAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_division_attributes.go b/pkg/client-go/model_ui_node_division_attributes.go new file mode 100644 index 0000000..4b84309 --- /dev/null +++ b/pkg/client-go/model_ui_node_division_attributes.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeDivisionAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeDivisionAttributes{} + +// UiNodeDivisionAttributes Division sections are used for interactive widgets that require a hook in the DOM / view. +type UiNodeDivisionAttributes struct { + // A classname that should be rendered into the DOM. + Class *string `json:"class,omitempty"` + // Data is a map of key-value pairs that are passed to the division. They may be used for `data-...` attributes. + Data *map[string]string `json:"data,omitempty"` + // A unique identifier + Id string `json:"id"` + // NodeType represents this node's type. 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 div Division + NodeType string `json:"node_type"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeDivisionAttributes UiNodeDivisionAttributes + +// NewUiNodeDivisionAttributes instantiates a new UiNodeDivisionAttributes 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 NewUiNodeDivisionAttributes(id string, nodeType string) *UiNodeDivisionAttributes { + this := UiNodeDivisionAttributes{} + this.Id = id + this.NodeType = nodeType + return &this +} + +// NewUiNodeDivisionAttributesWithDefaults instantiates a new UiNodeDivisionAttributes 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 NewUiNodeDivisionAttributesWithDefaults() *UiNodeDivisionAttributes { + this := UiNodeDivisionAttributes{} + return &this +} + +// GetClass returns the Class field value if set, zero value otherwise. +func (o *UiNodeDivisionAttributes) GetClass() string { + if o == nil || IsNil(o.Class) { + var ret string + return ret + } + return *o.Class +} + +// GetClassOk returns a tuple with the Class field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetClassOk() (*string, bool) { + if o == nil || IsNil(o.Class) { + return nil, false + } + return o.Class, true +} + +// HasClass returns a boolean if a field has been set. +func (o *UiNodeDivisionAttributes) HasClass() bool { + if o != nil && !IsNil(o.Class) { + return true + } + + return false +} + +// SetClass gets a reference to the given string and assigns it to the Class field. +func (o *UiNodeDivisionAttributes) SetClass(v string) { + o.Class = &v +} + +// GetData returns the Data field value if set, zero value otherwise. +func (o *UiNodeDivisionAttributes) GetData() map[string]string { + if o == nil || IsNil(o.Data) { + var ret map[string]string + return ret + } + return *o.Data +} + +// GetDataOk returns a tuple with the Data field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetDataOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Data) { + return nil, false + } + return o.Data, true +} + +// HasData returns a boolean if a field has been set. +func (o *UiNodeDivisionAttributes) HasData() bool { + if o != nil && !IsNil(o.Data) { + return true + } + + return false +} + +// SetData gets a reference to the given map[string]string and assigns it to the Data field. +func (o *UiNodeDivisionAttributes) SetData(v map[string]string) { + o.Data = &v +} + +// GetId returns the Id field value +func (o *UiNodeDivisionAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeDivisionAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeDivisionAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeDivisionAttributes) SetNodeType(v string) { + o.NodeType = v +} + +func (o UiNodeDivisionAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeDivisionAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Class) { + toSerialize["class"] = o.Class + } + if !IsNil(o.Data) { + toSerialize["data"] = o.Data + } + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeDivisionAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "node_type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeDivisionAttributes := _UiNodeDivisionAttributes{} + + err = json.Unmarshal(data, &varUiNodeDivisionAttributes) + + if err != nil { + return err + } + + *o = UiNodeDivisionAttributes(varUiNodeDivisionAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "class") + delete(additionalProperties, "data") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeDivisionAttributes struct { + value *UiNodeDivisionAttributes + isSet bool +} + +func (v NullableUiNodeDivisionAttributes) Get() *UiNodeDivisionAttributes { + return v.value +} + +func (v *NullableUiNodeDivisionAttributes) Set(val *UiNodeDivisionAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeDivisionAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeDivisionAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeDivisionAttributes(val *UiNodeDivisionAttributes) *NullableUiNodeDivisionAttributes { + return &NullableUiNodeDivisionAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeDivisionAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeDivisionAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_image_attributes.go b/pkg/client-go/model_ui_node_image_attributes.go new file mode 100644 index 0000000..604d823 --- /dev/null +++ b/pkg/client-go/model_ui_node_image_attributes.go @@ -0,0 +1,288 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeImageAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeImageAttributes{} + +// UiNodeImageAttributes struct for UiNodeImageAttributes +type UiNodeImageAttributes struct { + // Height of the image + 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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + // The image's source URL. format: uri + Src string `json:"src"` + // Width of the image + Width int64 `json:"width"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeImageAttributes UiNodeImageAttributes + +// NewUiNodeImageAttributes instantiates a new UiNodeImageAttributes 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 NewUiNodeImageAttributes(height int64, id string, nodeType string, src string, width int64) *UiNodeImageAttributes { + this := UiNodeImageAttributes{} + this.Height = height + this.Id = id + this.NodeType = nodeType + this.Src = src + this.Width = width + return &this +} + +// NewUiNodeImageAttributesWithDefaults instantiates a new UiNodeImageAttributes 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 NewUiNodeImageAttributesWithDefaults() *UiNodeImageAttributes { + this := UiNodeImageAttributes{} + return &this +} + +// GetHeight returns the Height field value +func (o *UiNodeImageAttributes) GetHeight() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Height +} + +// GetHeightOk returns a tuple with the Height field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetHeightOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Height, true +} + +// SetHeight sets field value +func (o *UiNodeImageAttributes) SetHeight(v int64) { + o.Height = v +} + +// GetId returns the Id field value +func (o *UiNodeImageAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeImageAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeImageAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeImageAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetSrc returns the Src field value +func (o *UiNodeImageAttributes) GetSrc() string { + if o == nil { + var ret string + return ret + } + + return o.Src +} + +// GetSrcOk returns a tuple with the Src field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetSrcOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Src, true +} + +// SetSrc sets field value +func (o *UiNodeImageAttributes) SetSrc(v string) { + o.Src = v +} + +// GetWidth returns the Width field value +func (o *UiNodeImageAttributes) GetWidth() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Width +} + +// GetWidthOk returns a tuple with the Width field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetWidthOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Width, true +} + +// SetWidth sets field value +func (o *UiNodeImageAttributes) SetWidth(v int64) { + o.Width = v +} + +func (o UiNodeImageAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeImageAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["height"] = o.Height + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["src"] = o.Src + toSerialize["width"] = o.Width + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeImageAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "height", + "id", + "node_type", + "src", + "width", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeImageAttributes := _UiNodeImageAttributes{} + + err = json.Unmarshal(data, &varUiNodeImageAttributes) + + if err != nil { + return err + } + + *o = UiNodeImageAttributes(varUiNodeImageAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "height") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "src") + delete(additionalProperties, "width") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeImageAttributes struct { + value *UiNodeImageAttributes + isSet bool +} + +func (v NullableUiNodeImageAttributes) Get() *UiNodeImageAttributes { + return v.value +} + +func (v *NullableUiNodeImageAttributes) Set(val *UiNodeImageAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeImageAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeImageAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeImageAttributes(val *UiNodeImageAttributes) *NullableUiNodeImageAttributes { + return &NullableUiNodeImageAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeImageAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeImageAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_input_attributes.go b/pkg/client-go/model_ui_node_input_attributes.go new file mode 100644 index 0000000..f329565 --- /dev/null +++ b/pkg/client-go/model_ui_node_input_attributes.go @@ -0,0 +1,676 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeInputAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeInputAttributes{} + +// UiNodeInputAttributes InputAttributes represents the attributes of an input node +type UiNodeInputAttributes struct { + // The autocomplete attribute for the input. email InputAttributeAutocompleteEmail tel InputAttributeAutocompleteTel url InputAttributeAutocompleteUrl current-password InputAttributeAutocompleteCurrentPassword new-password InputAttributeAutocompleteNewPassword one-time-code InputAttributeAutocompleteOneTimeCode username InputAttributeAutocompleteUsername username webauthn InputAttributeAutocompleteUsernameWebauthn + Autocomplete *string `json:"autocomplete,omitempty"` + // Sets the input's disabled field to true or false. + Disabled bool `json:"disabled"` + Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + // 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 allowed values for the input when the underlying JSON schema defines an `enum`. When present, clients should render the field as a select/dropdown rather than a free-form text input. When absent, clients continue to render a plain text input, so this field is backward compatible with UIs that do not look at it. + Options []UiNodeInputAttributesOption `json:"options,omitempty"` + // The input's pattern. + Pattern *string `json:"pattern,omitempty"` + // Mark this input field as required. + Required *bool `json:"required,omitempty"` + // The input's element type. text InputAttributeTypeText password InputAttributeTypePassword number InputAttributeTypeNumber checkbox InputAttributeTypeCheckbox hidden InputAttributeTypeHidden email InputAttributeTypeEmail tel InputAttributeTypeTel submit InputAttributeTypeSubmit button InputAttributeTypeButton datetime-local InputAttributeTypeDateTimeLocal date InputAttributeTypeDate url InputAttributeTypeURI + Type string `json:"type"` + // The input's value. + Value interface{} `json:"value,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeInputAttributes UiNodeInputAttributes + +// NewUiNodeInputAttributes instantiates a new UiNodeInputAttributes 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 NewUiNodeInputAttributes(disabled bool, name string, nodeType string, type_ string) *UiNodeInputAttributes { + this := UiNodeInputAttributes{} + this.Disabled = disabled + this.Name = name + this.NodeType = nodeType + this.Type = type_ + return &this +} + +// NewUiNodeInputAttributesWithDefaults instantiates a new UiNodeInputAttributes 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 NewUiNodeInputAttributesWithDefaults() *UiNodeInputAttributes { + this := UiNodeInputAttributes{} + return &this +} + +// GetAutocomplete returns the Autocomplete field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetAutocomplete() string { + if o == nil || IsNil(o.Autocomplete) { + var ret string + return ret + } + return *o.Autocomplete +} + +// GetAutocompleteOk returns a tuple with the Autocomplete field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetAutocompleteOk() (*string, bool) { + if o == nil || IsNil(o.Autocomplete) { + return nil, false + } + return o.Autocomplete, true +} + +// HasAutocomplete returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasAutocomplete() bool { + if o != nil && !IsNil(o.Autocomplete) { + return true + } + + return false +} + +// SetAutocomplete gets a reference to the given string and assigns it to the Autocomplete field. +func (o *UiNodeInputAttributes) SetAutocomplete(v string) { + o.Autocomplete = &v +} + +// GetDisabled returns the Disabled field value +func (o *UiNodeInputAttributes) GetDisabled() bool { + if o == nil { + var ret bool + return ret + } + + return o.Disabled +} + +// GetDisabledOk returns a tuple with the Disabled field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetDisabledOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Disabled, true +} + +// SetDisabled sets field value +func (o *UiNodeInputAttributes) SetDisabled(v bool) { + o.Disabled = v +} + +// GetLabel returns the Label field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetLabel() UiText { + if o == nil || IsNil(o.Label) { + var ret UiText + return ret + } + return *o.Label +} + +// GetLabelOk returns a tuple with the Label field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetLabelOk() (*UiText, bool) { + if o == nil || IsNil(o.Label) { + return nil, false + } + return o.Label, true +} + +// HasLabel returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasLabel() bool { + if o != nil && !IsNil(o.Label) { + return true + } + + return false +} + +// SetLabel gets a reference to the given UiText and assigns it to the Label field. +func (o *UiNodeInputAttributes) SetLabel(v UiText) { + o.Label = &v +} + +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || IsNil(o.Maxlength) { + var ret int64 + return ret + } + return *o.Maxlength +} + +// 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() (*int64, bool) { + if o == nil || IsNil(o.Maxlength) { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && !IsNil(o.Maxlength) { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + +// GetName returns the Name field value +func (o *UiNodeInputAttributes) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *UiNodeInputAttributes) SetName(v string) { + o.Name = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeInputAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeInputAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetOnclick returns the Onclick field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclick() string { + if o == nil || IsNil(o.Onclick) { + var ret string + return ret + } + return *o.Onclick +} + +// GetOnclickOk returns a tuple with the Onclick field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickOk() (*string, bool) { + if o == nil || IsNil(o.Onclick) { + return nil, false + } + return o.Onclick, true +} + +// HasOnclick returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclick() bool { + if o != nil && !IsNil(o.Onclick) { + return true + } + + return false +} + +// SetOnclick gets a reference to the given string and assigns it to the Onclick field. +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 || IsNil(o.OnclickTrigger) { + 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 || IsNil(o.OnclickTrigger) { + 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 && !IsNil(o.OnclickTrigger) { + 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 || IsNil(o.Onload) { + 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 || IsNil(o.Onload) { + 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 && !IsNil(o.Onload) { + 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 || IsNil(o.OnloadTrigger) { + 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 || IsNil(o.OnloadTrigger) { + 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 && !IsNil(o.OnloadTrigger) { + 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 +} + +// GetOptions returns the Options field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOptions() []UiNodeInputAttributesOption { + if o == nil || IsNil(o.Options) { + var ret []UiNodeInputAttributesOption + return ret + } + return o.Options +} + +// GetOptionsOk returns a tuple with the Options field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOptionsOk() ([]UiNodeInputAttributesOption, bool) { + if o == nil || IsNil(o.Options) { + return nil, false + } + return o.Options, true +} + +// HasOptions returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOptions() bool { + if o != nil && !IsNil(o.Options) { + return true + } + + return false +} + +// SetOptions gets a reference to the given []UiNodeInputAttributesOption and assigns it to the Options field. +func (o *UiNodeInputAttributes) SetOptions(v []UiNodeInputAttributesOption) { + o.Options = v +} + +// GetPattern returns the Pattern field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetPattern() string { + if o == nil || IsNil(o.Pattern) { + var ret string + return ret + } + return *o.Pattern +} + +// GetPatternOk returns a tuple with the Pattern field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetPatternOk() (*string, bool) { + if o == nil || IsNil(o.Pattern) { + return nil, false + } + return o.Pattern, true +} + +// HasPattern returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasPattern() bool { + if o != nil && !IsNil(o.Pattern) { + return true + } + + return false +} + +// SetPattern gets a reference to the given string and assigns it to the Pattern field. +func (o *UiNodeInputAttributes) SetPattern(v string) { + o.Pattern = &v +} + +// GetRequired returns the Required field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetRequired() bool { + if o == nil || IsNil(o.Required) { + var ret bool + return ret + } + return *o.Required +} + +// GetRequiredOk returns a tuple with the Required field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.Required) { + return nil, false + } + return o.Required, true +} + +// HasRequired returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasRequired() bool { + if o != nil && !IsNil(o.Required) { + return true + } + + return false +} + +// SetRequired gets a reference to the given bool and assigns it to the Required field. +func (o *UiNodeInputAttributes) SetRequired(v bool) { + o.Required = &v +} + +// GetType returns the Type field value +func (o *UiNodeInputAttributes) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNodeInputAttributes) SetType(v string) { + o.Type = v +} + +// GetValue returns the Value field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UiNodeInputAttributes) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value 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 *UiNodeInputAttributes) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given interface{} and assigns it to the Value field. +func (o *UiNodeInputAttributes) SetValue(v interface{}) { + o.Value = v +} + +func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeInputAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Autocomplete) { + toSerialize["autocomplete"] = o.Autocomplete + } + toSerialize["disabled"] = o.Disabled + if !IsNil(o.Label) { + toSerialize["label"] = o.Label + } + if !IsNil(o.Maxlength) { + toSerialize["maxlength"] = o.Maxlength + } + toSerialize["name"] = o.Name + toSerialize["node_type"] = o.NodeType + if !IsNil(o.Onclick) { + toSerialize["onclick"] = o.Onclick + } + if !IsNil(o.OnclickTrigger) { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } + if !IsNil(o.Onload) { + toSerialize["onload"] = o.Onload + } + if !IsNil(o.OnloadTrigger) { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } + if !IsNil(o.Options) { + toSerialize["options"] = o.Options + } + if !IsNil(o.Pattern) { + toSerialize["pattern"] = o.Pattern + } + if !IsNil(o.Required) { + toSerialize["required"] = o.Required + } + toSerialize["type"] = o.Type + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeInputAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "disabled", + "name", + "node_type", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeInputAttributes := _UiNodeInputAttributes{} + + err = json.Unmarshal(data, &varUiNodeInputAttributes) + + if err != nil { + return err + } + + *o = UiNodeInputAttributes(varUiNodeInputAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "autocomplete") + delete(additionalProperties, "disabled") + delete(additionalProperties, "label") + delete(additionalProperties, "maxlength") + delete(additionalProperties, "name") + delete(additionalProperties, "node_type") + delete(additionalProperties, "onclick") + delete(additionalProperties, "onclickTrigger") + delete(additionalProperties, "onload") + delete(additionalProperties, "onloadTrigger") + delete(additionalProperties, "options") + delete(additionalProperties, "pattern") + delete(additionalProperties, "required") + delete(additionalProperties, "type") + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeInputAttributes struct { + value *UiNodeInputAttributes + isSet bool +} + +func (v NullableUiNodeInputAttributes) Get() *UiNodeInputAttributes { + return v.value +} + +func (v *NullableUiNodeInputAttributes) Set(val *UiNodeInputAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeInputAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeInputAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeInputAttributes(val *UiNodeInputAttributes) *NullableUiNodeInputAttributes { + return &NullableUiNodeInputAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeInputAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeInputAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_input_attributes_option.go b/pkg/client-go/model_ui_node_input_attributes_option.go new file mode 100644 index 0000000..6de8af3 --- /dev/null +++ b/pkg/client-go/model_ui_node_input_attributes_option.go @@ -0,0 +1,172 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeInputAttributesOption type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeInputAttributesOption{} + +// UiNodeInputAttributesOption Represents a single selectable value for an input whose JSON schema defined an `enum`. The value is always a scalar JSON type (string, number, or boolean) serialized verbatim from the schema. When present, clients should render the parent input as a select/dropdown. +type UiNodeInputAttributesOption struct { + // The value that will be submitted when this option is picked. It is serialized verbatim from the JSON schema `enum` entry, so it is always a scalar JSON value (string, number, or boolean). + Value interface{} `json:"value"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeInputAttributesOption UiNodeInputAttributesOption + +// NewUiNodeInputAttributesOption instantiates a new UiNodeInputAttributesOption 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 NewUiNodeInputAttributesOption(value interface{}) *UiNodeInputAttributesOption { + this := UiNodeInputAttributesOption{} + this.Value = value + return &this +} + +// NewUiNodeInputAttributesOptionWithDefaults instantiates a new UiNodeInputAttributesOption 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 NewUiNodeInputAttributesOptionWithDefaults() *UiNodeInputAttributesOption { + this := UiNodeInputAttributesOption{} + return &this +} + +// GetValue returns the Value field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *UiNodeInputAttributesOption) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// 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 *UiNodeInputAttributesOption) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *UiNodeInputAttributesOption) SetValue(v interface{}) { + o.Value = v +} + +func (o UiNodeInputAttributesOption) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeInputAttributesOption) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeInputAttributesOption) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "value", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeInputAttributesOption := _UiNodeInputAttributesOption{} + + err = json.Unmarshal(data, &varUiNodeInputAttributesOption) + + if err != nil { + return err + } + + *o = UiNodeInputAttributesOption(varUiNodeInputAttributesOption) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeInputAttributesOption struct { + value *UiNodeInputAttributesOption + isSet bool +} + +func (v NullableUiNodeInputAttributesOption) Get() *UiNodeInputAttributesOption { + return v.value +} + +func (v *NullableUiNodeInputAttributesOption) Set(val *UiNodeInputAttributesOption) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeInputAttributesOption) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeInputAttributesOption) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeInputAttributesOption(val *UiNodeInputAttributesOption) *NullableUiNodeInputAttributesOption { + return &NullableUiNodeInputAttributesOption{value: val, isSet: true} +} + +func (v NullableUiNodeInputAttributesOption) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeInputAttributesOption) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_meta.go b/pkg/client-go/model_ui_node_meta.go new file mode 100644 index 0000000..80b52f0 --- /dev/null +++ b/pkg/client-go/model_ui_node_meta.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the UiNodeMeta type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeMeta{} + +// UiNodeMeta This might include a label and other information that can optionally be used to render UIs. +type UiNodeMeta struct { + Label *UiText `json:"label,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeMeta UiNodeMeta + +// NewUiNodeMeta instantiates a new UiNodeMeta 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 NewUiNodeMeta() *UiNodeMeta { + this := UiNodeMeta{} + return &this +} + +// NewUiNodeMetaWithDefaults instantiates a new UiNodeMeta 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 NewUiNodeMetaWithDefaults() *UiNodeMeta { + this := UiNodeMeta{} + return &this +} + +// GetLabel returns the Label field value if set, zero value otherwise. +func (o *UiNodeMeta) GetLabel() UiText { + if o == nil || IsNil(o.Label) { + var ret UiText + return ret + } + return *o.Label +} + +// GetLabelOk returns a tuple with the Label field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeMeta) GetLabelOk() (*UiText, bool) { + if o == nil || IsNil(o.Label) { + return nil, false + } + return o.Label, true +} + +// HasLabel returns a boolean if a field has been set. +func (o *UiNodeMeta) HasLabel() bool { + if o != nil && !IsNil(o.Label) { + return true + } + + return false +} + +// SetLabel gets a reference to the given UiText and assigns it to the Label field. +func (o *UiNodeMeta) SetLabel(v UiText) { + o.Label = &v +} + +func (o UiNodeMeta) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeMeta) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Label) { + toSerialize["label"] = o.Label + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeMeta) UnmarshalJSON(data []byte) (err error) { + varUiNodeMeta := _UiNodeMeta{} + + err = json.Unmarshal(data, &varUiNodeMeta) + + if err != nil { + return err + } + + *o = UiNodeMeta(varUiNodeMeta) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "label") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeMeta struct { + value *UiNodeMeta + isSet bool +} + +func (v NullableUiNodeMeta) Get() *UiNodeMeta { + return v.value +} + +func (v *NullableUiNodeMeta) Set(val *UiNodeMeta) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeMeta) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeMeta) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeMeta(val *UiNodeMeta) *NullableUiNodeMeta { + return &NullableUiNodeMeta{value: val, isSet: true} +} + +func (v NullableUiNodeMeta) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeMeta) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_script_attributes.go b/pkg/client-go/model_ui_node_script_attributes.go new file mode 100644 index 0000000..22b0765 --- /dev/null +++ b/pkg/client-go/model_ui_node_script_attributes.go @@ -0,0 +1,408 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeScriptAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeScriptAttributes{} + +// UiNodeScriptAttributes struct for UiNodeScriptAttributes +type UiNodeScriptAttributes struct { + // The script async type + Async bool `json:"async"` + // The script cross origin policy + Crossorigin string `json:"crossorigin"` + // A unique identifier + 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\". text Text input Input img Image a Anchor script Script div Division + 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"` + // The script referrer policy + Referrerpolicy string `json:"referrerpolicy"` + // The script source + Src string `json:"src"` + // The script MIME type + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeScriptAttributes UiNodeScriptAttributes + +// NewUiNodeScriptAttributes instantiates a new UiNodeScriptAttributes 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 NewUiNodeScriptAttributes(async bool, crossorigin string, id string, integrity string, nodeType string, nonce string, referrerpolicy string, src string, type_ string) *UiNodeScriptAttributes { + this := UiNodeScriptAttributes{} + this.Async = async + this.Crossorigin = crossorigin + this.Id = id + this.Integrity = integrity + this.NodeType = nodeType + this.Nonce = nonce + this.Referrerpolicy = referrerpolicy + this.Src = src + this.Type = type_ + return &this +} + +// NewUiNodeScriptAttributesWithDefaults instantiates a new UiNodeScriptAttributes 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 NewUiNodeScriptAttributesWithDefaults() *UiNodeScriptAttributes { + this := UiNodeScriptAttributes{} + return &this +} + +// GetAsync returns the Async field value +func (o *UiNodeScriptAttributes) GetAsync() bool { + if o == nil { + var ret bool + return ret + } + + return o.Async +} + +// GetAsyncOk returns a tuple with the Async field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetAsyncOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Async, true +} + +// SetAsync sets field value +func (o *UiNodeScriptAttributes) SetAsync(v bool) { + o.Async = v +} + +// GetCrossorigin returns the Crossorigin field value +func (o *UiNodeScriptAttributes) GetCrossorigin() string { + if o == nil { + var ret string + return ret + } + + return o.Crossorigin +} + +// GetCrossoriginOk returns a tuple with the Crossorigin field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetCrossoriginOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Crossorigin, true +} + +// SetCrossorigin sets field value +func (o *UiNodeScriptAttributes) SetCrossorigin(v string) { + o.Crossorigin = v +} + +// GetId returns the Id field value +func (o *UiNodeScriptAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeScriptAttributes) SetId(v string) { + o.Id = v +} + +// GetIntegrity returns the Integrity field value +func (o *UiNodeScriptAttributes) GetIntegrity() string { + if o == nil { + var ret string + return ret + } + + return o.Integrity +} + +// GetIntegrityOk returns a tuple with the Integrity field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetIntegrityOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Integrity, true +} + +// SetIntegrity sets field value +func (o *UiNodeScriptAttributes) SetIntegrity(v string) { + o.Integrity = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeScriptAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeScriptAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetNonce returns the Nonce field value +func (o *UiNodeScriptAttributes) GetNonce() string { + if o == nil { + var ret string + return ret + } + + return o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetNonceOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Nonce, true +} + +// SetNonce sets field value +func (o *UiNodeScriptAttributes) SetNonce(v string) { + o.Nonce = v +} + +// GetReferrerpolicy returns the Referrerpolicy field value +func (o *UiNodeScriptAttributes) GetReferrerpolicy() string { + if o == nil { + var ret string + return ret + } + + return o.Referrerpolicy +} + +// GetReferrerpolicyOk returns a tuple with the Referrerpolicy field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetReferrerpolicyOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Referrerpolicy, true +} + +// SetReferrerpolicy sets field value +func (o *UiNodeScriptAttributes) SetReferrerpolicy(v string) { + o.Referrerpolicy = v +} + +// GetSrc returns the Src field value +func (o *UiNodeScriptAttributes) GetSrc() string { + if o == nil { + var ret string + return ret + } + + return o.Src +} + +// GetSrcOk returns a tuple with the Src field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetSrcOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Src, true +} + +// SetSrc sets field value +func (o *UiNodeScriptAttributes) SetSrc(v string) { + o.Src = v +} + +// GetType returns the Type field value +func (o *UiNodeScriptAttributes) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNodeScriptAttributes) SetType(v string) { + o.Type = v +} + +func (o UiNodeScriptAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeScriptAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["async"] = o.Async + toSerialize["crossorigin"] = o.Crossorigin + toSerialize["id"] = o.Id + toSerialize["integrity"] = o.Integrity + toSerialize["node_type"] = o.NodeType + toSerialize["nonce"] = o.Nonce + toSerialize["referrerpolicy"] = o.Referrerpolicy + toSerialize["src"] = o.Src + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeScriptAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "async", + "crossorigin", + "id", + "integrity", + "node_type", + "nonce", + "referrerpolicy", + "src", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeScriptAttributes := _UiNodeScriptAttributes{} + + err = json.Unmarshal(data, &varUiNodeScriptAttributes) + + if err != nil { + return err + } + + *o = UiNodeScriptAttributes(varUiNodeScriptAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "async") + delete(additionalProperties, "crossorigin") + delete(additionalProperties, "id") + delete(additionalProperties, "integrity") + delete(additionalProperties, "node_type") + delete(additionalProperties, "nonce") + delete(additionalProperties, "referrerpolicy") + delete(additionalProperties, "src") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeScriptAttributes struct { + value *UiNodeScriptAttributes + isSet bool +} + +func (v NullableUiNodeScriptAttributes) Get() *UiNodeScriptAttributes { + return v.value +} + +func (v *NullableUiNodeScriptAttributes) Set(val *UiNodeScriptAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeScriptAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeScriptAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeScriptAttributes(val *UiNodeScriptAttributes) *NullableUiNodeScriptAttributes { + return &NullableUiNodeScriptAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeScriptAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeScriptAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_node_text_attributes.go b/pkg/client-go/model_ui_node_text_attributes.go new file mode 100644 index 0000000..6c7c3dc --- /dev/null +++ b/pkg/client-go/model_ui_node_text_attributes.go @@ -0,0 +1,227 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeTextAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeTextAttributes{} + +// UiNodeTextAttributes struct for UiNodeTextAttributes +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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + Text UiText `json:"text"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeTextAttributes UiNodeTextAttributes + +// NewUiNodeTextAttributes instantiates a new UiNodeTextAttributes 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 NewUiNodeTextAttributes(id string, nodeType string, text UiText) *UiNodeTextAttributes { + this := UiNodeTextAttributes{} + this.Id = id + this.NodeType = nodeType + this.Text = text + return &this +} + +// NewUiNodeTextAttributesWithDefaults instantiates a new UiNodeTextAttributes 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 NewUiNodeTextAttributesWithDefaults() *UiNodeTextAttributes { + this := UiNodeTextAttributes{} + return &this +} + +// GetId returns the Id field value +func (o *UiNodeTextAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeTextAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeTextAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeTextAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetText returns the Text field value +func (o *UiNodeTextAttributes) GetText() UiText { + if o == nil { + var ret UiText + return ret + } + + return o.Text +} + +// GetTextOk returns a tuple with the Text field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetTextOk() (*UiText, bool) { + if o == nil { + return nil, false + } + return &o.Text, true +} + +// SetText sets field value +func (o *UiNodeTextAttributes) SetText(v UiText) { + o.Text = v +} + +func (o UiNodeTextAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeTextAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["text"] = o.Text + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeTextAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "node_type", + "text", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeTextAttributes := _UiNodeTextAttributes{} + + err = json.Unmarshal(data, &varUiNodeTextAttributes) + + if err != nil { + return err + } + + *o = UiNodeTextAttributes(varUiNodeTextAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "text") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeTextAttributes struct { + value *UiNodeTextAttributes + isSet bool +} + +func (v NullableUiNodeTextAttributes) Get() *UiNodeTextAttributes { + return v.value +} + +func (v *NullableUiNodeTextAttributes) Set(val *UiNodeTextAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeTextAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeTextAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeTextAttributes(val *UiNodeTextAttributes) *NullableUiNodeTextAttributes { + return &NullableUiNodeTextAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeTextAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeTextAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_ui_text.go b/pkg/client-go/model_ui_text.go new file mode 100644 index 0000000..e4c93b5 --- /dev/null +++ b/pkg/client-go/model_ui_text.go @@ -0,0 +1,265 @@ +/* +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" + "fmt" +) + +// checks if the UiText type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiText{} + +// UiText struct for UiText +type UiText struct { + // The message's context. Useful when customizing messages. + Context map[string]interface{} `json:"context,omitempty"` + Id int64 `json:"id"` + // The message text. Written in american english. + Text string `json:"text"` + // The message type. info Info error Error success Success + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiText UiText + +// NewUiText instantiates a new UiText 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 NewUiText(id int64, text string, type_ string) *UiText { + this := UiText{} + this.Id = id + this.Text = text + this.Type = type_ + return &this +} + +// NewUiTextWithDefaults instantiates a new UiText 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 NewUiTextWithDefaults() *UiText { + this := UiText{} + return &this +} + +// GetContext returns the Context field value if set, zero value otherwise. +func (o *UiText) GetContext() map[string]interface{} { + if o == nil || IsNil(o.Context) { + var ret map[string]interface{} + return ret + } + return o.Context +} + +// GetContextOk returns a tuple with the Context field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiText) GetContextOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Context) { + return map[string]interface{}{}, false + } + return o.Context, true +} + +// HasContext returns a boolean if a field has been set. +func (o *UiText) HasContext() bool { + if o != nil && !IsNil(o.Context) { + return true + } + + return false +} + +// SetContext gets a reference to the given map[string]interface{} and assigns it to the Context field. +func (o *UiText) SetContext(v map[string]interface{}) { + o.Context = v +} + +// GetId returns the Id field value +func (o *UiText) GetId() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiText) GetIdOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiText) SetId(v int64) { + o.Id = v +} + +// GetText returns the Text field value +func (o *UiText) GetText() string { + if o == nil { + var ret string + return ret + } + + return o.Text +} + +// GetTextOk returns a tuple with the Text field value +// and a boolean to check if the value has been set. +func (o *UiText) GetTextOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Text, true +} + +// SetText sets field value +func (o *UiText) SetText(v string) { + o.Text = v +} + +// GetType returns the Type field value +func (o *UiText) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiText) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiText) SetType(v string) { + o.Type = v +} + +func (o UiText) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiText) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Context) { + toSerialize["context"] = o.Context + } + toSerialize["id"] = o.Id + toSerialize["text"] = o.Text + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiText) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "text", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiText := _UiText{} + + err = json.Unmarshal(data, &varUiText) + + if err != nil { + return err + } + + *o = UiText(varUiText) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "context") + delete(additionalProperties, "id") + delete(additionalProperties, "text") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiText struct { + value *UiText + isSet bool +} + +func (v NullableUiText) Get() *UiText { + return v.value +} + +func (v *NullableUiText) Set(val *UiText) { + v.value = val + v.isSet = true +} + +func (v NullableUiText) IsSet() bool { + return v.isSet +} + +func (v *NullableUiText) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiText(val *UiText) *NullableUiText { + return &NullableUiText{value: val, isSet: true} +} + +func (v NullableUiText) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiText) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_fedcm_flow_body.go b/pkg/client-go/model_update_fedcm_flow_body.go new file mode 100644 index 0000000..3f1bfdc --- /dev/null +++ b/pkg/client-go/model_update_fedcm_flow_body.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateFedcmFlowBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateFedcmFlowBody{} + +// UpdateFedcmFlowBody struct for UpdateFedcmFlowBody +type UpdateFedcmFlowBody struct { + // CSRFToken is the anti-CSRF token. + CsrfToken string `json:"csrf_token"` + // Nonce is the nonce that was used in the `navigator.credentials.get` call. If specified, it must match the `nonce` claim in the token. + Nonce *string `json:"nonce,omitempty"` + // Token contains the result of `navigator.credentials.get`. + Token string `json:"token"` + // Transient data to pass along to any webhooks. + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateFedcmFlowBody UpdateFedcmFlowBody + +// NewUpdateFedcmFlowBody instantiates a new UpdateFedcmFlowBody 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 NewUpdateFedcmFlowBody(csrfToken string, token string) *UpdateFedcmFlowBody { + this := UpdateFedcmFlowBody{} + this.CsrfToken = csrfToken + this.Token = token + return &this +} + +// NewUpdateFedcmFlowBodyWithDefaults instantiates a new UpdateFedcmFlowBody 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 NewUpdateFedcmFlowBodyWithDefaults() *UpdateFedcmFlowBody { + this := UpdateFedcmFlowBody{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateFedcmFlowBody) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateFedcmFlowBody) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetNonce returns the Nonce field value if set, zero value otherwise. +func (o *UpdateFedcmFlowBody) GetNonce() string { + if o == nil || IsNil(o.Nonce) { + var ret string + return ret + } + return *o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetNonceOk() (*string, bool) { + if o == nil || IsNil(o.Nonce) { + return nil, false + } + return o.Nonce, true +} + +// HasNonce returns a boolean if a field has been set. +func (o *UpdateFedcmFlowBody) HasNonce() bool { + if o != nil && !IsNil(o.Nonce) { + return true + } + + return false +} + +// SetNonce gets a reference to the given string and assigns it to the Nonce field. +func (o *UpdateFedcmFlowBody) SetNonce(v string) { + o.Nonce = &v +} + +// GetToken returns the Token field value +func (o *UpdateFedcmFlowBody) GetToken() string { + if o == nil { + var ret string + return ret + } + + return o.Token +} + +// GetTokenOk returns a tuple with the Token field value +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Token, true +} + +// SetToken sets field value +func (o *UpdateFedcmFlowBody) SetToken(v string) { + o.Token = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateFedcmFlowBody) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateFedcmFlowBody) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateFedcmFlowBody) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateFedcmFlowBody) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateFedcmFlowBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateFedcmFlowBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["csrf_token"] = o.CsrfToken + if !IsNil(o.Nonce) { + toSerialize["nonce"] = o.Nonce + } + toSerialize["token"] = o.Token + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateFedcmFlowBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "csrf_token", + "token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateFedcmFlowBody := _UpdateFedcmFlowBody{} + + err = json.Unmarshal(data, &varUpdateFedcmFlowBody) + + if err != nil { + return err + } + + *o = UpdateFedcmFlowBody(varUpdateFedcmFlowBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "nonce") + delete(additionalProperties, "token") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateFedcmFlowBody struct { + value *UpdateFedcmFlowBody + isSet bool +} + +func (v NullableUpdateFedcmFlowBody) Get() *UpdateFedcmFlowBody { + return v.value +} + +func (v *NullableUpdateFedcmFlowBody) Set(val *UpdateFedcmFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateFedcmFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateFedcmFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateFedcmFlowBody(val *UpdateFedcmFlowBody) *NullableUpdateFedcmFlowBody { + return &NullableUpdateFedcmFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateFedcmFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateFedcmFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_identity_body.go b/pkg/client-go/model_update_identity_body.go new file mode 100644 index 0000000..db29fa0 --- /dev/null +++ b/pkg/client-go/model_update_identity_body.go @@ -0,0 +1,419 @@ +/* +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" + "fmt" +) + +// checks if the UpdateIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateIdentityBody{} + +// UpdateIdentityBody Update Identity Body +type UpdateIdentityBody struct { + Credentials *IdentityWithCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // Store metadata about the identity which the identity itself can see when calling for example the session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic interface{} `json:"metadata_public,omitempty"` + // Region is the Ory Network region this identity is homed in. Optional; omit to leave the current region unchanged. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set will update the Identity's SchemaID. + SchemaId string `json:"schema_id"` + // State is the identity's state. active StateActive inactive StateInactive + State string `json:"state"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_id`. + Traits map[string]interface{} `json:"traits"` + AdditionalProperties map[string]interface{} +} + +type _UpdateIdentityBody UpdateIdentityBody + +// NewUpdateIdentityBody instantiates a new UpdateIdentityBody 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 NewUpdateIdentityBody(schemaId string, state string, traits map[string]interface{}) *UpdateIdentityBody { + this := UpdateIdentityBody{} + this.SchemaId = schemaId + this.State = state + this.Traits = traits + return &this +} + +// NewUpdateIdentityBodyWithDefaults instantiates a new UpdateIdentityBody 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 NewUpdateIdentityBodyWithDefaults() *UpdateIdentityBody { + this := UpdateIdentityBody{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetCredentials() IdentityWithCredentials { + if o == nil || IsNil(o.Credentials) { + var ret IdentityWithCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetCredentialsOk() (*IdentityWithCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given IdentityWithCredentials and assigns it to the Credentials field. +func (o *UpdateIdentityBody) SetCredentials(v IdentityWithCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *UpdateIdentityBody) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UpdateIdentityBody) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *UpdateIdentityBody) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *UpdateIdentityBody) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UpdateIdentityBody) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *UpdateIdentityBody) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *UpdateIdentityBody) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *UpdateIdentityBody) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *UpdateIdentityBody) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *UpdateIdentityBody) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetState returns the State field value +func (o *UpdateIdentityBody) GetState() string { + if o == nil { + var ret string + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetStateOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *UpdateIdentityBody) SetState(v string) { + o.State = v +} + +// GetTraits returns the Traits field value +func (o *UpdateIdentityBody) 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 *UpdateIdentityBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateIdentityBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +func (o UpdateIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + toSerialize["state"] = o.State + toSerialize["traits"] = o.Traits + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "schema_id", + "state", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateIdentityBody := _UpdateIdentityBody{} + + err = json.Unmarshal(data, &varUpdateIdentityBody) + + if err != nil { + return err + } + + *o = UpdateIdentityBody(varUpdateIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "state") + delete(additionalProperties, "traits") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateIdentityBody struct { + value *UpdateIdentityBody + isSet bool +} + +func (v NullableUpdateIdentityBody) Get() *UpdateIdentityBody { + return v.value +} + +func (v *NullableUpdateIdentityBody) Set(val *UpdateIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateIdentityBody(val *UpdateIdentityBody) *NullableUpdateIdentityBody { + return &NullableUpdateIdentityBody{value: val, isSet: true} +} + +func (v NullableUpdateIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_body.go b/pkg/client-go/model_update_login_flow_body.go new file mode 100644 index 0000000..a917194 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_body.go @@ -0,0 +1,486 @@ +/* +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" + "fmt" +) + +// UpdateLoginFlowBody - struct for UpdateLoginFlowBody +type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod + UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod + UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod + UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod + UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod + UpdateLoginFlowWithSamlMethod *UpdateLoginFlowWithSamlMethod + UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod + UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod +} + +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + +// 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{ + UpdateLoginFlowWithLookupSecretMethod: v, + } +} + +// UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithOidcMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOidcMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateLoginFlowWithPasswordMethod: v, + } +} + +// UpdateLoginFlowWithSamlMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithSamlMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithSamlMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithSamlMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithSamlMethod: v, + } +} + +// UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithTotpMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithTotpMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithTotpMethod: v, + } +} + +// UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithWebAuthnMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithWebAuthnMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateLoginFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithSamlMethod, return on the first match + } else { + dst.UpdateLoginFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateLoginFlowWithSamlMethod' + if jsonDict["method"] == "updateLoginFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithSamlMethod, return on the first match + } else { + dst.UpdateLoginFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithSamlMethod: %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 +func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + + if src.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod) + } + + if src.UpdateLoginFlowWithLookupSecretMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) + } + + if src.UpdateLoginFlowWithOidcMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) + } + + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + + if src.UpdateLoginFlowWithPasswordMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) + } + + if src.UpdateLoginFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithSamlMethod) + } + + if src.UpdateLoginFlowWithTotpMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithTotpMethod) + } + + if src.UpdateLoginFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return obj.UpdateLoginFlowWithIdentifierFirstMethod + } + + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { + return obj.UpdateLoginFlowWithLookupSecretMethod + } + + if obj.UpdateLoginFlowWithOidcMethod != nil { + return obj.UpdateLoginFlowWithOidcMethod + } + + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + + if obj.UpdateLoginFlowWithPasswordMethod != nil { + return obj.UpdateLoginFlowWithPasswordMethod + } + + if obj.UpdateLoginFlowWithSamlMethod != nil { + return obj.UpdateLoginFlowWithSamlMethod + } + + if obj.UpdateLoginFlowWithTotpMethod != nil { + return obj.UpdateLoginFlowWithTotpMethod + } + + if obj.UpdateLoginFlowWithWebAuthnMethod != nil { + return obj.UpdateLoginFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateLoginFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateLoginFlowWithCodeMethod != nil { + return *obj.UpdateLoginFlowWithCodeMethod + } + + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return *obj.UpdateLoginFlowWithIdentifierFirstMethod + } + + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { + return *obj.UpdateLoginFlowWithLookupSecretMethod + } + + if obj.UpdateLoginFlowWithOidcMethod != nil { + return *obj.UpdateLoginFlowWithOidcMethod + } + + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return *obj.UpdateLoginFlowWithPasskeyMethod + } + + if obj.UpdateLoginFlowWithPasswordMethod != nil { + return *obj.UpdateLoginFlowWithPasswordMethod + } + + if obj.UpdateLoginFlowWithSamlMethod != nil { + return *obj.UpdateLoginFlowWithSamlMethod + } + + if obj.UpdateLoginFlowWithTotpMethod != nil { + return *obj.UpdateLoginFlowWithTotpMethod + } + + if obj.UpdateLoginFlowWithWebAuthnMethod != nil { + return *obj.UpdateLoginFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateLoginFlowBody struct { + value *UpdateLoginFlowBody + isSet bool +} + +func (v NullableUpdateLoginFlowBody) Get() *UpdateLoginFlowBody { + return v.value +} + +func (v *NullableUpdateLoginFlowBody) Set(val *UpdateLoginFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowBody(val *UpdateLoginFlowBody) *NullableUpdateLoginFlowBody { + return &NullableUpdateLoginFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_code_method.go b/pkg/client-go/model_update_login_flow_with_code_method.go new file mode 100644 index 0000000..23aa030 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_code_method.go @@ -0,0 +1,388 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithCodeMethod{} + +// 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 + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithCodeMethod + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod 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 NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod 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 NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || IsNil(o.Address) { + 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 || IsNil(o.Address) { + 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 && !IsNil(o.Address) { + 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 || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || IsNil(o.Identifier) { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || IsNil(o.Identifier) { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && !IsNil(o.Identifier) { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) 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 *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || IsNil(o.Resend) { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || IsNil(o.Resend) { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && !IsNil(o.Resend) { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Address) { + toSerialize["address"] = o.Address + } + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + toSerialize["csrf_token"] = o.CsrfToken + if !IsNil(o.Identifier) { + toSerialize["identifier"] = o.Identifier + } + toSerialize["method"] = o.Method + if !IsNil(o.Resend) { + toSerialize["resend"] = o.Resend + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "csrf_token", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithCodeMethod := _UpdateLoginFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithCodeMethod(varUpdateLoginFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "address") + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "resend") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_identifier_first_method.go b/pkg/client-go/model_update_login_flow_with_identifier_first_method.go new file mode 100644 index 0000000..405356f --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_identifier_first_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithIdentifierFirstMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithIdentifierFirstMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithIdentifierFirstMethod UpdateLoginFlowWithIdentifierFirstMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithIdentifierFirstMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithIdentifierFirstMethod := _UpdateLoginFlowWithIdentifierFirstMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithIdentifierFirstMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithIdentifierFirstMethod(varUpdateLoginFlowWithIdentifierFirstMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_update_login_flow_with_lookup_secret_method.go b/pkg/client-go/model_update_login_flow_with_lookup_secret_method.go new file mode 100644 index 0000000..d522cde --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_lookup_secret_method.go @@ -0,0 +1,236 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithLookupSecretMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithLookupSecretMethod{} + +// UpdateLoginFlowWithLookupSecretMethod Update Login Flow with Lookup Secret Method +type UpdateLoginFlowWithLookupSecretMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The lookup secret. + LookupSecret string `json:"lookup_secret"` + // Method should be set to \"lookup_secret\" when logging in using the lookup_secret strategy. + Method string `json:"method"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithLookupSecretMethod + +// NewUpdateLoginFlowWithLookupSecretMethod instantiates a new UpdateLoginFlowWithLookupSecretMethod 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 NewUpdateLoginFlowWithLookupSecretMethod(lookupSecret string, method string) *UpdateLoginFlowWithLookupSecretMethod { + this := UpdateLoginFlowWithLookupSecretMethod{} + this.LookupSecret = lookupSecret + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithLookupSecretMethodWithDefaults instantiates a new UpdateLoginFlowWithLookupSecretMethod 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 NewUpdateLoginFlowWithLookupSecretMethodWithDefaults() *UpdateLoginFlowWithLookupSecretMethod { + this := UpdateLoginFlowWithLookupSecretMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithLookupSecretMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithLookupSecretMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithLookupSecretMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithLookupSecretMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetLookupSecret returns the LookupSecret field value +func (o *UpdateLoginFlowWithLookupSecretMethod) GetLookupSecret() string { + if o == nil { + var ret string + return ret + } + + return o.LookupSecret +} + +// GetLookupSecretOk returns a tuple with the LookupSecret field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithLookupSecretMethod) GetLookupSecretOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LookupSecret, true +} + +// SetLookupSecret sets field value +func (o *UpdateLoginFlowWithLookupSecretMethod) SetLookupSecret(v string) { + o.LookupSecret = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithLookupSecretMethod) 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 *UpdateLoginFlowWithLookupSecretMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithLookupSecretMethod) SetMethod(v string) { + o.Method = v +} + +func (o UpdateLoginFlowWithLookupSecretMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithLookupSecretMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["lookup_secret"] = o.LookupSecret + toSerialize["method"] = o.Method + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithLookupSecretMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "lookup_secret", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithLookupSecretMethod := _UpdateLoginFlowWithLookupSecretMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithLookupSecretMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithLookupSecretMethod(varUpdateLoginFlowWithLookupSecretMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "lookup_secret") + delete(additionalProperties, "method") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithLookupSecretMethod struct { + value *UpdateLoginFlowWithLookupSecretMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) Get() *UpdateLoginFlowWithLookupSecretMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) Set(val *UpdateLoginFlowWithLookupSecretMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithLookupSecretMethod(val *UpdateLoginFlowWithLookupSecretMethod) *NullableUpdateLoginFlowWithLookupSecretMethod { + return &NullableUpdateLoginFlowWithLookupSecretMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_oidc_method.go b/pkg/client-go/model_update_login_flow_with_oidc_method.go new file mode 100644 index 0000000..407c50f --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_oidc_method.go @@ -0,0 +1,426 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithOidcMethod{} + +// UpdateLoginFlowWithOidcMethod Update Login Flow with OpenID Connect Method +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 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"` + // Method to use This field must be set to `oidc` when using the oidc method. + Method string `json:"method"` + // The provider to register with + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithOidcMethod + +// NewUpdateLoginFlowWithOidcMethod instantiates a new UpdateLoginFlowWithOidcMethod 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 NewUpdateLoginFlowWithOidcMethod(method string, provider string) *UpdateLoginFlowWithOidcMethod { + this := UpdateLoginFlowWithOidcMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateLoginFlowWithOidcMethodWithDefaults instantiates a new UpdateLoginFlowWithOidcMethod 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 NewUpdateLoginFlowWithOidcMethodWithDefaults() *UpdateLoginFlowWithOidcMethod { + this := UpdateLoginFlowWithOidcMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithOidcMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdToken() string { + if o == nil || IsNil(o.IdToken) { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.IdToken) { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdToken() bool { + if o != nil && !IsNil(o.IdToken) { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || IsNil(o.IdTokenNonce) { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || IsNil(o.IdTokenNonce) { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && !IsNil(o.IdTokenNonce) { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithOidcMethod) 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 *UpdateLoginFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateLoginFlowWithOidcMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateLoginFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.IdToken) { + toSerialize["id_token"] = o.IdToken + } + if !IsNil(o.IdTokenNonce) { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithOidcMethod := _UpdateLoginFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithOidcMethod(varUpdateLoginFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "id_token") + delete(additionalProperties, "id_token_nonce") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithOidcMethod struct { + value *UpdateLoginFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithOidcMethod) Get() *UpdateLoginFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) Set(val *UpdateLoginFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithOidcMethod(val *UpdateLoginFlowWithOidcMethod) *NullableUpdateLoginFlowWithOidcMethod { + return &NullableUpdateLoginFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_passkey_method.go b/pkg/client-go/model_update_login_flow_with_passkey_method.go new file mode 100644 index 0000000..88277d8 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithPasskeyMethod UpdateLoginFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyLogin) { + 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 || IsNil(o.PasskeyLogin) { + 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 && !IsNil(o.PasskeyLogin) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyLogin) { + toSerialize["passkey_login"] = o.PasskeyLogin + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithPasskeyMethod := _UpdateLoginFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithPasskeyMethod(varUpdateLoginFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_login") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_update_login_flow_with_password_method.go b/pkg/client-go/model_update_login_flow_with_password_method.go new file mode 100644 index 0000000..d3491b7 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_password_method.go @@ -0,0 +1,342 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithPasswordMethod{} + +// UpdateLoginFlowWithPasswordMethod Update Login Flow with Password Method +type UpdateLoginFlowWithPasswordMethod 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"` + // The user's password. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithPasswordMethod UpdateLoginFlowWithPasswordMethod + +// NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod 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 NewUpdateLoginFlowWithPasswordMethod(identifier string, method string, password string) *UpdateLoginFlowWithPasswordMethod { + this := UpdateLoginFlowWithPasswordMethod{} + this.Identifier = identifier + this.Method = method + this.Password = password + return &this +} + +// NewUpdateLoginFlowWithPasswordMethodWithDefaults instantiates a new UpdateLoginFlowWithPasswordMethod 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 NewUpdateLoginFlowWithPasswordMethodWithDefaults() *UpdateLoginFlowWithPasswordMethod { + this := UpdateLoginFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithPasswordMethod) 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 *UpdateLoginFlowWithPasswordMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasswordMethod) 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 *UpdateLoginFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateLoginFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetPassword(v string) { + o.Password = v +} + +// GetPasswordIdentifier returns the PasswordIdentifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordIdentifier() string { + if o == nil || IsNil(o.PasswordIdentifier) { + var ret string + return ret + } + return *o.PasswordIdentifier +} + +// GetPasswordIdentifierOk returns a tuple with the PasswordIdentifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordIdentifierOk() (*string, bool) { + if o == nil || IsNil(o.PasswordIdentifier) { + return nil, false + } + return o.PasswordIdentifier, true +} + +// HasPasswordIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasPasswordIdentifier() bool { + if o != nil && !IsNil(o.PasswordIdentifier) { + return true + } + + return false +} + +// SetPasswordIdentifier gets a reference to the given string and assigns it to the PasswordIdentifier field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + if !IsNil(o.PasswordIdentifier) { + toSerialize["password_identifier"] = o.PasswordIdentifier + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + "password", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithPasswordMethod := _UpdateLoginFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithPasswordMethod(varUpdateLoginFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "password_identifier") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithPasswordMethod struct { + value *UpdateLoginFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) Get() *UpdateLoginFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) Set(val *UpdateLoginFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasswordMethod(val *UpdateLoginFlowWithPasswordMethod) *NullableUpdateLoginFlowWithPasswordMethod { + return &NullableUpdateLoginFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_saml_method.go b/pkg/client-go/model_update_login_flow_with_saml_method.go new file mode 100644 index 0000000..0457a88 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_saml_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithSamlMethod{} + +// UpdateLoginFlowWithSamlMethod Update login flow using SAML +type UpdateLoginFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `saml` when using the saml method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithSamlMethod UpdateLoginFlowWithSamlMethod + +// NewUpdateLoginFlowWithSamlMethod instantiates a new UpdateLoginFlowWithSamlMethod 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 NewUpdateLoginFlowWithSamlMethod(method string, provider string) *UpdateLoginFlowWithSamlMethod { + this := UpdateLoginFlowWithSamlMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateLoginFlowWithSamlMethodWithDefaults instantiates a new UpdateLoginFlowWithSamlMethod 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 NewUpdateLoginFlowWithSamlMethodWithDefaults() *UpdateLoginFlowWithSamlMethod { + this := UpdateLoginFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithSamlMethod) 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 *UpdateLoginFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateLoginFlowWithSamlMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithSamlMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateLoginFlowWithSamlMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateLoginFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateLoginFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithSamlMethod := _UpdateLoginFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithSamlMethod(varUpdateLoginFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithSamlMethod struct { + value *UpdateLoginFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithSamlMethod) Get() *UpdateLoginFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) Set(val *UpdateLoginFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithSamlMethod(val *UpdateLoginFlowWithSamlMethod) *NullableUpdateLoginFlowWithSamlMethod { + return &NullableUpdateLoginFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_totp_method.go b/pkg/client-go/model_update_login_flow_with_totp_method.go new file mode 100644 index 0000000..e108edf --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_totp_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithTotpMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithTotpMethod{} + +// UpdateLoginFlowWithTotpMethod Update Login Flow with TOTP Method +type UpdateLoginFlowWithTotpMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"totp\" when logging in using the TOTP strategy. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithTotpMethod UpdateLoginFlowWithTotpMethod + +// NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod 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 NewUpdateLoginFlowWithTotpMethod(method string, totpCode string) *UpdateLoginFlowWithTotpMethod { + this := UpdateLoginFlowWithTotpMethod{} + this.Method = method + this.TotpCode = totpCode + return &this +} + +// NewUpdateLoginFlowWithTotpMethodWithDefaults instantiates a new UpdateLoginFlowWithTotpMethod 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 NewUpdateLoginFlowWithTotpMethodWithDefaults() *UpdateLoginFlowWithTotpMethod { + this := UpdateLoginFlowWithTotpMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithTotpMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithTotpMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithTotpMethod) 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 *UpdateLoginFlowWithTotpMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithTotpMethod) SetMethod(v string) { + o.Method = v +} + +// GetTotpCode returns the TotpCode field value +func (o *UpdateLoginFlowWithTotpMethod) GetTotpCode() string { + if o == nil { + var ret string + return ret + } + + return o.TotpCode +} + +// GetTotpCodeOk returns a tuple with the TotpCode field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTotpCodeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.TotpCode, true +} + +// SetTotpCode sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithTotpMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["totp_code"] = o.TotpCode + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithTotpMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "totp_code", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithTotpMethod := _UpdateLoginFlowWithTotpMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithTotpMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithTotpMethod(varUpdateLoginFlowWithTotpMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "totp_code") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithTotpMethod struct { + value *UpdateLoginFlowWithTotpMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithTotpMethod) Get() *UpdateLoginFlowWithTotpMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) Set(val *UpdateLoginFlowWithTotpMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithTotpMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithTotpMethod(val *UpdateLoginFlowWithTotpMethod) *NullableUpdateLoginFlowWithTotpMethod { + return &NullableUpdateLoginFlowWithTotpMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_login_flow_with_web_authn_method.go b/pkg/client-go/model_update_login_flow_with_web_authn_method.go new file mode 100644 index 0000000..a79dee2 --- /dev/null +++ b/pkg/client-go/model_update_login_flow_with_web_authn_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithWebAuthnMethod{} + +// UpdateLoginFlowWithWebAuthnMethod Update Login Flow with WebAuthn Method +type UpdateLoginFlowWithWebAuthnMethod 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 \"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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithWebAuthnMethod UpdateLoginFlowWithWebAuthnMethod + +// NewUpdateLoginFlowWithWebAuthnMethod instantiates a new UpdateLoginFlowWithWebAuthnMethod 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 NewUpdateLoginFlowWithWebAuthnMethod(identifier string, method string) *UpdateLoginFlowWithWebAuthnMethod { + this := UpdateLoginFlowWithWebAuthnMethod{} + this.Identifier = identifier + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateLoginFlowWithWebAuthnMethod 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 NewUpdateLoginFlowWithWebAuthnMethodWithDefaults() *UpdateLoginFlowWithWebAuthnMethod { + this := UpdateLoginFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithWebAuthnMethod) 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 *UpdateLoginFlowWithWebAuthnMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithWebAuthnMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithWebAuthnMethod) 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 *UpdateLoginFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.WebauthnLogin) { + var ret string + return ret + } + return *o.WebauthnLogin +} + +// GetWebauthnLoginOk returns a tuple with the WebauthnLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLoginOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnLogin) { + return nil, false + } + return o.WebauthnLogin, true +} + +// HasWebauthnLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasWebauthnLogin() bool { + if o != nil && !IsNil(o.WebauthnLogin) { + return true + } + + return false +} + +// SetWebauthnLogin gets a reference to the given string and assigns it to the WebauthnLogin field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetWebauthnLogin(v string) { + o.WebauthnLogin = &v +} + +func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnLogin) { + toSerialize["webauthn_login"] = o.WebauthnLogin + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithWebAuthnMethod := _UpdateLoginFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithWebAuthnMethod(varUpdateLoginFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_login") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithWebAuthnMethod struct { + value *UpdateLoginFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) Get() *UpdateLoginFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) Set(val *UpdateLoginFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithWebAuthnMethod(val *UpdateLoginFlowWithWebAuthnMethod) *NullableUpdateLoginFlowWithWebAuthnMethod { + return &NullableUpdateLoginFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_recovery_flow_body.go b/pkg/client-go/model_update_recovery_flow_body.go new file mode 100644 index 0000000..c226e9e --- /dev/null +++ b/pkg/client-go/model_update_recovery_flow_body.go @@ -0,0 +1,178 @@ +/* +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" + "fmt" +) + +// UpdateRecoveryFlowBody - Update Recovery Flow Request Body +type UpdateRecoveryFlowBody struct { + UpdateRecoveryFlowWithCodeMethod *UpdateRecoveryFlowWithCodeMethod + UpdateRecoveryFlowWithLinkMethod *UpdateRecoveryFlowWithLinkMethod +} + +// UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody is a convenience function that returns UpdateRecoveryFlowWithCodeMethod wrapped in UpdateRecoveryFlowBody +func UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryFlowWithCodeMethod) UpdateRecoveryFlowBody { + return UpdateRecoveryFlowBody{ + UpdateRecoveryFlowWithCodeMethod: v, + } +} + +// UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody is a convenience function that returns UpdateRecoveryFlowWithLinkMethod wrapped in UpdateRecoveryFlowBody +func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryFlowWithLinkMethod) UpdateRecoveryFlowBody { + return UpdateRecoveryFlowBody{ + UpdateRecoveryFlowWithLinkMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UpdateRecoveryFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRecoveryFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRecoveryFlowWithCodeMethod) + } + + if src.UpdateRecoveryFlowWithLinkMethod != nil { + return json.Marshal(&src.UpdateRecoveryFlowWithLinkMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateRecoveryFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateRecoveryFlowWithCodeMethod != nil { + return obj.UpdateRecoveryFlowWithCodeMethod + } + + if obj.UpdateRecoveryFlowWithLinkMethod != nil { + return obj.UpdateRecoveryFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateRecoveryFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateRecoveryFlowWithCodeMethod != nil { + return *obj.UpdateRecoveryFlowWithCodeMethod + } + + if obj.UpdateRecoveryFlowWithLinkMethod != nil { + return *obj.UpdateRecoveryFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateRecoveryFlowBody struct { + value *UpdateRecoveryFlowBody + isSet bool +} + +func (v NullableUpdateRecoveryFlowBody) Get() *UpdateRecoveryFlowBody { + return v.value +} + +func (v *NullableUpdateRecoveryFlowBody) Set(val *UpdateRecoveryFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowBody(val *UpdateRecoveryFlowBody) *NullableUpdateRecoveryFlowBody { + return &NullableUpdateRecoveryFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_recovery_flow_with_code_method.go b/pkg/client-go/model_update_recovery_flow_with_code_method.go new file mode 100644 index 0000000..36f06ab --- /dev/null +++ b/pkg/client-go/model_update_recovery_flow_with_code_method.go @@ -0,0 +1,472 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRecoveryFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRecoveryFlowWithCodeMethod{} + +// UpdateRecoveryFlowWithCodeMethod Update Recovery Flow with Code Method +type UpdateRecoveryFlowWithCodeMethod struct { + // Code from the recovery email If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The email address of the account to recover If the email belongs to a valid account, a recovery email will be sent. If you want to notify the email address if the account does not exist, see the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/account-recovery-password-reset#attempted-recovery-notifications) If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. format: email + 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"` + // A recovery address that is registered for the user. It can be an email, a phone number (to receive the code via SMS), etc. Used in RecoveryV2. + RecoveryAddress *string `json:"recovery_address,omitempty"` + // If there are multiple recovery addresses registered for the user, and the initially provided address is different from the address chosen when the choice (of masked addresses) is presented, then we need to make sure that the user actually knows the full address to avoid information exfiltration, so we ask for the full address. Used in RecoveryV2. + RecoveryConfirmAddress *string `json:"recovery_confirm_address,omitempty"` + // If there are multiple addresses registered for the user, a choice is presented and this field stores the result of this choice. Addresses are 'masked' (never sent in full to the client and shown partially in the UI) since at this point in the recovery flow, the user has not yet proven that it knows the full address and we want to avoid information exfiltration. So for all intents and purposes, the value of this field should be treated as an opaque identifier. Used in RecoveryV2. + RecoverySelectAddress *string `json:"recovery_select_address,omitempty"` + // Set to \"previous\" to go back in the flow, meaningfully. Used in RecoveryV2. + Screen *string `json:"screen,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRecoveryFlowWithCodeMethod UpdateRecoveryFlowWithCodeMethod + +// NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod 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 NewUpdateRecoveryFlowWithCodeMethod(method string) *UpdateRecoveryFlowWithCodeMethod { + this := UpdateRecoveryFlowWithCodeMethod{} + this.Method = method + return &this +} + +// NewUpdateRecoveryFlowWithCodeMethodWithDefaults instantiates a new UpdateRecoveryFlowWithCodeMethod 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 NewUpdateRecoveryFlowWithCodeMethodWithDefaults() *UpdateRecoveryFlowWithCodeMethod { + this := UpdateRecoveryFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRecoveryFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetEmail() string { + if o == nil || IsNil(o.Email) { + var ret string + return ret + } + return *o.Email +} + +// GetEmailOk returns a tuple with the Email field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetEmailOk() (*string, bool) { + if o == nil || IsNil(o.Email) { + return nil, false + } + return o.Email, true +} + +// HasEmail returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasEmail() bool { + if o != nil && !IsNil(o.Email) { + return true + } + + return false +} + +// SetEmail gets a reference to the given string and assigns it to the Email field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetEmail(v string) { + o.Email = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRecoveryFlowWithCodeMethod) 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 *UpdateRecoveryFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetRecoveryAddress returns the RecoveryAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryAddress() string { + if o == nil || IsNil(o.RecoveryAddress) { + var ret string + return ret + } + return *o.RecoveryAddress +} + +// GetRecoveryAddressOk returns a tuple with the RecoveryAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoveryAddress) { + return nil, false + } + return o.RecoveryAddress, true +} + +// HasRecoveryAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoveryAddress() bool { + if o != nil && !IsNil(o.RecoveryAddress) { + return true + } + + return false +} + +// SetRecoveryAddress gets a reference to the given string and assigns it to the RecoveryAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoveryAddress(v string) { + o.RecoveryAddress = &v +} + +// GetRecoveryConfirmAddress returns the RecoveryConfirmAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryConfirmAddress() string { + if o == nil || IsNil(o.RecoveryConfirmAddress) { + var ret string + return ret + } + return *o.RecoveryConfirmAddress +} + +// GetRecoveryConfirmAddressOk returns a tuple with the RecoveryConfirmAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryConfirmAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoveryConfirmAddress) { + return nil, false + } + return o.RecoveryConfirmAddress, true +} + +// HasRecoveryConfirmAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoveryConfirmAddress() bool { + if o != nil && !IsNil(o.RecoveryConfirmAddress) { + return true + } + + return false +} + +// SetRecoveryConfirmAddress gets a reference to the given string and assigns it to the RecoveryConfirmAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoveryConfirmAddress(v string) { + o.RecoveryConfirmAddress = &v +} + +// GetRecoverySelectAddress returns the RecoverySelectAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoverySelectAddress() string { + if o == nil || IsNil(o.RecoverySelectAddress) { + var ret string + return ret + } + return *o.RecoverySelectAddress +} + +// GetRecoverySelectAddressOk returns a tuple with the RecoverySelectAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoverySelectAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoverySelectAddress) { + return nil, false + } + return o.RecoverySelectAddress, true +} + +// HasRecoverySelectAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoverySelectAddress() bool { + if o != nil && !IsNil(o.RecoverySelectAddress) { + return true + } + + return false +} + +// SetRecoverySelectAddress gets a reference to the given string and assigns it to the RecoverySelectAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoverySelectAddress(v string) { + o.RecoverySelectAddress = &v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetScreen() string { + if o == nil || IsNil(o.Screen) { + 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 *UpdateRecoveryFlowWithCodeMethod) GetScreenOk() (*string, bool) { + if o == nil || IsNil(o.Screen) { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasScreen() bool { + if o != nil && !IsNil(o.Screen) { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRecoveryFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Email) { + toSerialize["email"] = o.Email + } + toSerialize["method"] = o.Method + if !IsNil(o.RecoveryAddress) { + toSerialize["recovery_address"] = o.RecoveryAddress + } + if !IsNil(o.RecoveryConfirmAddress) { + toSerialize["recovery_confirm_address"] = o.RecoveryConfirmAddress + } + if !IsNil(o.RecoverySelectAddress) { + toSerialize["recovery_select_address"] = o.RecoverySelectAddress + } + if !IsNil(o.Screen) { + toSerialize["screen"] = o.Screen + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRecoveryFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRecoveryFlowWithCodeMethod := _UpdateRecoveryFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateRecoveryFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateRecoveryFlowWithCodeMethod(varUpdateRecoveryFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "recovery_address") + delete(additionalProperties, "recovery_confirm_address") + delete(additionalProperties, "recovery_select_address") + delete(additionalProperties, "screen") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRecoveryFlowWithCodeMethod struct { + value *UpdateRecoveryFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) Get() *UpdateRecoveryFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) Set(val *UpdateRecoveryFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowWithCodeMethod(val *UpdateRecoveryFlowWithCodeMethod) *NullableUpdateRecoveryFlowWithCodeMethod { + return &NullableUpdateRecoveryFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_recovery_flow_with_link_method.go b/pkg/client-go/model_update_recovery_flow_with_link_method.go new file mode 100644 index 0000000..a6d63a8 --- /dev/null +++ b/pkg/client-go/model_update_recovery_flow_with_link_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRecoveryFlowWithLinkMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRecoveryFlowWithLinkMethod{} + +// UpdateRecoveryFlowWithLinkMethod Update Recovery Flow with Link Method +type UpdateRecoveryFlowWithLinkMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Email to Recover Needs to be set when initiating the flow. If the email is a registered recovery email, a recovery link will be sent. If the email is not known, an email with details on what happened will be sent instead. format: email + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRecoveryFlowWithLinkMethod UpdateRecoveryFlowWithLinkMethod + +// NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod 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 NewUpdateRecoveryFlowWithLinkMethod(email string, method string) *UpdateRecoveryFlowWithLinkMethod { + this := UpdateRecoveryFlowWithLinkMethod{} + this.Email = email + this.Method = method + return &this +} + +// NewUpdateRecoveryFlowWithLinkMethodWithDefaults instantiates a new UpdateRecoveryFlowWithLinkMethod 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 NewUpdateRecoveryFlowWithLinkMethodWithDefaults() *UpdateRecoveryFlowWithLinkMethod { + this := UpdateRecoveryFlowWithLinkMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRecoveryFlowWithLinkMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value +func (o *UpdateRecoveryFlowWithLinkMethod) GetEmail() string { + if o == nil { + var ret string + return ret + } + + return o.Email +} + +// GetEmailOk returns a tuple with the Email field value +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetEmailOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Email, true +} + +// SetEmail sets field value +func (o *UpdateRecoveryFlowWithLinkMethod) SetEmail(v string) { + o.Email = v +} + +// GetMethod returns the Method field value +func (o *UpdateRecoveryFlowWithLinkMethod) 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 *UpdateRecoveryFlowWithLinkMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRecoveryFlowWithLinkMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["email"] = o.Email + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRecoveryFlowWithLinkMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "email", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRecoveryFlowWithLinkMethod := _UpdateRecoveryFlowWithLinkMethod{} + + err = json.Unmarshal(data, &varUpdateRecoveryFlowWithLinkMethod) + + if err != nil { + return err + } + + *o = UpdateRecoveryFlowWithLinkMethod(varUpdateRecoveryFlowWithLinkMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRecoveryFlowWithLinkMethod struct { + value *UpdateRecoveryFlowWithLinkMethod + isSet bool +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) Get() *UpdateRecoveryFlowWithLinkMethod { + return v.value +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) Set(val *UpdateRecoveryFlowWithLinkMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowWithLinkMethod(val *UpdateRecoveryFlowWithLinkMethod) *NullableUpdateRecoveryFlowWithLinkMethod { + return &NullableUpdateRecoveryFlowWithLinkMethod{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_body.go b/pkg/client-go/model_update_registration_flow_body.go new file mode 100644 index 0000000..07be3ba --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_body.go @@ -0,0 +1,398 @@ +/* +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" + "fmt" +) + +// UpdateRegistrationFlowBody - Update Registration Request Body +type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod + UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod + UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod + UpdateRegistrationFlowWithSamlMethod *UpdateRegistrationFlowWithSamlMethod + UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod +} + +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + +// UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateRegistrationFlowWithPasswordMethod: v, + } +} + +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + +// UpdateRegistrationFlowWithSamlMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithSamlMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithSamlMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithSamlMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithSamlMethod: v, + } +} + +// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithSamlMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateRegistrationFlowWithSamlMethod' + if jsonDict["method"] == "updateRegistrationFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithSamlMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithSamlMethod: %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 +func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + + if src.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithSamlMethod) + } + + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + + if obj.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return obj.UpdateRegistrationFlowWithSamlMethod + } + + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return obj.UpdateRegistrationFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateRegistrationFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return *obj.UpdateRegistrationFlowWithCodeMethod + } + + if obj.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return *obj.UpdateRegistrationFlowWithSamlMethod + } + + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return *obj.UpdateRegistrationFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateRegistrationFlowBody struct { + value *UpdateRegistrationFlowBody + isSet bool +} + +func (v NullableUpdateRegistrationFlowBody) Get() *UpdateRegistrationFlowBody { + return v.value +} + +func (v *NullableUpdateRegistrationFlowBody) Set(val *UpdateRegistrationFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowBody(val *UpdateRegistrationFlowBody) *NullableUpdateRegistrationFlowBody { + return &NullableUpdateRegistrationFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_with_code_method.go b/pkg/client-go/model_update_registration_flow_with_code_method.go new file mode 100644 index 0000000..e864d1b --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_code_method.go @@ -0,0 +1,350 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithCodeMethod{} + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithCodeMethod + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod 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 NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod 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 NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) 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 *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || IsNil(o.Resend) { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || IsNil(o.Resend) { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && !IsNil(o.Resend) { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) 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 *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.Resend) { + toSerialize["resend"] = o.Resend + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithCodeMethod := _UpdateRegistrationFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithCodeMethod(varUpdateRegistrationFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "resend") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_with_oidc_method.go b/pkg/client-go/model_update_registration_flow_with_oidc_method.go new file mode 100644 index 0000000..c8df11b --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_oidc_method.go @@ -0,0 +1,426 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithOidcMethod{} + +// UpdateRegistrationFlowWithOidcMethod Update Registration Flow with OpenID Connect Method +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 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"` + // Method to use This field must be set to `oidc` when using the oidc method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // The identity traits + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithOidcMethod + +// NewUpdateRegistrationFlowWithOidcMethod instantiates a new UpdateRegistrationFlowWithOidcMethod 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 NewUpdateRegistrationFlowWithOidcMethod(method string, provider string) *UpdateRegistrationFlowWithOidcMethod { + this := UpdateRegistrationFlowWithOidcMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateRegistrationFlowWithOidcMethodWithDefaults instantiates a new UpdateRegistrationFlowWithOidcMethod 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 NewUpdateRegistrationFlowWithOidcMethodWithDefaults() *UpdateRegistrationFlowWithOidcMethod { + this := UpdateRegistrationFlowWithOidcMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithOidcMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdToken() string { + if o == nil || IsNil(o.IdToken) { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.IdToken) { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdToken() bool { + if o != nil && !IsNil(o.IdToken) { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || IsNil(o.IdTokenNonce) { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || IsNil(o.IdTokenNonce) { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && !IsNil(o.IdTokenNonce) { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithOidcMethod) 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 *UpdateRegistrationFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateRegistrationFlowWithOidcMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { + if o == nil || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.IdToken) { + toSerialize["id_token"] = o.IdToken + } + if !IsNil(o.IdTokenNonce) { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithOidcMethod := _UpdateRegistrationFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithOidcMethod(varUpdateRegistrationFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "id_token") + delete(additionalProperties, "id_token_nonce") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithOidcMethod struct { + value *UpdateRegistrationFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) Get() *UpdateRegistrationFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) Set(val *UpdateRegistrationFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithOidcMethod(val *UpdateRegistrationFlowWithOidcMethod) *NullableUpdateRegistrationFlowWithOidcMethod { + return &NullableUpdateRegistrationFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_with_passkey_method.go b/pkg/client-go/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 0000000..a9a7ee1 --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyRegister) { + 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 || IsNil(o.PasskeyRegister) { + 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 && !IsNil(o.PasskeyRegister) { + 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 map[string]interface{}{}, 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyRegister) { + toSerialize["passkey_register"] = o.PasskeyRegister + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithPasskeyMethod := _UpdateRegistrationFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithPasskeyMethod(varUpdateRegistrationFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_register") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_update_registration_flow_with_password_method.go b/pkg/client-go/model_update_registration_flow_with_password_method.go new file mode 100644 index 0000000..3aaaf0b --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_password_method.go @@ -0,0 +1,304 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithPasswordMethod{} + +// UpdateRegistrationFlowWithPasswordMethod Update Registration Flow with Password Method +type UpdateRegistrationFlowWithPasswordMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `password` when using the password method. + Method string `json:"method"` + // Password to sign the user up with + Password string `json:"password"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithPasswordMethod + +// NewUpdateRegistrationFlowWithPasswordMethod instantiates a new UpdateRegistrationFlowWithPasswordMethod 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 NewUpdateRegistrationFlowWithPasswordMethod(method string, password string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasswordMethod { + this := UpdateRegistrationFlowWithPasswordMethod{} + this.Method = method + this.Password = password + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasswordMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasswordMethod 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 NewUpdateRegistrationFlowWithPasswordMethodWithDefaults() *UpdateRegistrationFlowWithPasswordMethod { + this := UpdateRegistrationFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasswordMethod) 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 *UpdateRegistrationFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateRegistrationFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetPassword(v string) { + o.Password = v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasswordMethod) 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 *UpdateRegistrationFlowWithPasswordMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "password", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithPasswordMethod := _UpdateRegistrationFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithPasswordMethod(varUpdateRegistrationFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithPasswordMethod struct { + value *UpdateRegistrationFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) Get() *UpdateRegistrationFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) Set(val *UpdateRegistrationFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasswordMethod(val *UpdateRegistrationFlowWithPasswordMethod) *NullableUpdateRegistrationFlowWithPasswordMethod { + return &NullableUpdateRegistrationFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_with_profile_method.go b/pkg/client-go/model_update_registration_flow_with_profile_method.go new file mode 100644 index 0000000..fbd35f3 --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithProfileMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithProfileMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithProfileMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.Screen) { + 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 || IsNil(o.Screen) { + 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 && !IsNil(o.Screen) { + 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 map[string]interface{}{}, 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithProfileMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.Screen) { + toSerialize["screen"] = o.Screen + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithProfileMethod := _UpdateRegistrationFlowWithProfileMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithProfileMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithProfileMethod(varUpdateRegistrationFlowWithProfileMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "screen") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_update_registration_flow_with_saml_method.go b/pkg/client-go/model_update_registration_flow_with_saml_method.go new file mode 100644 index 0000000..e217676 --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_saml_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithSamlMethod{} + +// UpdateRegistrationFlowWithSamlMethod Update registration flow using SAML +type UpdateRegistrationFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `saml` when using the saml method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // The identity traits + Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithSamlMethod UpdateRegistrationFlowWithSamlMethod + +// NewUpdateRegistrationFlowWithSamlMethod instantiates a new UpdateRegistrationFlowWithSamlMethod 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 NewUpdateRegistrationFlowWithSamlMethod(method string, provider string) *UpdateRegistrationFlowWithSamlMethod { + this := UpdateRegistrationFlowWithSamlMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateRegistrationFlowWithSamlMethodWithDefaults instantiates a new UpdateRegistrationFlowWithSamlMethod 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 NewUpdateRegistrationFlowWithSamlMethodWithDefaults() *UpdateRegistrationFlowWithSamlMethod { + this := UpdateRegistrationFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithSamlMethod) 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 *UpdateRegistrationFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateRegistrationFlowWithSamlMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateRegistrationFlowWithSamlMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithSamlMethod := _UpdateRegistrationFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithSamlMethod(varUpdateRegistrationFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithSamlMethod struct { + value *UpdateRegistrationFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) Get() *UpdateRegistrationFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) Set(val *UpdateRegistrationFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithSamlMethod(val *UpdateRegistrationFlowWithSamlMethod) *NullableUpdateRegistrationFlowWithSamlMethod { + return &NullableUpdateRegistrationFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_registration_flow_with_web_authn_method.go b/pkg/client-go/model_update_registration_flow_with_web_authn_method.go new file mode 100644 index 0000000..3688f8f --- /dev/null +++ b/pkg/client-go/model_update_registration_flow_with_web_authn_method.go @@ -0,0 +1,350 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithWebAuthnMethod{} + +// UpdateRegistrationFlowWithWebAuthnMethod Update Registration Flow with WebAuthn Method +type UpdateRegistrationFlowWithWebAuthnMethod struct { + // CSRFToken is the anti-CSRF token + 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"` + // 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"` + // 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. + WebauthnRegisterDisplayname *string `json:"webauthn_register_displayname,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithWebAuthnMethod UpdateRegistrationFlowWithWebAuthnMethod + +// NewUpdateRegistrationFlowWithWebAuthnMethod instantiates a new UpdateRegistrationFlowWithWebAuthnMethod 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 NewUpdateRegistrationFlowWithWebAuthnMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithWebAuthnMethod { + this := UpdateRegistrationFlowWithWebAuthnMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateRegistrationFlowWithWebAuthnMethod 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 NewUpdateRegistrationFlowWithWebAuthnMethodWithDefaults() *UpdateRegistrationFlowWithWebAuthnMethod { + this := UpdateRegistrationFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegister() string { + if o == nil || IsNil(o.WebauthnRegister) { + var ret string + return ret + } + return *o.WebauthnRegister +} + +// GetWebauthnRegisterOk returns a tuple with the WebauthnRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegister) { + return nil, false + } + return o.WebauthnRegister, true +} + +// HasWebauthnRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasWebauthnRegister() bool { + if o != nil && !IsNil(o.WebauthnRegister) { + return true + } + + return false +} + +// SetWebauthnRegister gets a reference to the given string and assigns it to the WebauthnRegister field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetWebauthnRegister(v string) { + o.WebauthnRegister = &v +} + +// GetWebauthnRegisterDisplayname returns the WebauthnRegisterDisplayname field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterDisplayname() string { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + var ret string + return ret + } + return *o.WebauthnRegisterDisplayname +} + +// GetWebauthnRegisterDisplaynameOk returns a tuple with the WebauthnRegisterDisplayname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterDisplaynameOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + return nil, false + } + return o.WebauthnRegisterDisplayname, true +} + +// HasWebauthnRegisterDisplayname returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasWebauthnRegisterDisplayname() bool { + if o != nil && !IsNil(o.WebauthnRegisterDisplayname) { + return true + } + + return false +} + +// SetWebauthnRegisterDisplayname gets a reference to the given string and assigns it to the WebauthnRegisterDisplayname field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetWebauthnRegisterDisplayname(v string) { + o.WebauthnRegisterDisplayname = &v +} + +func (o UpdateRegistrationFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnRegister) { + toSerialize["webauthn_register"] = o.WebauthnRegister + } + if !IsNil(o.WebauthnRegisterDisplayname) { + toSerialize["webauthn_register_displayname"] = o.WebauthnRegisterDisplayname + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithWebAuthnMethod := _UpdateRegistrationFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithWebAuthnMethod(varUpdateRegistrationFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_register") + delete(additionalProperties, "webauthn_register_displayname") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithWebAuthnMethod struct { + value *UpdateRegistrationFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) Get() *UpdateRegistrationFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) Set(val *UpdateRegistrationFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithWebAuthnMethod(val *UpdateRegistrationFlowWithWebAuthnMethod) *NullableUpdateRegistrationFlowWithWebAuthnMethod { + return &NullableUpdateRegistrationFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_body.go b/pkg/client-go/model_update_settings_flow_body.go new file mode 100644 index 0000000..a47984a --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_body.go @@ -0,0 +1,442 @@ +/* +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" + "fmt" +) + +// UpdateSettingsFlowBody - Update Settings Flow Request Body +type UpdateSettingsFlowBody struct { + UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod + UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod + UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod + UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod + UpdateSettingsFlowWithSamlMethod *UpdateSettingsFlowWithSamlMethod + UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod + UpdateSettingsFlowWithWebAuthnMethod *UpdateSettingsFlowWithWebAuthnMethod +} + +// UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithLookupMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithLookupMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithLookupMethod: v, + } +} + +// UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithOidcMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithOidcMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateSettingsFlowWithPasswordMethod: v, + } +} + +// UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithProfileMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithProfileMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithProfileMethod: v, + } +} + +// UpdateSettingsFlowWithSamlMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithSamlMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithSamlMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithSamlMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithSamlMethod: v, + } +} + +// UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithTotpMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithTotpMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithTotpMethod: v, + } +} + +// UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithWebAuthnMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithWebAuthnMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateSettingsFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithSamlMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateSettingsFlowWithSamlMethod' + if jsonDict["method"] == "updateSettingsFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithSamlMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithSamlMethod: %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 +func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateSettingsFlowWithLookupMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithLookupMethod) + } + + if src.UpdateSettingsFlowWithOidcMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) + } + + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + + if src.UpdateSettingsFlowWithPasswordMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) + } + + if src.UpdateSettingsFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithProfileMethod) + } + + if src.UpdateSettingsFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithSamlMethod) + } + + if src.UpdateSettingsFlowWithTotpMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithTotpMethod) + } + + if src.UpdateSettingsFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateSettingsFlowWithLookupMethod != nil { + return obj.UpdateSettingsFlowWithLookupMethod + } + + if obj.UpdateSettingsFlowWithOidcMethod != nil { + return obj.UpdateSettingsFlowWithOidcMethod + } + + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + + if obj.UpdateSettingsFlowWithPasswordMethod != nil { + return obj.UpdateSettingsFlowWithPasswordMethod + } + + if obj.UpdateSettingsFlowWithProfileMethod != nil { + return obj.UpdateSettingsFlowWithProfileMethod + } + + if obj.UpdateSettingsFlowWithSamlMethod != nil { + return obj.UpdateSettingsFlowWithSamlMethod + } + + if obj.UpdateSettingsFlowWithTotpMethod != nil { + return obj.UpdateSettingsFlowWithTotpMethod + } + + if obj.UpdateSettingsFlowWithWebAuthnMethod != nil { + return obj.UpdateSettingsFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateSettingsFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateSettingsFlowWithLookupMethod != nil { + return *obj.UpdateSettingsFlowWithLookupMethod + } + + if obj.UpdateSettingsFlowWithOidcMethod != nil { + return *obj.UpdateSettingsFlowWithOidcMethod + } + + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return *obj.UpdateSettingsFlowWithPasskeyMethod + } + + if obj.UpdateSettingsFlowWithPasswordMethod != nil { + return *obj.UpdateSettingsFlowWithPasswordMethod + } + + if obj.UpdateSettingsFlowWithProfileMethod != nil { + return *obj.UpdateSettingsFlowWithProfileMethod + } + + if obj.UpdateSettingsFlowWithSamlMethod != nil { + return *obj.UpdateSettingsFlowWithSamlMethod + } + + if obj.UpdateSettingsFlowWithTotpMethod != nil { + return *obj.UpdateSettingsFlowWithTotpMethod + } + + if obj.UpdateSettingsFlowWithWebAuthnMethod != nil { + return *obj.UpdateSettingsFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateSettingsFlowBody struct { + value *UpdateSettingsFlowBody + isSet bool +} + +func (v NullableUpdateSettingsFlowBody) Get() *UpdateSettingsFlowBody { + return v.value +} + +func (v *NullableUpdateSettingsFlowBody) Set(val *UpdateSettingsFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowBody(val *UpdateSettingsFlowBody) *NullableUpdateSettingsFlowBody { + return &NullableUpdateSettingsFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_lookup_method.go b/pkg/client-go/model_update_settings_flow_with_lookup_method.go new file mode 100644 index 0000000..8354fa0 --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_lookup_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithLookupMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithLookupMethod{} + +// UpdateSettingsFlowWithLookupMethod Update Settings Flow with Lookup Method +type UpdateSettingsFlowWithLookupMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // If set to true will save the regenerated lookup secrets + LookupSecretConfirm *bool `json:"lookup_secret_confirm,omitempty"` + // Disables this method if true. + LookupSecretDisable *bool `json:"lookup_secret_disable,omitempty"` + // If set to true will regenerate the lookup secrets + LookupSecretRegenerate *bool `json:"lookup_secret_regenerate,omitempty"` + // If set to true will reveal the lookup secrets + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithLookupMethod + +// NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod 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 NewUpdateSettingsFlowWithLookupMethod(method string) *UpdateSettingsFlowWithLookupMethod { + this := UpdateSettingsFlowWithLookupMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithLookupMethodWithDefaults instantiates a new UpdateSettingsFlowWithLookupMethod 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 NewUpdateSettingsFlowWithLookupMethodWithDefaults() *UpdateSettingsFlowWithLookupMethod { + this := UpdateSettingsFlowWithLookupMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithLookupMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithLookupMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetLookupSecretConfirm returns the LookupSecretConfirm field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretConfirm() bool { + if o == nil || IsNil(o.LookupSecretConfirm) { + var ret bool + return ret + } + return *o.LookupSecretConfirm +} + +// GetLookupSecretConfirmOk returns a tuple with the LookupSecretConfirm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretConfirmOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretConfirm) { + return nil, false + } + return o.LookupSecretConfirm, true +} + +// HasLookupSecretConfirm returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretConfirm() bool { + if o != nil && !IsNil(o.LookupSecretConfirm) { + return true + } + + return false +} + +// SetLookupSecretConfirm gets a reference to the given bool and assigns it to the LookupSecretConfirm field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretConfirm(v bool) { + o.LookupSecretConfirm = &v +} + +// GetLookupSecretDisable returns the LookupSecretDisable field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretDisable() bool { + if o == nil || IsNil(o.LookupSecretDisable) { + var ret bool + return ret + } + return *o.LookupSecretDisable +} + +// GetLookupSecretDisableOk returns a tuple with the LookupSecretDisable field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretDisableOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretDisable) { + return nil, false + } + return o.LookupSecretDisable, true +} + +// HasLookupSecretDisable returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretDisable() bool { + if o != nil && !IsNil(o.LookupSecretDisable) { + return true + } + + return false +} + +// SetLookupSecretDisable gets a reference to the given bool and assigns it to the LookupSecretDisable field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretDisable(v bool) { + o.LookupSecretDisable = &v +} + +// GetLookupSecretRegenerate returns the LookupSecretRegenerate field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRegenerate() bool { + if o == nil || IsNil(o.LookupSecretRegenerate) { + var ret bool + return ret + } + return *o.LookupSecretRegenerate +} + +// GetLookupSecretRegenerateOk returns a tuple with the LookupSecretRegenerate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRegenerateOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretRegenerate) { + return nil, false + } + return o.LookupSecretRegenerate, true +} + +// HasLookupSecretRegenerate returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretRegenerate() bool { + if o != nil && !IsNil(o.LookupSecretRegenerate) { + return true + } + + return false +} + +// SetLookupSecretRegenerate gets a reference to the given bool and assigns it to the LookupSecretRegenerate field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretRegenerate(v bool) { + o.LookupSecretRegenerate = &v +} + +// GetLookupSecretReveal returns the LookupSecretReveal field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretReveal() bool { + if o == nil || IsNil(o.LookupSecretReveal) { + var ret bool + return ret + } + return *o.LookupSecretReveal +} + +// GetLookupSecretRevealOk returns a tuple with the LookupSecretReveal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRevealOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretReveal) { + return nil, false + } + return o.LookupSecretReveal, true +} + +// HasLookupSecretReveal returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretReveal() bool { + if o != nil && !IsNil(o.LookupSecretReveal) { + return true + } + + return false +} + +// SetLookupSecretReveal gets a reference to the given bool and assigns it to the LookupSecretReveal field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretReveal(v bool) { + o.LookupSecretReveal = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithLookupMethod) 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 *UpdateSettingsFlowWithLookupMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithLookupMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.LookupSecretConfirm) { + toSerialize["lookup_secret_confirm"] = o.LookupSecretConfirm + } + if !IsNil(o.LookupSecretDisable) { + toSerialize["lookup_secret_disable"] = o.LookupSecretDisable + } + if !IsNil(o.LookupSecretRegenerate) { + toSerialize["lookup_secret_regenerate"] = o.LookupSecretRegenerate + } + if !IsNil(o.LookupSecretReveal) { + toSerialize["lookup_secret_reveal"] = o.LookupSecretReveal + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithLookupMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithLookupMethod := _UpdateSettingsFlowWithLookupMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithLookupMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithLookupMethod(varUpdateSettingsFlowWithLookupMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "lookup_secret_confirm") + delete(additionalProperties, "lookup_secret_disable") + delete(additionalProperties, "lookup_secret_regenerate") + delete(additionalProperties, "lookup_secret_reveal") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithLookupMethod struct { + value *UpdateSettingsFlowWithLookupMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) Get() *UpdateSettingsFlowWithLookupMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) Set(val *UpdateSettingsFlowWithLookupMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithLookupMethod(val *UpdateSettingsFlowWithLookupMethod) *NullableUpdateSettingsFlowWithLookupMethod { + return &NullableUpdateSettingsFlowWithLookupMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_oidc_method.go b/pkg/client-go/model_update_settings_flow_with_oidc_method.go new file mode 100644 index 0000000..1e8cc28 --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_oidc_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithOidcMethod{} + +// UpdateSettingsFlowWithOidcMethod Update Settings Flow with OpenID Connect Method +type UpdateSettingsFlowWithOidcMethod struct { + // Flow ID is the flow's ID. in: query + Flow *string `json:"flow,omitempty"` + // Link this provider Either this or `unlink` must be set. type: string in: body + Link *string `json:"link,omitempty"` + // Method Should be set to profile when trying to update a profile. + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithOidcMethod UpdateSettingsFlowWithOidcMethod + +// NewUpdateSettingsFlowWithOidcMethod instantiates a new UpdateSettingsFlowWithOidcMethod 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 NewUpdateSettingsFlowWithOidcMethod(method string) *UpdateSettingsFlowWithOidcMethod { + this := UpdateSettingsFlowWithOidcMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithOidcMethodWithDefaults instantiates a new UpdateSettingsFlowWithOidcMethod 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 NewUpdateSettingsFlowWithOidcMethodWithDefaults() *UpdateSettingsFlowWithOidcMethod { + this := UpdateSettingsFlowWithOidcMethod{} + return &this +} + +// GetFlow returns the Flow field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetFlow() string { + if o == nil || IsNil(o.Flow) { + var ret string + return ret + } + return *o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetFlowOk() (*string, bool) { + if o == nil || IsNil(o.Flow) { + return nil, false + } + return o.Flow, true +} + +// HasFlow returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasFlow() bool { + if o != nil && !IsNil(o.Flow) { + return true + } + + return false +} + +// SetFlow gets a reference to the given string and assigns it to the Flow field. +func (o *UpdateSettingsFlowWithOidcMethod) SetFlow(v string) { + o.Flow = &v +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *UpdateSettingsFlowWithOidcMethod) SetLink(v string) { + o.Link = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithOidcMethod) 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 *UpdateSettingsFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.Unlink) { + var ret string + return ret + } + return *o.Unlink +} + +// GetUnlinkOk returns a tuple with the Unlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetUnlinkOk() (*string, bool) { + if o == nil || IsNil(o.Unlink) { + return nil, false + } + return o.Unlink, true +} + +// HasUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasUnlink() bool { + if o != nil && !IsNil(o.Unlink) { + return true + } + + return false +} + +// SetUnlink gets a reference to the given string and assigns it to the Unlink field. +func (o *UpdateSettingsFlowWithOidcMethod) SetUnlink(v string) { + o.Unlink = &v +} + +// GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { + if o == nil || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateSettingsFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Flow) { + toSerialize["flow"] = o.Flow + } + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + toSerialize["method"] = o.Method + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.Unlink) { + toSerialize["unlink"] = o.Unlink + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithOidcMethod := _UpdateSettingsFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithOidcMethod(varUpdateSettingsFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "flow") + delete(additionalProperties, "link") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "unlink") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithOidcMethod struct { + value *UpdateSettingsFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) Get() *UpdateSettingsFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) Set(val *UpdateSettingsFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithOidcMethod(val *UpdateSettingsFlowWithOidcMethod) *NullableUpdateSettingsFlowWithOidcMethod { + return &NullableUpdateSettingsFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_passkey_method.go b/pkg/client-go/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 0000000..1e67672 --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyRemove) { + 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 || IsNil(o.PasskeyRemove) { + 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 && !IsNil(o.PasskeyRemove) { + 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 || IsNil(o.PasskeySettingsRegister) { + 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 || IsNil(o.PasskeySettingsRegister) { + 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 && !IsNil(o.PasskeySettingsRegister) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyRemove) { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if !IsNil(o.PasskeySettingsRegister) { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithPasskeyMethod := _UpdateSettingsFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithPasskeyMethod(varUpdateSettingsFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_remove") + delete(additionalProperties, "passkey_settings_register") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/client-go/model_update_settings_flow_with_password_method.go b/pkg/client-go/model_update_settings_flow_with_password_method.go new file mode 100644 index 0000000..1ecc2cd --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_password_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithPasswordMethod{} + +// UpdateSettingsFlowWithPasswordMethod Update Settings Flow with Password Method +type UpdateSettingsFlowWithPasswordMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to password when trying to update a password. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithPasswordMethod + +// NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod 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 NewUpdateSettingsFlowWithPasswordMethod(method string, password string) *UpdateSettingsFlowWithPasswordMethod { + this := UpdateSettingsFlowWithPasswordMethod{} + this.Method = method + this.Password = password + return &this +} + +// NewUpdateSettingsFlowWithPasswordMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasswordMethod 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 NewUpdateSettingsFlowWithPasswordMethodWithDefaults() *UpdateSettingsFlowWithPasswordMethod { + this := UpdateSettingsFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasswordMethod) 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 *UpdateSettingsFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateSettingsFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "password", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithPasswordMethod := _UpdateSettingsFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithPasswordMethod(varUpdateSettingsFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithPasswordMethod struct { + value *UpdateSettingsFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) Get() *UpdateSettingsFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) Set(val *UpdateSettingsFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasswordMethod(val *UpdateSettingsFlowWithPasswordMethod) *NullableUpdateSettingsFlowWithPasswordMethod { + return &NullableUpdateSettingsFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_profile_method.go b/pkg/client-go/model_update_settings_flow_with_profile_method.go new file mode 100644 index 0000000..14d33eb --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_profile_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithProfileMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithProfileMethod{} + +// UpdateSettingsFlowWithProfileMethod Update Settings Flow with Profile Method +type UpdateSettingsFlowWithProfileMethod 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"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithProfileMethod + +// NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod 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 NewUpdateSettingsFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateSettingsFlowWithProfileMethod { + this := UpdateSettingsFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateSettingsFlowWithProfileMethodWithDefaults instantiates a new UpdateSettingsFlowWithProfileMethod 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 NewUpdateSettingsFlowWithProfileMethodWithDefaults() *UpdateSettingsFlowWithProfileMethod { + this := UpdateSettingsFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithProfileMethod) 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 *UpdateSettingsFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *UpdateSettingsFlowWithProfileMethod) 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 *UpdateSettingsFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithProfileMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithProfileMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithProfileMethod := _UpdateSettingsFlowWithProfileMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithProfileMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithProfileMethod(varUpdateSettingsFlowWithProfileMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithProfileMethod struct { + value *UpdateSettingsFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) Get() *UpdateSettingsFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) Set(val *UpdateSettingsFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithProfileMethod(val *UpdateSettingsFlowWithProfileMethod) *NullableUpdateSettingsFlowWithProfileMethod { + return &NullableUpdateSettingsFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_saml_method.go b/pkg/client-go/model_update_settings_flow_with_saml_method.go new file mode 100644 index 0000000..063117a --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_saml_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithSamlMethod{} + +// UpdateSettingsFlowWithSamlMethod Update settings flow using SAML +type UpdateSettingsFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Flow ID is the flow's ID. in: query + Flow *string `json:"flow,omitempty"` + // Link this provider Either this or `unlink` must be set. type: string in: body + Link *string `json:"link,omitempty"` + // Method Should be set to saml when trying to update a profile. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithSamlMethod UpdateSettingsFlowWithSamlMethod + +// NewUpdateSettingsFlowWithSamlMethod instantiates a new UpdateSettingsFlowWithSamlMethod 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 NewUpdateSettingsFlowWithSamlMethod(method string) *UpdateSettingsFlowWithSamlMethod { + this := UpdateSettingsFlowWithSamlMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithSamlMethodWithDefaults instantiates a new UpdateSettingsFlowWithSamlMethod 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 NewUpdateSettingsFlowWithSamlMethodWithDefaults() *UpdateSettingsFlowWithSamlMethod { + this := UpdateSettingsFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetFlow returns the Flow field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetFlow() string { + if o == nil || IsNil(o.Flow) { + var ret string + return ret + } + return *o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetFlowOk() (*string, bool) { + if o == nil || IsNil(o.Flow) { + return nil, false + } + return o.Flow, true +} + +// HasFlow returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasFlow() bool { + if o != nil && !IsNil(o.Flow) { + return true + } + + return false +} + +// SetFlow gets a reference to the given string and assigns it to the Flow field. +func (o *UpdateSettingsFlowWithSamlMethod) SetFlow(v string) { + o.Flow = &v +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *UpdateSettingsFlowWithSamlMethod) SetLink(v string) { + o.Link = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithSamlMethod) 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 *UpdateSettingsFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateSettingsFlowWithSamlMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateSettingsFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetUnlink returns the Unlink field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetUnlink() string { + if o == nil || IsNil(o.Unlink) { + var ret string + return ret + } + return *o.Unlink +} + +// GetUnlinkOk returns a tuple with the Unlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetUnlinkOk() (*string, bool) { + if o == nil || IsNil(o.Unlink) { + return nil, false + } + return o.Unlink, true +} + +// HasUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasUnlink() bool { + if o != nil && !IsNil(o.Unlink) { + return true + } + + return false +} + +// SetUnlink gets a reference to the given string and assigns it to the Unlink field. +func (o *UpdateSettingsFlowWithSamlMethod) SetUnlink(v string) { + o.Unlink = &v +} + +func (o UpdateSettingsFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Flow) { + toSerialize["flow"] = o.Flow + } + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + toSerialize["method"] = o.Method + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.Unlink) { + toSerialize["unlink"] = o.Unlink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithSamlMethod := _UpdateSettingsFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithSamlMethod(varUpdateSettingsFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "flow") + delete(additionalProperties, "link") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "unlink") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithSamlMethod struct { + value *UpdateSettingsFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) Get() *UpdateSettingsFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) Set(val *UpdateSettingsFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithSamlMethod(val *UpdateSettingsFlowWithSamlMethod) *NullableUpdateSettingsFlowWithSamlMethod { + return &NullableUpdateSettingsFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_totp_method.go b/pkg/client-go/model_update_settings_flow_with_totp_method.go new file mode 100644 index 0000000..0e77ab4 --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_totp_method.go @@ -0,0 +1,320 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithTotpMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithTotpMethod{} + +// UpdateSettingsFlowWithTotpMethod Update Settings Flow with TOTP Method +type UpdateSettingsFlowWithTotpMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"totp\" when trying to add, update, or remove a totp pairing. + Method string `json:"method"` + // ValidationTOTP must contain a valid TOTP based on the + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithTotpMethod UpdateSettingsFlowWithTotpMethod + +// NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod 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 NewUpdateSettingsFlowWithTotpMethod(method string) *UpdateSettingsFlowWithTotpMethod { + this := UpdateSettingsFlowWithTotpMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithTotpMethodWithDefaults instantiates a new UpdateSettingsFlowWithTotpMethod 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 NewUpdateSettingsFlowWithTotpMethodWithDefaults() *UpdateSettingsFlowWithTotpMethod { + this := UpdateSettingsFlowWithTotpMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithTotpMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithTotpMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithTotpMethod) 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 *UpdateSettingsFlowWithTotpMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithTotpMethod) SetMethod(v string) { + o.Method = v +} + +// GetTotpCode returns the TotpCode field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpCode() string { + if o == nil || IsNil(o.TotpCode) { + var ret string + return ret + } + return *o.TotpCode +} + +// GetTotpCodeOk returns a tuple with the TotpCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpCodeOk() (*string, bool) { + if o == nil || IsNil(o.TotpCode) { + return nil, false + } + return o.TotpCode, true +} + +// HasTotpCode returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTotpCode() bool { + if o != nil && !IsNil(o.TotpCode) { + return true + } + + return false +} + +// SetTotpCode gets a reference to the given string and assigns it to the TotpCode field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTotpCode(v string) { + o.TotpCode = &v +} + +// GetTotpUnlink returns the TotpUnlink field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpUnlink() bool { + if o == nil || IsNil(o.TotpUnlink) { + var ret bool + return ret + } + return *o.TotpUnlink +} + +// GetTotpUnlinkOk returns a tuple with the TotpUnlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpUnlinkOk() (*bool, bool) { + if o == nil || IsNil(o.TotpUnlink) { + return nil, false + } + return o.TotpUnlink, true +} + +// HasTotpUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTotpUnlink() bool { + if o != nil && !IsNil(o.TotpUnlink) { + return true + } + + return false +} + +// SetTotpUnlink gets a reference to the given bool and assigns it to the TotpUnlink field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithTotpMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.TotpCode) { + toSerialize["totp_code"] = o.TotpCode + } + if !IsNil(o.TotpUnlink) { + toSerialize["totp_unlink"] = o.TotpUnlink + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithTotpMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithTotpMethod := _UpdateSettingsFlowWithTotpMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithTotpMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithTotpMethod(varUpdateSettingsFlowWithTotpMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "totp_code") + delete(additionalProperties, "totp_unlink") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithTotpMethod struct { + value *UpdateSettingsFlowWithTotpMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) Get() *UpdateSettingsFlowWithTotpMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) Set(val *UpdateSettingsFlowWithTotpMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithTotpMethod(val *UpdateSettingsFlowWithTotpMethod) *NullableUpdateSettingsFlowWithTotpMethod { + return &NullableUpdateSettingsFlowWithTotpMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_settings_flow_with_web_authn_method.go b/pkg/client-go/model_update_settings_flow_with_web_authn_method.go new file mode 100644 index 0000000..549b2e8 --- /dev/null +++ b/pkg/client-go/model_update_settings_flow_with_web_authn_method.go @@ -0,0 +1,358 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithWebAuthnMethod{} + +// UpdateSettingsFlowWithWebAuthnMethod Update Settings Flow with WebAuthn Method +type UpdateSettingsFlowWithWebAuthnMethod struct { + // CSRFToken is the anti-CSRF token + 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. + WebauthnRegisterDisplayname *string `json:"webauthn_register_displayname,omitempty"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + WebauthnRemove *string `json:"webauthn_remove,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithWebAuthnMethod UpdateSettingsFlowWithWebAuthnMethod + +// NewUpdateSettingsFlowWithWebAuthnMethod instantiates a new UpdateSettingsFlowWithWebAuthnMethod 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 NewUpdateSettingsFlowWithWebAuthnMethod(method string) *UpdateSettingsFlowWithWebAuthnMethod { + this := UpdateSettingsFlowWithWebAuthnMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateSettingsFlowWithWebAuthnMethod 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 NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults() *UpdateSettingsFlowWithWebAuthnMethod { + this := UpdateSettingsFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithWebAuthnMethod) 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 *UpdateSettingsFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.WebauthnRegister) { + var ret string + return ret + } + return *o.WebauthnRegister +} + +// GetWebauthnRegisterOk returns a tuple with the WebauthnRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegister) { + return nil, false + } + return o.WebauthnRegister, true +} + +// HasWebauthnRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRegister() bool { + if o != nil && !IsNil(o.WebauthnRegister) { + return true + } + + return false +} + +// SetWebauthnRegister gets a reference to the given string and assigns it to the WebauthnRegister field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRegister(v string) { + o.WebauthnRegister = &v +} + +// GetWebauthnRegisterDisplayname returns the WebauthnRegisterDisplayname field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterDisplayname() string { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + var ret string + return ret + } + return *o.WebauthnRegisterDisplayname +} + +// GetWebauthnRegisterDisplaynameOk returns a tuple with the WebauthnRegisterDisplayname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterDisplaynameOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + return nil, false + } + return o.WebauthnRegisterDisplayname, true +} + +// HasWebauthnRegisterDisplayname returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRegisterDisplayname() bool { + if o != nil && !IsNil(o.WebauthnRegisterDisplayname) { + return true + } + + return false +} + +// SetWebauthnRegisterDisplayname gets a reference to the given string and assigns it to the WebauthnRegisterDisplayname field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRegisterDisplayname(v string) { + o.WebauthnRegisterDisplayname = &v +} + +// GetWebauthnRemove returns the WebauthnRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRemove() string { + if o == nil || IsNil(o.WebauthnRemove) { + var ret string + return ret + } + return *o.WebauthnRemove +} + +// GetWebauthnRemoveOk returns a tuple with the WebauthnRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRemoveOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRemove) { + return nil, false + } + return o.WebauthnRemove, true +} + +// HasWebauthnRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRemove() bool { + if o != nil && !IsNil(o.WebauthnRemove) { + return true + } + + return false +} + +// SetWebauthnRemove gets a reference to the given string and assigns it to the WebauthnRemove field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRemove(v string) { + o.WebauthnRemove = &v +} + +func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnRegister) { + toSerialize["webauthn_register"] = o.WebauthnRegister + } + if !IsNil(o.WebauthnRegisterDisplayname) { + toSerialize["webauthn_register_displayname"] = o.WebauthnRegisterDisplayname + } + if !IsNil(o.WebauthnRemove) { + toSerialize["webauthn_remove"] = o.WebauthnRemove + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithWebAuthnMethod := _UpdateSettingsFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithWebAuthnMethod(varUpdateSettingsFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_register") + delete(additionalProperties, "webauthn_register_displayname") + delete(additionalProperties, "webauthn_remove") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithWebAuthnMethod struct { + value *UpdateSettingsFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) Get() *UpdateSettingsFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) Set(val *UpdateSettingsFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithWebAuthnMethod(val *UpdateSettingsFlowWithWebAuthnMethod) *NullableUpdateSettingsFlowWithWebAuthnMethod { + return &NullableUpdateSettingsFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_verification_flow_body.go b/pkg/client-go/model_update_verification_flow_body.go new file mode 100644 index 0000000..84f0e40 --- /dev/null +++ b/pkg/client-go/model_update_verification_flow_body.go @@ -0,0 +1,178 @@ +/* +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" + "fmt" +) + +// UpdateVerificationFlowBody - Update Verification Flow Request Body +type UpdateVerificationFlowBody struct { + UpdateVerificationFlowWithCodeMethod *UpdateVerificationFlowWithCodeMethod + UpdateVerificationFlowWithLinkMethod *UpdateVerificationFlowWithLinkMethod +} + +// UpdateVerificationFlowWithCodeMethodAsUpdateVerificationFlowBody is a convenience function that returns UpdateVerificationFlowWithCodeMethod wrapped in UpdateVerificationFlowBody +func UpdateVerificationFlowWithCodeMethodAsUpdateVerificationFlowBody(v *UpdateVerificationFlowWithCodeMethod) UpdateVerificationFlowBody { + return UpdateVerificationFlowBody{ + UpdateVerificationFlowWithCodeMethod: v, + } +} + +// UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody is a convenience function that returns UpdateVerificationFlowWithLinkMethod wrapped in UpdateVerificationFlowBody +func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateVerificationFlowWithLinkMethod) UpdateVerificationFlowBody { + return UpdateVerificationFlowBody{ + UpdateVerificationFlowWithLinkMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UpdateVerificationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateVerificationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateVerificationFlowWithCodeMethod) + } + + if src.UpdateVerificationFlowWithLinkMethod != nil { + return json.Marshal(&src.UpdateVerificationFlowWithLinkMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateVerificationFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateVerificationFlowWithCodeMethod != nil { + return obj.UpdateVerificationFlowWithCodeMethod + } + + if obj.UpdateVerificationFlowWithLinkMethod != nil { + return obj.UpdateVerificationFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateVerificationFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateVerificationFlowWithCodeMethod != nil { + return *obj.UpdateVerificationFlowWithCodeMethod + } + + if obj.UpdateVerificationFlowWithLinkMethod != nil { + return *obj.UpdateVerificationFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateVerificationFlowBody struct { + value *UpdateVerificationFlowBody + isSet bool +} + +func (v NullableUpdateVerificationFlowBody) Get() *UpdateVerificationFlowBody { + return v.value +} + +func (v *NullableUpdateVerificationFlowBody) Set(val *UpdateVerificationFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowBody(val *UpdateVerificationFlowBody) *NullableUpdateVerificationFlowBody { + return &NullableUpdateVerificationFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_verification_flow_with_code_method.go b/pkg/client-go/model_update_verification_flow_with_code_method.go new file mode 100644 index 0000000..63d9008 --- /dev/null +++ b/pkg/client-go/model_update_verification_flow_with_code_method.go @@ -0,0 +1,320 @@ +/* +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" + "fmt" +) + +// checks if the UpdateVerificationFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateVerificationFlowWithCodeMethod{} + +// UpdateVerificationFlowWithCodeMethod struct for UpdateVerificationFlowWithCodeMethod +type UpdateVerificationFlowWithCodeMethod struct { + // Code from the recovery email If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The email address or phone number to verify If the provided address belongs to a valid account, a verification email or SMS will be sent. If you want to notify the email address if the account does not exist, see the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation#attempted-verification-notifications) If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateVerificationFlowWithCodeMethod UpdateVerificationFlowWithCodeMethod + +// NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod 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 NewUpdateVerificationFlowWithCodeMethod(method string) *UpdateVerificationFlowWithCodeMethod { + this := UpdateVerificationFlowWithCodeMethod{} + this.Method = method + return &this +} + +// NewUpdateVerificationFlowWithCodeMethodWithDefaults instantiates a new UpdateVerificationFlowWithCodeMethod 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 NewUpdateVerificationFlowWithCodeMethodWithDefaults() *UpdateVerificationFlowWithCodeMethod { + this := UpdateVerificationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateVerificationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateVerificationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateVerificationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetEmail() string { + if o == nil || IsNil(o.Email) { + var ret string + return ret + } + return *o.Email +} + +// GetEmailOk returns a tuple with the Email field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetEmailOk() (*string, bool) { + if o == nil || IsNil(o.Email) { + return nil, false + } + return o.Email, true +} + +// HasEmail returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasEmail() bool { + if o != nil && !IsNil(o.Email) { + return true + } + + return false +} + +// SetEmail gets a reference to the given string and assigns it to the Email field. +func (o *UpdateVerificationFlowWithCodeMethod) SetEmail(v string) { + o.Email = &v +} + +// GetMethod returns the Method field value +func (o *UpdateVerificationFlowWithCodeMethod) 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 *UpdateVerificationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateVerificationFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Email) { + toSerialize["email"] = o.Email + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateVerificationFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateVerificationFlowWithCodeMethod := _UpdateVerificationFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateVerificationFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateVerificationFlowWithCodeMethod(varUpdateVerificationFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateVerificationFlowWithCodeMethod struct { + value *UpdateVerificationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) Get() *UpdateVerificationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) Set(val *UpdateVerificationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowWithCodeMethod(val *UpdateVerificationFlowWithCodeMethod) *NullableUpdateVerificationFlowWithCodeMethod { + return &NullableUpdateVerificationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_update_verification_flow_with_link_method.go b/pkg/client-go/model_update_verification_flow_with_link_method.go new file mode 100644 index 0000000..aed4593 --- /dev/null +++ b/pkg/client-go/model_update_verification_flow_with_link_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateVerificationFlowWithLinkMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateVerificationFlowWithLinkMethod{} + +// UpdateVerificationFlowWithLinkMethod Update Verification Flow with Link Method +type UpdateVerificationFlowWithLinkMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Email to Verify Needs to be set when initiating the flow. If the email is a registered verification email, a verification link will be sent. If the email is not known, a email with details on what happened will be sent instead. format: email + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateVerificationFlowWithLinkMethod UpdateVerificationFlowWithLinkMethod + +// NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod 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 NewUpdateVerificationFlowWithLinkMethod(email string, method string) *UpdateVerificationFlowWithLinkMethod { + this := UpdateVerificationFlowWithLinkMethod{} + this.Email = email + this.Method = method + return &this +} + +// NewUpdateVerificationFlowWithLinkMethodWithDefaults instantiates a new UpdateVerificationFlowWithLinkMethod 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 NewUpdateVerificationFlowWithLinkMethodWithDefaults() *UpdateVerificationFlowWithLinkMethod { + this := UpdateVerificationFlowWithLinkMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateVerificationFlowWithLinkMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateVerificationFlowWithLinkMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value +func (o *UpdateVerificationFlowWithLinkMethod) GetEmail() string { + if o == nil { + var ret string + return ret + } + + return o.Email +} + +// GetEmailOk returns a tuple with the Email field value +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetEmailOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Email, true +} + +// SetEmail sets field value +func (o *UpdateVerificationFlowWithLinkMethod) SetEmail(v string) { + o.Email = v +} + +// GetMethod returns the Method field value +func (o *UpdateVerificationFlowWithLinkMethod) 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 *UpdateVerificationFlowWithLinkMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateVerificationFlowWithLinkMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["email"] = o.Email + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateVerificationFlowWithLinkMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "email", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateVerificationFlowWithLinkMethod := _UpdateVerificationFlowWithLinkMethod{} + + err = json.Unmarshal(data, &varUpdateVerificationFlowWithLinkMethod) + + if err != nil { + return err + } + + *o = UpdateVerificationFlowWithLinkMethod(varUpdateVerificationFlowWithLinkMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateVerificationFlowWithLinkMethod struct { + value *UpdateVerificationFlowWithLinkMethod + isSet bool +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) Get() *UpdateVerificationFlowWithLinkMethod { + return v.value +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) Set(val *UpdateVerificationFlowWithLinkMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowWithLinkMethod(val *UpdateVerificationFlowWithLinkMethod) *NullableUpdateVerificationFlowWithLinkMethod { + return &NullableUpdateVerificationFlowWithLinkMethod{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_user_verification.go b/pkg/client-go/model_user_verification.go new file mode 100644 index 0000000..289a085 --- /dev/null +++ b/pkg/client-go/model_user_verification.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// UserVerification \"pin\" means the holder proves knowledge of a PIN via pin_proof at login; \"platform\" means the device gates key use behind platform biometrics or an equivalent lock screen; \"none\" marks a possession-only key, usable as a second factor but never as the sole first factor. The empty value marks a legacy key from before user verification existed; such keys are rejected at login and must be re-enrolled. +type UserVerification string + +// List of UserVerification +const ( + USERVERIFICATION_PIN UserVerification = "pin" + USERVERIFICATION_PLATFORM UserVerification = "platform" + USERVERIFICATION_NONE UserVerification = "none" +) + +// All allowed values of UserVerification enum +var AllowedUserVerificationEnumValues = []UserVerification{ + "pin", + "platform", + "none", +} + +func (v *UserVerification) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := UserVerification(value) + for _, existing := range AllowedUserVerificationEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid UserVerification", value) +} + +// NewUserVerificationFromValue returns a pointer to a valid UserVerification +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewUserVerificationFromValue(v string) (*UserVerification, error) { + ev := UserVerification(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for UserVerification: valid values are %v", v, AllowedUserVerificationEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v UserVerification) IsValid() bool { + for _, existing := range AllowedUserVerificationEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to UserVerification value +func (v UserVerification) Ptr() *UserVerification { + return &v +} + +type NullableUserVerification struct { + value *UserVerification + isSet bool +} + +func (v NullableUserVerification) Get() *UserVerification { + return v.value +} + +func (v *NullableUserVerification) Set(val *UserVerification) { + v.value = val + v.isSet = true +} + +func (v NullableUserVerification) IsSet() bool { + return v.isSet +} + +func (v *NullableUserVerification) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUserVerification(val *UserVerification) *NullableUserVerification { + return &NullableUserVerification{value: val, isSet: true} +} + +func (v NullableUserVerification) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUserVerification) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_verifiable_identity_address.go b/pkg/client-go/model_verifiable_identity_address.go new file mode 100644 index 0000000..d51bc64 --- /dev/null +++ b/pkg/client-go/model_verifiable_identity_address.go @@ -0,0 +1,410 @@ +/* +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" + "fmt" + "time" +) + +// checks if the VerifiableIdentityAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerifiableIdentityAddress{} + +// VerifiableIdentityAddress VerifiableAddress is an identity's verifiable address +type VerifiableIdentityAddress struct { + // When this entry was created + CreatedAt *time.Time `json:"created_at,omitempty"` + // The ID + Id *string `json:"id,omitempty"` + // VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema + Status string `json:"status"` + // When this entry was last updated + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // The address value example foo@user.com + Value string `json:"value"` + // Indicates if the address has already been verified + Verified bool `json:"verified"` + VerifiedAt *time.Time `json:"verified_at,omitempty"` + // The delivery method + Via string `json:"via"` + AdditionalProperties map[string]interface{} +} + +type _VerifiableIdentityAddress VerifiableIdentityAddress + +// NewVerifiableIdentityAddress instantiates a new VerifiableIdentityAddress 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 NewVerifiableIdentityAddress(status string, value string, verified bool, via string) *VerifiableIdentityAddress { + this := VerifiableIdentityAddress{} + this.Status = status + this.Value = value + this.Verified = verified + this.Via = via + return &this +} + +// NewVerifiableIdentityAddressWithDefaults instantiates a new VerifiableIdentityAddress 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 NewVerifiableIdentityAddressWithDefaults() *VerifiableIdentityAddress { + this := VerifiableIdentityAddress{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *VerifiableIdentityAddress) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *VerifiableIdentityAddress) SetId(v string) { + o.Id = &v +} + +// GetStatus returns the Status field value +func (o *VerifiableIdentityAddress) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *VerifiableIdentityAddress) SetStatus(v string) { + o.Status = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *VerifiableIdentityAddress) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetValue returns the Value field value +func (o *VerifiableIdentityAddress) GetValue() string { + if o == nil { + var ret string + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetValueOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *VerifiableIdentityAddress) SetValue(v string) { + o.Value = v +} + +// GetVerified returns the Verified field value +func (o *VerifiableIdentityAddress) GetVerified() bool { + if o == nil { + var ret bool + return ret + } + + return o.Verified +} + +// GetVerifiedOk returns a tuple with the Verified field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetVerifiedOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Verified, true +} + +// SetVerified sets field value +func (o *VerifiableIdentityAddress) SetVerified(v bool) { + o.Verified = v +} + +// GetVerifiedAt returns the VerifiedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetVerifiedAt() time.Time { + if o == nil || IsNil(o.VerifiedAt) { + var ret time.Time + return ret + } + return *o.VerifiedAt +} + +// GetVerifiedAtOk returns a tuple with the VerifiedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetVerifiedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.VerifiedAt) { + return nil, false + } + return o.VerifiedAt, true +} + +// HasVerifiedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasVerifiedAt() bool { + if o != nil && !IsNil(o.VerifiedAt) { + return true + } + + return false +} + +// SetVerifiedAt gets a reference to the given time.Time and assigns it to the VerifiedAt field. +func (o *VerifiableIdentityAddress) SetVerifiedAt(v time.Time) { + o.VerifiedAt = &v +} + +// GetVia returns the Via field value +func (o *VerifiableIdentityAddress) GetVia() string { + if o == nil { + var ret string + return ret + } + + return o.Via +} + +// GetViaOk returns a tuple with the Via field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetViaOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Via, true +} + +// SetVia sets field value +func (o *VerifiableIdentityAddress) SetVia(v string) { + o.Via = v +} + +func (o VerifiableIdentityAddress) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerifiableIdentityAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + toSerialize["status"] = o.Status + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["value"] = o.Value + toSerialize["verified"] = o.Verified + if !IsNil(o.VerifiedAt) { + toSerialize["verified_at"] = o.VerifiedAt + } + toSerialize["via"] = o.Via + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *VerifiableIdentityAddress) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "status", + "value", + "verified", + "via", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varVerifiableIdentityAddress := _VerifiableIdentityAddress{} + + err = json.Unmarshal(data, &varVerifiableIdentityAddress) + + if err != nil { + return err + } + + *o = VerifiableIdentityAddress(varVerifiableIdentityAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "id") + delete(additionalProperties, "status") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "value") + delete(additionalProperties, "verified") + delete(additionalProperties, "verified_at") + delete(additionalProperties, "via") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVerifiableIdentityAddress struct { + value *VerifiableIdentityAddress + isSet bool +} + +func (v NullableVerifiableIdentityAddress) Get() *VerifiableIdentityAddress { + return v.value +} + +func (v *NullableVerifiableIdentityAddress) Set(val *VerifiableIdentityAddress) { + v.value = val + v.isSet = true +} + +func (v NullableVerifiableIdentityAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableVerifiableIdentityAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerifiableIdentityAddress(val *VerifiableIdentityAddress) *NullableVerifiableIdentityAddress { + return &NullableVerifiableIdentityAddress{value: val, isSet: true} +} + +func (v NullableVerifiableIdentityAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerifiableIdentityAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_verification_flow.go b/pkg/client-go/model_verification_flow.go new file mode 100644 index 0000000..b70dca4 --- /dev/null +++ b/pkg/client-go/model_verification_flow.go @@ -0,0 +1,490 @@ +/* +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" + "fmt" + "time" +) + +// checks if the VerificationFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerificationFlow{} + +// VerificationFlow Used to verify an out-of-band communication channel such as an email address or a phone number. For more information head over to: https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation +type VerificationFlow struct { + // Active, if set, contains the registration method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address, a new request has to be initiated. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // ID represents the request's unique ID. When performing the verification flow, this represents the id in the verify ui's query parameter: http://?request= type: string format: uuid + Id string `json:"id"` + // IssuedAt is the time (UTC) when the request occurred. + IssuedAt *time.Time `json:"issued_at,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl *string `json:"request_url,omitempty"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _VerificationFlow VerificationFlow + +// NewVerificationFlow instantiates a new VerificationFlow 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 NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { + this := VerificationFlow{} + this.Id = id + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewVerificationFlowWithDefaults instantiates a new VerificationFlow 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 NewVerificationFlowWithDefaults() *VerificationFlow { + this := VerificationFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *VerificationFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *VerificationFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *VerificationFlow) SetActive(v string) { + o.Active = &v +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *VerificationFlow) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *VerificationFlow) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *VerificationFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetId returns the Id field value +func (o *VerificationFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *VerificationFlow) SetId(v string) { + o.Id = v +} + +// GetIssuedAt returns the IssuedAt field value if set, zero value otherwise. +func (o *VerificationFlow) GetIssuedAt() time.Time { + if o == nil || IsNil(o.IssuedAt) { + var ret time.Time + return ret + } + return *o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.IssuedAt) { + return nil, false + } + return o.IssuedAt, true +} + +// HasIssuedAt returns a boolean if a field has been set. +func (o *VerificationFlow) HasIssuedAt() bool { + if o != nil && !IsNil(o.IssuedAt) { + return true + } + + return false +} + +// SetIssuedAt gets a reference to the given time.Time and assigns it to the IssuedAt field. +func (o *VerificationFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *VerificationFlow) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *VerificationFlow) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *VerificationFlow) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *VerificationFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *VerificationFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *VerificationFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *VerificationFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *VerificationFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *VerificationFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o VerificationFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerificationFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["id"] = o.Id + if !IsNil(o.IssuedAt) { + toSerialize["issued_at"] = o.IssuedAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *VerificationFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varVerificationFlow := _VerificationFlow{} + + err = json.Unmarshal(data, &varVerificationFlow) + + if err != nil { + return err + } + + *o = VerificationFlow(varVerificationFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVerificationFlow struct { + value *VerificationFlow + isSet bool +} + +func (v NullableVerificationFlow) Get() *VerificationFlow { + return v.value +} + +func (v *NullableVerificationFlow) Set(val *VerificationFlow) { + v.value = val + v.isSet = true +} + +func (v NullableVerificationFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableVerificationFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerificationFlow(val *VerificationFlow) *NullableVerificationFlow { + return &NullableVerificationFlow{value: val, isSet: true} +} + +func (v NullableVerificationFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerificationFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_verification_flow_state.go b/pkg/client-go/model_verification_flow_state.go new file mode 100644 index 0000000..82a55c6 --- /dev/null +++ b/pkg/client-go/model_verification_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + VERIFICATIONFLOWSTATE_CHOOSE_METHOD VerificationFlowState = "choose_method" + VERIFICATIONFLOWSTATE_SENT_EMAIL VerificationFlowState = "sent_email" + VERIFICATIONFLOWSTATE_PASSED_CHALLENGE VerificationFlowState = "passed_challenge" +) + +// All allowed values of VerificationFlowState enum +var AllowedVerificationFlowStateEnumValues = []VerificationFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *VerificationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := VerificationFlowState(value) + for _, existing := range AllowedVerificationFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid VerificationFlowState", value) +} + +// NewVerificationFlowStateFromValue returns a pointer to a valid VerificationFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewVerificationFlowStateFromValue(v string) (*VerificationFlowState, error) { + ev := VerificationFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for VerificationFlowState: valid values are %v", v, AllowedVerificationFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v VerificationFlowState) IsValid() bool { + for _, existing := range AllowedVerificationFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to verificationFlowState value +func (v VerificationFlowState) Ptr() *VerificationFlowState { + return &v +} + +type NullableVerificationFlowState struct { + value *VerificationFlowState + isSet bool +} + +func (v NullableVerificationFlowState) Get() *VerificationFlowState { + return v.value +} + +func (v *NullableVerificationFlowState) Set(val *VerificationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableVerificationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableVerificationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerificationFlowState(val *VerificationFlowState) *NullableVerificationFlowState { + return &NullableVerificationFlowState{value: val, isSet: true} +} + +func (v NullableVerificationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerificationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/model_version.go b/pkg/client-go/model_version.go new file mode 100644 index 0000000..26b7d51 --- /dev/null +++ b/pkg/client-go/model_version.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the Version type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Version{} + +// Version struct for Version +type Version struct { + // Version is the service's version. + Version *string `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Version Version + +// NewVersion instantiates a new Version 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 NewVersion() *Version { + this := Version{} + return &this +} + +// NewVersionWithDefaults instantiates a new Version 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 NewVersionWithDefaults() *Version { + this := Version{} + return &this +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *Version) GetVersion() string { + if o == nil || IsNil(o.Version) { + var ret string + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Version) GetVersionOk() (*string, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *Version) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given string and assigns it to the Version field. +func (o *Version) SetVersion(v string) { + o.Version = &v +} + +func (o Version) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Version) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Version) UnmarshalJSON(data []byte) (err error) { + varVersion := _Version{} + + err = json.Unmarshal(data, &varVersion) + + if err != nil { + return err + } + + *o = Version(varVersion) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVersion struct { + value *Version + isSet bool +} + +func (v NullableVersion) Get() *Version { + return v.value +} + +func (v *NullableVersion) Set(val *Version) { + v.value = val + v.isSet = true +} + +func (v NullableVersion) IsSet() bool { + return v.isSet +} + +func (v *NullableVersion) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVersion(val *Version) *NullableVersion { + return &NullableVersion{value: val, isSet: true} +} + +func (v NullableVersion) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVersion) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/client-go/response.go b/pkg/client-go/response.go new file mode 100644 index 0000000..50599b1 --- /dev/null +++ b/pkg/client-go/response.go @@ -0,0 +1,48 @@ +/* +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 ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResponse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/pkg/client-go/utils.go b/pkg/client-go/utils.go new file mode 100644 index 0000000..d6fa017 --- /dev/null +++ b/pkg/client-go/utils.go @@ -0,0 +1,362 @@ +/* +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 ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "time" +) + +// PtrBool is a helper routine that returns a pointer to given boolean value. +func PtrBool(v bool) *bool { return &v } + +// PtrInt is a helper routine that returns a pointer to given integer value. +func PtrInt(v int) *int { return &v } + +// PtrInt32 is a helper routine that returns a pointer to given integer value. +func PtrInt32(v int32) *int32 { return &v } + +// PtrInt64 is a helper routine that returns a pointer to given integer value. +func PtrInt64(v int64) *int64 { return &v } + +// PtrFloat32 is a helper routine that returns a pointer to given float value. +func PtrFloat32(v float32) *float32 { return &v } + +// PtrFloat64 is a helper routine that returns a pointer to given float value. +func PtrFloat64(v float64) *float64 { return &v } + +// PtrString is a helper routine that returns a pointer to given string value. +func PtrString(v string) *string { return &v } + +// PtrTime is helper routine that returns a pointer to given Time value. +func PtrTime(v time.Time) *time.Time { return &v } + +type NullableBool struct { + value *bool + isSet bool +} + +func (v NullableBool) Get() *bool { + return v.value +} + +func (v *NullableBool) Set(val *bool) { + v.value = val + v.isSet = true +} + +func (v NullableBool) IsSet() bool { + return v.isSet +} + +func (v *NullableBool) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBool(val *bool) *NullableBool { + return &NullableBool{value: val, isSet: true} +} + +func (v NullableBool) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBool) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt struct { + value *int + isSet bool +} + +func (v NullableInt) Get() *int { + return v.value +} + +func (v *NullableInt) Set(val *int) { + v.value = val + v.isSet = true +} + +func (v NullableInt) IsSet() bool { + return v.isSet +} + +func (v *NullableInt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt(val *int) *NullableInt { + return &NullableInt{value: val, isSet: true} +} + +func (v NullableInt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt32 struct { + value *int32 + isSet bool +} + +func (v NullableInt32) Get() *int32 { + return v.value +} + +func (v *NullableInt32) Set(val *int32) { + v.value = val + v.isSet = true +} + +func (v NullableInt32) IsSet() bool { + return v.isSet +} + +func (v *NullableInt32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt32(val *int32) *NullableInt32 { + return &NullableInt32{value: val, isSet: true} +} + +func (v NullableInt32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt64 struct { + value *int64 + isSet bool +} + +func (v NullableInt64) Get() *int64 { + return v.value +} + +func (v *NullableInt64) Set(val *int64) { + v.value = val + v.isSet = true +} + +func (v NullableInt64) IsSet() bool { + return v.isSet +} + +func (v *NullableInt64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt64(val *int64) *NullableInt64 { + return &NullableInt64{value: val, isSet: true} +} + +func (v NullableInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat32 struct { + value *float32 + isSet bool +} + +func (v NullableFloat32) Get() *float32 { + return v.value +} + +func (v *NullableFloat32) Set(val *float32) { + v.value = val + v.isSet = true +} + +func (v NullableFloat32) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat32(val *float32) *NullableFloat32 { + return &NullableFloat32{value: val, isSet: true} +} + +func (v NullableFloat32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat64 struct { + value *float64 + isSet bool +} + +func (v NullableFloat64) Get() *float64 { + return v.value +} + +func (v *NullableFloat64) Set(val *float64) { + v.value = val + v.isSet = true +} + +func (v NullableFloat64) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat64(val *float64) *NullableFloat64 { + return &NullableFloat64{value: val, isSet: true} +} + +func (v NullableFloat64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableString struct { + value *string + isSet bool +} + +func (v NullableString) Get() *string { + return v.value +} + +func (v *NullableString) Set(val *string) { + v.value = val + v.isSet = true +} + +func (v NullableString) IsSet() bool { + return v.isSet +} + +func (v *NullableString) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableString(val *string) *NullableString { + return &NullableString{value: val, isSet: true} +} + +func (v NullableString) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableString) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableTime struct { + value *time.Time + isSet bool +} + +func (v NullableTime) Get() *time.Time { + return v.value +} + +func (v *NullableTime) Set(val *time.Time) { + v.value = val + v.isSet = true +} + +func (v NullableTime) IsSet() bool { + return v.isSet +} + +func (v *NullableTime) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTime(val *time.Time) *NullableTime { + return &NullableTime{value: val, isSet: true} +} + +func (v NullableTime) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTime) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +// IsNil checks if an input is nil +func IsNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return reflect.ValueOf(i).IsNil() + case reflect.Array: + return reflect.ValueOf(i).IsZero() + } + return false +} + +type MappedNullable interface { + ToMap() (map[string]interface{}, error) +} + +// A wrapper for strict JSON decoding +func newStrictDecoder(data []byte) *json.Decoder { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.DisallowUnknownFields() + return dec +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} diff --git a/pkg/clihelpers/helpers.go b/pkg/clihelpers/helpers.go new file mode 100644 index 0000000..85ebd6d --- /dev/null +++ b/pkg/clihelpers/helpers.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package clihelpers + +const ( + WarningJQIsComplicated = "We have to admit, this is not easy if you don't speak jq fluently. What about opening an issue and telling us what predefined selectors you want to have? https://github.com/ory/kratos/issues/new/choose" +) + +// SDKError normalizes an error returned by the generated SDK. The SDK reports +// some non-failures as errors with an empty message; those are not real errors. +func SDKError(err error) error { + if err == nil { + return nil + } + + if err.Error() == "" { + return nil + } + + return err +} diff --git a/pkg/driver.go b/pkg/driver.go new file mode 100644 index 0000000..7fcd6d5 --- /dev/null +++ b/pkg/driver.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pkg + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/embedx" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/dbal" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/randx" +) + +func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`OK`)) + })) + t.Cleanup(ts.Close) + + configOpts := append([]configx.OptionModifier{ + configx.WithValues(map[string]interface{}{ + "log.level": "error", + config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), + config.ViperKeyHasherArgon2ConfigMemory: 16384, + config.ViperKeyHasherArgon2ConfigIterations: 1, + config.ViperKeyHasherArgon2ConfigParallelism: 1, + config.ViperKeyHasherArgon2ConfigSaltLength: 16, + // The invalid-credentials login/webauthn paths sleep for + // RandomDelay(expected_duration, expected_deviation) to mask timing + // attacks. In tests this adds 0–1s per negative path; shrink it to + // keep the timing-defense code exercised without the wall-clock cost. + config.ViperKeyHasherArgon2ConfigExpectedDuration: "1ns", + config.ViperKeyHasherArgon2ConfigExpectedDeviation: "1ns", + config.ViperKeyHasherBcryptCost: 4, + config.ViperKeyHasherArgon2ConfigKeyLength: 16, + config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/", + config.ViperKeySelfServiceBrowserDefaultReturnTo: ts.URL, + config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, + config.ViperKeySecretsPagination: []string{uuid.Must(uuid.NewV4()).String()}, + config.ViperKeySelfServiceLoginFlowStyle: "unified", + }), + configx.SkipValidation(), + configx.DisableEnvLoading(), + }, opts...) + return config.MustNew(t, logrusx.New("", ""), + contextx.NewTestConfigProvider(embedx.ConfigSchema, configOpts...), + configOpts..., + ) +} + +// 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, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { + conf, reg := NewRegistryDefaultWithDSN(t, "", opts...) + reg.WithCSRFTokenGenerator(nosurfx.FakeCSRFTokenGenerator) + reg.WithCSRFHandler(nosurfx.NewFakeCSRFHandler(reg)) + reg.WithHooks(map[string]driver.NewHookFn{ + "err": func(c config.SelfServiceHook, _ driver.Registry) interface{} { + return &hook.Error{Config: c.Config} + }, + }) + + return conf, reg +} + +// NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! +func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { + if dsn == "" { + dsn = dbal.NewSQLiteTestDatabase(t) + } + c := NewConfigurationWithDefaults(t, append([]configx.OptionModifier{configx.WithValues(map[string]interface{}{ + config.ViperKeyDSN: dsn, + "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(t.Context(), c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) + require.NoError(t, err) + + reg.SetJSONNetVMProvider(jsonnetsecure.NewTestProvider(t)) + + require.NoError(t, reg.Init(t.Context(), contextx.NewTestConfigProvider(embedx.ConfigSchema), driver.SkipNetworkInit, driver.WithDisabledMigrationLogging())) + require.NoError(t, reg.Persister().MigrateUp(t.Context())) // always migrate up + + actual, err := reg.Persister().DetermineNetwork(t.Context()) + require.NoError(t, err) + reg.SetPersister(reg.Persister().WithNetworkID(actual.ID)) + + require.EqualValues(t, reg.Persister().NetworkID(t.Context()), actual.ID) + require.NotEqual(t, uuid.Nil, reg.Persister().NetworkID(t.Context())) + + return c, reg +} + +func NewVeryFastRegistryWithoutDB(t *testing.T) (*config.Config, *driver.RegistryDefault) { + c := NewConfigurationWithDefaults(t) + reg, err := driver.NewRegistryFromDSN(t.Context(), c, logrusx.New("", "")) + require.NoError(t, err) + return c, reg +} diff --git a/pkg/httpclient/.gitignore b/pkg/httpclient/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/pkg/httpclient/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/pkg/httpclient/.openapi-generator-ignore b/pkg/httpclient/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/pkg/httpclient/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/pkg/httpclient/.openapi-generator/FILES b/pkg/httpclient/.openapi-generator/FILES new file mode 100644 index 0000000..cdc0e6a --- /dev/null +++ b/pkg/httpclient/.openapi-generator/FILES @@ -0,0 +1,352 @@ +.gitignore +.openapi-generator-ignore +.travis.yml +README.md +api/openapi.yaml +api_courier.go +api_frontend.go +api_identity.go +api_metadata.go +client.go +configuration.go +docs/AdminIdentityImportCredentialsLookupSecret.md +docs/AdminIdentityImportCredentialsLookupSecretConfig.md +docs/AuthenticatorAssuranceLevel.md +docs/BatchPatchIdentitiesResponse.md +docs/ConsistencyRequestParameters.md +docs/ContinueWith.md +docs/ContinueWithDeviceAuthnPinEntryUi.md +docs/ContinueWithDeviceAuthnPinEntryUiData.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/CourierMessageStatus.md +docs/CourierMessageType.md +docs/CreateFedcmFlowResponse.md +docs/CreateIdentityBody.md +docs/CreateRecoveryCodeForIdentityBody.md +docs/CreateRecoveryLinkForIdentityBody.md +docs/CreateTestLoginFlowBody.md +docs/DeleteMySessionsCount.md +docs/DeviceAuthnAndroidAuthorizationList.md +docs/DeviceAuthnAndroidKeyDescription.md +docs/DeviceAuthnAndroidRootOfTrust.md +docs/DeviceAuthnAttestation.md +docs/DeviceAuthnIOSAttStmt.md +docs/DeviceAuthnIOSAttestation.md +docs/DeviceAuthnKey.md +docs/DeviceType.md +docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md +docs/ErrorBrowserLocationChangeRequired.md +docs/ErrorFlowReplaced.md +docs/ErrorGeneric.md +docs/FlowError.md +docs/FrontendAPI.md +docs/GenericError.md +docs/GetVersion200Response.md +docs/HealthNotReadyStatus.md +docs/HealthStatus.md +docs/Identity.md +docs/IdentityAPI.md +docs/IdentityCredentials.md +docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md +docs/IdentityCredentialsLookupSecretCode.md +docs/IdentityCredentialsOidc.md +docs/IdentityCredentialsOidcProvider.md +docs/IdentityCredentialsPassword.md +docs/IdentityCredentialsWebAuthn.md +docs/IdentityCredentialsWebAuthnAttestation.md +docs/IdentityCredentialsWebAuthnAuthenticator.md +docs/IdentityCredentialsWebAuthnFlags.md +docs/IdentityPatch.md +docs/IdentityPatchResponse.md +docs/IdentitySchemaContainer.md +docs/IdentityWithCredentials.md +docs/IdentityWithCredentialsOidc.md +docs/IdentityWithCredentialsOidcConfig.md +docs/IdentityWithCredentialsOidcConfigProvider.md +docs/IdentityWithCredentialsPasskey.md +docs/IdentityWithCredentialsPasskeyConfig.md +docs/IdentityWithCredentialsPassword.md +docs/IdentityWithCredentialsPasswordConfig.md +docs/IdentityWithCredentialsSaml.md +docs/IdentityWithCredentialsSamlConfig.md +docs/IdentityWithCredentialsSamlConfigProvider.md +docs/IdentityWithCredentialsTotp.md +docs/IdentityWithCredentialsTotpConfig.md +docs/IdentityWithCredentialsWebAuthn.md +docs/IdentityWithCredentialsWebAuthnConfig.md +docs/IsAlive200Response.md +docs/IsReady503Response.md +docs/JsonPatch.md +docs/KeyState.md +docs/LoginFlow.md +docs/LoginFlowState.md +docs/LoginFlowTestContext.md +docs/LoginFlowTestDebugPayload.md +docs/LoginFlowTestSchemaValidationError.md +docs/LoginFlowTestStepError.md +docs/LogoutFlow.md +docs/ManageSessionsBody.md +docs/ManageSessionsResponse.md +docs/Message.md +docs/MessageDispatch.md +docs/MetadataAPI.md +docs/NeedsPrivilegedSessionError.md +docs/OAuth2Client.md +docs/OAuth2ConsentRequestOpenIDConnectContext.md +docs/OAuth2LoginRequest.md +docs/PINConfig.md +docs/PatchIdentitiesBody.md +docs/PerformNativeLogoutBody.md +docs/Provider.md +docs/RecoveryCodeForIdentity.md +docs/RecoveryFlow.md +docs/RecoveryFlowState.md +docs/RecoveryIdentityAddress.md +docs/RecoveryLinkForIdentity.md +docs/RegistrationFlow.md +docs/RegistrationFlowState.md +docs/SelfServiceFlowExpiredError.md +docs/Session.md +docs/SessionAuthenticationMethod.md +docs/SessionDevice.md +docs/SettingsFlow.md +docs/SettingsFlowState.md +docs/SuccessfulCodeExchangeResponse.md +docs/SuccessfulNativeLogin.md +docs/SuccessfulNativeRegistration.md +docs/TokenPagination.md +docs/TokenPaginationHeaders.md +docs/UiContainer.md +docs/UiNode.md +docs/UiNodeAnchorAttributes.md +docs/UiNodeAttributes.md +docs/UiNodeDivisionAttributes.md +docs/UiNodeImageAttributes.md +docs/UiNodeInputAttributes.md +docs/UiNodeInputAttributesOption.md +docs/UiNodeMeta.md +docs/UiNodeScriptAttributes.md +docs/UiNodeTextAttributes.md +docs/UiText.md +docs/UpdateFedcmFlowBody.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/UpdateLoginFlowWithSamlMethod.md +docs/UpdateLoginFlowWithTotpMethod.md +docs/UpdateLoginFlowWithWebAuthnMethod.md +docs/UpdateRecoveryFlowBody.md +docs/UpdateRecoveryFlowWithCodeMethod.md +docs/UpdateRecoveryFlowWithLinkMethod.md +docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md +docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md +docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md +docs/UpdateRegistrationFlowWithSamlMethod.md +docs/UpdateRegistrationFlowWithWebAuthnMethod.md +docs/UpdateSettingsFlowBody.md +docs/UpdateSettingsFlowWithLookupMethod.md +docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md +docs/UpdateSettingsFlowWithPasswordMethod.md +docs/UpdateSettingsFlowWithProfileMethod.md +docs/UpdateSettingsFlowWithSamlMethod.md +docs/UpdateSettingsFlowWithTotpMethod.md +docs/UpdateSettingsFlowWithWebAuthnMethod.md +docs/UpdateVerificationFlowBody.md +docs/UpdateVerificationFlowWithCodeMethod.md +docs/UpdateVerificationFlowWithLinkMethod.md +docs/UserVerification.md +docs/VerifiableIdentityAddress.md +docs/VerificationFlow.md +docs/VerificationFlowState.md +docs/Version.md +git_push.sh +go.mod +go.sum +model_admin_identity_import_credentials_lookup_secret.go +model_admin_identity_import_credentials_lookup_secret_config.go +model_authenticator_assurance_level.go +model_batch_patch_identities_response.go +model_consistency_request_parameters.go +model_continue_with.go +model_continue_with_device_authn_pin_entry_ui.go +model_continue_with_device_authn_pin_entry_ui_data.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 +model_continue_with_verification_ui.go +model_continue_with_verification_ui_flow.go +model_courier_message_status.go +model_courier_message_type.go +model_create_fedcm_flow_response.go +model_create_identity_body.go +model_create_recovery_code_for_identity_body.go +model_create_recovery_link_for_identity_body.go +model_create_test_login_flow_body.go +model_delete_my_sessions_count.go +model_device_authn_android_authorization_list.go +model_device_authn_android_key_description.go +model_device_authn_android_root_of_trust.go +model_device_authn_attestation.go +model_device_authn_ios_att_stmt.go +model_device_authn_ios_attestation.go +model_device_authn_key.go +model_device_type.go +model_error_authenticator_assurance_level_not_satisfied.go +model_error_browser_location_change_required.go +model_error_flow_replaced.go +model_error_generic.go +model_flow_error.go +model_generic_error.go +model_get_version_200_response.go +model_health_not_ready_status.go +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_lookup_secret_code.go +model_identity_credentials_oidc.go +model_identity_credentials_oidc_provider.go +model_identity_credentials_password.go +model_identity_credentials_web_authn.go +model_identity_credentials_web_authn_attestation.go +model_identity_credentials_web_authn_authenticator.go +model_identity_credentials_web_authn_flags.go +model_identity_patch.go +model_identity_patch_response.go +model_identity_schema_container.go +model_identity_with_credentials.go +model_identity_with_credentials_oidc.go +model_identity_with_credentials_oidc_config.go +model_identity_with_credentials_oidc_config_provider.go +model_identity_with_credentials_passkey.go +model_identity_with_credentials_passkey_config.go +model_identity_with_credentials_password.go +model_identity_with_credentials_password_config.go +model_identity_with_credentials_saml.go +model_identity_with_credentials_saml_config.go +model_identity_with_credentials_saml_config_provider.go +model_identity_with_credentials_totp.go +model_identity_with_credentials_totp_config.go +model_identity_with_credentials_web_authn.go +model_identity_with_credentials_web_authn_config.go +model_is_alive_200_response.go +model_is_ready_503_response.go +model_json_patch.go +model_key_state.go +model_login_flow.go +model_login_flow_state.go +model_login_flow_test_context.go +model_login_flow_test_debug_payload.go +model_login_flow_test_schema_validation_error.go +model_login_flow_test_step_error.go +model_logout_flow.go +model_manage_sessions_body.go +model_manage_sessions_response.go +model_message.go +model_message_dispatch.go +model_needs_privileged_session_error.go +model_o_auth2_client.go +model_o_auth2_consent_request_open_id_connect_context.go +model_o_auth2_login_request.go +model_patch_identities_body.go +model_perform_native_logout_body.go +model_pin_config.go +model_provider.go +model_recovery_code_for_identity.go +model_recovery_flow.go +model_recovery_flow_state.go +model_recovery_identity_address.go +model_recovery_link_for_identity.go +model_registration_flow.go +model_registration_flow_state.go +model_self_service_flow_expired_error.go +model_session.go +model_session_authentication_method.go +model_session_device.go +model_settings_flow.go +model_settings_flow_state.go +model_successful_code_exchange_response.go +model_successful_native_login.go +model_successful_native_registration.go +model_token_pagination.go +model_token_pagination_headers.go +model_ui_container.go +model_ui_node.go +model_ui_node_anchor_attributes.go +model_ui_node_attributes.go +model_ui_node_division_attributes.go +model_ui_node_image_attributes.go +model_ui_node_input_attributes.go +model_ui_node_input_attributes_option.go +model_ui_node_meta.go +model_ui_node_script_attributes.go +model_ui_node_text_attributes.go +model_ui_text.go +model_update_fedcm_flow_body.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_saml_method.go +model_update_login_flow_with_totp_method.go +model_update_login_flow_with_web_authn_method.go +model_update_recovery_flow_body.go +model_update_recovery_flow_with_code_method.go +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_saml_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_saml_method.go +model_update_settings_flow_with_totp_method.go +model_update_settings_flow_with_web_authn_method.go +model_update_verification_flow_body.go +model_update_verification_flow_with_code_method.go +model_update_verification_flow_with_link_method.go +model_user_verification.go +model_verifiable_identity_address.go +model_verification_flow.go +model_verification_flow_state.go +model_version.go +response.go +test/api_courier_test.go +test/api_frontend_test.go +test/api_identity_test.go +test/api_metadata_test.go +utils.go diff --git a/pkg/httpclient/.openapi-generator/VERSION b/pkg/httpclient/.openapi-generator/VERSION new file mode 100644 index 0000000..5f84a81 --- /dev/null +++ b/pkg/httpclient/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.12.0 diff --git a/pkg/httpclient/.travis.yml b/pkg/httpclient/.travis.yml new file mode 100644 index 0000000..f5cb2ce --- /dev/null +++ b/pkg/httpclient/.travis.yml @@ -0,0 +1,8 @@ +language: go + +install: + - go get -d -v . + +script: + - go build -v ./ + diff --git a/pkg/httpclient/README.md b/pkg/httpclient/README.md new file mode 100644 index 0000000..a40c775 --- /dev/null +++ b/pkg/httpclient/README.md @@ -0,0 +1,357 @@ +# Go API client for client + +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. + + +## 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: +- Package version: 1.0.0 +- Generator version: 7.12.0 +- Build package: org.openapitools.codegen.languages.GoClientCodegen + +## Installation + +Install the following dependencies: + +```sh +go get github.com/stretchr/testify/assert +go get golang.org/x/net/context +``` + +Put the package under your project folder and add the following in import: + +```go +import client "github.com/ory/client-go" +``` + +To use a proxy, set the environment variable `HTTP_PROXY`: + +```go +os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") +``` + +## Configuration of Server URL + +Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. + +### Select Server Configuration + +For using other server than the one defined on index 0 set context value `client.ContextServerIndex` of type `int`. + +```go +ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) +``` + +### Templated Server URL + +Templated server URL is formatted using default variables from configuration or from context value `client.ContextServerVariables` of type `map[string]string`. + +```go +ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ + "basePath": "v2", +}) +``` + +Note, enum values are always validated and all unused variables are silently ignored. + +### URLs Configuration per Operation + +Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. +An operation is uniquely identified by `"{classname}Service.{nickname}"` string. +Similar rules for overriding default operation server index and variables applies by using `client.ContextOperationServerIndices` and `client.ContextOperationServerVariables` context maps. + +```go +ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ + "{classname}Service.{nickname}": 2, +}) +ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ + "{classname}Service.{nickname}": { + "port": "8443", + }, +}) +``` + +## Documentation for API Endpoints + +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* | [**CreateFedcmFlow**](docs/FrontendAPI.md#createfedcmflow) | **Get** /self-service/fed-cm/parameters | Get FedCM Parameters +*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* | [**DeleteTestLoginFlow**](docs/FrontendAPI.md#deletetestloginflow) | **Delete** /self-service/login/test | Delete a test OIDC login flow +*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* | [**GetWellKnownChangePassword**](docs/FrontendAPI.md#getwellknownchangepassword) | **Get** /.well-known/change-password | Change Password URL +*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* | [**UpdateFedcmFlow**](docs/FrontendAPI.md#updatefedcmflow) | **Post** /self-service/fed-cm/token | Submit a FedCM token +*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* | [**CreateTestLoginFlow**](docs/IdentityAPI.md#createtestloginflow) | **Post** /admin/test-login-flows | Create a test OIDC login flow +*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* | [**GetIdentityByExternalID**](docs/IdentityAPI.md#getidentitybyexternalid) | **Get** /admin/identities/by/external/{externalID} | Get an Identity by its External ID +*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* | [**ManageSessions**](docs/IdentityAPI.md#managesessions) | **Post** /admin/sessions | Manage sessions in bulk +*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 + + - [AdminIdentityImportCredentialsLookupSecret](docs/AdminIdentityImportCredentialsLookupSecret.md) + - [AdminIdentityImportCredentialsLookupSecretConfig](docs/AdminIdentityImportCredentialsLookupSecretConfig.md) + - [AuthenticatorAssuranceLevel](docs/AuthenticatorAssuranceLevel.md) + - [BatchPatchIdentitiesResponse](docs/BatchPatchIdentitiesResponse.md) + - [ConsistencyRequestParameters](docs/ConsistencyRequestParameters.md) + - [ContinueWith](docs/ContinueWith.md) + - [ContinueWithDeviceAuthnPinEntryUi](docs/ContinueWithDeviceAuthnPinEntryUi.md) + - [ContinueWithDeviceAuthnPinEntryUiData](docs/ContinueWithDeviceAuthnPinEntryUiData.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) + - [ContinueWithVerificationUi](docs/ContinueWithVerificationUi.md) + - [ContinueWithVerificationUiFlow](docs/ContinueWithVerificationUiFlow.md) + - [CourierMessageStatus](docs/CourierMessageStatus.md) + - [CourierMessageType](docs/CourierMessageType.md) + - [CreateFedcmFlowResponse](docs/CreateFedcmFlowResponse.md) + - [CreateIdentityBody](docs/CreateIdentityBody.md) + - [CreateRecoveryCodeForIdentityBody](docs/CreateRecoveryCodeForIdentityBody.md) + - [CreateRecoveryLinkForIdentityBody](docs/CreateRecoveryLinkForIdentityBody.md) + - [CreateTestLoginFlowBody](docs/CreateTestLoginFlowBody.md) + - [DeleteMySessionsCount](docs/DeleteMySessionsCount.md) + - [DeviceAuthnAndroidAuthorizationList](docs/DeviceAuthnAndroidAuthorizationList.md) + - [DeviceAuthnAndroidKeyDescription](docs/DeviceAuthnAndroidKeyDescription.md) + - [DeviceAuthnAndroidRootOfTrust](docs/DeviceAuthnAndroidRootOfTrust.md) + - [DeviceAuthnAttestation](docs/DeviceAuthnAttestation.md) + - [DeviceAuthnIOSAttStmt](docs/DeviceAuthnIOSAttStmt.md) + - [DeviceAuthnIOSAttestation](docs/DeviceAuthnIOSAttestation.md) + - [DeviceAuthnKey](docs/DeviceAuthnKey.md) + - [DeviceType](docs/DeviceType.md) + - [ErrorAuthenticatorAssuranceLevelNotSatisfied](docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md) + - [ErrorBrowserLocationChangeRequired](docs/ErrorBrowserLocationChangeRequired.md) + - [ErrorFlowReplaced](docs/ErrorFlowReplaced.md) + - [ErrorGeneric](docs/ErrorGeneric.md) + - [FlowError](docs/FlowError.md) + - [GenericError](docs/GenericError.md) + - [GetVersion200Response](docs/GetVersion200Response.md) + - [HealthNotReadyStatus](docs/HealthNotReadyStatus.md) + - [HealthStatus](docs/HealthStatus.md) + - [Identity](docs/Identity.md) + - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) + - [IdentityCredentialsLookupSecretCode](docs/IdentityCredentialsLookupSecretCode.md) + - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) + - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) + - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) + - [IdentityCredentialsWebAuthn](docs/IdentityCredentialsWebAuthn.md) + - [IdentityCredentialsWebAuthnAttestation](docs/IdentityCredentialsWebAuthnAttestation.md) + - [IdentityCredentialsWebAuthnAuthenticator](docs/IdentityCredentialsWebAuthnAuthenticator.md) + - [IdentityCredentialsWebAuthnFlags](docs/IdentityCredentialsWebAuthnFlags.md) + - [IdentityPatch](docs/IdentityPatch.md) + - [IdentityPatchResponse](docs/IdentityPatchResponse.md) + - [IdentitySchemaContainer](docs/IdentitySchemaContainer.md) + - [IdentityWithCredentials](docs/IdentityWithCredentials.md) + - [IdentityWithCredentialsOidc](docs/IdentityWithCredentialsOidc.md) + - [IdentityWithCredentialsOidcConfig](docs/IdentityWithCredentialsOidcConfig.md) + - [IdentityWithCredentialsOidcConfigProvider](docs/IdentityWithCredentialsOidcConfigProvider.md) + - [IdentityWithCredentialsPasskey](docs/IdentityWithCredentialsPasskey.md) + - [IdentityWithCredentialsPasskeyConfig](docs/IdentityWithCredentialsPasskeyConfig.md) + - [IdentityWithCredentialsPassword](docs/IdentityWithCredentialsPassword.md) + - [IdentityWithCredentialsPasswordConfig](docs/IdentityWithCredentialsPasswordConfig.md) + - [IdentityWithCredentialsSaml](docs/IdentityWithCredentialsSaml.md) + - [IdentityWithCredentialsSamlConfig](docs/IdentityWithCredentialsSamlConfig.md) + - [IdentityWithCredentialsSamlConfigProvider](docs/IdentityWithCredentialsSamlConfigProvider.md) + - [IdentityWithCredentialsTotp](docs/IdentityWithCredentialsTotp.md) + - [IdentityWithCredentialsTotpConfig](docs/IdentityWithCredentialsTotpConfig.md) + - [IdentityWithCredentialsWebAuthn](docs/IdentityWithCredentialsWebAuthn.md) + - [IdentityWithCredentialsWebAuthnConfig](docs/IdentityWithCredentialsWebAuthnConfig.md) + - [IsAlive200Response](docs/IsAlive200Response.md) + - [IsReady503Response](docs/IsReady503Response.md) + - [JsonPatch](docs/JsonPatch.md) + - [KeyState](docs/KeyState.md) + - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) + - [LoginFlowTestContext](docs/LoginFlowTestContext.md) + - [LoginFlowTestDebugPayload](docs/LoginFlowTestDebugPayload.md) + - [LoginFlowTestSchemaValidationError](docs/LoginFlowTestSchemaValidationError.md) + - [LoginFlowTestStepError](docs/LoginFlowTestStepError.md) + - [LogoutFlow](docs/LogoutFlow.md) + - [ManageSessionsBody](docs/ManageSessionsBody.md) + - [ManageSessionsResponse](docs/ManageSessionsResponse.md) + - [Message](docs/Message.md) + - [MessageDispatch](docs/MessageDispatch.md) + - [NeedsPrivilegedSessionError](docs/NeedsPrivilegedSessionError.md) + - [OAuth2Client](docs/OAuth2Client.md) + - [OAuth2ConsentRequestOpenIDConnectContext](docs/OAuth2ConsentRequestOpenIDConnectContext.md) + - [OAuth2LoginRequest](docs/OAuth2LoginRequest.md) + - [PINConfig](docs/PINConfig.md) + - [PatchIdentitiesBody](docs/PatchIdentitiesBody.md) + - [PerformNativeLogoutBody](docs/PerformNativeLogoutBody.md) + - [Provider](docs/Provider.md) + - [RecoveryCodeForIdentity](docs/RecoveryCodeForIdentity.md) + - [RecoveryFlow](docs/RecoveryFlow.md) + - [RecoveryFlowState](docs/RecoveryFlowState.md) + - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) + - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) + - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) + - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) + - [Session](docs/Session.md) + - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) + - [SessionDevice](docs/SessionDevice.md) + - [SettingsFlow](docs/SettingsFlow.md) + - [SettingsFlowState](docs/SettingsFlowState.md) + - [SuccessfulCodeExchangeResponse](docs/SuccessfulCodeExchangeResponse.md) + - [SuccessfulNativeLogin](docs/SuccessfulNativeLogin.md) + - [SuccessfulNativeRegistration](docs/SuccessfulNativeRegistration.md) + - [TokenPagination](docs/TokenPagination.md) + - [TokenPaginationHeaders](docs/TokenPaginationHeaders.md) + - [UiContainer](docs/UiContainer.md) + - [UiNode](docs/UiNode.md) + - [UiNodeAnchorAttributes](docs/UiNodeAnchorAttributes.md) + - [UiNodeAttributes](docs/UiNodeAttributes.md) + - [UiNodeDivisionAttributes](docs/UiNodeDivisionAttributes.md) + - [UiNodeImageAttributes](docs/UiNodeImageAttributes.md) + - [UiNodeInputAttributes](docs/UiNodeInputAttributes.md) + - [UiNodeInputAttributesOption](docs/UiNodeInputAttributesOption.md) + - [UiNodeMeta](docs/UiNodeMeta.md) + - [UiNodeScriptAttributes](docs/UiNodeScriptAttributes.md) + - [UiNodeTextAttributes](docs/UiNodeTextAttributes.md) + - [UiText](docs/UiText.md) + - [UpdateFedcmFlowBody](docs/UpdateFedcmFlowBody.md) + - [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) + - [UpdateLoginFlowWithSamlMethod](docs/UpdateLoginFlowWithSamlMethod.md) + - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) + - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) + - [UpdateRecoveryFlowBody](docs/UpdateRecoveryFlowBody.md) + - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) + - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) + - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) + - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) + - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) + - [UpdateRegistrationFlowWithSamlMethod](docs/UpdateRegistrationFlowWithSamlMethod.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) + - [UpdateSettingsFlowWithSamlMethod](docs/UpdateSettingsFlowWithSamlMethod.md) + - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) + - [UpdateSettingsFlowWithWebAuthnMethod](docs/UpdateSettingsFlowWithWebAuthnMethod.md) + - [UpdateVerificationFlowBody](docs/UpdateVerificationFlowBody.md) + - [UpdateVerificationFlowWithCodeMethod](docs/UpdateVerificationFlowWithCodeMethod.md) + - [UpdateVerificationFlowWithLinkMethod](docs/UpdateVerificationFlowWithLinkMethod.md) + - [UserVerification](docs/UserVerification.md) + - [VerifiableIdentityAddress](docs/VerifiableIdentityAddress.md) + - [VerificationFlow](docs/VerificationFlow.md) + - [VerificationFlowState](docs/VerificationFlowState.md) + - [Version](docs/Version.md) + + +## Documentation For Authorization + + +Authentication schemes defined for the API: +### oryAccessToken + +- **Type**: API key +- **API key parameter name**: Authorization +- **Location**: HTTP header + +Note, each API key must be added to a map of `map[string]APIKey` where the key is: oryAccessToken and passed in as the auth context for each request. + +Example + +```go +auth := context.WithValue( + context.Background(), + client.ContextAPIKeys, + map[string]client.APIKey{ + "oryAccessToken": {Key: "API_KEY_STRING"}, + }, + ) +r, err := client.Service.Operation(auth, args) +``` + + +## Documentation for Utility Methods + +Due to the fact that model structure members are all pointers, this package contains +a number of utility functions to easily obtain pointers to values of basic types. +Each of these functions takes a value of the given basic type and returns a pointer to it: + +* `PtrBool` +* `PtrInt` +* `PtrInt32` +* `PtrInt64` +* `PtrFloat` +* `PtrFloat32` +* `PtrFloat64` +* `PtrString` +* `PtrTime` + +## Author + +office@ory.sh + diff --git a/pkg/httpclient/api_courier.go b/pkg/httpclient/api_courier.go new file mode 100644 index 0000000..0fc15fc --- /dev/null +++ b/pkg/httpclient/api_courier.go @@ -0,0 +1,369 @@ +/* +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 ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + +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 CourierAPIGetCourierMessageRequest + */ + GetCourierMessage(ctx context.Context, id string) CourierAPIGetCourierMessageRequest + + // GetCourierMessageExecute executes the request + // @return Message + GetCourierMessageExecute(r CourierAPIGetCourierMessageRequest) (*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 CourierAPIListCourierMessagesRequest + */ + ListCourierMessages(ctx context.Context) CourierAPIListCourierMessagesRequest + + // ListCourierMessagesExecute executes the request + // @return []Message + ListCourierMessagesExecute(r CourierAPIListCourierMessagesRequest) ([]Message, *http.Response, error) +} + +// CourierAPIService CourierAPI service +type CourierAPIService service + +type CourierAPIGetCourierMessageRequest struct { + ctx context.Context + ApiService CourierAPI + id string +} + +func (r CourierAPIGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { + return r.ApiService.GetCourierMessageExecute(r) +} + +/* +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 CourierAPIGetCourierMessageRequest +*/ +func (a *CourierAPIService) GetCourierMessage(ctx context.Context, id string) CourierAPIGetCourierMessageRequest { + return CourierAPIGetCourierMessageRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +// Execute executes the request +// +// @return Message +func (a *CourierAPIService) GetCourierMessageExecute(r CourierAPIGetCourierMessageRequest) (*Message, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *Message + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.GetCourierMessage") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/courier/messages/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type CourierAPIListCourierMessagesRequest struct { + ctx context.Context + ApiService CourierAPI + pageSize *int64 + pageToken *string + status *CourierMessageStatus + recipient *string +} + +// Items per Page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +func (r CourierAPIListCourierMessagesRequest) PageSize(pageSize int64) CourierAPIListCourierMessagesRequest { + r.pageSize = &pageSize + return r +} + +// Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +func (r CourierAPIListCourierMessagesRequest) PageToken(pageToken string) CourierAPIListCourierMessagesRequest { + r.pageToken = &pageToken + return r +} + +// Status filters out messages based on status. If no value is provided, it doesn't take effect on filter. +func (r CourierAPIListCourierMessagesRequest) Status(status CourierMessageStatus) CourierAPIListCourierMessagesRequest { + r.status = &status + return r +} + +// Recipient filters out messages based on recipient. If no value is provided, it doesn't take effect on filter. +func (r CourierAPIListCourierMessagesRequest) Recipient(recipient string) CourierAPIListCourierMessagesRequest { + r.recipient = &recipient + return r +} + +func (r CourierAPIListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { + return r.ApiService.ListCourierMessagesExecute(r) +} + +/* +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 CourierAPIListCourierMessagesRequest +*/ +func (a *CourierAPIService) ListCourierMessages(ctx context.Context) CourierAPIListCourierMessagesRequest { + return CourierAPIListCourierMessagesRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return []Message +func (a *CourierAPIService) ListCourierMessagesExecute(r CourierAPIListCourierMessagesRequest) ([]Message, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue []Message + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.ListCourierMessages") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/courier/messages" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.pageSize != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "page_size", r.pageSize, "form", "") + } else { + var defaultValue int64 = 250 + r.pageSize = &defaultValue + } + if r.pageToken != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "page_token", r.pageToken, "form", "") + } + if r.status != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "status", r.status, "form", "") + } + if r.recipient != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "recipient", r.recipient, "form", "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/pkg/httpclient/api_metadata.go b/pkg/httpclient/api_metadata.go new file mode 100644 index 0000000..498c7d3 --- /dev/null +++ b/pkg/httpclient/api_metadata.go @@ -0,0 +1,435 @@ +/* +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 ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +type MetadataAPI interface { + + /* + GetVersion Return Running Software Version. + + This endpoint returns the version of Ory Kratos. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIGetVersionRequest + */ + GetVersion(ctx context.Context) MetadataAPIGetVersionRequest + + // GetVersionExecute executes the request + // @return GetVersion200Response + GetVersionExecute(r MetadataAPIGetVersionRequest) (*GetVersion200Response, *http.Response, error) + + /* + IsAlive Check HTTP Server Status + + This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIIsAliveRequest + */ + IsAlive(ctx context.Context) MetadataAPIIsAliveRequest + + // IsAliveExecute executes the request + // @return IsAlive200Response + IsAliveExecute(r MetadataAPIIsAliveRequest) (*IsAlive200Response, *http.Response, error) + + /* + IsReady Check HTTP Server and Database Status + + This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + 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 MetadataAPIIsReadyRequest + */ + IsReady(ctx context.Context) MetadataAPIIsReadyRequest + + // IsReadyExecute executes the request + // @return IsAlive200Response + IsReadyExecute(r MetadataAPIIsReadyRequest) (*IsAlive200Response, *http.Response, error) +} + +// MetadataAPIService MetadataAPI service +type MetadataAPIService service + +type MetadataAPIGetVersionRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { + return r.ApiService.GetVersionExecute(r) +} + +/* +GetVersion Return Running Software Version. + +This endpoint returns the version of Ory Kratos. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIGetVersionRequest +*/ +func (a *MetadataAPIService) GetVersion(ctx context.Context) MetadataAPIGetVersionRequest { + return MetadataAPIGetVersionRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return GetVersion200Response +func (a *MetadataAPIService) GetVersionExecute(r MetadataAPIGetVersionRequest) (*GetVersion200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *GetVersion200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.GetVersion") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/version" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataAPIIsAliveRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsAliveExecute(r) +} + +/* +IsAlive Check HTTP Server Status + +This endpoint returns a HTTP 200 status code when Ory Kratos is accepting incoming +HTTP requests. This status does currently not include checks whether the database connection is working. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIIsAliveRequest +*/ +func (a *MetadataAPIService) IsAlive(ctx context.Context) MetadataAPIIsAliveRequest { + return MetadataAPIIsAliveRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return IsAlive200Response +func (a *MetadataAPIService) IsAliveExecute(r MetadataAPIIsAliveRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsAlive") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/alive" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataAPIIsReadyRequest struct { + ctx context.Context + ApiService MetadataAPI +} + +func (r MetadataAPIIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsReadyExecute(r) +} + +/* +IsReady Check HTTP Server and Database Status + +This endpoint returns a HTTP 200 status code when Ory Kratos is up running and the environment dependencies (e.g. +the database) are responsive as well. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +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 MetadataAPIIsReadyRequest +*/ +func (a *MetadataAPIService) IsReady(ctx context.Context) MetadataAPIIsReadyRequest { + return MetadataAPIIsReadyRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return IsAlive200Response +func (a *MetadataAPIService) IsReadyExecute(r MetadataAPIIsReadyRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsReady") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/ready" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 503 { + var v IsReady503Response + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go new file mode 100644 index 0000000..8246c31 --- /dev/null +++ b/pkg/httpclient/client.go @@ -0,0 +1,665 @@ +/* +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 ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +var ( + JsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?json)`) + XmlCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?xml)`) + queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`) + queryDescape = strings.NewReplacer("%5B", "[", "%5D", "]") +) + +// APIClient manages communication with the Ory Identities API API v +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services + + CourierAPI CourierAPI + + FrontendAPI FrontendAPI + + IdentityAPI IdentityAPI + + MetadataAPI MetadataAPI +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + 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) + + return c +} + +func atoi(in string) (int, error) { + return strconv.Atoi(in) +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insensitive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.EqualFold(a, needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +func parameterValueToString(obj interface{}, key string) string { + if reflect.TypeOf(obj).Kind() != reflect.Ptr { + if actualObj, ok := obj.(interface{ GetActualInstanceValue() interface{} }); ok { + return fmt.Sprintf("%v", actualObj.GetActualInstanceValue()) + } + + return fmt.Sprintf("%v", obj) + } + var param, ok = obj.(MappedNullable) + if !ok { + return "" + } + dataMap, err := param.ToMap() + if err != nil { + return "" + } + return fmt.Sprintf("%v", dataMap[key]) +} + +// parameterAddToHeaderOrQuery adds the provided object to the request header or url query +// supporting deep object syntax +func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix string, obj interface{}, style string, collectionType string) { + var v = reflect.ValueOf(obj) + var value = "" + if v == reflect.ValueOf(nil) { + value = "null" + } else { + switch v.Kind() { + case reflect.Invalid: + value = "invalid" + + case reflect.Struct: + if t, ok := obj.(MappedNullable); ok { + dataMap, err := t.ToMap() + if err != nil { + return + } + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, style, collectionType) + return + } + if t, ok := obj.(time.Time); ok { + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339Nano), style, collectionType) + return + } + value = v.Type().String() + " value" + case reflect.Slice: + var indValue = reflect.ValueOf(obj) + if indValue == reflect.ValueOf(nil) { + return + } + var lenIndValue = indValue.Len() + for i := 0; i < lenIndValue; i++ { + var arrayValue = indValue.Index(i) + var keyPrefixForCollectionType = keyPrefix + if style == "deepObject" { + keyPrefixForCollectionType = keyPrefix + "[" + strconv.Itoa(i) + "]" + } + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), style, collectionType) + } + return + + case reflect.Map: + var indValue = reflect.ValueOf(obj) + if indValue == reflect.ValueOf(nil) { + return + } + iter := indValue.MapRange() + for iter.Next() { + k, v := iter.Key(), iter.Value() + parameterAddToHeaderOrQuery(headerOrQueryParams, fmt.Sprintf("%s[%s]", keyPrefix, k.String()), v.Interface(), style, collectionType) + } + return + + case reflect.Interface: + fallthrough + case reflect.Ptr: + parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, v.Elem().Interface(), style, collectionType) + return + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + value = strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + value = strconv.FormatUint(v.Uint(), 10) + case reflect.Float32, reflect.Float64: + value = strconv.FormatFloat(v.Float(), 'g', -1, 32) + case reflect.Bool: + value = strconv.FormatBool(v.Bool()) + case reflect.String: + value = v.String() + default: + value = v.Type().String() + " value" + } + } + + switch valuesMap := headerOrQueryParams.(type) { + case url.Values: + if collectionType == "csv" && valuesMap.Get(keyPrefix) != "" { + valuesMap.Set(keyPrefix, valuesMap.Get(keyPrefix)+","+value) + } else { + valuesMap.Add(keyPrefix, value) + } + break + case map[string]string: + valuesMap[keyPrefix] = value + break + } +} + +// helper for converting interface{} parameters to json strings +func parameterToJson(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + +// callAPI do the request. +func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + return resp, err +} + +// Allow modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +type formFile struct { + fileBytes []byte + fileName string + formFileName string +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams url.Values, + formFiles []formFile) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // add form parameters and file if available. + if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(formFiles) > 0) { + if body != nil { + return nil, errors.New("Cannot specify postBody and multipart form at the same time.") + } + body = &bytes.Buffer{} + w := multipart.NewWriter(body) + + for k, v := range formParams { + for _, iv := range v { + if strings.HasPrefix(k, "@") { // file + err = addFile(w, k[1:], iv) + if err != nil { + return nil, err + } + } else { // form value + w.WriteField(k, iv) + } + } + } + for _, formFile := range formFiles { + if len(formFile.fileBytes) > 0 && formFile.fileName != "" { + w.Boundary() + part, err := w.CreateFormFile(formFile.formFileName, filepath.Base(formFile.fileName)) + if err != nil { + return nil, err + } + _, err = part.Write(formFile.fileBytes) + if err != nil { + return nil, err + } + } + } + + // Set the Boundary in the Content-Type + headerParams["Content-Type"] = w.FormDataContentType() + + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + w.Close() + } + + if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { + if body != nil { + return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") + } + body = &bytes.Buffer{} + body.WriteString(formParams.Encode()) + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Adding Query Param + query := url.Query() + for k, v := range queryParams { + for _, iv := range v { + query.Add(k, iv) + } + } + + // Encode the parameters. + url.RawQuery = queryParamSplit.ReplaceAllStringFunc(query.Encode(), func(s string) string { + pieces := strings.Split(s, "=") + pieces[0] = queryDescape.Replace(pieces[0]) + return strings.Join(pieces, "=") + }) + + // Generate a new request + if body != nil { + localVarRequest, err = http.NewRequest(method, url.String(), body) + } else { + localVarRequest, err = http.NewRequest(method, url.String(), nil) + } + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers[h] = []string{v} + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + + // Walk through any authentication. + + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if f, ok := v.(*os.File); ok { + f, err = os.CreateTemp("", "HttpClientFile") + if err != nil { + return + } + _, err = f.Write(b) + if err != nil { + return + } + _, err = f.Seek(0, io.SeekStart) + return + } + if f, ok := v.(**os.File); ok { + *f, err = os.CreateTemp("", "HttpClientFile") + if err != nil { + return + } + _, err = (*f).Write(b) + if err != nil { + return + } + _, err = (*f).Seek(0, io.SeekStart) + return + } + if XmlCheck.MatchString(contentType) { + if err = xml.Unmarshal(b, v); err != nil { + return err + } + return nil + } + if JsonCheck.MatchString(contentType) { + if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas + if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined + if err = unmarshalObj.UnmarshalJSON(b); err != nil { + return err + } + } else { + return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") + } + } else if err = json.Unmarshal(b, v); err != nil { // simple model + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Add a file to the multipart request +func addFile(w *multipart.Writer, fieldName, path string) error { + file, err := os.Open(filepath.Clean(path)) + if err != nil { + return err + } + err = file.Close() + if err != nil { + return err + } + + part, err := w.CreateFormFile(fieldName, filepath.Base(path)) + if err != nil { + return err + } + _, err = io.Copy(part, file) + + return err +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if fp, ok := body.(*os.File); ok { + _, err = bodyBuf.ReadFrom(fp) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if JsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } else if XmlCheck.MatchString(contentType) { + var bs []byte + bs, err = xml.Marshal(body) + if err == nil { + bodyBuf.Write(bs) + } + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("invalid body type %s\n", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + return contentType +} + +// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go +type cacheControl map[string]string + +func parseCacheControl(headers http.Header) cacheControl { + cc := cacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc +} + +// CacheExpires helper function to determine remaining time before repeating a request. +func CacheExpires(r *http.Response) time.Time { + // Figure out when the cache expires. + var expires time.Time + now, err := time.Parse(time.RFC1123, r.Header.Get("date")) + if err != nil { + return time.Now() + } + respCacheControl := parseCacheControl(r.Header) + + if maxAge, ok := respCacheControl["max-age"]; ok { + lifetime, err := time.ParseDuration(maxAge + "s") + if err != nil { + expires = now + } else { + expires = now.Add(lifetime) + } + } else { + expiresHeader := r.Header.Get("Expires") + if expiresHeader != "" { + expires, err = time.Parse(time.RFC1123, expiresHeader) + if err != nil { + expires = now + } + } + } + return expires +} + +func strlen(s string) int { + return utf8.RuneCountInString(s) +} + +// GenericOpenAPIError Provides access to the body, error and model on returned errors. +type GenericOpenAPIError struct { + body []byte + error string + model interface{} +} + +// Error returns non-empty string if there was an error. +func (e GenericOpenAPIError) Error() string { + return e.error +} + +// Body returns the raw bytes of the response +func (e GenericOpenAPIError) Body() []byte { + return e.body +} + +// Model returns the unpacked model of the error +func (e GenericOpenAPIError) Model() interface{} { + return e.model +} + +// format error message using title and detail when model implements rfc7807 +func formatErrorMessage(status string, v interface{}) string { + str := "" + metaValue := reflect.ValueOf(v).Elem() + + if metaValue.Kind() == reflect.Struct { + field := metaValue.FieldByName("Title") + if field != (reflect.Value{}) { + str = fmt.Sprintf("%s", field.Interface()) + } + + field = metaValue.FieldByName("Detail") + if field != (reflect.Value{}) { + str = fmt.Sprintf("%s (%s)", str, field.Interface()) + } + } + + return strings.TrimSpace(fmt.Sprintf("%s %s", status, str)) +} diff --git a/pkg/httpclient/configuration.go b/pkg/httpclient/configuration.go new file mode 100644 index 0000000..c383daa --- /dev/null +++ b/pkg/httpclient/configuration.go @@ -0,0 +1,218 @@ +/* +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 ( + "context" + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextAPIKeys takes a string apikey as authentication for the request + ContextAPIKeys = contextKey("apiKeys") + + // ContextServerIndex uses a server configuration from the index. + ContextServerIndex = contextKey("serverIndex") + + // ContextOperationServerIndices uses a server configuration from the index mapping. + ContextOperationServerIndices = contextKey("serverOperationIndices") + + // ContextServerVariables overrides a server configuration variables. + ContextServerVariables = contextKey("serverVariables") + + // ContextOperationServerVariables overrides a server configuration variables using operation specific values. + ContextOperationServerVariables = contextKey("serverOperationVariables") +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + URL string + Description string + Variables map[string]ServerVariable +} + +// ServerConfigurations stores multiple ServerConfiguration items +type ServerConfigurations []ServerConfiguration + +// Configuration stores the configuration of the API client +type Configuration struct { + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers ServerConfigurations + OperationServers map[string]ServerConfigurations + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration() *Configuration { + cfg := &Configuration{ + DefaultHeader: make(map[string]string), + UserAgent: "OpenAPI-Generator/1.0.0/go", + Debug: false, + Servers: ServerConfigurations{ + { + URL: "", + Description: "No description provided", + }, + }, + OperationServers: map[string]ServerConfigurations{}, + } + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// URL formats template on a index using given variables +func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { + if index < 0 || len(sc) <= index { + return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1) + } + server := sc[index] + url := server.URL + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} + +// ServerURL returns URL based on server settings +func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { + return c.Servers.URL(index, variables) +} + +func getServerIndex(ctx context.Context) (int, error) { + si := ctx.Value(ContextServerIndex) + if si != nil { + if index, ok := si.(int); ok { + return index, nil + } + return 0, reportError("Invalid type %T should be int", si) + } + return 0, nil +} + +func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { + osi := ctx.Value(ContextOperationServerIndices) + if osi != nil { + if operationIndices, ok := osi.(map[string]int); !ok { + return 0, reportError("Invalid type %T should be map[string]int", osi) + } else { + index, ok := operationIndices[endpoint] + if ok { + return index, nil + } + } + } + return getServerIndex(ctx) +} + +func getServerVariables(ctx context.Context) (map[string]string, error) { + sv := ctx.Value(ContextServerVariables) + if sv != nil { + if variables, ok := sv.(map[string]string); ok { + return variables, nil + } + return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) + } + return nil, nil +} + +func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { + osv := ctx.Value(ContextOperationServerVariables) + if osv != nil { + if operationVariables, ok := osv.(map[string]map[string]string); !ok { + return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) + } else { + variables, ok := operationVariables[endpoint] + if ok { + return variables, nil + } + } + } + return getServerVariables(ctx) +} + +// ServerURLWithContext returns a new server URL given an endpoint +func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { + sc, ok := c.OperationServers[endpoint] + if !ok { + sc = c.Servers + } + + if ctx == nil { + return sc.URL(0, nil) + } + + index, err := getServerOperationIndex(ctx, endpoint) + if err != nil { + return "", err + } + + variables, err := getServerOperationVariables(ctx, endpoint) + if err != nil { + return "", err + } + + return sc.URL(index, variables) +} diff --git a/pkg/httpclient/git_push.sh b/pkg/httpclient/git_push.sh new file mode 100644 index 0000000..b036751 --- /dev/null +++ b/pkg/httpclient/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="ory" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="client-go" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret.go b/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret.go new file mode 100644 index 0000000..ee04513 --- /dev/null +++ b/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the AdminIdentityImportCredentialsLookupSecret type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AdminIdentityImportCredentialsLookupSecret{} + +// AdminIdentityImportCredentialsLookupSecret Create Identity and Import Lookup Secret Credentials +type AdminIdentityImportCredentialsLookupSecret struct { + Config *AdminIdentityImportCredentialsLookupSecretConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _AdminIdentityImportCredentialsLookupSecret AdminIdentityImportCredentialsLookupSecret + +// NewAdminIdentityImportCredentialsLookupSecret instantiates a new AdminIdentityImportCredentialsLookupSecret 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 NewAdminIdentityImportCredentialsLookupSecret() *AdminIdentityImportCredentialsLookupSecret { + this := AdminIdentityImportCredentialsLookupSecret{} + return &this +} + +// NewAdminIdentityImportCredentialsLookupSecretWithDefaults instantiates a new AdminIdentityImportCredentialsLookupSecret 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 NewAdminIdentityImportCredentialsLookupSecretWithDefaults() *AdminIdentityImportCredentialsLookupSecret { + this := AdminIdentityImportCredentialsLookupSecret{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *AdminIdentityImportCredentialsLookupSecret) GetConfig() AdminIdentityImportCredentialsLookupSecretConfig { + if o == nil || IsNil(o.Config) { + var ret AdminIdentityImportCredentialsLookupSecretConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AdminIdentityImportCredentialsLookupSecret) GetConfigOk() (*AdminIdentityImportCredentialsLookupSecretConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *AdminIdentityImportCredentialsLookupSecret) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given AdminIdentityImportCredentialsLookupSecretConfig and assigns it to the Config field. +func (o *AdminIdentityImportCredentialsLookupSecret) SetConfig(v AdminIdentityImportCredentialsLookupSecretConfig) { + o.Config = &v +} + +func (o AdminIdentityImportCredentialsLookupSecret) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AdminIdentityImportCredentialsLookupSecret) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *AdminIdentityImportCredentialsLookupSecret) UnmarshalJSON(data []byte) (err error) { + varAdminIdentityImportCredentialsLookupSecret := _AdminIdentityImportCredentialsLookupSecret{} + + err = json.Unmarshal(data, &varAdminIdentityImportCredentialsLookupSecret) + + if err != nil { + return err + } + + *o = AdminIdentityImportCredentialsLookupSecret(varAdminIdentityImportCredentialsLookupSecret) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableAdminIdentityImportCredentialsLookupSecret struct { + value *AdminIdentityImportCredentialsLookupSecret + isSet bool +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) Get() *AdminIdentityImportCredentialsLookupSecret { + return v.value +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) Set(val *AdminIdentityImportCredentialsLookupSecret) { + v.value = val + v.isSet = true +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) IsSet() bool { + return v.isSet +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAdminIdentityImportCredentialsLookupSecret(val *AdminIdentityImportCredentialsLookupSecret) *NullableAdminIdentityImportCredentialsLookupSecret { + return &NullableAdminIdentityImportCredentialsLookupSecret{value: val, isSet: true} +} + +func (v NullableAdminIdentityImportCredentialsLookupSecret) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecret) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret_config.go b/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret_config.go new file mode 100644 index 0000000..87c5428 --- /dev/null +++ b/pkg/httpclient/model_admin_identity_import_credentials_lookup_secret_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the AdminIdentityImportCredentialsLookupSecretConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AdminIdentityImportCredentialsLookupSecretConfig{} + +// AdminIdentityImportCredentialsLookupSecretConfig Create Identity and Import Lookup Secret Credentials Configuration +type AdminIdentityImportCredentialsLookupSecretConfig struct { + // Codes is a list of \"lookup secret\" codes configured for the user. + Codes []IdentityCredentialsLookupSecretCode `json:"codes,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _AdminIdentityImportCredentialsLookupSecretConfig AdminIdentityImportCredentialsLookupSecretConfig + +// NewAdminIdentityImportCredentialsLookupSecretConfig instantiates a new AdminIdentityImportCredentialsLookupSecretConfig 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 NewAdminIdentityImportCredentialsLookupSecretConfig() *AdminIdentityImportCredentialsLookupSecretConfig { + this := AdminIdentityImportCredentialsLookupSecretConfig{} + return &this +} + +// NewAdminIdentityImportCredentialsLookupSecretConfigWithDefaults instantiates a new AdminIdentityImportCredentialsLookupSecretConfig 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 NewAdminIdentityImportCredentialsLookupSecretConfigWithDefaults() *AdminIdentityImportCredentialsLookupSecretConfig { + this := AdminIdentityImportCredentialsLookupSecretConfig{} + return &this +} + +// GetCodes returns the Codes field value if set, zero value otherwise. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) GetCodes() []IdentityCredentialsLookupSecretCode { + if o == nil || IsNil(o.Codes) { + var ret []IdentityCredentialsLookupSecretCode + return ret + } + return o.Codes +} + +// GetCodesOk returns a tuple with the Codes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) GetCodesOk() ([]IdentityCredentialsLookupSecretCode, bool) { + if o == nil || IsNil(o.Codes) { + return nil, false + } + return o.Codes, true +} + +// HasCodes returns a boolean if a field has been set. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) HasCodes() bool { + if o != nil && !IsNil(o.Codes) { + return true + } + + return false +} + +// SetCodes gets a reference to the given []IdentityCredentialsLookupSecretCode and assigns it to the Codes field. +func (o *AdminIdentityImportCredentialsLookupSecretConfig) SetCodes(v []IdentityCredentialsLookupSecretCode) { + o.Codes = v +} + +func (o AdminIdentityImportCredentialsLookupSecretConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AdminIdentityImportCredentialsLookupSecretConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Codes) { + toSerialize["codes"] = o.Codes + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *AdminIdentityImportCredentialsLookupSecretConfig) UnmarshalJSON(data []byte) (err error) { + varAdminIdentityImportCredentialsLookupSecretConfig := _AdminIdentityImportCredentialsLookupSecretConfig{} + + err = json.Unmarshal(data, &varAdminIdentityImportCredentialsLookupSecretConfig) + + if err != nil { + return err + } + + *o = AdminIdentityImportCredentialsLookupSecretConfig(varAdminIdentityImportCredentialsLookupSecretConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "codes") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableAdminIdentityImportCredentialsLookupSecretConfig struct { + value *AdminIdentityImportCredentialsLookupSecretConfig + isSet bool +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) Get() *AdminIdentityImportCredentialsLookupSecretConfig { + return v.value +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) Set(val *AdminIdentityImportCredentialsLookupSecretConfig) { + v.value = val + v.isSet = true +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAdminIdentityImportCredentialsLookupSecretConfig(val *AdminIdentityImportCredentialsLookupSecretConfig) *NullableAdminIdentityImportCredentialsLookupSecretConfig { + return &NullableAdminIdentityImportCredentialsLookupSecretConfig{value: val, isSet: true} +} + +func (v NullableAdminIdentityImportCredentialsLookupSecretConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAdminIdentityImportCredentialsLookupSecretConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_authenticator_assurance_level.go b/pkg/httpclient/model_authenticator_assurance_level.go new file mode 100644 index 0000000..58c1786 --- /dev/null +++ b/pkg/httpclient/model_authenticator_assurance_level.go @@ -0,0 +1,115 @@ +/* +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" + "fmt" +) + +// AuthenticatorAssuranceLevel The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder for an attacker to compromise the account. Generally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g. password + TOTP) have been used. To learn more about these levels please head over to: https://www.ory.com/kratos/docs/concepts/credentials +type AuthenticatorAssuranceLevel string + +// List of authenticatorAssuranceLevel +const ( + AUTHENTICATORASSURANCELEVEL_AAL0 AuthenticatorAssuranceLevel = "aal0" + AUTHENTICATORASSURANCELEVEL_AAL1 AuthenticatorAssuranceLevel = "aal1" + AUTHENTICATORASSURANCELEVEL_AAL2 AuthenticatorAssuranceLevel = "aal2" + AUTHENTICATORASSURANCELEVEL_AAL3 AuthenticatorAssuranceLevel = "aal3" +) + +// All allowed values of AuthenticatorAssuranceLevel enum +var AllowedAuthenticatorAssuranceLevelEnumValues = []AuthenticatorAssuranceLevel{ + "aal0", + "aal1", + "aal2", + "aal3", +} + +func (v *AuthenticatorAssuranceLevel) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := AuthenticatorAssuranceLevel(value) + for _, existing := range AllowedAuthenticatorAssuranceLevelEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid AuthenticatorAssuranceLevel", value) +} + +// NewAuthenticatorAssuranceLevelFromValue returns a pointer to a valid AuthenticatorAssuranceLevel +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewAuthenticatorAssuranceLevelFromValue(v string) (*AuthenticatorAssuranceLevel, error) { + ev := AuthenticatorAssuranceLevel(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for AuthenticatorAssuranceLevel: valid values are %v", v, AllowedAuthenticatorAssuranceLevelEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v AuthenticatorAssuranceLevel) IsValid() bool { + for _, existing := range AllowedAuthenticatorAssuranceLevelEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to authenticatorAssuranceLevel value +func (v AuthenticatorAssuranceLevel) Ptr() *AuthenticatorAssuranceLevel { + return &v +} + +type NullableAuthenticatorAssuranceLevel struct { + value *AuthenticatorAssuranceLevel + isSet bool +} + +func (v NullableAuthenticatorAssuranceLevel) Get() *AuthenticatorAssuranceLevel { + return v.value +} + +func (v *NullableAuthenticatorAssuranceLevel) Set(val *AuthenticatorAssuranceLevel) { + v.value = val + v.isSet = true +} + +func (v NullableAuthenticatorAssuranceLevel) IsSet() bool { + return v.isSet +} + +func (v *NullableAuthenticatorAssuranceLevel) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAuthenticatorAssuranceLevel(val *AuthenticatorAssuranceLevel) *NullableAuthenticatorAssuranceLevel { + return &NullableAuthenticatorAssuranceLevel{value: val, isSet: true} +} + +func (v NullableAuthenticatorAssuranceLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAuthenticatorAssuranceLevel) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_batch_patch_identities_response.go b/pkg/httpclient/model_batch_patch_identities_response.go new file mode 100644 index 0000000..d66356e --- /dev/null +++ b/pkg/httpclient/model_batch_patch_identities_response.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the BatchPatchIdentitiesResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &BatchPatchIdentitiesResponse{} + +// BatchPatchIdentitiesResponse Patch identities response +type BatchPatchIdentitiesResponse struct { + // The patch responses for the individual identities. + Identities []IdentityPatchResponse `json:"identities,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _BatchPatchIdentitiesResponse BatchPatchIdentitiesResponse + +// NewBatchPatchIdentitiesResponse instantiates a new BatchPatchIdentitiesResponse 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 NewBatchPatchIdentitiesResponse() *BatchPatchIdentitiesResponse { + this := BatchPatchIdentitiesResponse{} + return &this +} + +// NewBatchPatchIdentitiesResponseWithDefaults instantiates a new BatchPatchIdentitiesResponse 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 NewBatchPatchIdentitiesResponseWithDefaults() *BatchPatchIdentitiesResponse { + this := BatchPatchIdentitiesResponse{} + return &this +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *BatchPatchIdentitiesResponse) GetIdentities() []IdentityPatchResponse { + if o == nil || IsNil(o.Identities) { + var ret []IdentityPatchResponse + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BatchPatchIdentitiesResponse) GetIdentitiesOk() ([]IdentityPatchResponse, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *BatchPatchIdentitiesResponse) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []IdentityPatchResponse and assigns it to the Identities field. +func (o *BatchPatchIdentitiesResponse) SetIdentities(v []IdentityPatchResponse) { + o.Identities = v +} + +func (o BatchPatchIdentitiesResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o BatchPatchIdentitiesResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *BatchPatchIdentitiesResponse) UnmarshalJSON(data []byte) (err error) { + varBatchPatchIdentitiesResponse := _BatchPatchIdentitiesResponse{} + + err = json.Unmarshal(data, &varBatchPatchIdentitiesResponse) + + if err != nil { + return err + } + + *o = BatchPatchIdentitiesResponse(varBatchPatchIdentitiesResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "identities") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableBatchPatchIdentitiesResponse struct { + value *BatchPatchIdentitiesResponse + isSet bool +} + +func (v NullableBatchPatchIdentitiesResponse) Get() *BatchPatchIdentitiesResponse { + return v.value +} + +func (v *NullableBatchPatchIdentitiesResponse) Set(val *BatchPatchIdentitiesResponse) { + v.value = val + v.isSet = true +} + +func (v NullableBatchPatchIdentitiesResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableBatchPatchIdentitiesResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBatchPatchIdentitiesResponse(val *BatchPatchIdentitiesResponse) *NullableBatchPatchIdentitiesResponse { + return &NullableBatchPatchIdentitiesResponse{value: val, isSet: true} +} + +func (v NullableBatchPatchIdentitiesResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBatchPatchIdentitiesResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_consistency_request_parameters.go b/pkg/httpclient/model_consistency_request_parameters.go new file mode 100644 index 0000000..0628cba --- /dev/null +++ b/pkg/httpclient/model_consistency_request_parameters.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the ConsistencyRequestParameters type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ConsistencyRequestParameters{} + +// ConsistencyRequestParameters Control API consistency guarantees +type ConsistencyRequestParameters struct { + // Read Consistency Level (preview) The read consistency level determines the consistency guarantee for reads: strong (slow): The read is guaranteed to return the most recent data committed at the start of the read. eventual (very fast): The result will return data that is about 4.8 seconds old. The default consistency guarantee can be changed in the Ory Network Console or using the Ory CLI with `ory patch project --replace '/previews/default_read_consistency_level=\"strong\"'`. Setting the default consistency level to `eventual` may cause regressions in the future as we add consistency controls to more APIs. Currently, the following APIs will be affected by this setting: `GET /admin/identities` This feature is in preview and only available in Ory Network. ConsistencyLevelUnset ConsistencyLevelUnset is the unset / default consistency level. strong ConsistencyLevelStrong ConsistencyLevelStrong is the strong consistency level. eventual ConsistencyLevelEventual ConsistencyLevelEventual is the eventual consistency level using follower read timestamps. + Consistency *string `json:"consistency,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ConsistencyRequestParameters ConsistencyRequestParameters + +// NewConsistencyRequestParameters instantiates a new ConsistencyRequestParameters 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 NewConsistencyRequestParameters() *ConsistencyRequestParameters { + this := ConsistencyRequestParameters{} + return &this +} + +// NewConsistencyRequestParametersWithDefaults instantiates a new ConsistencyRequestParameters 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 NewConsistencyRequestParametersWithDefaults() *ConsistencyRequestParameters { + this := ConsistencyRequestParameters{} + return &this +} + +// GetConsistency returns the Consistency field value if set, zero value otherwise. +func (o *ConsistencyRequestParameters) GetConsistency() string { + if o == nil || IsNil(o.Consistency) { + var ret string + return ret + } + return *o.Consistency +} + +// GetConsistencyOk returns a tuple with the Consistency field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ConsistencyRequestParameters) GetConsistencyOk() (*string, bool) { + if o == nil || IsNil(o.Consistency) { + return nil, false + } + return o.Consistency, true +} + +// HasConsistency returns a boolean if a field has been set. +func (o *ConsistencyRequestParameters) HasConsistency() bool { + if o != nil && !IsNil(o.Consistency) { + return true + } + + return false +} + +// SetConsistency gets a reference to the given string and assigns it to the Consistency field. +func (o *ConsistencyRequestParameters) SetConsistency(v string) { + o.Consistency = &v +} + +func (o ConsistencyRequestParameters) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ConsistencyRequestParameters) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Consistency) { + toSerialize["consistency"] = o.Consistency + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ConsistencyRequestParameters) UnmarshalJSON(data []byte) (err error) { + varConsistencyRequestParameters := _ConsistencyRequestParameters{} + + err = json.Unmarshal(data, &varConsistencyRequestParameters) + + if err != nil { + return err + } + + *o = ConsistencyRequestParameters(varConsistencyRequestParameters) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "consistency") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableConsistencyRequestParameters struct { + value *ConsistencyRequestParameters + isSet bool +} + +func (v NullableConsistencyRequestParameters) Get() *ConsistencyRequestParameters { + return v.value +} + +func (v *NullableConsistencyRequestParameters) Set(val *ConsistencyRequestParameters) { + v.value = val + v.isSet = true +} + +func (v NullableConsistencyRequestParameters) IsSet() bool { + return v.isSet +} + +func (v *NullableConsistencyRequestParameters) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableConsistencyRequestParameters(val *ConsistencyRequestParameters) *NullableConsistencyRequestParameters { + return &NullableConsistencyRequestParameters{value: val, isSet: true} +} + +func (v NullableConsistencyRequestParameters) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableConsistencyRequestParameters) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with.go b/pkg/httpclient/model_continue_with.go new file mode 100644 index 0000000..af6ec30 --- /dev/null +++ b/pkg/httpclient/model_continue_with.go @@ -0,0 +1,354 @@ +/* +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" + "fmt" +) + +// ContinueWith - struct for ContinueWith +type ContinueWith struct { + ContinueWithDeviceAuthnPinEntryUi *ContinueWithDeviceAuthnPinEntryUi + ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo + ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken + ContinueWithSettingsUi *ContinueWithSettingsUi + ContinueWithVerificationUi *ContinueWithVerificationUi +} + +// ContinueWithDeviceAuthnPinEntryUiAsContinueWith is a convenience function that returns ContinueWithDeviceAuthnPinEntryUi wrapped in ContinueWith +func ContinueWithDeviceAuthnPinEntryUiAsContinueWith(v *ContinueWithDeviceAuthnPinEntryUi) ContinueWith { + return ContinueWith{ + ContinueWithDeviceAuthnPinEntryUi: v, + } +} + +// ContinueWithRecoveryUiAsContinueWith is a convenience function that returns ContinueWithRecoveryUi wrapped in ContinueWith +func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWith { + return ContinueWith{ + ContinueWithRecoveryUi: v, + } +} + +// 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{ + ContinueWithSetOrySessionToken: v, + } +} + +// ContinueWithSettingsUiAsContinueWith is a convenience function that returns ContinueWithSettingsUi wrapped in ContinueWith +func ContinueWithSettingsUiAsContinueWith(v *ContinueWithSettingsUi) ContinueWith { + return ContinueWith{ + ContinueWithSettingsUi: v, + } +} + +// ContinueWithVerificationUiAsContinueWith is a convenience function that returns ContinueWithVerificationUi wrapped in ContinueWith +func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) ContinueWith { + return ContinueWith{ + ContinueWithVerificationUi: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *ContinueWith) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + + // 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_pin_entry_ui' + if jsonDict["action"] == "show_pin_entry_ui" { + // try to unmarshal JSON data into ContinueWithDeviceAuthnPinEntryUi + err = json.Unmarshal(data, &dst.ContinueWithDeviceAuthnPinEntryUi) + if err == nil { + return nil // data stored in dst.ContinueWithDeviceAuthnPinEntryUi, return on the first match + } else { + dst.ContinueWithDeviceAuthnPinEntryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithDeviceAuthnPinEntryUi: %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 { + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) + } + } + + // 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 { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } + + // 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 'continueWithDeviceAuthnPinEntryUi' + if jsonDict["action"] == "continueWithDeviceAuthnPinEntryUi" { + // try to unmarshal JSON data into ContinueWithDeviceAuthnPinEntryUi + err = json.Unmarshal(data, &dst.ContinueWithDeviceAuthnPinEntryUi) + if err == nil { + return nil // data stored in dst.ContinueWithDeviceAuthnPinEntryUi, return on the first match + } else { + dst.ContinueWithDeviceAuthnPinEntryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithDeviceAuthnPinEntryUi: %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 { + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src ContinueWith) MarshalJSON() ([]byte, error) { + if src.ContinueWithDeviceAuthnPinEntryUi != nil { + return json.Marshal(&src.ContinueWithDeviceAuthnPinEntryUi) + } + + if src.ContinueWithRecoveryUi != nil { + return json.Marshal(&src.ContinueWithRecoveryUi) + } + + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + + if src.ContinueWithSetOrySessionToken != nil { + return json.Marshal(&src.ContinueWithSetOrySessionToken) + } + + if src.ContinueWithSettingsUi != nil { + return json.Marshal(&src.ContinueWithSettingsUi) + } + + if src.ContinueWithVerificationUi != nil { + return json.Marshal(&src.ContinueWithVerificationUi) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *ContinueWith) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.ContinueWithDeviceAuthnPinEntryUi != nil { + return obj.ContinueWithDeviceAuthnPinEntryUi + } + + if obj.ContinueWithRecoveryUi != nil { + return obj.ContinueWithRecoveryUi + } + + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + + if obj.ContinueWithSetOrySessionToken != nil { + return obj.ContinueWithSetOrySessionToken + } + + if obj.ContinueWithSettingsUi != nil { + return obj.ContinueWithSettingsUi + } + + if obj.ContinueWithVerificationUi != nil { + return obj.ContinueWithVerificationUi + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj ContinueWith) GetActualInstanceValue() interface{} { + if obj.ContinueWithDeviceAuthnPinEntryUi != nil { + return *obj.ContinueWithDeviceAuthnPinEntryUi + } + + if obj.ContinueWithRecoveryUi != nil { + return *obj.ContinueWithRecoveryUi + } + + if obj.ContinueWithRedirectBrowserTo != nil { + return *obj.ContinueWithRedirectBrowserTo + } + + if obj.ContinueWithSetOrySessionToken != nil { + return *obj.ContinueWithSetOrySessionToken + } + + if obj.ContinueWithSettingsUi != nil { + return *obj.ContinueWithSettingsUi + } + + if obj.ContinueWithVerificationUi != nil { + return *obj.ContinueWithVerificationUi + } + + // all schemas are nil + return nil +} + +type NullableContinueWith struct { + value *ContinueWith + isSet bool +} + +func (v NullableContinueWith) Get() *ContinueWith { + return v.value +} + +func (v *NullableContinueWith) Set(val *ContinueWith) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWith) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWith) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWith(val *ContinueWith) *NullableContinueWith { + return &NullableContinueWith{value: val, isSet: true} +} + +func (v NullableContinueWith) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWith) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui.go b/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui.go new file mode 100644 index 0000000..b54a5eb --- /dev/null +++ b/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithDeviceAuthnPinEntryUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithDeviceAuthnPinEntryUi{} + +// ContinueWithDeviceAuthnPinEntryUi Returned after a PIN-protected DeviceAuthn enrollment or secret rotation. It carries the one-time HPKE-sealed pin_secret, which the device opens with the transport private key it generated for this enrollment and binds to the user's PIN. The plaintext secret never leaves the device; the server only ever stores its at-rest ciphertext. The enrolled key's client_key_id is not included: it is the SHA-256 fingerprint of the device's own public key, which the device derives locally. Non-PIN keys need no client action at all and therefore produce no continue_with. +type ContinueWithDeviceAuthnPinEntryUi struct { + // Action will always be `show_pin_entry_ui` show_pin_entry_ui ContinueWithActionShowPINEntryUIString + Action string `json:"action"` + // The one-time HPKE-sealed pin_secret material the device opens to bind the user's PIN after a PIN-protected DeviceAuthn enrollment or secret rotation. + Data ContinueWithDeviceAuthnPinEntryUiData `json:"data"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithDeviceAuthnPinEntryUi ContinueWithDeviceAuthnPinEntryUi + +// NewContinueWithDeviceAuthnPinEntryUi instantiates a new ContinueWithDeviceAuthnPinEntryUi 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 NewContinueWithDeviceAuthnPinEntryUi(action string, data ContinueWithDeviceAuthnPinEntryUiData) *ContinueWithDeviceAuthnPinEntryUi { + this := ContinueWithDeviceAuthnPinEntryUi{} + this.Action = action + this.Data = data + return &this +} + +// NewContinueWithDeviceAuthnPinEntryUiWithDefaults instantiates a new ContinueWithDeviceAuthnPinEntryUi 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 NewContinueWithDeviceAuthnPinEntryUiWithDefaults() *ContinueWithDeviceAuthnPinEntryUi { + this := ContinueWithDeviceAuthnPinEntryUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithDeviceAuthnPinEntryUi) 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 *ContinueWithDeviceAuthnPinEntryUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithDeviceAuthnPinEntryUi) SetAction(v string) { + o.Action = v +} + +// GetData returns the Data field value +func (o *ContinueWithDeviceAuthnPinEntryUi) GetData() ContinueWithDeviceAuthnPinEntryUiData { + if o == nil { + var ret ContinueWithDeviceAuthnPinEntryUiData + return ret + } + + return o.Data +} + +// GetDataOk returns a tuple with the Data field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUi) GetDataOk() (*ContinueWithDeviceAuthnPinEntryUiData, bool) { + if o == nil { + return nil, false + } + return &o.Data, true +} + +// SetData sets field value +func (o *ContinueWithDeviceAuthnPinEntryUi) SetData(v ContinueWithDeviceAuthnPinEntryUiData) { + o.Data = v +} + +func (o ContinueWithDeviceAuthnPinEntryUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithDeviceAuthnPinEntryUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["data"] = o.Data + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithDeviceAuthnPinEntryUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "data", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithDeviceAuthnPinEntryUi := _ContinueWithDeviceAuthnPinEntryUi{} + + err = json.Unmarshal(data, &varContinueWithDeviceAuthnPinEntryUi) + + if err != nil { + return err + } + + *o = ContinueWithDeviceAuthnPinEntryUi(varContinueWithDeviceAuthnPinEntryUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "data") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithDeviceAuthnPinEntryUi struct { + value *ContinueWithDeviceAuthnPinEntryUi + isSet bool +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) Get() *ContinueWithDeviceAuthnPinEntryUi { + return v.value +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) Set(val *ContinueWithDeviceAuthnPinEntryUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithDeviceAuthnPinEntryUi(val *ContinueWithDeviceAuthnPinEntryUi) *NullableContinueWithDeviceAuthnPinEntryUi { + return &NullableContinueWithDeviceAuthnPinEntryUi{value: val, isSet: true} +} + +func (v NullableContinueWithDeviceAuthnPinEntryUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui_data.go b/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui_data.go new file mode 100644 index 0000000..446cfde --- /dev/null +++ b/pkg/httpclient/model_continue_with_device_authn_pin_entry_ui_data.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithDeviceAuthnPinEntryUiData type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithDeviceAuthnPinEntryUiData{} + +// ContinueWithDeviceAuthnPinEntryUiData Contains the encapsulated key and the sealed ciphertext that the device opens with the X25519 transport private key it generated for this enrollment or rotation. Open it with HPKE (RFC 9180) using the suite DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, the ASCII info string \"ory/deviceauthn/pin-secret/v1\", and the key's client_key_id (its ASCII hex form) as the AAD. +type ContinueWithDeviceAuthnPinEntryUiData struct { + // The base64-encoded HPKE ciphertext of the sealed pin_secret. + Ciphertext string `json:"ciphertext"` + // The base64-encoded HPKE encapsulated key (the `enc` output of the seal operation). + Enc string `json:"enc"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithDeviceAuthnPinEntryUiData ContinueWithDeviceAuthnPinEntryUiData + +// NewContinueWithDeviceAuthnPinEntryUiData instantiates a new ContinueWithDeviceAuthnPinEntryUiData 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 NewContinueWithDeviceAuthnPinEntryUiData(ciphertext string, enc string) *ContinueWithDeviceAuthnPinEntryUiData { + this := ContinueWithDeviceAuthnPinEntryUiData{} + this.Ciphertext = ciphertext + this.Enc = enc + return &this +} + +// NewContinueWithDeviceAuthnPinEntryUiDataWithDefaults instantiates a new ContinueWithDeviceAuthnPinEntryUiData 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 NewContinueWithDeviceAuthnPinEntryUiDataWithDefaults() *ContinueWithDeviceAuthnPinEntryUiData { + this := ContinueWithDeviceAuthnPinEntryUiData{} + return &this +} + +// GetCiphertext returns the Ciphertext field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetCiphertext() string { + if o == nil { + var ret string + return ret + } + + return o.Ciphertext +} + +// GetCiphertextOk returns a tuple with the Ciphertext field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetCiphertextOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Ciphertext, true +} + +// SetCiphertext sets field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) SetCiphertext(v string) { + o.Ciphertext = v +} + +// GetEnc returns the Enc field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetEnc() string { + if o == nil { + var ret string + return ret + } + + return o.Enc +} + +// GetEncOk returns a tuple with the Enc field value +// and a boolean to check if the value has been set. +func (o *ContinueWithDeviceAuthnPinEntryUiData) GetEncOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Enc, true +} + +// SetEnc sets field value +func (o *ContinueWithDeviceAuthnPinEntryUiData) SetEnc(v string) { + o.Enc = v +} + +func (o ContinueWithDeviceAuthnPinEntryUiData) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithDeviceAuthnPinEntryUiData) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["ciphertext"] = o.Ciphertext + toSerialize["enc"] = o.Enc + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithDeviceAuthnPinEntryUiData) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "ciphertext", + "enc", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithDeviceAuthnPinEntryUiData := _ContinueWithDeviceAuthnPinEntryUiData{} + + err = json.Unmarshal(data, &varContinueWithDeviceAuthnPinEntryUiData) + + if err != nil { + return err + } + + *o = ContinueWithDeviceAuthnPinEntryUiData(varContinueWithDeviceAuthnPinEntryUiData) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "ciphertext") + delete(additionalProperties, "enc") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithDeviceAuthnPinEntryUiData struct { + value *ContinueWithDeviceAuthnPinEntryUiData + isSet bool +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) Get() *ContinueWithDeviceAuthnPinEntryUiData { + return v.value +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) Set(val *ContinueWithDeviceAuthnPinEntryUiData) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithDeviceAuthnPinEntryUiData(val *ContinueWithDeviceAuthnPinEntryUiData) *NullableContinueWithDeviceAuthnPinEntryUiData { + return &NullableContinueWithDeviceAuthnPinEntryUiData{value: val, isSet: true} +} + +func (v NullableContinueWithDeviceAuthnPinEntryUiData) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithDeviceAuthnPinEntryUiData) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_recovery_ui.go b/pkg/httpclient/model_continue_with_recovery_ui.go new file mode 100644 index 0000000..4ecc198 --- /dev/null +++ b/pkg/httpclient/model_continue_with_recovery_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRecoveryUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRecoveryUi{} + +// ContinueWithRecoveryUi Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRecoveryUi struct { + // Action will always be `show_recovery_ui` show_recovery_ui ContinueWithActionShowRecoveryUIString + Action string `json:"action"` + Flow ContinueWithRecoveryUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRecoveryUi ContinueWithRecoveryUi + +// NewContinueWithRecoveryUi instantiates a new ContinueWithRecoveryUi 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 NewContinueWithRecoveryUi(action string, flow ContinueWithRecoveryUiFlow) *ContinueWithRecoveryUi { + this := ContinueWithRecoveryUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithRecoveryUiWithDefaults instantiates a new ContinueWithRecoveryUi 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 NewContinueWithRecoveryUiWithDefaults() *ContinueWithRecoveryUi { + this := ContinueWithRecoveryUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithRecoveryUi) 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 *ContinueWithRecoveryUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRecoveryUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithRecoveryUi) GetFlow() ContinueWithRecoveryUiFlow { + if o == nil { + var ret ContinueWithRecoveryUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRecoveryUi) GetFlowOk() (*ContinueWithRecoveryUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithRecoveryUi) SetFlow(v ContinueWithRecoveryUiFlow) { + o.Flow = v +} + +func (o ContinueWithRecoveryUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRecoveryUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRecoveryUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRecoveryUi := _ContinueWithRecoveryUi{} + + err = json.Unmarshal(data, &varContinueWithRecoveryUi) + + if err != nil { + return err + } + + *o = ContinueWithRecoveryUi(varContinueWithRecoveryUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithRecoveryUi struct { + value *ContinueWithRecoveryUi + isSet bool +} + +func (v NullableContinueWithRecoveryUi) Get() *ContinueWithRecoveryUi { + return v.value +} + +func (v *NullableContinueWithRecoveryUi) Set(val *ContinueWithRecoveryUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRecoveryUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRecoveryUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRecoveryUi(val *ContinueWithRecoveryUi) *NullableContinueWithRecoveryUi { + return &NullableContinueWithRecoveryUi{value: val, isSet: true} +} + +func (v NullableContinueWithRecoveryUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRecoveryUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_recovery_ui_flow.go b/pkg/httpclient/model_continue_with_recovery_ui_flow.go new file mode 100644 index 0000000..9190751 --- /dev/null +++ b/pkg/httpclient/model_continue_with_recovery_ui_flow.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRecoveryUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRecoveryUiFlow{} + +// ContinueWithRecoveryUiFlow struct for ContinueWithRecoveryUiFlow +type ContinueWithRecoveryUiFlow struct { + // The ID of the recovery flow + Id string `json:"id"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRecoveryUiFlow ContinueWithRecoveryUiFlow + +// NewContinueWithRecoveryUiFlow instantiates a new ContinueWithRecoveryUiFlow 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 NewContinueWithRecoveryUiFlow(id string) *ContinueWithRecoveryUiFlow { + this := ContinueWithRecoveryUiFlow{} + this.Id = id + return &this +} + +// NewContinueWithRecoveryUiFlowWithDefaults instantiates a new ContinueWithRecoveryUiFlow 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 NewContinueWithRecoveryUiFlowWithDefaults() *ContinueWithRecoveryUiFlow { + this := ContinueWithRecoveryUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithRecoveryUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRecoveryUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *ContinueWithRecoveryUiFlow) SetId(v string) { + o.Id = v +} + +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithRecoveryUiFlow) GetUrl() string { + if o == nil || IsNil(o.Url) { + 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 *ContinueWithRecoveryUiFlow) GetUrlOk() (*string, bool) { + if o == nil || IsNil(o.Url) { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithRecoveryUiFlow) HasUrl() bool { + if o != nil && !IsNil(o.Url) { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithRecoveryUiFlow) SetUrl(v string) { + o.Url = &v +} + +func (o ContinueWithRecoveryUiFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRecoveryUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRecoveryUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRecoveryUiFlow := _ContinueWithRecoveryUiFlow{} + + err = json.Unmarshal(data, &varContinueWithRecoveryUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithRecoveryUiFlow(varContinueWithRecoveryUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithRecoveryUiFlow struct { + value *ContinueWithRecoveryUiFlow + isSet bool +} + +func (v NullableContinueWithRecoveryUiFlow) Get() *ContinueWithRecoveryUiFlow { + return v.value +} + +func (v *NullableContinueWithRecoveryUiFlow) Set(val *ContinueWithRecoveryUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRecoveryUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRecoveryUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRecoveryUiFlow(val *ContinueWithRecoveryUiFlow) *NullableContinueWithRecoveryUiFlow { + return &NullableContinueWithRecoveryUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithRecoveryUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRecoveryUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_redirect_browser_to.go b/pkg/httpclient/model_continue_with_redirect_browser_to.go new file mode 100644 index 0000000..aa5dc91 --- /dev/null +++ b/pkg/httpclient/model_continue_with_redirect_browser_to.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithRedirectBrowserTo type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithRedirectBrowserTo{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithRedirectBrowserTo ContinueWithRedirectBrowserTo + +// 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithRedirectBrowserTo) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithRedirectBrowserTo) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "redirect_browser_to", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithRedirectBrowserTo := _ContinueWithRedirectBrowserTo{} + + err = json.Unmarshal(data, &varContinueWithRedirectBrowserTo) + + if err != nil { + return err + } + + *o = ContinueWithRedirectBrowserTo(varContinueWithRedirectBrowserTo) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_continue_with_set_ory_session_token.go b/pkg/httpclient/model_continue_with_set_ory_session_token.go new file mode 100644 index 0000000..c8f9b62 --- /dev/null +++ b/pkg/httpclient/model_continue_with_set_ory_session_token.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSetOrySessionToken type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSetOrySessionToken{} + +// ContinueWithSetOrySessionToken Indicates that a session was issued, and the application should use this token for authenticated requests +type ContinueWithSetOrySessionToken struct { + // Action will always be `set_ory_session_token` set_ory_session_token ContinueWithActionSetOrySessionTokenString + Action string `json:"action"` + // Token is the token of the session + OrySessionToken string `json:"ory_session_token"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSetOrySessionToken ContinueWithSetOrySessionToken + +// NewContinueWithSetOrySessionToken instantiates a new ContinueWithSetOrySessionToken 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 NewContinueWithSetOrySessionToken(action string, orySessionToken string) *ContinueWithSetOrySessionToken { + this := ContinueWithSetOrySessionToken{} + this.Action = action + this.OrySessionToken = orySessionToken + return &this +} + +// NewContinueWithSetOrySessionTokenWithDefaults instantiates a new ContinueWithSetOrySessionToken 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 NewContinueWithSetOrySessionTokenWithDefaults() *ContinueWithSetOrySessionToken { + this := ContinueWithSetOrySessionToken{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithSetOrySessionToken) 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 *ContinueWithSetOrySessionToken) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithSetOrySessionToken) SetAction(v string) { + o.Action = v +} + +// GetOrySessionToken returns the OrySessionToken field value +func (o *ContinueWithSetOrySessionToken) GetOrySessionToken() string { + if o == nil { + var ret string + return ret + } + + return o.OrySessionToken +} + +// GetOrySessionTokenOk returns a tuple with the OrySessionToken field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSetOrySessionToken) GetOrySessionTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.OrySessionToken, true +} + +// SetOrySessionToken sets field value +func (o *ContinueWithSetOrySessionToken) SetOrySessionToken(v string) { + o.OrySessionToken = v +} + +func (o ContinueWithSetOrySessionToken) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSetOrySessionToken) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["ory_session_token"] = o.OrySessionToken + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSetOrySessionToken) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "ory_session_token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSetOrySessionToken := _ContinueWithSetOrySessionToken{} + + err = json.Unmarshal(data, &varContinueWithSetOrySessionToken) + + if err != nil { + return err + } + + *o = ContinueWithSetOrySessionToken(varContinueWithSetOrySessionToken) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "ory_session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSetOrySessionToken struct { + value *ContinueWithSetOrySessionToken + isSet bool +} + +func (v NullableContinueWithSetOrySessionToken) Get() *ContinueWithSetOrySessionToken { + return v.value +} + +func (v *NullableContinueWithSetOrySessionToken) Set(val *ContinueWithSetOrySessionToken) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSetOrySessionToken) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSetOrySessionToken) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSetOrySessionToken(val *ContinueWithSetOrySessionToken) *NullableContinueWithSetOrySessionToken { + return &NullableContinueWithSetOrySessionToken{value: val, isSet: true} +} + +func (v NullableContinueWithSetOrySessionToken) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSetOrySessionToken) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_settings_ui.go b/pkg/httpclient/model_continue_with_settings_ui.go new file mode 100644 index 0000000..d9903db --- /dev/null +++ b/pkg/httpclient/model_continue_with_settings_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSettingsUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSettingsUi{} + +// ContinueWithSettingsUi Indicates, that the UI flow could be continued by showing a settings ui +type ContinueWithSettingsUi struct { + // Action will always be `show_settings_ui` show_settings_ui ContinueWithActionShowSettingsUIString + Action string `json:"action"` + Flow ContinueWithSettingsUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSettingsUi ContinueWithSettingsUi + +// NewContinueWithSettingsUi instantiates a new ContinueWithSettingsUi 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 NewContinueWithSettingsUi(action string, flow ContinueWithSettingsUiFlow) *ContinueWithSettingsUi { + this := ContinueWithSettingsUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithSettingsUiWithDefaults instantiates a new ContinueWithSettingsUi 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 NewContinueWithSettingsUiWithDefaults() *ContinueWithSettingsUi { + this := ContinueWithSettingsUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithSettingsUi) 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 *ContinueWithSettingsUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithSettingsUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithSettingsUi) GetFlow() ContinueWithSettingsUiFlow { + if o == nil { + var ret ContinueWithSettingsUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUi) GetFlowOk() (*ContinueWithSettingsUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithSettingsUi) SetFlow(v ContinueWithSettingsUiFlow) { + o.Flow = v +} + +func (o ContinueWithSettingsUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSettingsUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSettingsUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSettingsUi := _ContinueWithSettingsUi{} + + err = json.Unmarshal(data, &varContinueWithSettingsUi) + + if err != nil { + return err + } + + *o = ContinueWithSettingsUi(varContinueWithSettingsUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSettingsUi struct { + value *ContinueWithSettingsUi + isSet bool +} + +func (v NullableContinueWithSettingsUi) Get() *ContinueWithSettingsUi { + return v.value +} + +func (v *NullableContinueWithSettingsUi) Set(val *ContinueWithSettingsUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSettingsUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSettingsUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSettingsUi(val *ContinueWithSettingsUi) *NullableContinueWithSettingsUi { + return &NullableContinueWithSettingsUi{value: val, isSet: true} +} + +func (v NullableContinueWithSettingsUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSettingsUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_settings_ui_flow.go b/pkg/httpclient/model_continue_with_settings_ui_flow.go new file mode 100644 index 0000000..37c95fa --- /dev/null +++ b/pkg/httpclient/model_continue_with_settings_ui_flow.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithSettingsUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithSettingsUiFlow{} + +// ContinueWithSettingsUiFlow struct for ContinueWithSettingsUiFlow +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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithSettingsUiFlow ContinueWithSettingsUiFlow + +// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow 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 NewContinueWithSettingsUiFlow(id string) *ContinueWithSettingsUiFlow { + this := ContinueWithSettingsUiFlow{} + this.Id = id + return &this +} + +// NewContinueWithSettingsUiFlowWithDefaults instantiates a new ContinueWithSettingsUiFlow 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 NewContinueWithSettingsUiFlowWithDefaults() *ContinueWithSettingsUiFlow { + this := ContinueWithSettingsUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithSettingsUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +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 || IsNil(o.Url) { + 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 || IsNil(o.Url) { + 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 && !IsNil(o.Url) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithSettingsUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithSettingsUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithSettingsUiFlow := _ContinueWithSettingsUiFlow{} + + err = json.Unmarshal(data, &varContinueWithSettingsUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithSettingsUiFlow(varContinueWithSettingsUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithSettingsUiFlow struct { + value *ContinueWithSettingsUiFlow + isSet bool +} + +func (v NullableContinueWithSettingsUiFlow) Get() *ContinueWithSettingsUiFlow { + return v.value +} + +func (v *NullableContinueWithSettingsUiFlow) Set(val *ContinueWithSettingsUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithSettingsUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithSettingsUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithSettingsUiFlow(val *ContinueWithSettingsUiFlow) *NullableContinueWithSettingsUiFlow { + return &NullableContinueWithSettingsUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithSettingsUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_verification_ui.go b/pkg/httpclient/model_continue_with_verification_ui.go new file mode 100644 index 0000000..6a84f48 --- /dev/null +++ b/pkg/httpclient/model_continue_with_verification_ui.go @@ -0,0 +1,197 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithVerificationUi type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithVerificationUi{} + +// ContinueWithVerificationUi Indicates, that the UI flow could be continued by showing a verification ui +type ContinueWithVerificationUi struct { + // Action will always be `show_verification_ui` show_verification_ui ContinueWithActionShowVerificationUIString + Action string `json:"action"` + Flow ContinueWithVerificationUiFlow `json:"flow"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithVerificationUi ContinueWithVerificationUi + +// NewContinueWithVerificationUi instantiates a new ContinueWithVerificationUi 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 NewContinueWithVerificationUi(action string, flow ContinueWithVerificationUiFlow) *ContinueWithVerificationUi { + this := ContinueWithVerificationUi{} + this.Action = action + this.Flow = flow + return &this +} + +// NewContinueWithVerificationUiWithDefaults instantiates a new ContinueWithVerificationUi 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 NewContinueWithVerificationUiWithDefaults() *ContinueWithVerificationUi { + this := ContinueWithVerificationUi{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithVerificationUi) 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 *ContinueWithVerificationUi) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithVerificationUi) SetAction(v string) { + o.Action = v +} + +// GetFlow returns the Flow field value +func (o *ContinueWithVerificationUi) GetFlow() ContinueWithVerificationUiFlow { + if o == nil { + var ret ContinueWithVerificationUiFlow + return ret + } + + return o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUi) GetFlowOk() (*ContinueWithVerificationUiFlow, bool) { + if o == nil { + return nil, false + } + return &o.Flow, true +} + +// SetFlow sets field value +func (o *ContinueWithVerificationUi) SetFlow(v ContinueWithVerificationUiFlow) { + o.Flow = v +} + +func (o ContinueWithVerificationUi) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithVerificationUi) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + toSerialize["flow"] = o.Flow + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithVerificationUi) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "flow", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithVerificationUi := _ContinueWithVerificationUi{} + + err = json.Unmarshal(data, &varContinueWithVerificationUi) + + if err != nil { + return err + } + + *o = ContinueWithVerificationUi(varContinueWithVerificationUi) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "flow") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithVerificationUi struct { + value *ContinueWithVerificationUi + isSet bool +} + +func (v NullableContinueWithVerificationUi) Get() *ContinueWithVerificationUi { + return v.value +} + +func (v *NullableContinueWithVerificationUi) Set(val *ContinueWithVerificationUi) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithVerificationUi) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithVerificationUi) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithVerificationUi(val *ContinueWithVerificationUi) *NullableContinueWithVerificationUi { + return &NullableContinueWithVerificationUi{value: val, isSet: true} +} + +func (v NullableContinueWithVerificationUi) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithVerificationUi) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_continue_with_verification_ui_flow.go b/pkg/httpclient/model_continue_with_verification_ui_flow.go new file mode 100644 index 0000000..398b9d4 --- /dev/null +++ b/pkg/httpclient/model_continue_with_verification_ui_flow.go @@ -0,0 +1,236 @@ +/* +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" + "fmt" +) + +// checks if the ContinueWithVerificationUiFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ContinueWithVerificationUiFlow{} + +// ContinueWithVerificationUiFlow struct for ContinueWithVerificationUiFlow +type ContinueWithVerificationUiFlow struct { + // The ID of the verification flow + Id string `json:"id"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _ContinueWithVerificationUiFlow ContinueWithVerificationUiFlow + +// NewContinueWithVerificationUiFlow instantiates a new ContinueWithVerificationUiFlow 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 NewContinueWithVerificationUiFlow(id string, verifiableAddress string) *ContinueWithVerificationUiFlow { + this := ContinueWithVerificationUiFlow{} + this.Id = id + this.VerifiableAddress = verifiableAddress + return &this +} + +// NewContinueWithVerificationUiFlowWithDefaults instantiates a new ContinueWithVerificationUiFlow 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 NewContinueWithVerificationUiFlowWithDefaults() *ContinueWithVerificationUiFlow { + this := ContinueWithVerificationUiFlow{} + return &this +} + +// GetId returns the Id field value +func (o *ContinueWithVerificationUiFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUiFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *ContinueWithVerificationUiFlow) SetId(v string) { + o.Id = v +} + +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithVerificationUiFlow) GetUrl() string { + if o == nil || IsNil(o.Url) { + 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 *ContinueWithVerificationUiFlow) GetUrlOk() (*string, bool) { + if o == nil || IsNil(o.Url) { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithVerificationUiFlow) HasUrl() bool { + if o != nil && !IsNil(o.Url) { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithVerificationUiFlow) SetUrl(v string) { + o.Url = &v +} + +// GetVerifiableAddress returns the VerifiableAddress field value +func (o *ContinueWithVerificationUiFlow) GetVerifiableAddress() string { + if o == nil { + var ret string + return ret + } + + return o.VerifiableAddress +} + +// GetVerifiableAddressOk returns a tuple with the VerifiableAddress field value +// and a boolean to check if the value has been set. +func (o *ContinueWithVerificationUiFlow) GetVerifiableAddressOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.VerifiableAddress, true +} + +// SetVerifiableAddress sets field value +func (o *ContinueWithVerificationUiFlow) SetVerifiableAddress(v string) { + o.VerifiableAddress = v +} + +func (o ContinueWithVerificationUiFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ContinueWithVerificationUiFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.Url) { + toSerialize["url"] = o.Url + } + toSerialize["verifiable_address"] = o.VerifiableAddress + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ContinueWithVerificationUiFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "verifiable_address", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varContinueWithVerificationUiFlow := _ContinueWithVerificationUiFlow{} + + err = json.Unmarshal(data, &varContinueWithVerificationUiFlow) + + if err != nil { + return err + } + + *o = ContinueWithVerificationUiFlow(varContinueWithVerificationUiFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "url") + delete(additionalProperties, "verifiable_address") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableContinueWithVerificationUiFlow struct { + value *ContinueWithVerificationUiFlow + isSet bool +} + +func (v NullableContinueWithVerificationUiFlow) Get() *ContinueWithVerificationUiFlow { + return v.value +} + +func (v *NullableContinueWithVerificationUiFlow) Set(val *ContinueWithVerificationUiFlow) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithVerificationUiFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithVerificationUiFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithVerificationUiFlow(val *ContinueWithVerificationUiFlow) *NullableContinueWithVerificationUiFlow { + return &NullableContinueWithVerificationUiFlow{value: val, isSet: true} +} + +func (v NullableContinueWithVerificationUiFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithVerificationUiFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_courier_message_status.go b/pkg/httpclient/model_courier_message_status.go new file mode 100644 index 0000000..d152440 --- /dev/null +++ b/pkg/httpclient/model_courier_message_status.go @@ -0,0 +1,115 @@ +/* +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" + "fmt" +) + +// CourierMessageStatus A Message's Status +type CourierMessageStatus string + +// List of courierMessageStatus +const ( + COURIERMESSAGESTATUS_QUEUED CourierMessageStatus = "queued" + COURIERMESSAGESTATUS_SENT CourierMessageStatus = "sent" + COURIERMESSAGESTATUS_PROCESSING CourierMessageStatus = "processing" + COURIERMESSAGESTATUS_ABANDONED CourierMessageStatus = "abandoned" +) + +// All allowed values of CourierMessageStatus enum +var AllowedCourierMessageStatusEnumValues = []CourierMessageStatus{ + "queued", + "sent", + "processing", + "abandoned", +} + +func (v *CourierMessageStatus) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := CourierMessageStatus(value) + for _, existing := range AllowedCourierMessageStatusEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid CourierMessageStatus", value) +} + +// NewCourierMessageStatusFromValue returns a pointer to a valid CourierMessageStatus +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewCourierMessageStatusFromValue(v string) (*CourierMessageStatus, error) { + ev := CourierMessageStatus(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for CourierMessageStatus: valid values are %v", v, AllowedCourierMessageStatusEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v CourierMessageStatus) IsValid() bool { + for _, existing := range AllowedCourierMessageStatusEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to courierMessageStatus value +func (v CourierMessageStatus) Ptr() *CourierMessageStatus { + return &v +} + +type NullableCourierMessageStatus struct { + value *CourierMessageStatus + isSet bool +} + +func (v NullableCourierMessageStatus) Get() *CourierMessageStatus { + return v.value +} + +func (v *NullableCourierMessageStatus) Set(val *CourierMessageStatus) { + v.value = val + v.isSet = true +} + +func (v NullableCourierMessageStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableCourierMessageStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCourierMessageStatus(val *CourierMessageStatus) *NullableCourierMessageStatus { + return &NullableCourierMessageStatus{value: val, isSet: true} +} + +func (v NullableCourierMessageStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCourierMessageStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_courier_message_type.go b/pkg/httpclient/model_courier_message_type.go new file mode 100644 index 0000000..28e0a35 --- /dev/null +++ b/pkg/httpclient/model_courier_message_type.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// CourierMessageType It can either be `email` or `phone` +type CourierMessageType string + +// List of courierMessageType +const ( + COURIERMESSAGETYPE_EMAIL CourierMessageType = "email" + COURIERMESSAGETYPE_PHONE CourierMessageType = "phone" +) + +// All allowed values of CourierMessageType enum +var AllowedCourierMessageTypeEnumValues = []CourierMessageType{ + "email", + "phone", +} + +func (v *CourierMessageType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := CourierMessageType(value) + for _, existing := range AllowedCourierMessageTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid CourierMessageType", value) +} + +// NewCourierMessageTypeFromValue returns a pointer to a valid CourierMessageType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewCourierMessageTypeFromValue(v string) (*CourierMessageType, error) { + ev := CourierMessageType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for CourierMessageType: valid values are %v", v, AllowedCourierMessageTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v CourierMessageType) IsValid() bool { + for _, existing := range AllowedCourierMessageTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to courierMessageType value +func (v CourierMessageType) Ptr() *CourierMessageType { + return &v +} + +type NullableCourierMessageType struct { + value *CourierMessageType + isSet bool +} + +func (v NullableCourierMessageType) Get() *CourierMessageType { + return v.value +} + +func (v *NullableCourierMessageType) Set(val *CourierMessageType) { + v.value = val + v.isSet = true +} + +func (v NullableCourierMessageType) IsSet() bool { + return v.isSet +} + +func (v *NullableCourierMessageType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCourierMessageType(val *CourierMessageType) *NullableCourierMessageType { + return &NullableCourierMessageType{value: val, isSet: true} +} + +func (v NullableCourierMessageType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCourierMessageType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_create_fedcm_flow_response.go b/pkg/httpclient/model_create_fedcm_flow_response.go new file mode 100644 index 0000000..499f8e5 --- /dev/null +++ b/pkg/httpclient/model_create_fedcm_flow_response.go @@ -0,0 +1,191 @@ +/* +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" +) + +// checks if the CreateFedcmFlowResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateFedcmFlowResponse{} + +// CreateFedcmFlowResponse Contains a list of all available FedCM providers. +type CreateFedcmFlowResponse struct { + CsrfToken *string `json:"csrf_token,omitempty"` + Providers []Provider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _CreateFedcmFlowResponse CreateFedcmFlowResponse + +// NewCreateFedcmFlowResponse instantiates a new CreateFedcmFlowResponse 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 NewCreateFedcmFlowResponse() *CreateFedcmFlowResponse { + this := CreateFedcmFlowResponse{} + return &this +} + +// NewCreateFedcmFlowResponseWithDefaults instantiates a new CreateFedcmFlowResponse 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 NewCreateFedcmFlowResponseWithDefaults() *CreateFedcmFlowResponse { + this := CreateFedcmFlowResponse{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *CreateFedcmFlowResponse) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *CreateFedcmFlowResponse) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *CreateFedcmFlowResponse) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *CreateFedcmFlowResponse) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *CreateFedcmFlowResponse) GetProviders() []Provider { + if o == nil || IsNil(o.Providers) { + var ret []Provider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateFedcmFlowResponse) GetProvidersOk() ([]Provider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *CreateFedcmFlowResponse) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []Provider and assigns it to the Providers field. +func (o *CreateFedcmFlowResponse) SetProviders(v []Provider) { + o.Providers = v +} + +func (o CreateFedcmFlowResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateFedcmFlowResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateFedcmFlowResponse) UnmarshalJSON(data []byte) (err error) { + varCreateFedcmFlowResponse := _CreateFedcmFlowResponse{} + + err = json.Unmarshal(data, &varCreateFedcmFlowResponse) + + if err != nil { + return err + } + + *o = CreateFedcmFlowResponse(varCreateFedcmFlowResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateFedcmFlowResponse struct { + value *CreateFedcmFlowResponse + isSet bool +} + +func (v NullableCreateFedcmFlowResponse) Get() *CreateFedcmFlowResponse { + return v.value +} + +func (v *NullableCreateFedcmFlowResponse) Set(val *CreateFedcmFlowResponse) { + v.value = val + v.isSet = true +} + +func (v NullableCreateFedcmFlowResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateFedcmFlowResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateFedcmFlowResponse(val *CreateFedcmFlowResponse) *NullableCreateFedcmFlowResponse { + return &NullableCreateFedcmFlowResponse{value: val, isSet: true} +} + +func (v NullableCreateFedcmFlowResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateFedcmFlowResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_create_identity_body.go b/pkg/httpclient/model_create_identity_body.go new file mode 100644 index 0000000..2caeb1f --- /dev/null +++ b/pkg/httpclient/model_create_identity_body.go @@ -0,0 +1,551 @@ +/* +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" + "fmt" +) + +// checks if the CreateIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateIdentityBody{} + +// CreateIdentityBody Create Identity Body +type CreateIdentityBody struct { + Credentials *IdentityWithCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // Store metadata about the identity which the identity itself can see when calling for example the session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic interface{} `json:"metadata_public,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RecoveryAddresses contains all the addresses that can be used to recover an identity. Use this structure to import recovery addresses for an identity. Please keep in mind that the address needs to be represented in the Identity Schema or this field will be overwritten on the next identity update. + RecoveryAddresses []RecoveryIdentityAddress `json:"recovery_addresses,omitempty"` + // Region is the Ory Network region this identity will be created in. Optional; defaults to the project home region if omitted. Only effective on the Ory Network. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + SchemaId string `json:"schema_id"` + // State is the identity's state. active StateActive inactive StateInactive + State *string `json:"state,omitempty"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_url`. + Traits map[string]interface{} `json:"traits"` + // VerifiableAddresses contains all the addresses that can be verified by the user. Use this structure to import verified addresses for an identity. Please keep in mind that the address needs to be represented in the Identity Schema or this field will be overwritten on the next identity update. + VerifiableAddresses []VerifiableIdentityAddress `json:"verifiable_addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _CreateIdentityBody CreateIdentityBody + +// NewCreateIdentityBody instantiates a new CreateIdentityBody 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 NewCreateIdentityBody(schemaId string, traits map[string]interface{}) *CreateIdentityBody { + this := CreateIdentityBody{} + this.SchemaId = schemaId + this.Traits = traits + return &this +} + +// NewCreateIdentityBodyWithDefaults instantiates a new CreateIdentityBody 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 NewCreateIdentityBodyWithDefaults() *CreateIdentityBody { + this := CreateIdentityBody{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetCredentials() IdentityWithCredentials { + if o == nil || IsNil(o.Credentials) { + var ret IdentityWithCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetCredentialsOk() (*IdentityWithCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given IdentityWithCredentials and assigns it to the Credentials field. +func (o *CreateIdentityBody) SetCredentials(v IdentityWithCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *CreateIdentityBody) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *CreateIdentityBody) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *CreateIdentityBody) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *CreateIdentityBody) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *CreateIdentityBody) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateIdentityBody) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *CreateIdentityBody) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *CreateIdentityBody) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *CreateIdentityBody) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *CreateIdentityBody) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRecoveryAddresses returns the RecoveryAddresses field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetRecoveryAddresses() []RecoveryIdentityAddress { + if o == nil || IsNil(o.RecoveryAddresses) { + var ret []RecoveryIdentityAddress + return ret + } + return o.RecoveryAddresses +} + +// GetRecoveryAddressesOk returns a tuple with the RecoveryAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetRecoveryAddressesOk() ([]RecoveryIdentityAddress, bool) { + if o == nil || IsNil(o.RecoveryAddresses) { + return nil, false + } + return o.RecoveryAddresses, true +} + +// HasRecoveryAddresses returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasRecoveryAddresses() bool { + if o != nil && !IsNil(o.RecoveryAddresses) { + return true + } + + return false +} + +// SetRecoveryAddresses gets a reference to the given []RecoveryIdentityAddress and assigns it to the RecoveryAddresses field. +func (o *CreateIdentityBody) SetRecoveryAddresses(v []RecoveryIdentityAddress) { + o.RecoveryAddresses = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *CreateIdentityBody) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *CreateIdentityBody) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *CreateIdentityBody) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetState() string { + if o == nil || IsNil(o.State) { + var ret string + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetStateOk() (*string, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given string and assigns it to the State field. +func (o *CreateIdentityBody) SetState(v string) { + o.State = &v +} + +// GetTraits returns the Traits field value +func (o *CreateIdentityBody) 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 *CreateIdentityBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *CreateIdentityBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetVerifiableAddresses returns the VerifiableAddresses field value if set, zero value otherwise. +func (o *CreateIdentityBody) GetVerifiableAddresses() []VerifiableIdentityAddress { + if o == nil || IsNil(o.VerifiableAddresses) { + var ret []VerifiableIdentityAddress + return ret + } + return o.VerifiableAddresses +} + +// GetVerifiableAddressesOk returns a tuple with the VerifiableAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateIdentityBody) GetVerifiableAddressesOk() ([]VerifiableIdentityAddress, bool) { + if o == nil || IsNil(o.VerifiableAddresses) { + return nil, false + } + return o.VerifiableAddresses, true +} + +// HasVerifiableAddresses returns a boolean if a field has been set. +func (o *CreateIdentityBody) HasVerifiableAddresses() bool { + if o != nil && !IsNil(o.VerifiableAddresses) { + return true + } + + return false +} + +// SetVerifiableAddresses gets a reference to the given []VerifiableIdentityAddress and assigns it to the VerifiableAddresses field. +func (o *CreateIdentityBody) SetVerifiableAddresses(v []VerifiableIdentityAddress) { + o.VerifiableAddresses = v +} + +func (o CreateIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.RecoveryAddresses) { + toSerialize["recovery_addresses"] = o.RecoveryAddresses + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + toSerialize["traits"] = o.Traits + if !IsNil(o.VerifiableAddresses) { + toSerialize["verifiable_addresses"] = o.VerifiableAddresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "schema_id", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateIdentityBody := _CreateIdentityBody{} + + err = json.Unmarshal(data, &varCreateIdentityBody) + + if err != nil { + return err + } + + *o = CreateIdentityBody(varCreateIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "recovery_addresses") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "state") + delete(additionalProperties, "traits") + delete(additionalProperties, "verifiable_addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateIdentityBody struct { + value *CreateIdentityBody + isSet bool +} + +func (v NullableCreateIdentityBody) Get() *CreateIdentityBody { + return v.value +} + +func (v *NullableCreateIdentityBody) Set(val *CreateIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateIdentityBody(val *CreateIdentityBody) *NullableCreateIdentityBody { + return &NullableCreateIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_create_recovery_code_for_identity_body.go b/pkg/httpclient/model_create_recovery_code_for_identity_body.go new file mode 100644 index 0000000..e23a27f --- /dev/null +++ b/pkg/httpclient/model_create_recovery_code_for_identity_body.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the CreateRecoveryCodeForIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateRecoveryCodeForIdentityBody{} + +// CreateRecoveryCodeForIdentityBody Create Recovery Code for Identity Request Body +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" validate:"regexp=^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _CreateRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody + +// NewCreateRecoveryCodeForIdentityBody instantiates a new CreateRecoveryCodeForIdentityBody 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 NewCreateRecoveryCodeForIdentityBody(identityId string) *CreateRecoveryCodeForIdentityBody { + this := CreateRecoveryCodeForIdentityBody{} + this.IdentityId = identityId + return &this +} + +// NewCreateRecoveryCodeForIdentityBodyWithDefaults instantiates a new CreateRecoveryCodeForIdentityBody 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 NewCreateRecoveryCodeForIdentityBodyWithDefaults() *CreateRecoveryCodeForIdentityBody { + this := CreateRecoveryCodeForIdentityBody{} + return &this +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetExpiresIn() string { + if o == nil || IsNil(o.ExpiresIn) { + var ret string + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetExpiresInOk() (*string, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given string and assigns it to the ExpiresIn field. +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 || IsNil(o.FlowType) { + 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 || IsNil(o.FlowType) { + 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 && !IsNil(o.FlowType) { + 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 { + var ret string + return ret + } + + return o.IdentityId +} + +// GetIdentityIdOk returns a tuple with the IdentityId field value +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetIdentityIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.IdentityId, true +} + +// SetIdentityId sets field value +func (o *CreateRecoveryCodeForIdentityBody) SetIdentityId(v string) { + o.IdentityId = v +} + +func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateRecoveryCodeForIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + if !IsNil(o.FlowType) { + toSerialize["flow_type"] = o.FlowType + } + toSerialize["identity_id"] = o.IdentityId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateRecoveryCodeForIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateRecoveryCodeForIdentityBody := _CreateRecoveryCodeForIdentityBody{} + + err = json.Unmarshal(data, &varCreateRecoveryCodeForIdentityBody) + + if err != nil { + return err + } + + *o = CreateRecoveryCodeForIdentityBody(varCreateRecoveryCodeForIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_in") + delete(additionalProperties, "flow_type") + delete(additionalProperties, "identity_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateRecoveryCodeForIdentityBody struct { + value *CreateRecoveryCodeForIdentityBody + isSet bool +} + +func (v NullableCreateRecoveryCodeForIdentityBody) Get() *CreateRecoveryCodeForIdentityBody { + return v.value +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) Set(val *CreateRecoveryCodeForIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateRecoveryCodeForIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateRecoveryCodeForIdentityBody(val *CreateRecoveryCodeForIdentityBody) *NullableCreateRecoveryCodeForIdentityBody { + return &NullableCreateRecoveryCodeForIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateRecoveryCodeForIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_create_recovery_link_for_identity_body.go b/pkg/httpclient/model_create_recovery_link_for_identity_body.go new file mode 100644 index 0000000..2a1ce64 --- /dev/null +++ b/pkg/httpclient/model_create_recovery_link_for_identity_body.go @@ -0,0 +1,206 @@ +/* +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" + "fmt" +) + +// checks if the CreateRecoveryLinkForIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateRecoveryLinkForIdentityBody{} + +// CreateRecoveryLinkForIdentityBody Create Recovery Link for Identity Request Body +type CreateRecoveryLinkForIdentityBody struct { + // Link Expires In The recovery link 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" validate:"regexp=^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$"` + // Identity to Recover The identity's ID you wish to recover. + IdentityId string `json:"identity_id"` + AdditionalProperties map[string]interface{} +} + +type _CreateRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody + +// NewCreateRecoveryLinkForIdentityBody instantiates a new CreateRecoveryLinkForIdentityBody 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 NewCreateRecoveryLinkForIdentityBody(identityId string) *CreateRecoveryLinkForIdentityBody { + this := CreateRecoveryLinkForIdentityBody{} + this.IdentityId = identityId + return &this +} + +// NewCreateRecoveryLinkForIdentityBodyWithDefaults instantiates a new CreateRecoveryLinkForIdentityBody 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 NewCreateRecoveryLinkForIdentityBodyWithDefaults() *CreateRecoveryLinkForIdentityBody { + this := CreateRecoveryLinkForIdentityBody{} + return &this +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *CreateRecoveryLinkForIdentityBody) GetExpiresIn() string { + if o == nil || IsNil(o.ExpiresIn) { + var ret string + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryLinkForIdentityBody) GetExpiresInOk() (*string, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *CreateRecoveryLinkForIdentityBody) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given string and assigns it to the ExpiresIn field. +func (o *CreateRecoveryLinkForIdentityBody) SetExpiresIn(v string) { + o.ExpiresIn = &v +} + +// GetIdentityId returns the IdentityId field value +func (o *CreateRecoveryLinkForIdentityBody) GetIdentityId() string { + if o == nil { + var ret string + return ret + } + + return o.IdentityId +} + +// GetIdentityIdOk returns a tuple with the IdentityId field value +// and a boolean to check if the value has been set. +func (o *CreateRecoveryLinkForIdentityBody) GetIdentityIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.IdentityId, true +} + +// SetIdentityId sets field value +func (o *CreateRecoveryLinkForIdentityBody) SetIdentityId(v string) { + o.IdentityId = v +} + +func (o CreateRecoveryLinkForIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateRecoveryLinkForIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + toSerialize["identity_id"] = o.IdentityId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateRecoveryLinkForIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateRecoveryLinkForIdentityBody := _CreateRecoveryLinkForIdentityBody{} + + err = json.Unmarshal(data, &varCreateRecoveryLinkForIdentityBody) + + if err != nil { + return err + } + + *o = CreateRecoveryLinkForIdentityBody(varCreateRecoveryLinkForIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_in") + delete(additionalProperties, "identity_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateRecoveryLinkForIdentityBody struct { + value *CreateRecoveryLinkForIdentityBody + isSet bool +} + +func (v NullableCreateRecoveryLinkForIdentityBody) Get() *CreateRecoveryLinkForIdentityBody { + return v.value +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) Set(val *CreateRecoveryLinkForIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateRecoveryLinkForIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateRecoveryLinkForIdentityBody(val *CreateRecoveryLinkForIdentityBody) *NullableCreateRecoveryLinkForIdentityBody { + return &NullableCreateRecoveryLinkForIdentityBody{value: val, isSet: true} +} + +func (v NullableCreateRecoveryLinkForIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateRecoveryLinkForIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_create_test_login_flow_body.go b/pkg/httpclient/model_create_test_login_flow_body.go new file mode 100644 index 0000000..e74173a --- /dev/null +++ b/pkg/httpclient/model_create_test_login_flow_body.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the CreateTestLoginFlowBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CreateTestLoginFlowBody{} + +// CreateTestLoginFlowBody struct for CreateTestLoginFlowBody +type CreateTestLoginFlowBody struct { + // ID of the OIDC provider to test. Must match a provider configured on the project that serves this request. + ProviderId string `json:"provider_id"` + AdditionalProperties map[string]interface{} +} + +type _CreateTestLoginFlowBody CreateTestLoginFlowBody + +// NewCreateTestLoginFlowBody instantiates a new CreateTestLoginFlowBody 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 NewCreateTestLoginFlowBody(providerId string) *CreateTestLoginFlowBody { + this := CreateTestLoginFlowBody{} + this.ProviderId = providerId + return &this +} + +// NewCreateTestLoginFlowBodyWithDefaults instantiates a new CreateTestLoginFlowBody 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 NewCreateTestLoginFlowBodyWithDefaults() *CreateTestLoginFlowBody { + this := CreateTestLoginFlowBody{} + return &this +} + +// GetProviderId returns the ProviderId field value +func (o *CreateTestLoginFlowBody) GetProviderId() string { + if o == nil { + var ret string + return ret + } + + return o.ProviderId +} + +// GetProviderIdOk returns a tuple with the ProviderId field value +// and a boolean to check if the value has been set. +func (o *CreateTestLoginFlowBody) GetProviderIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ProviderId, true +} + +// SetProviderId sets field value +func (o *CreateTestLoginFlowBody) SetProviderId(v string) { + o.ProviderId = v +} + +func (o CreateTestLoginFlowBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CreateTestLoginFlowBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["provider_id"] = o.ProviderId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *CreateTestLoginFlowBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varCreateTestLoginFlowBody := _CreateTestLoginFlowBody{} + + err = json.Unmarshal(data, &varCreateTestLoginFlowBody) + + if err != nil { + return err + } + + *o = CreateTestLoginFlowBody(varCreateTestLoginFlowBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "provider_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableCreateTestLoginFlowBody struct { + value *CreateTestLoginFlowBody + isSet bool +} + +func (v NullableCreateTestLoginFlowBody) Get() *CreateTestLoginFlowBody { + return v.value +} + +func (v *NullableCreateTestLoginFlowBody) Set(val *CreateTestLoginFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateTestLoginFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateTestLoginFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateTestLoginFlowBody(val *CreateTestLoginFlowBody) *NullableCreateTestLoginFlowBody { + return &NullableCreateTestLoginFlowBody{value: val, isSet: true} +} + +func (v NullableCreateTestLoginFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateTestLoginFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_delete_my_sessions_count.go b/pkg/httpclient/model_delete_my_sessions_count.go new file mode 100644 index 0000000..ca207b0 --- /dev/null +++ b/pkg/httpclient/model_delete_my_sessions_count.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the DeleteMySessionsCount type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeleteMySessionsCount{} + +// DeleteMySessionsCount Deleted Session Count +type DeleteMySessionsCount struct { + // The number of sessions that were revoked. + Count *int64 `json:"count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeleteMySessionsCount DeleteMySessionsCount + +// NewDeleteMySessionsCount instantiates a new DeleteMySessionsCount 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 NewDeleteMySessionsCount() *DeleteMySessionsCount { + this := DeleteMySessionsCount{} + return &this +} + +// NewDeleteMySessionsCountWithDefaults instantiates a new DeleteMySessionsCount 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 NewDeleteMySessionsCountWithDefaults() *DeleteMySessionsCount { + this := DeleteMySessionsCount{} + return &this +} + +// GetCount returns the Count field value if set, zero value otherwise. +func (o *DeleteMySessionsCount) GetCount() int64 { + if o == nil || IsNil(o.Count) { + var ret int64 + return ret + } + return *o.Count +} + +// GetCountOk returns a tuple with the Count field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeleteMySessionsCount) GetCountOk() (*int64, bool) { + if o == nil || IsNil(o.Count) { + return nil, false + } + return o.Count, true +} + +// HasCount returns a boolean if a field has been set. +func (o *DeleteMySessionsCount) HasCount() bool { + if o != nil && !IsNil(o.Count) { + return true + } + + return false +} + +// SetCount gets a reference to the given int64 and assigns it to the Count field. +func (o *DeleteMySessionsCount) SetCount(v int64) { + o.Count = &v +} + +func (o DeleteMySessionsCount) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeleteMySessionsCount) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Count) { + toSerialize["count"] = o.Count + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeleteMySessionsCount) UnmarshalJSON(data []byte) (err error) { + varDeleteMySessionsCount := _DeleteMySessionsCount{} + + err = json.Unmarshal(data, &varDeleteMySessionsCount) + + if err != nil { + return err + } + + *o = DeleteMySessionsCount(varDeleteMySessionsCount) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeleteMySessionsCount struct { + value *DeleteMySessionsCount + isSet bool +} + +func (v NullableDeleteMySessionsCount) Get() *DeleteMySessionsCount { + return v.value +} + +func (v *NullableDeleteMySessionsCount) Set(val *DeleteMySessionsCount) { + v.value = val + v.isSet = true +} + +func (v NullableDeleteMySessionsCount) IsSet() bool { + return v.isSet +} + +func (v *NullableDeleteMySessionsCount) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeleteMySessionsCount(val *DeleteMySessionsCount) *NullableDeleteMySessionsCount { + return &NullableDeleteMySessionsCount{value: val, isSet: true} +} + +func (v NullableDeleteMySessionsCount) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeleteMySessionsCount) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_android_authorization_list.go b/pkg/httpclient/model_device_authn_android_authorization_list.go new file mode 100644 index 0000000..26dc776 --- /dev/null +++ b/pkg/httpclient/model_device_authn_android_authorization_list.go @@ -0,0 +1,1116 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidAuthorizationList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidAuthorizationList{} + +// DeviceAuthnAndroidAuthorizationList Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +type DeviceAuthnAndroidAuthorizationList struct { + ActiveDateTime *int64 `json:"active_date_time,omitempty"` + Algorithm *int64 `json:"algorithm,omitempty"` + ApplicationId []int32 `json:"application_id,omitempty"` + AttestationApplicationId []int32 `json:"attestation_application_id,omitempty"` + AttestationIdBrand []int32 `json:"attestation_id_brand,omitempty"` + AttestationIdDevice []int32 `json:"attestation_id_device,omitempty"` + AttestationIdImei []int32 `json:"attestation_id_imei,omitempty"` + AttestationIdManufacturer []int32 `json:"attestation_id_manufacturer,omitempty"` + AttestationIdMeid []int32 `json:"attestation_id_meid,omitempty"` + AttestationIdModel []int32 `json:"attestation_id_model,omitempty"` + AttestationIdProduct []int32 `json:"attestation_id_product,omitempty"` + AttestationIdSerial []int32 `json:"attestation_id_serial,omitempty"` + AuthTimeout *int64 `json:"auth_timeout,omitempty"` + BootPatchLevel *int64 `json:"boot_patch_level,omitempty"` + CreationDateTime *int64 `json:"creation_date_time,omitempty"` + Digest []int64 `json:"digest,omitempty"` + EcCurve *int64 `json:"ec_curve,omitempty"` + KeySize *int64 `json:"key_size,omitempty"` + Origin *int64 `json:"origin,omitempty"` + OriginationExpireDateTime *int64 `json:"origination_expire_date_time,omitempty"` + OsPatchLevel *int64 `json:"os_patch_level,omitempty"` + OsVersion *int64 `json:"os_version,omitempty"` + Purpose []int64 `json:"purpose,omitempty"` + RootOfTrust *DeviceAuthnAndroidRootOfTrust `json:"root_of_trust,omitempty"` + UsageExpireDateTime *int64 `json:"usage_expire_date_time,omitempty"` + UserAuthType *int64 `json:"user_auth_type,omitempty"` + VendorPatchLevel *int64 `json:"vendor_patch_level,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidAuthorizationList DeviceAuthnAndroidAuthorizationList + +// NewDeviceAuthnAndroidAuthorizationList instantiates a new DeviceAuthnAndroidAuthorizationList 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 NewDeviceAuthnAndroidAuthorizationList() *DeviceAuthnAndroidAuthorizationList { + this := DeviceAuthnAndroidAuthorizationList{} + return &this +} + +// NewDeviceAuthnAndroidAuthorizationListWithDefaults instantiates a new DeviceAuthnAndroidAuthorizationList 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 NewDeviceAuthnAndroidAuthorizationListWithDefaults() *DeviceAuthnAndroidAuthorizationList { + this := DeviceAuthnAndroidAuthorizationList{} + return &this +} + +// GetActiveDateTime returns the ActiveDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetActiveDateTime() int64 { + if o == nil || IsNil(o.ActiveDateTime) { + var ret int64 + return ret + } + return *o.ActiveDateTime +} + +// GetActiveDateTimeOk returns a tuple with the ActiveDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetActiveDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.ActiveDateTime) { + return nil, false + } + return o.ActiveDateTime, true +} + +// HasActiveDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasActiveDateTime() bool { + if o != nil && !IsNil(o.ActiveDateTime) { + return true + } + + return false +} + +// SetActiveDateTime gets a reference to the given int64 and assigns it to the ActiveDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetActiveDateTime(v int64) { + o.ActiveDateTime = &v +} + +// GetAlgorithm returns the Algorithm field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAlgorithm() int64 { + if o == nil || IsNil(o.Algorithm) { + var ret int64 + return ret + } + return *o.Algorithm +} + +// GetAlgorithmOk returns a tuple with the Algorithm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAlgorithmOk() (*int64, bool) { + if o == nil || IsNil(o.Algorithm) { + return nil, false + } + return o.Algorithm, true +} + +// HasAlgorithm returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAlgorithm() bool { + if o != nil && !IsNil(o.Algorithm) { + return true + } + + return false +} + +// SetAlgorithm gets a reference to the given int64 and assigns it to the Algorithm field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAlgorithm(v int64) { + o.Algorithm = &v +} + +// GetApplicationId returns the ApplicationId field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetApplicationId() []int32 { + if o == nil || IsNil(o.ApplicationId) { + var ret []int32 + return ret + } + return o.ApplicationId +} + +// GetApplicationIdOk returns a tuple with the ApplicationId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetApplicationIdOk() ([]int32, bool) { + if o == nil || IsNil(o.ApplicationId) { + return nil, false + } + return o.ApplicationId, true +} + +// HasApplicationId returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasApplicationId() bool { + if o != nil && !IsNil(o.ApplicationId) { + return true + } + + return false +} + +// SetApplicationId gets a reference to the given []int32 and assigns it to the ApplicationId field. +func (o *DeviceAuthnAndroidAuthorizationList) SetApplicationId(v []int32) { + o.ApplicationId = v +} + +// GetAttestationApplicationId returns the AttestationApplicationId field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationApplicationId() []int32 { + if o == nil || IsNil(o.AttestationApplicationId) { + var ret []int32 + return ret + } + return o.AttestationApplicationId +} + +// GetAttestationApplicationIdOk returns a tuple with the AttestationApplicationId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationApplicationIdOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationApplicationId) { + return nil, false + } + return o.AttestationApplicationId, true +} + +// HasAttestationApplicationId returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationApplicationId() bool { + if o != nil && !IsNil(o.AttestationApplicationId) { + return true + } + + return false +} + +// SetAttestationApplicationId gets a reference to the given []int32 and assigns it to the AttestationApplicationId field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationApplicationId(v []int32) { + o.AttestationApplicationId = v +} + +// GetAttestationIdBrand returns the AttestationIdBrand field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdBrand() []int32 { + if o == nil || IsNil(o.AttestationIdBrand) { + var ret []int32 + return ret + } + return o.AttestationIdBrand +} + +// GetAttestationIdBrandOk returns a tuple with the AttestationIdBrand field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdBrandOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdBrand) { + return nil, false + } + return o.AttestationIdBrand, true +} + +// HasAttestationIdBrand returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdBrand() bool { + if o != nil && !IsNil(o.AttestationIdBrand) { + return true + } + + return false +} + +// SetAttestationIdBrand gets a reference to the given []int32 and assigns it to the AttestationIdBrand field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdBrand(v []int32) { + o.AttestationIdBrand = v +} + +// GetAttestationIdDevice returns the AttestationIdDevice field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdDevice() []int32 { + if o == nil || IsNil(o.AttestationIdDevice) { + var ret []int32 + return ret + } + return o.AttestationIdDevice +} + +// GetAttestationIdDeviceOk returns a tuple with the AttestationIdDevice field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdDeviceOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdDevice) { + return nil, false + } + return o.AttestationIdDevice, true +} + +// HasAttestationIdDevice returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdDevice() bool { + if o != nil && !IsNil(o.AttestationIdDevice) { + return true + } + + return false +} + +// SetAttestationIdDevice gets a reference to the given []int32 and assigns it to the AttestationIdDevice field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdDevice(v []int32) { + o.AttestationIdDevice = v +} + +// GetAttestationIdImei returns the AttestationIdImei field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdImei() []int32 { + if o == nil || IsNil(o.AttestationIdImei) { + var ret []int32 + return ret + } + return o.AttestationIdImei +} + +// GetAttestationIdImeiOk returns a tuple with the AttestationIdImei field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdImeiOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdImei) { + return nil, false + } + return o.AttestationIdImei, true +} + +// HasAttestationIdImei returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdImei() bool { + if o != nil && !IsNil(o.AttestationIdImei) { + return true + } + + return false +} + +// SetAttestationIdImei gets a reference to the given []int32 and assigns it to the AttestationIdImei field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdImei(v []int32) { + o.AttestationIdImei = v +} + +// GetAttestationIdManufacturer returns the AttestationIdManufacturer field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdManufacturer() []int32 { + if o == nil || IsNil(o.AttestationIdManufacturer) { + var ret []int32 + return ret + } + return o.AttestationIdManufacturer +} + +// GetAttestationIdManufacturerOk returns a tuple with the AttestationIdManufacturer field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdManufacturerOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdManufacturer) { + return nil, false + } + return o.AttestationIdManufacturer, true +} + +// HasAttestationIdManufacturer returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdManufacturer() bool { + if o != nil && !IsNil(o.AttestationIdManufacturer) { + return true + } + + return false +} + +// SetAttestationIdManufacturer gets a reference to the given []int32 and assigns it to the AttestationIdManufacturer field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdManufacturer(v []int32) { + o.AttestationIdManufacturer = v +} + +// GetAttestationIdMeid returns the AttestationIdMeid field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdMeid() []int32 { + if o == nil || IsNil(o.AttestationIdMeid) { + var ret []int32 + return ret + } + return o.AttestationIdMeid +} + +// GetAttestationIdMeidOk returns a tuple with the AttestationIdMeid field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdMeidOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdMeid) { + return nil, false + } + return o.AttestationIdMeid, true +} + +// HasAttestationIdMeid returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdMeid() bool { + if o != nil && !IsNil(o.AttestationIdMeid) { + return true + } + + return false +} + +// SetAttestationIdMeid gets a reference to the given []int32 and assigns it to the AttestationIdMeid field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdMeid(v []int32) { + o.AttestationIdMeid = v +} + +// GetAttestationIdModel returns the AttestationIdModel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdModel() []int32 { + if o == nil || IsNil(o.AttestationIdModel) { + var ret []int32 + return ret + } + return o.AttestationIdModel +} + +// GetAttestationIdModelOk returns a tuple with the AttestationIdModel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdModelOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdModel) { + return nil, false + } + return o.AttestationIdModel, true +} + +// HasAttestationIdModel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdModel() bool { + if o != nil && !IsNil(o.AttestationIdModel) { + return true + } + + return false +} + +// SetAttestationIdModel gets a reference to the given []int32 and assigns it to the AttestationIdModel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdModel(v []int32) { + o.AttestationIdModel = v +} + +// GetAttestationIdProduct returns the AttestationIdProduct field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdProduct() []int32 { + if o == nil || IsNil(o.AttestationIdProduct) { + var ret []int32 + return ret + } + return o.AttestationIdProduct +} + +// GetAttestationIdProductOk returns a tuple with the AttestationIdProduct field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdProductOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdProduct) { + return nil, false + } + return o.AttestationIdProduct, true +} + +// HasAttestationIdProduct returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdProduct() bool { + if o != nil && !IsNil(o.AttestationIdProduct) { + return true + } + + return false +} + +// SetAttestationIdProduct gets a reference to the given []int32 and assigns it to the AttestationIdProduct field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdProduct(v []int32) { + o.AttestationIdProduct = v +} + +// GetAttestationIdSerial returns the AttestationIdSerial field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdSerial() []int32 { + if o == nil || IsNil(o.AttestationIdSerial) { + var ret []int32 + return ret + } + return o.AttestationIdSerial +} + +// GetAttestationIdSerialOk returns a tuple with the AttestationIdSerial field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAttestationIdSerialOk() ([]int32, bool) { + if o == nil || IsNil(o.AttestationIdSerial) { + return nil, false + } + return o.AttestationIdSerial, true +} + +// HasAttestationIdSerial returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAttestationIdSerial() bool { + if o != nil && !IsNil(o.AttestationIdSerial) { + return true + } + + return false +} + +// SetAttestationIdSerial gets a reference to the given []int32 and assigns it to the AttestationIdSerial field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAttestationIdSerial(v []int32) { + o.AttestationIdSerial = v +} + +// GetAuthTimeout returns the AuthTimeout field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetAuthTimeout() int64 { + if o == nil || IsNil(o.AuthTimeout) { + var ret int64 + return ret + } + return *o.AuthTimeout +} + +// GetAuthTimeoutOk returns a tuple with the AuthTimeout field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetAuthTimeoutOk() (*int64, bool) { + if o == nil || IsNil(o.AuthTimeout) { + return nil, false + } + return o.AuthTimeout, true +} + +// HasAuthTimeout returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasAuthTimeout() bool { + if o != nil && !IsNil(o.AuthTimeout) { + return true + } + + return false +} + +// SetAuthTimeout gets a reference to the given int64 and assigns it to the AuthTimeout field. +func (o *DeviceAuthnAndroidAuthorizationList) SetAuthTimeout(v int64) { + o.AuthTimeout = &v +} + +// GetBootPatchLevel returns the BootPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetBootPatchLevel() int64 { + if o == nil || IsNil(o.BootPatchLevel) { + var ret int64 + return ret + } + return *o.BootPatchLevel +} + +// GetBootPatchLevelOk returns a tuple with the BootPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetBootPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.BootPatchLevel) { + return nil, false + } + return o.BootPatchLevel, true +} + +// HasBootPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasBootPatchLevel() bool { + if o != nil && !IsNil(o.BootPatchLevel) { + return true + } + + return false +} + +// SetBootPatchLevel gets a reference to the given int64 and assigns it to the BootPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetBootPatchLevel(v int64) { + o.BootPatchLevel = &v +} + +// GetCreationDateTime returns the CreationDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetCreationDateTime() int64 { + if o == nil || IsNil(o.CreationDateTime) { + var ret int64 + return ret + } + return *o.CreationDateTime +} + +// GetCreationDateTimeOk returns a tuple with the CreationDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetCreationDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.CreationDateTime) { + return nil, false + } + return o.CreationDateTime, true +} + +// HasCreationDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasCreationDateTime() bool { + if o != nil && !IsNil(o.CreationDateTime) { + return true + } + + return false +} + +// SetCreationDateTime gets a reference to the given int64 and assigns it to the CreationDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetCreationDateTime(v int64) { + o.CreationDateTime = &v +} + +// GetDigest returns the Digest field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetDigest() []int64 { + if o == nil || IsNil(o.Digest) { + var ret []int64 + return ret + } + return o.Digest +} + +// GetDigestOk returns a tuple with the Digest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetDigestOk() ([]int64, bool) { + if o == nil || IsNil(o.Digest) { + return nil, false + } + return o.Digest, true +} + +// HasDigest returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasDigest() bool { + if o != nil && !IsNil(o.Digest) { + return true + } + + return false +} + +// SetDigest gets a reference to the given []int64 and assigns it to the Digest field. +func (o *DeviceAuthnAndroidAuthorizationList) SetDigest(v []int64) { + o.Digest = v +} + +// GetEcCurve returns the EcCurve field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetEcCurve() int64 { + if o == nil || IsNil(o.EcCurve) { + var ret int64 + return ret + } + return *o.EcCurve +} + +// GetEcCurveOk returns a tuple with the EcCurve field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetEcCurveOk() (*int64, bool) { + if o == nil || IsNil(o.EcCurve) { + return nil, false + } + return o.EcCurve, true +} + +// HasEcCurve returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasEcCurve() bool { + if o != nil && !IsNil(o.EcCurve) { + return true + } + + return false +} + +// SetEcCurve gets a reference to the given int64 and assigns it to the EcCurve field. +func (o *DeviceAuthnAndroidAuthorizationList) SetEcCurve(v int64) { + o.EcCurve = &v +} + +// GetKeySize returns the KeySize field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetKeySize() int64 { + if o == nil || IsNil(o.KeySize) { + var ret int64 + return ret + } + return *o.KeySize +} + +// GetKeySizeOk returns a tuple with the KeySize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetKeySizeOk() (*int64, bool) { + if o == nil || IsNil(o.KeySize) { + return nil, false + } + return o.KeySize, true +} + +// HasKeySize returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasKeySize() bool { + if o != nil && !IsNil(o.KeySize) { + return true + } + + return false +} + +// SetKeySize gets a reference to the given int64 and assigns it to the KeySize field. +func (o *DeviceAuthnAndroidAuthorizationList) SetKeySize(v int64) { + o.KeySize = &v +} + +// GetOrigin returns the Origin field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOrigin() int64 { + if o == nil || IsNil(o.Origin) { + var ret int64 + return ret + } + return *o.Origin +} + +// GetOriginOk returns a tuple with the Origin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginOk() (*int64, bool) { + if o == nil || IsNil(o.Origin) { + return nil, false + } + return o.Origin, true +} + +// HasOrigin returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOrigin() bool { + if o != nil && !IsNil(o.Origin) { + return true + } + + return false +} + +// SetOrigin gets a reference to the given int64 and assigns it to the Origin field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOrigin(v int64) { + o.Origin = &v +} + +// GetOriginationExpireDateTime returns the OriginationExpireDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginationExpireDateTime() int64 { + if o == nil || IsNil(o.OriginationExpireDateTime) { + var ret int64 + return ret + } + return *o.OriginationExpireDateTime +} + +// GetOriginationExpireDateTimeOk returns a tuple with the OriginationExpireDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOriginationExpireDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.OriginationExpireDateTime) { + return nil, false + } + return o.OriginationExpireDateTime, true +} + +// HasOriginationExpireDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOriginationExpireDateTime() bool { + if o != nil && !IsNil(o.OriginationExpireDateTime) { + return true + } + + return false +} + +// SetOriginationExpireDateTime gets a reference to the given int64 and assigns it to the OriginationExpireDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOriginationExpireDateTime(v int64) { + o.OriginationExpireDateTime = &v +} + +// GetOsPatchLevel returns the OsPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsPatchLevel() int64 { + if o == nil || IsNil(o.OsPatchLevel) { + var ret int64 + return ret + } + return *o.OsPatchLevel +} + +// GetOsPatchLevelOk returns a tuple with the OsPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.OsPatchLevel) { + return nil, false + } + return o.OsPatchLevel, true +} + +// HasOsPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOsPatchLevel() bool { + if o != nil && !IsNil(o.OsPatchLevel) { + return true + } + + return false +} + +// SetOsPatchLevel gets a reference to the given int64 and assigns it to the OsPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOsPatchLevel(v int64) { + o.OsPatchLevel = &v +} + +// GetOsVersion returns the OsVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsVersion() int64 { + if o == nil || IsNil(o.OsVersion) { + var ret int64 + return ret + } + return *o.OsVersion +} + +// GetOsVersionOk returns a tuple with the OsVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetOsVersionOk() (*int64, bool) { + if o == nil || IsNil(o.OsVersion) { + return nil, false + } + return o.OsVersion, true +} + +// HasOsVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasOsVersion() bool { + if o != nil && !IsNil(o.OsVersion) { + return true + } + + return false +} + +// SetOsVersion gets a reference to the given int64 and assigns it to the OsVersion field. +func (o *DeviceAuthnAndroidAuthorizationList) SetOsVersion(v int64) { + o.OsVersion = &v +} + +// GetPurpose returns the Purpose field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetPurpose() []int64 { + if o == nil || IsNil(o.Purpose) { + var ret []int64 + return ret + } + return o.Purpose +} + +// GetPurposeOk returns a tuple with the Purpose field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetPurposeOk() ([]int64, bool) { + if o == nil || IsNil(o.Purpose) { + return nil, false + } + return o.Purpose, true +} + +// HasPurpose returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasPurpose() bool { + if o != nil && !IsNil(o.Purpose) { + return true + } + + return false +} + +// SetPurpose gets a reference to the given []int64 and assigns it to the Purpose field. +func (o *DeviceAuthnAndroidAuthorizationList) SetPurpose(v []int64) { + o.Purpose = v +} + +// GetRootOfTrust returns the RootOfTrust field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetRootOfTrust() DeviceAuthnAndroidRootOfTrust { + if o == nil || IsNil(o.RootOfTrust) { + var ret DeviceAuthnAndroidRootOfTrust + return ret + } + return *o.RootOfTrust +} + +// GetRootOfTrustOk returns a tuple with the RootOfTrust field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetRootOfTrustOk() (*DeviceAuthnAndroidRootOfTrust, bool) { + if o == nil || IsNil(o.RootOfTrust) { + return nil, false + } + return o.RootOfTrust, true +} + +// HasRootOfTrust returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasRootOfTrust() bool { + if o != nil && !IsNil(o.RootOfTrust) { + return true + } + + return false +} + +// SetRootOfTrust gets a reference to the given DeviceAuthnAndroidRootOfTrust and assigns it to the RootOfTrust field. +func (o *DeviceAuthnAndroidAuthorizationList) SetRootOfTrust(v DeviceAuthnAndroidRootOfTrust) { + o.RootOfTrust = &v +} + +// GetUsageExpireDateTime returns the UsageExpireDateTime field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetUsageExpireDateTime() int64 { + if o == nil || IsNil(o.UsageExpireDateTime) { + var ret int64 + return ret + } + return *o.UsageExpireDateTime +} + +// GetUsageExpireDateTimeOk returns a tuple with the UsageExpireDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetUsageExpireDateTimeOk() (*int64, bool) { + if o == nil || IsNil(o.UsageExpireDateTime) { + return nil, false + } + return o.UsageExpireDateTime, true +} + +// HasUsageExpireDateTime returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasUsageExpireDateTime() bool { + if o != nil && !IsNil(o.UsageExpireDateTime) { + return true + } + + return false +} + +// SetUsageExpireDateTime gets a reference to the given int64 and assigns it to the UsageExpireDateTime field. +func (o *DeviceAuthnAndroidAuthorizationList) SetUsageExpireDateTime(v int64) { + o.UsageExpireDateTime = &v +} + +// GetUserAuthType returns the UserAuthType field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetUserAuthType() int64 { + if o == nil || IsNil(o.UserAuthType) { + var ret int64 + return ret + } + return *o.UserAuthType +} + +// GetUserAuthTypeOk returns a tuple with the UserAuthType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetUserAuthTypeOk() (*int64, bool) { + if o == nil || IsNil(o.UserAuthType) { + return nil, false + } + return o.UserAuthType, true +} + +// HasUserAuthType returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasUserAuthType() bool { + if o != nil && !IsNil(o.UserAuthType) { + return true + } + + return false +} + +// SetUserAuthType gets a reference to the given int64 and assigns it to the UserAuthType field. +func (o *DeviceAuthnAndroidAuthorizationList) SetUserAuthType(v int64) { + o.UserAuthType = &v +} + +// GetVendorPatchLevel returns the VendorPatchLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidAuthorizationList) GetVendorPatchLevel() int64 { + if o == nil || IsNil(o.VendorPatchLevel) { + var ret int64 + return ret + } + return *o.VendorPatchLevel +} + +// GetVendorPatchLevelOk returns a tuple with the VendorPatchLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidAuthorizationList) GetVendorPatchLevelOk() (*int64, bool) { + if o == nil || IsNil(o.VendorPatchLevel) { + return nil, false + } + return o.VendorPatchLevel, true +} + +// HasVendorPatchLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidAuthorizationList) HasVendorPatchLevel() bool { + if o != nil && !IsNil(o.VendorPatchLevel) { + return true + } + + return false +} + +// SetVendorPatchLevel gets a reference to the given int64 and assigns it to the VendorPatchLevel field. +func (o *DeviceAuthnAndroidAuthorizationList) SetVendorPatchLevel(v int64) { + o.VendorPatchLevel = &v +} + +func (o DeviceAuthnAndroidAuthorizationList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidAuthorizationList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ActiveDateTime) { + toSerialize["active_date_time"] = o.ActiveDateTime + } + if !IsNil(o.Algorithm) { + toSerialize["algorithm"] = o.Algorithm + } + if !IsNil(o.ApplicationId) { + toSerialize["application_id"] = o.ApplicationId + } + if !IsNil(o.AttestationApplicationId) { + toSerialize["attestation_application_id"] = o.AttestationApplicationId + } + if !IsNil(o.AttestationIdBrand) { + toSerialize["attestation_id_brand"] = o.AttestationIdBrand + } + if !IsNil(o.AttestationIdDevice) { + toSerialize["attestation_id_device"] = o.AttestationIdDevice + } + if !IsNil(o.AttestationIdImei) { + toSerialize["attestation_id_imei"] = o.AttestationIdImei + } + if !IsNil(o.AttestationIdManufacturer) { + toSerialize["attestation_id_manufacturer"] = o.AttestationIdManufacturer + } + if !IsNil(o.AttestationIdMeid) { + toSerialize["attestation_id_meid"] = o.AttestationIdMeid + } + if !IsNil(o.AttestationIdModel) { + toSerialize["attestation_id_model"] = o.AttestationIdModel + } + if !IsNil(o.AttestationIdProduct) { + toSerialize["attestation_id_product"] = o.AttestationIdProduct + } + if !IsNil(o.AttestationIdSerial) { + toSerialize["attestation_id_serial"] = o.AttestationIdSerial + } + if !IsNil(o.AuthTimeout) { + toSerialize["auth_timeout"] = o.AuthTimeout + } + if !IsNil(o.BootPatchLevel) { + toSerialize["boot_patch_level"] = o.BootPatchLevel + } + if !IsNil(o.CreationDateTime) { + toSerialize["creation_date_time"] = o.CreationDateTime + } + if !IsNil(o.Digest) { + toSerialize["digest"] = o.Digest + } + if !IsNil(o.EcCurve) { + toSerialize["ec_curve"] = o.EcCurve + } + if !IsNil(o.KeySize) { + toSerialize["key_size"] = o.KeySize + } + if !IsNil(o.Origin) { + toSerialize["origin"] = o.Origin + } + if !IsNil(o.OriginationExpireDateTime) { + toSerialize["origination_expire_date_time"] = o.OriginationExpireDateTime + } + if !IsNil(o.OsPatchLevel) { + toSerialize["os_patch_level"] = o.OsPatchLevel + } + if !IsNil(o.OsVersion) { + toSerialize["os_version"] = o.OsVersion + } + if !IsNil(o.Purpose) { + toSerialize["purpose"] = o.Purpose + } + if !IsNil(o.RootOfTrust) { + toSerialize["root_of_trust"] = o.RootOfTrust + } + if !IsNil(o.UsageExpireDateTime) { + toSerialize["usage_expire_date_time"] = o.UsageExpireDateTime + } + if !IsNil(o.UserAuthType) { + toSerialize["user_auth_type"] = o.UserAuthType + } + if !IsNil(o.VendorPatchLevel) { + toSerialize["vendor_patch_level"] = o.VendorPatchLevel + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidAuthorizationList) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidAuthorizationList := _DeviceAuthnAndroidAuthorizationList{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidAuthorizationList) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidAuthorizationList(varDeviceAuthnAndroidAuthorizationList) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active_date_time") + delete(additionalProperties, "algorithm") + delete(additionalProperties, "application_id") + delete(additionalProperties, "attestation_application_id") + delete(additionalProperties, "attestation_id_brand") + delete(additionalProperties, "attestation_id_device") + delete(additionalProperties, "attestation_id_imei") + delete(additionalProperties, "attestation_id_manufacturer") + delete(additionalProperties, "attestation_id_meid") + delete(additionalProperties, "attestation_id_model") + delete(additionalProperties, "attestation_id_product") + delete(additionalProperties, "attestation_id_serial") + delete(additionalProperties, "auth_timeout") + delete(additionalProperties, "boot_patch_level") + delete(additionalProperties, "creation_date_time") + delete(additionalProperties, "digest") + delete(additionalProperties, "ec_curve") + delete(additionalProperties, "key_size") + delete(additionalProperties, "origin") + delete(additionalProperties, "origination_expire_date_time") + delete(additionalProperties, "os_patch_level") + delete(additionalProperties, "os_version") + delete(additionalProperties, "purpose") + delete(additionalProperties, "root_of_trust") + delete(additionalProperties, "usage_expire_date_time") + delete(additionalProperties, "user_auth_type") + delete(additionalProperties, "vendor_patch_level") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidAuthorizationList struct { + value *DeviceAuthnAndroidAuthorizationList + isSet bool +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) Get() *DeviceAuthnAndroidAuthorizationList { + return v.value +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) Set(val *DeviceAuthnAndroidAuthorizationList) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidAuthorizationList(val *DeviceAuthnAndroidAuthorizationList) *NullableDeviceAuthnAndroidAuthorizationList { + return &NullableDeviceAuthnAndroidAuthorizationList{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidAuthorizationList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidAuthorizationList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_android_key_description.go b/pkg/httpclient/model_device_authn_android_key_description.go new file mode 100644 index 0000000..5f55985 --- /dev/null +++ b/pkg/httpclient/model_device_authn_android_key_description.go @@ -0,0 +1,339 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidKeyDescription type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidKeyDescription{} + +// DeviceAuthnAndroidKeyDescription Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +type DeviceAuthnAndroidKeyDescription struct { + AttestationSecurityLevel *int64 `json:"attestation_security_level,omitempty"` + AttestationVersion *int64 `json:"attestation_version,omitempty"` + KeymasterSecurityLevel *int64 `json:"keymaster_security_level,omitempty"` + KeymasterVersion *int64 `json:"keymaster_version,omitempty"` + SoftwareEnforced *DeviceAuthnAndroidAuthorizationList `json:"software_enforced,omitempty"` + TeeEnforced *DeviceAuthnAndroidAuthorizationList `json:"tee_enforced,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidKeyDescription DeviceAuthnAndroidKeyDescription + +// NewDeviceAuthnAndroidKeyDescription instantiates a new DeviceAuthnAndroidKeyDescription 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 NewDeviceAuthnAndroidKeyDescription() *DeviceAuthnAndroidKeyDescription { + this := DeviceAuthnAndroidKeyDescription{} + return &this +} + +// NewDeviceAuthnAndroidKeyDescriptionWithDefaults instantiates a new DeviceAuthnAndroidKeyDescription 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 NewDeviceAuthnAndroidKeyDescriptionWithDefaults() *DeviceAuthnAndroidKeyDescription { + this := DeviceAuthnAndroidKeyDescription{} + return &this +} + +// GetAttestationSecurityLevel returns the AttestationSecurityLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationSecurityLevel() int64 { + if o == nil || IsNil(o.AttestationSecurityLevel) { + var ret int64 + return ret + } + return *o.AttestationSecurityLevel +} + +// GetAttestationSecurityLevelOk returns a tuple with the AttestationSecurityLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationSecurityLevelOk() (*int64, bool) { + if o == nil || IsNil(o.AttestationSecurityLevel) { + return nil, false + } + return o.AttestationSecurityLevel, true +} + +// HasAttestationSecurityLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasAttestationSecurityLevel() bool { + if o != nil && !IsNil(o.AttestationSecurityLevel) { + return true + } + + return false +} + +// SetAttestationSecurityLevel gets a reference to the given int64 and assigns it to the AttestationSecurityLevel field. +func (o *DeviceAuthnAndroidKeyDescription) SetAttestationSecurityLevel(v int64) { + o.AttestationSecurityLevel = &v +} + +// GetAttestationVersion returns the AttestationVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationVersion() int64 { + if o == nil || IsNil(o.AttestationVersion) { + var ret int64 + return ret + } + return *o.AttestationVersion +} + +// GetAttestationVersionOk returns a tuple with the AttestationVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetAttestationVersionOk() (*int64, bool) { + if o == nil || IsNil(o.AttestationVersion) { + return nil, false + } + return o.AttestationVersion, true +} + +// HasAttestationVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasAttestationVersion() bool { + if o != nil && !IsNil(o.AttestationVersion) { + return true + } + + return false +} + +// SetAttestationVersion gets a reference to the given int64 and assigns it to the AttestationVersion field. +func (o *DeviceAuthnAndroidKeyDescription) SetAttestationVersion(v int64) { + o.AttestationVersion = &v +} + +// GetKeymasterSecurityLevel returns the KeymasterSecurityLevel field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterSecurityLevel() int64 { + if o == nil || IsNil(o.KeymasterSecurityLevel) { + var ret int64 + return ret + } + return *o.KeymasterSecurityLevel +} + +// GetKeymasterSecurityLevelOk returns a tuple with the KeymasterSecurityLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterSecurityLevelOk() (*int64, bool) { + if o == nil || IsNil(o.KeymasterSecurityLevel) { + return nil, false + } + return o.KeymasterSecurityLevel, true +} + +// HasKeymasterSecurityLevel returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasKeymasterSecurityLevel() bool { + if o != nil && !IsNil(o.KeymasterSecurityLevel) { + return true + } + + return false +} + +// SetKeymasterSecurityLevel gets a reference to the given int64 and assigns it to the KeymasterSecurityLevel field. +func (o *DeviceAuthnAndroidKeyDescription) SetKeymasterSecurityLevel(v int64) { + o.KeymasterSecurityLevel = &v +} + +// GetKeymasterVersion returns the KeymasterVersion field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterVersion() int64 { + if o == nil || IsNil(o.KeymasterVersion) { + var ret int64 + return ret + } + return *o.KeymasterVersion +} + +// GetKeymasterVersionOk returns a tuple with the KeymasterVersion field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetKeymasterVersionOk() (*int64, bool) { + if o == nil || IsNil(o.KeymasterVersion) { + return nil, false + } + return o.KeymasterVersion, true +} + +// HasKeymasterVersion returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasKeymasterVersion() bool { + if o != nil && !IsNil(o.KeymasterVersion) { + return true + } + + return false +} + +// SetKeymasterVersion gets a reference to the given int64 and assigns it to the KeymasterVersion field. +func (o *DeviceAuthnAndroidKeyDescription) SetKeymasterVersion(v int64) { + o.KeymasterVersion = &v +} + +// GetSoftwareEnforced returns the SoftwareEnforced field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetSoftwareEnforced() DeviceAuthnAndroidAuthorizationList { + if o == nil || IsNil(o.SoftwareEnforced) { + var ret DeviceAuthnAndroidAuthorizationList + return ret + } + return *o.SoftwareEnforced +} + +// GetSoftwareEnforcedOk returns a tuple with the SoftwareEnforced field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetSoftwareEnforcedOk() (*DeviceAuthnAndroidAuthorizationList, bool) { + if o == nil || IsNil(o.SoftwareEnforced) { + return nil, false + } + return o.SoftwareEnforced, true +} + +// HasSoftwareEnforced returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasSoftwareEnforced() bool { + if o != nil && !IsNil(o.SoftwareEnforced) { + return true + } + + return false +} + +// SetSoftwareEnforced gets a reference to the given DeviceAuthnAndroidAuthorizationList and assigns it to the SoftwareEnforced field. +func (o *DeviceAuthnAndroidKeyDescription) SetSoftwareEnforced(v DeviceAuthnAndroidAuthorizationList) { + o.SoftwareEnforced = &v +} + +// GetTeeEnforced returns the TeeEnforced field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidKeyDescription) GetTeeEnforced() DeviceAuthnAndroidAuthorizationList { + if o == nil || IsNil(o.TeeEnforced) { + var ret DeviceAuthnAndroidAuthorizationList + return ret + } + return *o.TeeEnforced +} + +// GetTeeEnforcedOk returns a tuple with the TeeEnforced field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidKeyDescription) GetTeeEnforcedOk() (*DeviceAuthnAndroidAuthorizationList, bool) { + if o == nil || IsNil(o.TeeEnforced) { + return nil, false + } + return o.TeeEnforced, true +} + +// HasTeeEnforced returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidKeyDescription) HasTeeEnforced() bool { + if o != nil && !IsNil(o.TeeEnforced) { + return true + } + + return false +} + +// SetTeeEnforced gets a reference to the given DeviceAuthnAndroidAuthorizationList and assigns it to the TeeEnforced field. +func (o *DeviceAuthnAndroidKeyDescription) SetTeeEnforced(v DeviceAuthnAndroidAuthorizationList) { + o.TeeEnforced = &v +} + +func (o DeviceAuthnAndroidKeyDescription) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidKeyDescription) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AttestationSecurityLevel) { + toSerialize["attestation_security_level"] = o.AttestationSecurityLevel + } + if !IsNil(o.AttestationVersion) { + toSerialize["attestation_version"] = o.AttestationVersion + } + if !IsNil(o.KeymasterSecurityLevel) { + toSerialize["keymaster_security_level"] = o.KeymasterSecurityLevel + } + if !IsNil(o.KeymasterVersion) { + toSerialize["keymaster_version"] = o.KeymasterVersion + } + if !IsNil(o.SoftwareEnforced) { + toSerialize["software_enforced"] = o.SoftwareEnforced + } + if !IsNil(o.TeeEnforced) { + toSerialize["tee_enforced"] = o.TeeEnforced + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidKeyDescription) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidKeyDescription := _DeviceAuthnAndroidKeyDescription{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidKeyDescription) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidKeyDescription(varDeviceAuthnAndroidKeyDescription) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attestation_security_level") + delete(additionalProperties, "attestation_version") + delete(additionalProperties, "keymaster_security_level") + delete(additionalProperties, "keymaster_version") + delete(additionalProperties, "software_enforced") + delete(additionalProperties, "tee_enforced") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidKeyDescription struct { + value *DeviceAuthnAndroidKeyDescription + isSet bool +} + +func (v NullableDeviceAuthnAndroidKeyDescription) Get() *DeviceAuthnAndroidKeyDescription { + return v.value +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) Set(val *DeviceAuthnAndroidKeyDescription) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidKeyDescription) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidKeyDescription(val *DeviceAuthnAndroidKeyDescription) *NullableDeviceAuthnAndroidKeyDescription { + return &NullableDeviceAuthnAndroidKeyDescription{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidKeyDescription) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidKeyDescription) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_android_root_of_trust.go b/pkg/httpclient/model_device_authn_android_root_of_trust.go new file mode 100644 index 0000000..d462bf2 --- /dev/null +++ b/pkg/httpclient/model_device_authn_android_root_of_trust.go @@ -0,0 +1,265 @@ +/* +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" +) + +// checks if the DeviceAuthnAndroidRootOfTrust type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAndroidRootOfTrust{} + +// DeviceAuthnAndroidRootOfTrust struct for DeviceAuthnAndroidRootOfTrust +type DeviceAuthnAndroidRootOfTrust struct { + DeviceLocked *bool `json:"device_locked,omitempty"` + VerifiedBootHash []int32 `json:"verified_boot_hash,omitempty"` + VerifiedBootKey []int32 `json:"verified_boot_key,omitempty"` + VerifiedBootState *int64 `json:"verified_boot_state,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAndroidRootOfTrust DeviceAuthnAndroidRootOfTrust + +// NewDeviceAuthnAndroidRootOfTrust instantiates a new DeviceAuthnAndroidRootOfTrust 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 NewDeviceAuthnAndroidRootOfTrust() *DeviceAuthnAndroidRootOfTrust { + this := DeviceAuthnAndroidRootOfTrust{} + return &this +} + +// NewDeviceAuthnAndroidRootOfTrustWithDefaults instantiates a new DeviceAuthnAndroidRootOfTrust 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 NewDeviceAuthnAndroidRootOfTrustWithDefaults() *DeviceAuthnAndroidRootOfTrust { + this := DeviceAuthnAndroidRootOfTrust{} + return &this +} + +// GetDeviceLocked returns the DeviceLocked field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetDeviceLocked() bool { + if o == nil || IsNil(o.DeviceLocked) { + var ret bool + return ret + } + return *o.DeviceLocked +} + +// GetDeviceLockedOk returns a tuple with the DeviceLocked field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetDeviceLockedOk() (*bool, bool) { + if o == nil || IsNil(o.DeviceLocked) { + return nil, false + } + return o.DeviceLocked, true +} + +// HasDeviceLocked returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasDeviceLocked() bool { + if o != nil && !IsNil(o.DeviceLocked) { + return true + } + + return false +} + +// SetDeviceLocked gets a reference to the given bool and assigns it to the DeviceLocked field. +func (o *DeviceAuthnAndroidRootOfTrust) SetDeviceLocked(v bool) { + o.DeviceLocked = &v +} + +// GetVerifiedBootHash returns the VerifiedBootHash field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootHash() []int32 { + if o == nil || IsNil(o.VerifiedBootHash) { + var ret []int32 + return ret + } + return o.VerifiedBootHash +} + +// GetVerifiedBootHashOk returns a tuple with the VerifiedBootHash field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootHashOk() ([]int32, bool) { + if o == nil || IsNil(o.VerifiedBootHash) { + return nil, false + } + return o.VerifiedBootHash, true +} + +// HasVerifiedBootHash returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootHash() bool { + if o != nil && !IsNil(o.VerifiedBootHash) { + return true + } + + return false +} + +// SetVerifiedBootHash gets a reference to the given []int32 and assigns it to the VerifiedBootHash field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootHash(v []int32) { + o.VerifiedBootHash = v +} + +// GetVerifiedBootKey returns the VerifiedBootKey field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootKey() []int32 { + if o == nil || IsNil(o.VerifiedBootKey) { + var ret []int32 + return ret + } + return o.VerifiedBootKey +} + +// GetVerifiedBootKeyOk returns a tuple with the VerifiedBootKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootKeyOk() ([]int32, bool) { + if o == nil || IsNil(o.VerifiedBootKey) { + return nil, false + } + return o.VerifiedBootKey, true +} + +// HasVerifiedBootKey returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootKey() bool { + if o != nil && !IsNil(o.VerifiedBootKey) { + return true + } + + return false +} + +// SetVerifiedBootKey gets a reference to the given []int32 and assigns it to the VerifiedBootKey field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootKey(v []int32) { + o.VerifiedBootKey = v +} + +// GetVerifiedBootState returns the VerifiedBootState field value if set, zero value otherwise. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootState() int64 { + if o == nil || IsNil(o.VerifiedBootState) { + var ret int64 + return ret + } + return *o.VerifiedBootState +} + +// GetVerifiedBootStateOk returns a tuple with the VerifiedBootState field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAndroidRootOfTrust) GetVerifiedBootStateOk() (*int64, bool) { + if o == nil || IsNil(o.VerifiedBootState) { + return nil, false + } + return o.VerifiedBootState, true +} + +// HasVerifiedBootState returns a boolean if a field has been set. +func (o *DeviceAuthnAndroidRootOfTrust) HasVerifiedBootState() bool { + if o != nil && !IsNil(o.VerifiedBootState) { + return true + } + + return false +} + +// SetVerifiedBootState gets a reference to the given int64 and assigns it to the VerifiedBootState field. +func (o *DeviceAuthnAndroidRootOfTrust) SetVerifiedBootState(v int64) { + o.VerifiedBootState = &v +} + +func (o DeviceAuthnAndroidRootOfTrust) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAndroidRootOfTrust) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DeviceLocked) { + toSerialize["device_locked"] = o.DeviceLocked + } + if !IsNil(o.VerifiedBootHash) { + toSerialize["verified_boot_hash"] = o.VerifiedBootHash + } + if !IsNil(o.VerifiedBootKey) { + toSerialize["verified_boot_key"] = o.VerifiedBootKey + } + if !IsNil(o.VerifiedBootState) { + toSerialize["verified_boot_state"] = o.VerifiedBootState + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAndroidRootOfTrust) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAndroidRootOfTrust := _DeviceAuthnAndroidRootOfTrust{} + + err = json.Unmarshal(data, &varDeviceAuthnAndroidRootOfTrust) + + if err != nil { + return err + } + + *o = DeviceAuthnAndroidRootOfTrust(varDeviceAuthnAndroidRootOfTrust) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "device_locked") + delete(additionalProperties, "verified_boot_hash") + delete(additionalProperties, "verified_boot_key") + delete(additionalProperties, "verified_boot_state") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAndroidRootOfTrust struct { + value *DeviceAuthnAndroidRootOfTrust + isSet bool +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) Get() *DeviceAuthnAndroidRootOfTrust { + return v.value +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) Set(val *DeviceAuthnAndroidRootOfTrust) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAndroidRootOfTrust(val *DeviceAuthnAndroidRootOfTrust) *NullableDeviceAuthnAndroidRootOfTrust { + return &NullableDeviceAuthnAndroidRootOfTrust{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAndroidRootOfTrust) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAndroidRootOfTrust) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_attestation.go b/pkg/httpclient/model_device_authn_attestation.go new file mode 100644 index 0000000..5280065 --- /dev/null +++ b/pkg/httpclient/model_device_authn_attestation.go @@ -0,0 +1,191 @@ +/* +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" +) + +// checks if the DeviceAuthnAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnAttestation{} + +// DeviceAuthnAttestation Exactly one of `android` or `ios` is set, matching the key's device_type. +type DeviceAuthnAttestation struct { + Android *DeviceAuthnAndroidKeyDescription `json:"android,omitempty"` + Ios *DeviceAuthnIOSAttestation `json:"ios,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnAttestation DeviceAuthnAttestation + +// NewDeviceAuthnAttestation instantiates a new DeviceAuthnAttestation 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 NewDeviceAuthnAttestation() *DeviceAuthnAttestation { + this := DeviceAuthnAttestation{} + return &this +} + +// NewDeviceAuthnAttestationWithDefaults instantiates a new DeviceAuthnAttestation 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 NewDeviceAuthnAttestationWithDefaults() *DeviceAuthnAttestation { + this := DeviceAuthnAttestation{} + return &this +} + +// GetAndroid returns the Android field value if set, zero value otherwise. +func (o *DeviceAuthnAttestation) GetAndroid() DeviceAuthnAndroidKeyDescription { + if o == nil || IsNil(o.Android) { + var ret DeviceAuthnAndroidKeyDescription + return ret + } + return *o.Android +} + +// GetAndroidOk returns a tuple with the Android field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAttestation) GetAndroidOk() (*DeviceAuthnAndroidKeyDescription, bool) { + if o == nil || IsNil(o.Android) { + return nil, false + } + return o.Android, true +} + +// HasAndroid returns a boolean if a field has been set. +func (o *DeviceAuthnAttestation) HasAndroid() bool { + if o != nil && !IsNil(o.Android) { + return true + } + + return false +} + +// SetAndroid gets a reference to the given DeviceAuthnAndroidKeyDescription and assigns it to the Android field. +func (o *DeviceAuthnAttestation) SetAndroid(v DeviceAuthnAndroidKeyDescription) { + o.Android = &v +} + +// GetIos returns the Ios field value if set, zero value otherwise. +func (o *DeviceAuthnAttestation) GetIos() DeviceAuthnIOSAttestation { + if o == nil || IsNil(o.Ios) { + var ret DeviceAuthnIOSAttestation + return ret + } + return *o.Ios +} + +// GetIosOk returns a tuple with the Ios field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnAttestation) GetIosOk() (*DeviceAuthnIOSAttestation, bool) { + if o == nil || IsNil(o.Ios) { + return nil, false + } + return o.Ios, true +} + +// HasIos returns a boolean if a field has been set. +func (o *DeviceAuthnAttestation) HasIos() bool { + if o != nil && !IsNil(o.Ios) { + return true + } + + return false +} + +// SetIos gets a reference to the given DeviceAuthnIOSAttestation and assigns it to the Ios field. +func (o *DeviceAuthnAttestation) SetIos(v DeviceAuthnIOSAttestation) { + o.Ios = &v +} + +func (o DeviceAuthnAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Android) { + toSerialize["android"] = o.Android + } + if !IsNil(o.Ios) { + toSerialize["ios"] = o.Ios + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnAttestation) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnAttestation := _DeviceAuthnAttestation{} + + err = json.Unmarshal(data, &varDeviceAuthnAttestation) + + if err != nil { + return err + } + + *o = DeviceAuthnAttestation(varDeviceAuthnAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "android") + delete(additionalProperties, "ios") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnAttestation struct { + value *DeviceAuthnAttestation + isSet bool +} + +func (v NullableDeviceAuthnAttestation) Get() *DeviceAuthnAttestation { + return v.value +} + +func (v *NullableDeviceAuthnAttestation) Set(val *DeviceAuthnAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnAttestation(val *DeviceAuthnAttestation) *NullableDeviceAuthnAttestation { + return &NullableDeviceAuthnAttestation{value: val, isSet: true} +} + +func (v NullableDeviceAuthnAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_ios_att_stmt.go b/pkg/httpclient/model_device_authn_ios_att_stmt.go new file mode 100644 index 0000000..fb35706 --- /dev/null +++ b/pkg/httpclient/model_device_authn_ios_att_stmt.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the DeviceAuthnIOSAttStmt type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnIOSAttStmt{} + +// DeviceAuthnIOSAttStmt Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +type DeviceAuthnIOSAttStmt struct { + // X5c is the leaf-first certificate chain encoded as base64(DER), per the JOSE convention defined in RFC 7515 §4.1.6. + X5c [][]int32 `json:"x5c,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnIOSAttStmt DeviceAuthnIOSAttStmt + +// NewDeviceAuthnIOSAttStmt instantiates a new DeviceAuthnIOSAttStmt 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 NewDeviceAuthnIOSAttStmt() *DeviceAuthnIOSAttStmt { + this := DeviceAuthnIOSAttStmt{} + return &this +} + +// NewDeviceAuthnIOSAttStmtWithDefaults instantiates a new DeviceAuthnIOSAttStmt 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 NewDeviceAuthnIOSAttStmtWithDefaults() *DeviceAuthnIOSAttStmt { + this := DeviceAuthnIOSAttStmt{} + return &this +} + +// GetX5c returns the X5c field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttStmt) GetX5c() [][]int32 { + if o == nil || IsNil(o.X5c) { + var ret [][]int32 + return ret + } + return o.X5c +} + +// GetX5cOk returns a tuple with the X5c field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttStmt) GetX5cOk() ([][]int32, bool) { + if o == nil || IsNil(o.X5c) { + return nil, false + } + return o.X5c, true +} + +// HasX5c returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttStmt) HasX5c() bool { + if o != nil && !IsNil(o.X5c) { + return true + } + + return false +} + +// SetX5c gets a reference to the given [][]int32 and assigns it to the X5c field. +func (o *DeviceAuthnIOSAttStmt) SetX5c(v [][]int32) { + o.X5c = v +} + +func (o DeviceAuthnIOSAttStmt) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnIOSAttStmt) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.X5c) { + toSerialize["x5c"] = o.X5c + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnIOSAttStmt) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnIOSAttStmt := _DeviceAuthnIOSAttStmt{} + + err = json.Unmarshal(data, &varDeviceAuthnIOSAttStmt) + + if err != nil { + return err + } + + *o = DeviceAuthnIOSAttStmt(varDeviceAuthnIOSAttStmt) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "x5c") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnIOSAttStmt struct { + value *DeviceAuthnIOSAttStmt + isSet bool +} + +func (v NullableDeviceAuthnIOSAttStmt) Get() *DeviceAuthnIOSAttStmt { + return v.value +} + +func (v *NullableDeviceAuthnIOSAttStmt) Set(val *DeviceAuthnIOSAttStmt) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnIOSAttStmt) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnIOSAttStmt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnIOSAttStmt(val *DeviceAuthnIOSAttStmt) *NullableDeviceAuthnIOSAttStmt { + return &NullableDeviceAuthnIOSAttStmt{value: val, isSet: true} +} + +func (v NullableDeviceAuthnIOSAttStmt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnIOSAttStmt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_ios_attestation.go b/pkg/httpclient/model_device_authn_ios_attestation.go new file mode 100644 index 0000000..be826db --- /dev/null +++ b/pkg/httpclient/model_device_authn_ios_attestation.go @@ -0,0 +1,228 @@ +/* +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" +) + +// checks if the DeviceAuthnIOSAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnIOSAttestation{} + +// DeviceAuthnIOSAttestation Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +type DeviceAuthnIOSAttestation struct { + AttStmt *DeviceAuthnIOSAttStmt `json:"att_stmt,omitempty"` + AuthData []int32 `json:"auth_data,omitempty"` + Fmt *string `json:"fmt,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnIOSAttestation DeviceAuthnIOSAttestation + +// NewDeviceAuthnIOSAttestation instantiates a new DeviceAuthnIOSAttestation 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 NewDeviceAuthnIOSAttestation() *DeviceAuthnIOSAttestation { + this := DeviceAuthnIOSAttestation{} + return &this +} + +// NewDeviceAuthnIOSAttestationWithDefaults instantiates a new DeviceAuthnIOSAttestation 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 NewDeviceAuthnIOSAttestationWithDefaults() *DeviceAuthnIOSAttestation { + this := DeviceAuthnIOSAttestation{} + return &this +} + +// GetAttStmt returns the AttStmt field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetAttStmt() DeviceAuthnIOSAttStmt { + if o == nil || IsNil(o.AttStmt) { + var ret DeviceAuthnIOSAttStmt + return ret + } + return *o.AttStmt +} + +// GetAttStmtOk returns a tuple with the AttStmt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetAttStmtOk() (*DeviceAuthnIOSAttStmt, bool) { + if o == nil || IsNil(o.AttStmt) { + return nil, false + } + return o.AttStmt, true +} + +// HasAttStmt returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasAttStmt() bool { + if o != nil && !IsNil(o.AttStmt) { + return true + } + + return false +} + +// SetAttStmt gets a reference to the given DeviceAuthnIOSAttStmt and assigns it to the AttStmt field. +func (o *DeviceAuthnIOSAttestation) SetAttStmt(v DeviceAuthnIOSAttStmt) { + o.AttStmt = &v +} + +// GetAuthData returns the AuthData field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetAuthData() []int32 { + if o == nil || IsNil(o.AuthData) { + var ret []int32 + return ret + } + return o.AuthData +} + +// GetAuthDataOk returns a tuple with the AuthData field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetAuthDataOk() ([]int32, bool) { + if o == nil || IsNil(o.AuthData) { + return nil, false + } + return o.AuthData, true +} + +// HasAuthData returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasAuthData() bool { + if o != nil && !IsNil(o.AuthData) { + return true + } + + return false +} + +// SetAuthData gets a reference to the given []int32 and assigns it to the AuthData field. +func (o *DeviceAuthnIOSAttestation) SetAuthData(v []int32) { + o.AuthData = v +} + +// GetFmt returns the Fmt field value if set, zero value otherwise. +func (o *DeviceAuthnIOSAttestation) GetFmt() string { + if o == nil || IsNil(o.Fmt) { + var ret string + return ret + } + return *o.Fmt +} + +// GetFmtOk returns a tuple with the Fmt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnIOSAttestation) GetFmtOk() (*string, bool) { + if o == nil || IsNil(o.Fmt) { + return nil, false + } + return o.Fmt, true +} + +// HasFmt returns a boolean if a field has been set. +func (o *DeviceAuthnIOSAttestation) HasFmt() bool { + if o != nil && !IsNil(o.Fmt) { + return true + } + + return false +} + +// SetFmt gets a reference to the given string and assigns it to the Fmt field. +func (o *DeviceAuthnIOSAttestation) SetFmt(v string) { + o.Fmt = &v +} + +func (o DeviceAuthnIOSAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnIOSAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AttStmt) { + toSerialize["att_stmt"] = o.AttStmt + } + if !IsNil(o.AuthData) { + toSerialize["auth_data"] = o.AuthData + } + if !IsNil(o.Fmt) { + toSerialize["fmt"] = o.Fmt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnIOSAttestation) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnIOSAttestation := _DeviceAuthnIOSAttestation{} + + err = json.Unmarshal(data, &varDeviceAuthnIOSAttestation) + + if err != nil { + return err + } + + *o = DeviceAuthnIOSAttestation(varDeviceAuthnIOSAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "att_stmt") + delete(additionalProperties, "auth_data") + delete(additionalProperties, "fmt") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnIOSAttestation struct { + value *DeviceAuthnIOSAttestation + isSet bool +} + +func (v NullableDeviceAuthnIOSAttestation) Get() *DeviceAuthnIOSAttestation { + return v.value +} + +func (v *NullableDeviceAuthnIOSAttestation) Set(val *DeviceAuthnIOSAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnIOSAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnIOSAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnIOSAttestation(val *DeviceAuthnIOSAttestation) *NullableDeviceAuthnIOSAttestation { + return &NullableDeviceAuthnIOSAttestation{value: val, isSet: true} +} + +func (v NullableDeviceAuthnIOSAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnIOSAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_authn_key.go b/pkg/httpclient/model_device_authn_key.go new file mode 100644 index 0000000..931f179 --- /dev/null +++ b/pkg/httpclient/model_device_authn_key.go @@ -0,0 +1,531 @@ +/* +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" + "time" +) + +// checks if the DeviceAuthnKey type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthnKey{} + +// DeviceAuthnKey Represents a hardware-backed signing key enrolled from a mobile device. The private key resides inside the device and never exists on the server. To list the identity's enrolled keys, fetch a settings flow: each key's remove button (a `ui.nodes` entry named `deviceauthn_remove` in group `deviceauthn`) carries the key, with its PIN state redacted, in the node label's `context`. +type DeviceAuthnKey struct { + Attestation *DeviceAuthnAttestation `json:"attestation,omitempty"` + // The key's stable id, unique per identity. Submit it as the `client_key_id` when logging in with the key, deleting it, or rotating its pin_secret. The device can also compute the id without reading it back from the server: it is the lowercase-hex SHA-256 of `public_key` (the key's PKIX, ASN.1 DER encoding). Keys enrolled before the server derived the id keep their original client-chosen value, so prefer reading this field over recomputing it for older keys. + ClientKeyId *string `json:"client_key_id,omitempty"` + // When the key was enrolled. Only used for troubleshooting and UI. + CreatedAt *time.Time `json:"created_at,omitempty"` + // A human-readable name for the device, helping the user tell this key apart from others. + DeviceName *string `json:"device_name,omitempty"` + DeviceType *DeviceType `json:"device_type,omitempty"` + Pin *PINConfig `json:"pin,omitempty"` + // The device's public key (an elliptic-curve key on P-224, P-256, P-384, or P-521 in version 1) in PKIX, ASN.1 DER (SubjectPublicKeyInfo) form, base64-encoded. Signatures are verified against this key. + PublicKey *string `json:"public_key,omitempty"` + // Set only when the key's attestation chain was accepted under relaxed rules (software roots, expired certificates, software security level) rather than strict hardware attestation. Such keys are refused at login after this time, or immediately once relaxed attestation is turned off. Absent for hardware-attested keys that pass strict validation. + RelaxedAttestationExpiresAt *time.Time `json:"relaxed_attestation_expires_at,omitempty"` + State *KeyState `json:"state,omitempty"` + UserVerification *UserVerification `json:"user_verification,omitempty"` + // The cryptography version of the key. Version 1 uses ECDSA with SHA-256 on an elliptic curve (P-224, P-256, P-384, or P-521); further versions are reserved for future signature suites. + Version *int64 `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _DeviceAuthnKey DeviceAuthnKey + +// NewDeviceAuthnKey instantiates a new DeviceAuthnKey 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 NewDeviceAuthnKey() *DeviceAuthnKey { + this := DeviceAuthnKey{} + return &this +} + +// NewDeviceAuthnKeyWithDefaults instantiates a new DeviceAuthnKey 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 NewDeviceAuthnKeyWithDefaults() *DeviceAuthnKey { + this := DeviceAuthnKey{} + return &this +} + +// GetAttestation returns the Attestation field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetAttestation() DeviceAuthnAttestation { + if o == nil || IsNil(o.Attestation) { + var ret DeviceAuthnAttestation + return ret + } + return *o.Attestation +} + +// GetAttestationOk returns a tuple with the Attestation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetAttestationOk() (*DeviceAuthnAttestation, bool) { + if o == nil || IsNil(o.Attestation) { + return nil, false + } + return o.Attestation, true +} + +// HasAttestation returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasAttestation() bool { + if o != nil && !IsNil(o.Attestation) { + return true + } + + return false +} + +// SetAttestation gets a reference to the given DeviceAuthnAttestation and assigns it to the Attestation field. +func (o *DeviceAuthnKey) SetAttestation(v DeviceAuthnAttestation) { + o.Attestation = &v +} + +// GetClientKeyId returns the ClientKeyId field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetClientKeyId() string { + if o == nil || IsNil(o.ClientKeyId) { + var ret string + return ret + } + return *o.ClientKeyId +} + +// GetClientKeyIdOk returns a tuple with the ClientKeyId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetClientKeyIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientKeyId) { + return nil, false + } + return o.ClientKeyId, true +} + +// HasClientKeyId returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasClientKeyId() bool { + if o != nil && !IsNil(o.ClientKeyId) { + return true + } + + return false +} + +// SetClientKeyId gets a reference to the given string and assigns it to the ClientKeyId field. +func (o *DeviceAuthnKey) SetClientKeyId(v string) { + o.ClientKeyId = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *DeviceAuthnKey) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetDeviceName returns the DeviceName field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetDeviceName() string { + if o == nil || IsNil(o.DeviceName) { + var ret string + return ret + } + return *o.DeviceName +} + +// GetDeviceNameOk returns a tuple with the DeviceName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetDeviceNameOk() (*string, bool) { + if o == nil || IsNil(o.DeviceName) { + return nil, false + } + return o.DeviceName, true +} + +// HasDeviceName returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasDeviceName() bool { + if o != nil && !IsNil(o.DeviceName) { + return true + } + + return false +} + +// SetDeviceName gets a reference to the given string and assigns it to the DeviceName field. +func (o *DeviceAuthnKey) SetDeviceName(v string) { + o.DeviceName = &v +} + +// GetDeviceType returns the DeviceType field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetDeviceType() DeviceType { + if o == nil || IsNil(o.DeviceType) { + var ret DeviceType + return ret + } + return *o.DeviceType +} + +// GetDeviceTypeOk returns a tuple with the DeviceType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetDeviceTypeOk() (*DeviceType, bool) { + if o == nil || IsNil(o.DeviceType) { + return nil, false + } + return o.DeviceType, true +} + +// HasDeviceType returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasDeviceType() bool { + if o != nil && !IsNil(o.DeviceType) { + return true + } + + return false +} + +// SetDeviceType gets a reference to the given DeviceType and assigns it to the DeviceType field. +func (o *DeviceAuthnKey) SetDeviceType(v DeviceType) { + o.DeviceType = &v +} + +// GetPin returns the Pin field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetPin() PINConfig { + if o == nil || IsNil(o.Pin) { + var ret PINConfig + return ret + } + return *o.Pin +} + +// GetPinOk returns a tuple with the Pin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetPinOk() (*PINConfig, bool) { + if o == nil || IsNil(o.Pin) { + return nil, false + } + return o.Pin, true +} + +// HasPin returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasPin() bool { + if o != nil && !IsNil(o.Pin) { + return true + } + + return false +} + +// SetPin gets a reference to the given PINConfig and assigns it to the Pin field. +func (o *DeviceAuthnKey) SetPin(v PINConfig) { + o.Pin = &v +} + +// GetPublicKey returns the PublicKey field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetPublicKey() string { + if o == nil || IsNil(o.PublicKey) { + var ret string + return ret + } + return *o.PublicKey +} + +// GetPublicKeyOk returns a tuple with the PublicKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetPublicKeyOk() (*string, bool) { + if o == nil || IsNil(o.PublicKey) { + return nil, false + } + return o.PublicKey, true +} + +// HasPublicKey returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasPublicKey() bool { + if o != nil && !IsNil(o.PublicKey) { + return true + } + + return false +} + +// SetPublicKey gets a reference to the given string and assigns it to the PublicKey field. +func (o *DeviceAuthnKey) SetPublicKey(v string) { + o.PublicKey = &v +} + +// GetRelaxedAttestationExpiresAt returns the RelaxedAttestationExpiresAt field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetRelaxedAttestationExpiresAt() time.Time { + if o == nil || IsNil(o.RelaxedAttestationExpiresAt) { + var ret time.Time + return ret + } + return *o.RelaxedAttestationExpiresAt +} + +// GetRelaxedAttestationExpiresAtOk returns a tuple with the RelaxedAttestationExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetRelaxedAttestationExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.RelaxedAttestationExpiresAt) { + return nil, false + } + return o.RelaxedAttestationExpiresAt, true +} + +// HasRelaxedAttestationExpiresAt returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasRelaxedAttestationExpiresAt() bool { + if o != nil && !IsNil(o.RelaxedAttestationExpiresAt) { + return true + } + + return false +} + +// SetRelaxedAttestationExpiresAt gets a reference to the given time.Time and assigns it to the RelaxedAttestationExpiresAt field. +func (o *DeviceAuthnKey) SetRelaxedAttestationExpiresAt(v time.Time) { + o.RelaxedAttestationExpiresAt = &v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetState() KeyState { + if o == nil || IsNil(o.State) { + var ret KeyState + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetStateOk() (*KeyState, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given KeyState and assigns it to the State field. +func (o *DeviceAuthnKey) SetState(v KeyState) { + o.State = &v +} + +// GetUserVerification returns the UserVerification field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetUserVerification() UserVerification { + if o == nil || IsNil(o.UserVerification) { + var ret UserVerification + return ret + } + return *o.UserVerification +} + +// GetUserVerificationOk returns a tuple with the UserVerification field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetUserVerificationOk() (*UserVerification, bool) { + if o == nil || IsNil(o.UserVerification) { + return nil, false + } + return o.UserVerification, true +} + +// HasUserVerification returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasUserVerification() bool { + if o != nil && !IsNil(o.UserVerification) { + return true + } + + return false +} + +// SetUserVerification gets a reference to the given UserVerification and assigns it to the UserVerification field. +func (o *DeviceAuthnKey) SetUserVerification(v UserVerification) { + o.UserVerification = &v +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *DeviceAuthnKey) GetVersion() int64 { + if o == nil || IsNil(o.Version) { + var ret int64 + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthnKey) GetVersionOk() (*int64, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *DeviceAuthnKey) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given int64 and assigns it to the Version field. +func (o *DeviceAuthnKey) SetVersion(v int64) { + o.Version = &v +} + +func (o DeviceAuthnKey) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthnKey) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Attestation) { + toSerialize["attestation"] = o.Attestation + } + if !IsNil(o.ClientKeyId) { + toSerialize["client_key_id"] = o.ClientKeyId + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.DeviceName) { + toSerialize["device_name"] = o.DeviceName + } + if !IsNil(o.DeviceType) { + toSerialize["device_type"] = o.DeviceType + } + if !IsNil(o.Pin) { + toSerialize["pin"] = o.Pin + } + if !IsNil(o.PublicKey) { + toSerialize["public_key"] = o.PublicKey + } + if !IsNil(o.RelaxedAttestationExpiresAt) { + toSerialize["relaxed_attestation_expires_at"] = o.RelaxedAttestationExpiresAt + } + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + if !IsNil(o.UserVerification) { + toSerialize["user_verification"] = o.UserVerification + } + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *DeviceAuthnKey) UnmarshalJSON(data []byte) (err error) { + varDeviceAuthnKey := _DeviceAuthnKey{} + + err = json.Unmarshal(data, &varDeviceAuthnKey) + + if err != nil { + return err + } + + *o = DeviceAuthnKey(varDeviceAuthnKey) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attestation") + delete(additionalProperties, "client_key_id") + delete(additionalProperties, "created_at") + delete(additionalProperties, "device_name") + delete(additionalProperties, "device_type") + delete(additionalProperties, "pin") + delete(additionalProperties, "public_key") + delete(additionalProperties, "relaxed_attestation_expires_at") + delete(additionalProperties, "state") + delete(additionalProperties, "user_verification") + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableDeviceAuthnKey struct { + value *DeviceAuthnKey + isSet bool +} + +func (v NullableDeviceAuthnKey) Get() *DeviceAuthnKey { + return v.value +} + +func (v *NullableDeviceAuthnKey) Set(val *DeviceAuthnKey) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthnKey) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthnKey) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthnKey(val *DeviceAuthnKey) *NullableDeviceAuthnKey { + return &NullableDeviceAuthnKey{value: val, isSet: true} +} + +func (v NullableDeviceAuthnKey) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthnKey) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_device_type.go b/pkg/httpclient/model_device_type.go new file mode 100644 index 0000000..e00804f --- /dev/null +++ b/pkg/httpclient/model_device_type.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// DeviceType \"Android\" and \"iOS\" use different attestation and signature encodings. +type DeviceType string + +// List of DeviceType +const ( + DEVICETYPE_ANDROID DeviceType = "Android" + DEVICETYPE_IOS DeviceType = "iOS" +) + +// All allowed values of DeviceType enum +var AllowedDeviceTypeEnumValues = []DeviceType{ + "Android", + "iOS", +} + +func (v *DeviceType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := DeviceType(value) + for _, existing := range AllowedDeviceTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid DeviceType", value) +} + +// NewDeviceTypeFromValue returns a pointer to a valid DeviceType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewDeviceTypeFromValue(v string) (*DeviceType, error) { + ev := DeviceType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for DeviceType: valid values are %v", v, AllowedDeviceTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v DeviceType) IsValid() bool { + for _, existing := range AllowedDeviceTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to DeviceType value +func (v DeviceType) Ptr() *DeviceType { + return &v +} + +type NullableDeviceType struct { + value *DeviceType + isSet bool +} + +func (v NullableDeviceType) Get() *DeviceType { + return v.value +} + +func (v *NullableDeviceType) Set(val *DeviceType) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceType) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceType(val *DeviceType) *NullableDeviceType { + return &NullableDeviceType{value: val, isSet: true} +} + +func (v NullableDeviceType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_error_authenticator_assurance_level_not_satisfied.go b/pkg/httpclient/model_error_authenticator_assurance_level_not_satisfied.go new file mode 100644 index 0000000..62f965a --- /dev/null +++ b/pkg/httpclient/model_error_authenticator_assurance_level_not_satisfied.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorAuthenticatorAssuranceLevelNotSatisfied type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorAuthenticatorAssuranceLevelNotSatisfied{} + +// ErrorAuthenticatorAssuranceLevelNotSatisfied struct for ErrorAuthenticatorAssuranceLevelNotSatisfied +type ErrorAuthenticatorAssuranceLevelNotSatisfied struct { + Error *GenericError `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorAuthenticatorAssuranceLevelNotSatisfied ErrorAuthenticatorAssuranceLevelNotSatisfied + +// NewErrorAuthenticatorAssuranceLevelNotSatisfied instantiates a new ErrorAuthenticatorAssuranceLevelNotSatisfied 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 NewErrorAuthenticatorAssuranceLevelNotSatisfied() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + this := ErrorAuthenticatorAssuranceLevelNotSatisfied{} + return &this +} + +// NewErrorAuthenticatorAssuranceLevelNotSatisfiedWithDefaults instantiates a new ErrorAuthenticatorAssuranceLevelNotSatisfied 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 NewErrorAuthenticatorAssuranceLevelNotSatisfiedWithDefaults() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + this := ErrorAuthenticatorAssuranceLevelNotSatisfied{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) SetError(v GenericError) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetRedirectBrowserTo() string { + if o == nil || IsNil(o.RedirectBrowserTo) { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || IsNil(o.RedirectBrowserTo) { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) HasRedirectBrowserTo() bool { + if o != nil && !IsNil(o.RedirectBrowserTo) { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ErrorAuthenticatorAssuranceLevelNotSatisfied) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorAuthenticatorAssuranceLevelNotSatisfied) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.RedirectBrowserTo) { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorAuthenticatorAssuranceLevelNotSatisfied) UnmarshalJSON(data []byte) (err error) { + varErrorAuthenticatorAssuranceLevelNotSatisfied := _ErrorAuthenticatorAssuranceLevelNotSatisfied{} + + err = json.Unmarshal(data, &varErrorAuthenticatorAssuranceLevelNotSatisfied) + + if err != nil { + return err + } + + *o = ErrorAuthenticatorAssuranceLevelNotSatisfied(varErrorAuthenticatorAssuranceLevelNotSatisfied) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorAuthenticatorAssuranceLevelNotSatisfied struct { + value *ErrorAuthenticatorAssuranceLevelNotSatisfied + isSet bool +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Get() *ErrorAuthenticatorAssuranceLevelNotSatisfied { + return v.value +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Set(val *ErrorAuthenticatorAssuranceLevelNotSatisfied) { + v.value = val + v.isSet = true +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorAuthenticatorAssuranceLevelNotSatisfied(val *ErrorAuthenticatorAssuranceLevelNotSatisfied) *NullableErrorAuthenticatorAssuranceLevelNotSatisfied { + return &NullableErrorAuthenticatorAssuranceLevelNotSatisfied{value: val, isSet: true} +} + +func (v NullableErrorAuthenticatorAssuranceLevelNotSatisfied) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorAuthenticatorAssuranceLevelNotSatisfied) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_error_browser_location_change_required.go b/pkg/httpclient/model_error_browser_location_change_required.go new file mode 100644 index 0000000..b048cf6 --- /dev/null +++ b/pkg/httpclient/model_error_browser_location_change_required.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorBrowserLocationChangeRequired type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorBrowserLocationChangeRequired{} + +// ErrorBrowserLocationChangeRequired struct for ErrorBrowserLocationChangeRequired +type ErrorBrowserLocationChangeRequired struct { + Error *ErrorGeneric `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorBrowserLocationChangeRequired ErrorBrowserLocationChangeRequired + +// NewErrorBrowserLocationChangeRequired instantiates a new ErrorBrowserLocationChangeRequired 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 NewErrorBrowserLocationChangeRequired() *ErrorBrowserLocationChangeRequired { + this := ErrorBrowserLocationChangeRequired{} + return &this +} + +// NewErrorBrowserLocationChangeRequiredWithDefaults instantiates a new ErrorBrowserLocationChangeRequired 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 NewErrorBrowserLocationChangeRequiredWithDefaults() *ErrorBrowserLocationChangeRequired { + this := ErrorBrowserLocationChangeRequired{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorBrowserLocationChangeRequired) GetError() ErrorGeneric { + if o == nil || IsNil(o.Error) { + var ret ErrorGeneric + 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. +func (o *ErrorBrowserLocationChangeRequired) GetErrorOk() (*ErrorGeneric, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorBrowserLocationChangeRequired) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given ErrorGeneric and assigns it to the Error field. +func (o *ErrorBrowserLocationChangeRequired) SetError(v ErrorGeneric) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ErrorBrowserLocationChangeRequired) GetRedirectBrowserTo() string { + if o == nil || IsNil(o.RedirectBrowserTo) { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorBrowserLocationChangeRequired) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || IsNil(o.RedirectBrowserTo) { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ErrorBrowserLocationChangeRequired) HasRedirectBrowserTo() bool { + if o != nil && !IsNil(o.RedirectBrowserTo) { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ErrorBrowserLocationChangeRequired) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ErrorBrowserLocationChangeRequired) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorBrowserLocationChangeRequired) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.RedirectBrowserTo) { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorBrowserLocationChangeRequired) UnmarshalJSON(data []byte) (err error) { + varErrorBrowserLocationChangeRequired := _ErrorBrowserLocationChangeRequired{} + + err = json.Unmarshal(data, &varErrorBrowserLocationChangeRequired) + + if err != nil { + return err + } + + *o = ErrorBrowserLocationChangeRequired(varErrorBrowserLocationChangeRequired) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorBrowserLocationChangeRequired struct { + value *ErrorBrowserLocationChangeRequired + isSet bool +} + +func (v NullableErrorBrowserLocationChangeRequired) Get() *ErrorBrowserLocationChangeRequired { + return v.value +} + +func (v *NullableErrorBrowserLocationChangeRequired) Set(val *ErrorBrowserLocationChangeRequired) { + v.value = val + v.isSet = true +} + +func (v NullableErrorBrowserLocationChangeRequired) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorBrowserLocationChangeRequired) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorBrowserLocationChangeRequired(val *ErrorBrowserLocationChangeRequired) *NullableErrorBrowserLocationChangeRequired { + return &NullableErrorBrowserLocationChangeRequired{value: val, isSet: true} +} + +func (v NullableErrorBrowserLocationChangeRequired) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorBrowserLocationChangeRequired) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_error_flow_replaced.go b/pkg/httpclient/model_error_flow_replaced.go new file mode 100644 index 0000000..bf6d84d --- /dev/null +++ b/pkg/httpclient/model_error_flow_replaced.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the ErrorFlowReplaced type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorFlowReplaced{} + +// ErrorFlowReplaced Is sent when a flow is replaced by a different flow of the same class +type ErrorFlowReplaced struct { + Error *GenericError `json:"error,omitempty"` + // The flow ID that should be used for the new flow as it contains the correct messages. + UseFlowId *string `json:"use_flow_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ErrorFlowReplaced ErrorFlowReplaced + +// NewErrorFlowReplaced instantiates a new ErrorFlowReplaced 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 NewErrorFlowReplaced() *ErrorFlowReplaced { + this := ErrorFlowReplaced{} + return &this +} + +// NewErrorFlowReplacedWithDefaults instantiates a new ErrorFlowReplaced 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 NewErrorFlowReplacedWithDefaults() *ErrorFlowReplaced { + this := ErrorFlowReplaced{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *ErrorFlowReplaced) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *ErrorFlowReplaced) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *ErrorFlowReplaced) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *ErrorFlowReplaced) SetError(v GenericError) { + o.Error = &v +} + +// GetUseFlowId returns the UseFlowId field value if set, zero value otherwise. +func (o *ErrorFlowReplaced) GetUseFlowId() string { + if o == nil || IsNil(o.UseFlowId) { + var ret string + return ret + } + return *o.UseFlowId +} + +// GetUseFlowIdOk returns a tuple with the UseFlowId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ErrorFlowReplaced) GetUseFlowIdOk() (*string, bool) { + if o == nil || IsNil(o.UseFlowId) { + return nil, false + } + return o.UseFlowId, true +} + +// HasUseFlowId returns a boolean if a field has been set. +func (o *ErrorFlowReplaced) HasUseFlowId() bool { + if o != nil && !IsNil(o.UseFlowId) { + return true + } + + return false +} + +// SetUseFlowId gets a reference to the given string and assigns it to the UseFlowId field. +func (o *ErrorFlowReplaced) SetUseFlowId(v string) { + o.UseFlowId = &v +} + +func (o ErrorFlowReplaced) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorFlowReplaced) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.UseFlowId) { + toSerialize["use_flow_id"] = o.UseFlowId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorFlowReplaced) UnmarshalJSON(data []byte) (err error) { + varErrorFlowReplaced := _ErrorFlowReplaced{} + + err = json.Unmarshal(data, &varErrorFlowReplaced) + + if err != nil { + return err + } + + *o = ErrorFlowReplaced(varErrorFlowReplaced) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "use_flow_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorFlowReplaced struct { + value *ErrorFlowReplaced + isSet bool +} + +func (v NullableErrorFlowReplaced) Get() *ErrorFlowReplaced { + return v.value +} + +func (v *NullableErrorFlowReplaced) Set(val *ErrorFlowReplaced) { + v.value = val + v.isSet = true +} + +func (v NullableErrorFlowReplaced) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorFlowReplaced) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorFlowReplaced(val *ErrorFlowReplaced) *NullableErrorFlowReplaced { + return &NullableErrorFlowReplaced{value: val, isSet: true} +} + +func (v NullableErrorFlowReplaced) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorFlowReplaced) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_error_generic.go b/pkg/httpclient/model_error_generic.go new file mode 100644 index 0000000..3484d04 --- /dev/null +++ b/pkg/httpclient/model_error_generic.go @@ -0,0 +1,167 @@ +/* +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" + "fmt" +) + +// checks if the ErrorGeneric type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ErrorGeneric{} + +// ErrorGeneric The standard Ory JSON API error format. +type ErrorGeneric struct { + Error GenericError `json:"error"` + AdditionalProperties map[string]interface{} +} + +type _ErrorGeneric ErrorGeneric + +// NewErrorGeneric instantiates a new ErrorGeneric 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 NewErrorGeneric(error_ GenericError) *ErrorGeneric { + this := ErrorGeneric{} + this.Error = error_ + return &this +} + +// NewErrorGenericWithDefaults instantiates a new ErrorGeneric 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 NewErrorGenericWithDefaults() *ErrorGeneric { + this := ErrorGeneric{} + return &this +} + +// GetError returns the Error field value +func (o *ErrorGeneric) GetError() GenericError { + if o == nil { + var ret GenericError + return ret + } + + return o.Error +} + +// GetErrorOk returns a tuple with the Error field value +// and a boolean to check if the value has been set. +func (o *ErrorGeneric) GetErrorOk() (*GenericError, bool) { + if o == nil { + return nil, false + } + return &o.Error, true +} + +// SetError sets field value +func (o *ErrorGeneric) SetError(v GenericError) { + o.Error = v +} + +func (o ErrorGeneric) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ErrorGeneric) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["error"] = o.Error + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ErrorGeneric) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "error", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varErrorGeneric := _ErrorGeneric{} + + err = json.Unmarshal(data, &varErrorGeneric) + + if err != nil { + return err + } + + *o = ErrorGeneric(varErrorGeneric) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableErrorGeneric struct { + value *ErrorGeneric + isSet bool +} + +func (v NullableErrorGeneric) Get() *ErrorGeneric { + return v.value +} + +func (v *NullableErrorGeneric) Set(val *ErrorGeneric) { + v.value = val + v.isSet = true +} + +func (v NullableErrorGeneric) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorGeneric) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorGeneric(val *ErrorGeneric) *NullableErrorGeneric { + return &NullableErrorGeneric{value: val, isSet: true} +} + +func (v NullableErrorGeneric) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorGeneric) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_flow_error.go b/pkg/httpclient/model_flow_error.go new file mode 100644 index 0000000..8e755ea --- /dev/null +++ b/pkg/httpclient/model_flow_error.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" + "time" +) + +// checks if the FlowError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &FlowError{} + +// FlowError struct for FlowError +type FlowError struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + Error map[string]interface{} `json:"error,omitempty"` + // ID of the error container. + Id string `json:"id"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _FlowError FlowError + +// NewFlowError instantiates a new FlowError 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 NewFlowError(id string) *FlowError { + this := FlowError{} + this.Id = id + return &this +} + +// NewFlowErrorWithDefaults instantiates a new FlowError 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 NewFlowErrorWithDefaults() *FlowError { + this := FlowError{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *FlowError) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FlowError) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *FlowError) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *FlowError) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *FlowError) GetError() map[string]interface{} { + if o == nil || IsNil(o.Error) { + var ret map[string]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. +func (o *FlowError) GetErrorOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Error) { + return map[string]interface{}{}, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *FlowError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given map[string]interface{} and assigns it to the Error field. +func (o *FlowError) SetError(v map[string]interface{}) { + o.Error = v +} + +// GetId returns the Id field value +func (o *FlowError) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *FlowError) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *FlowError) SetId(v string) { + o.Id = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *FlowError) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FlowError) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *FlowError) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *FlowError) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +func (o FlowError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o FlowError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["id"] = o.Id + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *FlowError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varFlowError := _FlowError{} + + err = json.Unmarshal(data, &varFlowError) + + if err != nil { + return err + } + + *o = FlowError(varFlowError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "error") + delete(additionalProperties, "id") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableFlowError struct { + value *FlowError + isSet bool +} + +func (v NullableFlowError) Get() *FlowError { + return v.value +} + +func (v *NullableFlowError) Set(val *FlowError) { + v.value = val + v.isSet = true +} + +func (v NullableFlowError) IsSet() bool { + return v.isSet +} + +func (v *NullableFlowError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFlowError(val *FlowError) *NullableFlowError { + return &NullableFlowError{value: val, isSet: true} +} + +func (v NullableFlowError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFlowError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_generic_error.go b/pkg/httpclient/model_generic_error.go new file mode 100644 index 0000000..1931ebe --- /dev/null +++ b/pkg/httpclient/model_generic_error.go @@ -0,0 +1,434 @@ +/* +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" + "fmt" +) + +// checks if the GenericError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GenericError{} + +// GenericError struct for GenericError +type GenericError struct { + // The status code + Code *int64 `json:"code,omitempty"` + // Debug information This field is often not exposed to protect against leaking sensitive information. + Debug *string `json:"debug,omitempty"` + // Further error details + Details map[string]interface{} `json:"details,omitempty"` + // The error ID Useful when trying to identify various errors in application logic. + Id *string `json:"id,omitempty"` + // Error message The error's message. + Message string `json:"message"` + // A human-readable reason for the error + Reason *string `json:"reason,omitempty"` + // The request ID The request ID is often exposed internally in order to trace errors across service architectures. This is often a UUID. + Request *string `json:"request,omitempty"` + // The status description + Status *string `json:"status,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _GenericError GenericError + +// NewGenericError instantiates a new GenericError 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 NewGenericError(message string) *GenericError { + this := GenericError{} + this.Message = message + return &this +} + +// NewGenericErrorWithDefaults instantiates a new GenericError 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 NewGenericErrorWithDefaults() *GenericError { + this := GenericError{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *GenericError) GetCode() int64 { + if o == nil || IsNil(o.Code) { + var ret int64 + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetCodeOk() (*int64, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *GenericError) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given int64 and assigns it to the Code field. +func (o *GenericError) SetCode(v int64) { + o.Code = &v +} + +// GetDebug returns the Debug field value if set, zero value otherwise. +func (o *GenericError) GetDebug() string { + if o == nil || IsNil(o.Debug) { + var ret string + return ret + } + return *o.Debug +} + +// GetDebugOk returns a tuple with the Debug field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDebugOk() (*string, bool) { + if o == nil || IsNil(o.Debug) { + return nil, false + } + return o.Debug, true +} + +// HasDebug returns a boolean if a field has been set. +func (o *GenericError) HasDebug() bool { + if o != nil && !IsNil(o.Debug) { + return true + } + + return false +} + +// SetDebug gets a reference to the given string and assigns it to the Debug field. +func (o *GenericError) SetDebug(v string) { + o.Debug = &v +} + +// GetDetails returns the Details field value if set, zero value otherwise. +func (o *GenericError) GetDetails() map[string]interface{} { + if o == nil || IsNil(o.Details) { + var ret map[string]interface{} + return ret + } + return o.Details +} + +// GetDetailsOk returns a tuple with the Details field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDetailsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Details) { + return map[string]interface{}{}, false + } + return o.Details, true +} + +// HasDetails returns a boolean if a field has been set. +func (o *GenericError) HasDetails() bool { + if o != nil && !IsNil(o.Details) { + return true + } + + return false +} + +// SetDetails gets a reference to the given map[string]interface{} and assigns it to the Details field. +func (o *GenericError) SetDetails(v map[string]interface{}) { + o.Details = v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *GenericError) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *GenericError) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *GenericError) SetId(v string) { + o.Id = &v +} + +// GetMessage returns the Message field value +func (o *GenericError) GetMessage() string { + if o == nil { + var ret string + return ret + } + + return o.Message +} + +// GetMessageOk returns a tuple with the Message field value +// and a boolean to check if the value has been set. +func (o *GenericError) GetMessageOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Message, true +} + +// SetMessage sets field value +func (o *GenericError) SetMessage(v string) { + o.Message = v +} + +// GetReason returns the Reason field value if set, zero value otherwise. +func (o *GenericError) GetReason() string { + if o == nil || IsNil(o.Reason) { + var ret string + return ret + } + return *o.Reason +} + +// GetReasonOk returns a tuple with the Reason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetReasonOk() (*string, bool) { + if o == nil || IsNil(o.Reason) { + return nil, false + } + return o.Reason, true +} + +// HasReason returns a boolean if a field has been set. +func (o *GenericError) HasReason() bool { + if o != nil && !IsNil(o.Reason) { + return true + } + + return false +} + +// SetReason gets a reference to the given string and assigns it to the Reason field. +func (o *GenericError) SetReason(v string) { + o.Reason = &v +} + +// GetRequest returns the Request field value if set, zero value otherwise. +func (o *GenericError) GetRequest() string { + if o == nil || IsNil(o.Request) { + var ret string + return ret + } + return *o.Request +} + +// GetRequestOk returns a tuple with the Request field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetRequestOk() (*string, bool) { + if o == nil || IsNil(o.Request) { + return nil, false + } + return o.Request, true +} + +// HasRequest returns a boolean if a field has been set. +func (o *GenericError) HasRequest() bool { + if o != nil && !IsNil(o.Request) { + return true + } + + return false +} + +// SetRequest gets a reference to the given string and assigns it to the Request field. +func (o *GenericError) SetRequest(v string) { + o.Request = &v +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *GenericError) GetStatus() string { + if o == nil || IsNil(o.Status) { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetStatusOk() (*string, bool) { + if o == nil || IsNil(o.Status) { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *GenericError) HasStatus() bool { + if o != nil && !IsNil(o.Status) { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *GenericError) SetStatus(v string) { + o.Status = &v +} + +func (o GenericError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GenericError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.Debug) { + toSerialize["debug"] = o.Debug + } + if !IsNil(o.Details) { + toSerialize["details"] = o.Details + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + toSerialize["message"] = o.Message + if !IsNil(o.Reason) { + toSerialize["reason"] = o.Reason + } + if !IsNil(o.Request) { + toSerialize["request"] = o.Request + } + if !IsNil(o.Status) { + toSerialize["status"] = o.Status + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *GenericError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "message", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varGenericError := _GenericError{} + + err = json.Unmarshal(data, &varGenericError) + + if err != nil { + return err + } + + *o = GenericError(varGenericError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "debug") + delete(additionalProperties, "details") + delete(additionalProperties, "id") + delete(additionalProperties, "message") + delete(additionalProperties, "reason") + delete(additionalProperties, "request") + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableGenericError struct { + value *GenericError + isSet bool +} + +func (v NullableGenericError) Get() *GenericError { + return v.value +} + +func (v *NullableGenericError) Set(val *GenericError) { + v.value = val + v.isSet = true +} + +func (v NullableGenericError) IsSet() bool { + return v.isSet +} + +func (v *NullableGenericError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGenericError(val *GenericError) *NullableGenericError { + return &NullableGenericError{value: val, isSet: true} +} + +func (v NullableGenericError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGenericError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_get_version_200_response.go b/pkg/httpclient/model_get_version_200_response.go new file mode 100644 index 0000000..a60de02 --- /dev/null +++ b/pkg/httpclient/model_get_version_200_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the GetVersion200Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GetVersion200Response{} + +// GetVersion200Response struct for GetVersion200Response +type GetVersion200Response struct { + // The version of Ory Kratos. + Version string `json:"version"` + AdditionalProperties map[string]interface{} +} + +type _GetVersion200Response GetVersion200Response + +// NewGetVersion200Response instantiates a new GetVersion200Response 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 NewGetVersion200Response(version string) *GetVersion200Response { + this := GetVersion200Response{} + this.Version = version + return &this +} + +// NewGetVersion200ResponseWithDefaults instantiates a new GetVersion200Response 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 NewGetVersion200ResponseWithDefaults() *GetVersion200Response { + this := GetVersion200Response{} + return &this +} + +// GetVersion returns the Version field value +func (o *GetVersion200Response) GetVersion() string { + if o == nil { + var ret string + return ret + } + + return o.Version +} + +// GetVersionOk returns a tuple with the Version field value +// and a boolean to check if the value has been set. +func (o *GetVersion200Response) GetVersionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Version, true +} + +// SetVersion sets field value +func (o *GetVersion200Response) SetVersion(v string) { + o.Version = v +} + +func (o GetVersion200Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GetVersion200Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["version"] = o.Version + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *GetVersion200Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "version", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varGetVersion200Response := _GetVersion200Response{} + + err = json.Unmarshal(data, &varGetVersion200Response) + + if err != nil { + return err + } + + *o = GetVersion200Response(varGetVersion200Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableGetVersion200Response struct { + value *GetVersion200Response + isSet bool +} + +func (v NullableGetVersion200Response) Get() *GetVersion200Response { + return v.value +} + +func (v *NullableGetVersion200Response) Set(val *GetVersion200Response) { + v.value = val + v.isSet = true +} + +func (v NullableGetVersion200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableGetVersion200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGetVersion200Response(val *GetVersion200Response) *NullableGetVersion200Response { + return &NullableGetVersion200Response{value: val, isSet: true} +} + +func (v NullableGetVersion200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGetVersion200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_health_not_ready_status.go b/pkg/httpclient/model_health_not_ready_status.go new file mode 100644 index 0000000..5e05db7 --- /dev/null +++ b/pkg/httpclient/model_health_not_ready_status.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the HealthNotReadyStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &HealthNotReadyStatus{} + +// HealthNotReadyStatus struct for HealthNotReadyStatus +type HealthNotReadyStatus struct { + // Errors contains a list of errors that caused the not ready status. + Errors *map[string]string `json:"errors,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _HealthNotReadyStatus HealthNotReadyStatus + +// NewHealthNotReadyStatus instantiates a new HealthNotReadyStatus 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 NewHealthNotReadyStatus() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// NewHealthNotReadyStatusWithDefaults instantiates a new HealthNotReadyStatus 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 NewHealthNotReadyStatusWithDefaults() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// GetErrors returns the Errors field value if set, zero value otherwise. +func (o *HealthNotReadyStatus) GetErrors() map[string]string { + if o == nil || IsNil(o.Errors) { + var ret map[string]string + return ret + } + return *o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthNotReadyStatus) GetErrorsOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Errors) { + return nil, false + } + return o.Errors, true +} + +// HasErrors returns a boolean if a field has been set. +func (o *HealthNotReadyStatus) HasErrors() bool { + if o != nil && !IsNil(o.Errors) { + return true + } + + return false +} + +// SetErrors gets a reference to the given map[string]string and assigns it to the Errors field. +func (o *HealthNotReadyStatus) SetErrors(v map[string]string) { + o.Errors = &v +} + +func (o HealthNotReadyStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o HealthNotReadyStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Errors) { + toSerialize["errors"] = o.Errors + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *HealthNotReadyStatus) UnmarshalJSON(data []byte) (err error) { + varHealthNotReadyStatus := _HealthNotReadyStatus{} + + err = json.Unmarshal(data, &varHealthNotReadyStatus) + + if err != nil { + return err + } + + *o = HealthNotReadyStatus(varHealthNotReadyStatus) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "errors") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableHealthNotReadyStatus struct { + value *HealthNotReadyStatus + isSet bool +} + +func (v NullableHealthNotReadyStatus) Get() *HealthNotReadyStatus { + return v.value +} + +func (v *NullableHealthNotReadyStatus) Set(val *HealthNotReadyStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthNotReadyStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthNotReadyStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthNotReadyStatus(val *HealthNotReadyStatus) *NullableHealthNotReadyStatus { + return &NullableHealthNotReadyStatus{value: val, isSet: true} +} + +func (v NullableHealthNotReadyStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthNotReadyStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_health_status.go b/pkg/httpclient/model_health_status.go new file mode 100644 index 0000000..c09ccf4 --- /dev/null +++ b/pkg/httpclient/model_health_status.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the HealthStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &HealthStatus{} + +// HealthStatus struct for HealthStatus +type HealthStatus struct { + // Status always contains \"ok\". + Status *string `json:"status,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _HealthStatus HealthStatus + +// NewHealthStatus instantiates a new HealthStatus 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 NewHealthStatus() *HealthStatus { + this := HealthStatus{} + return &this +} + +// NewHealthStatusWithDefaults instantiates a new HealthStatus 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 NewHealthStatusWithDefaults() *HealthStatus { + this := HealthStatus{} + return &this +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *HealthStatus) GetStatus() string { + if o == nil || IsNil(o.Status) { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthStatus) GetStatusOk() (*string, bool) { + if o == nil || IsNil(o.Status) { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *HealthStatus) HasStatus() bool { + if o != nil && !IsNil(o.Status) { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *HealthStatus) SetStatus(v string) { + o.Status = &v +} + +func (o HealthStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o HealthStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Status) { + toSerialize["status"] = o.Status + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *HealthStatus) UnmarshalJSON(data []byte) (err error) { + varHealthStatus := _HealthStatus{} + + err = json.Unmarshal(data, &varHealthStatus) + + if err != nil { + return err + } + + *o = HealthStatus(varHealthStatus) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableHealthStatus struct { + value *HealthStatus + isSet bool +} + +func (v NullableHealthStatus) Get() *HealthStatus { + return v.value +} + +func (v *NullableHealthStatus) Set(val *HealthStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthStatus(val *HealthStatus) *NullableHealthStatus { + return &NullableHealthStatus{value: val, isSet: true} +} + +func (v NullableHealthStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity.go b/pkg/httpclient/model_identity.go new file mode 100644 index 0000000..f99ffe9 --- /dev/null +++ b/pkg/httpclient/model_identity.go @@ -0,0 +1,730 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Identity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Identity{} + +// Identity An [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) represents a (human) user in Ory. +type Identity struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + // Credentials represents all credentials that can be used for authenticating this identity. + Credentials *map[string]IdentityCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // ID is the identity's unique identifier. The Identity ID can not be changed and can not be chosen. This ensures future compatibility and optimization for distributed stores such as CockroachDB. + Id string `json:"id"` + // NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- + MetadataPublic interface{} `json:"metadata_public,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RecoveryAddresses contains all the addresses that can be used to recover an identity. + RecoveryAddresses []RecoveryIdentityAddress `json:"recovery_addresses,omitempty"` + // Region is the Ory Network region this identity is homed in. Set by the multi-region persister; empty on OSS and single-region deployments. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. + SchemaId string `json:"schema_id"` + // SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from. format: url + SchemaUrl string `json:"schema_url"` + // State is the identity's state. This value has currently no effect. active StateActive inactive StateInactive + State *string `json:"state,omitempty"` + StateChangedAt *time.Time `json:"state_changed_at,omitempty"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_url`. + Traits interface{} `json:"traits"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // VerifiableAddresses contains all the addresses that can be verified by the user. + VerifiableAddresses []VerifiableIdentityAddress `json:"verifiable_addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Identity Identity + +// NewIdentity instantiates a new Identity 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 NewIdentity(id string, schemaId string, schemaUrl string, traits interface{}) *Identity { + this := Identity{} + this.Id = id + this.SchemaId = schemaId + this.SchemaUrl = schemaUrl + this.Traits = traits + return &this +} + +// NewIdentityWithDefaults instantiates a new Identity 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 NewIdentityWithDefaults() *Identity { + this := Identity{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *Identity) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *Identity) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *Identity) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *Identity) GetCredentials() map[string]IdentityCredentials { + if o == nil || IsNil(o.Credentials) { + var ret map[string]IdentityCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetCredentialsOk() (*map[string]IdentityCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *Identity) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given map[string]IdentityCredentials and assigns it to the Credentials field. +func (o *Identity) SetCredentials(v map[string]IdentityCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *Identity) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *Identity) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *Identity) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetId returns the Id field value +func (o *Identity) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Identity) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Identity) SetId(v string) { + o.Id = v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *Identity) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *Identity) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *Identity) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *Identity) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *Identity) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *Identity) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *Identity) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *Identity) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *Identity) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *Identity) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *Identity) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *Identity) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRecoveryAddresses returns the RecoveryAddresses field value if set, zero value otherwise. +func (o *Identity) GetRecoveryAddresses() []RecoveryIdentityAddress { + if o == nil || IsNil(o.RecoveryAddresses) { + var ret []RecoveryIdentityAddress + return ret + } + return o.RecoveryAddresses +} + +// GetRecoveryAddressesOk returns a tuple with the RecoveryAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetRecoveryAddressesOk() ([]RecoveryIdentityAddress, bool) { + if o == nil || IsNil(o.RecoveryAddresses) { + return nil, false + } + return o.RecoveryAddresses, true +} + +// HasRecoveryAddresses returns a boolean if a field has been set. +func (o *Identity) HasRecoveryAddresses() bool { + if o != nil && !IsNil(o.RecoveryAddresses) { + return true + } + + return false +} + +// SetRecoveryAddresses gets a reference to the given []RecoveryIdentityAddress and assigns it to the RecoveryAddresses field. +func (o *Identity) SetRecoveryAddresses(v []RecoveryIdentityAddress) { + o.RecoveryAddresses = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *Identity) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *Identity) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *Identity) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *Identity) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *Identity) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *Identity) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetSchemaUrl returns the SchemaUrl field value +func (o *Identity) GetSchemaUrl() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaUrl +} + +// GetSchemaUrlOk returns a tuple with the SchemaUrl field value +// and a boolean to check if the value has been set. +func (o *Identity) GetSchemaUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaUrl, true +} + +// SetSchemaUrl sets field value +func (o *Identity) SetSchemaUrl(v string) { + o.SchemaUrl = v +} + +// GetState returns the State field value if set, zero value otherwise. +func (o *Identity) GetState() string { + if o == nil || IsNil(o.State) { + var ret string + return ret + } + return *o.State +} + +// GetStateOk returns a tuple with the State field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetStateOk() (*string, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return o.State, true +} + +// HasState returns a boolean if a field has been set. +func (o *Identity) HasState() bool { + if o != nil && !IsNil(o.State) { + return true + } + + return false +} + +// SetState gets a reference to the given string and assigns it to the State field. +func (o *Identity) SetState(v string) { + o.State = &v +} + +// GetStateChangedAt returns the StateChangedAt field value if set, zero value otherwise. +func (o *Identity) GetStateChangedAt() time.Time { + if o == nil || IsNil(o.StateChangedAt) { + var ret time.Time + return ret + } + return *o.StateChangedAt +} + +// GetStateChangedAtOk returns a tuple with the StateChangedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetStateChangedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.StateChangedAt) { + return nil, false + } + return o.StateChangedAt, true +} + +// HasStateChangedAt returns a boolean if a field has been set. +func (o *Identity) HasStateChangedAt() bool { + if o != nil && !IsNil(o.StateChangedAt) { + return true + } + + return false +} + +// SetStateChangedAt gets a reference to the given time.Time and assigns it to the StateChangedAt field. +func (o *Identity) SetStateChangedAt(v time.Time) { + o.StateChangedAt = &v +} + +// GetTraits returns the Traits field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *Identity) GetTraits() interface{} { + if o == nil { + var ret 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. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *Identity) GetTraitsOk() (*interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return nil, false + } + return &o.Traits, true +} + +// SetTraits sets field value +func (o *Identity) SetTraits(v interface{}) { + o.Traits = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *Identity) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *Identity) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *Identity) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetVerifiableAddresses returns the VerifiableAddresses field value if set, zero value otherwise. +func (o *Identity) GetVerifiableAddresses() []VerifiableIdentityAddress { + if o == nil || IsNil(o.VerifiableAddresses) { + var ret []VerifiableIdentityAddress + return ret + } + return o.VerifiableAddresses +} + +// GetVerifiableAddressesOk returns a tuple with the VerifiableAddresses field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Identity) GetVerifiableAddressesOk() ([]VerifiableIdentityAddress, bool) { + if o == nil || IsNil(o.VerifiableAddresses) { + return nil, false + } + return o.VerifiableAddresses, true +} + +// HasVerifiableAddresses returns a boolean if a field has been set. +func (o *Identity) HasVerifiableAddresses() bool { + if o != nil && !IsNil(o.VerifiableAddresses) { + return true + } + + return false +} + +// SetVerifiableAddresses gets a reference to the given []VerifiableIdentityAddress and assigns it to the VerifiableAddresses field. +func (o *Identity) SetVerifiableAddresses(v []VerifiableIdentityAddress) { + o.VerifiableAddresses = v +} + +func (o Identity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Identity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + toSerialize["id"] = o.Id + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.RecoveryAddresses) { + toSerialize["recovery_addresses"] = o.RecoveryAddresses + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + toSerialize["schema_url"] = o.SchemaUrl + if !IsNil(o.State) { + toSerialize["state"] = o.State + } + if !IsNil(o.StateChangedAt) { + toSerialize["state_changed_at"] = o.StateChangedAt + } + if o.Traits != nil { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.VerifiableAddresses) { + toSerialize["verifiable_addresses"] = o.VerifiableAddresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Identity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "schema_id", + "schema_url", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentity := _Identity{} + + err = json.Unmarshal(data, &varIdentity) + + if err != nil { + return err + } + + *o = Identity(varIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "recovery_addresses") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "schema_url") + delete(additionalProperties, "state") + delete(additionalProperties, "state_changed_at") + delete(additionalProperties, "traits") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "verifiable_addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentity struct { + value *Identity + isSet bool +} + +func (v NullableIdentity) Get() *Identity { + return v.value +} + +func (v *NullableIdentity) Set(val *Identity) { + v.value = val + v.isSet = true +} + +func (v NullableIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentity(val *Identity) *NullableIdentity { + return &NullableIdentity{value: val, isSet: true} +} + +func (v NullableIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials.go b/pkg/httpclient/model_identity_credentials.go new file mode 100644 index 0000000..9ee0497 --- /dev/null +++ b/pkg/httpclient/model_identity_credentials.go @@ -0,0 +1,345 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentials type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentials{} + +// IdentityCredentials Credentials represents a specific credential type +type IdentityCredentials struct { + Config map[string]interface{} `json:"config,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + // Identifiers represent 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // Version refers to the version of the credential. Useful when changing the config schema. + Version *int64 `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentials IdentityCredentials + +// NewIdentityCredentials instantiates a new IdentityCredentials 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 NewIdentityCredentials() *IdentityCredentials { + this := IdentityCredentials{} + return &this +} + +// NewIdentityCredentialsWithDefaults instantiates a new IdentityCredentials 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 NewIdentityCredentialsWithDefaults() *IdentityCredentials { + this := IdentityCredentials{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityCredentials) GetConfig() map[string]interface{} { + if o == nil || IsNil(o.Config) { + var ret map[string]interface{} + return ret + } + return o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetConfigOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Config) { + return map[string]interface{}{}, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityCredentials) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given map[string]interface{} and assigns it to the Config field. +func (o *IdentityCredentials) SetConfig(v map[string]interface{}) { + o.Config = v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *IdentityCredentials) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *IdentityCredentials) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *IdentityCredentials) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetIdentifiers returns the Identifiers field value if set, zero value otherwise. +func (o *IdentityCredentials) GetIdentifiers() []string { + if o == nil || IsNil(o.Identifiers) { + var ret []string + return ret + } + return o.Identifiers +} + +// GetIdentifiersOk returns a tuple with the Identifiers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetIdentifiersOk() ([]string, bool) { + if o == nil || IsNil(o.Identifiers) { + return nil, false + } + return o.Identifiers, true +} + +// HasIdentifiers returns a boolean if a field has been set. +func (o *IdentityCredentials) HasIdentifiers() bool { + if o != nil && !IsNil(o.Identifiers) { + return true + } + + return false +} + +// SetIdentifiers gets a reference to the given []string and assigns it to the Identifiers field. +func (o *IdentityCredentials) SetIdentifiers(v []string) { + o.Identifiers = v +} + +// GetType returns the Type field value if set, zero value otherwise. +func (o *IdentityCredentials) GetType() string { + if o == nil || IsNil(o.Type) { + var ret string + return ret + } + return *o.Type +} + +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetTypeOk() (*string, bool) { + if o == nil || IsNil(o.Type) { + return nil, false + } + return o.Type, true +} + +// HasType returns a boolean if a field has been set. +func (o *IdentityCredentials) HasType() bool { + if o != nil && !IsNil(o.Type) { + return true + } + + return false +} + +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *IdentityCredentials) SetType(v string) { + o.Type = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *IdentityCredentials) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *IdentityCredentials) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *IdentityCredentials) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *IdentityCredentials) GetVersion() int64 { + if o == nil || IsNil(o.Version) { + var ret int64 + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentials) GetVersionOk() (*int64, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *IdentityCredentials) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given int64 and assigns it to the Version field. +func (o *IdentityCredentials) SetVersion(v int64) { + o.Version = &v +} + +func (o IdentityCredentials) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentials) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Identifiers) { + toSerialize["identifiers"] = o.Identifiers + } + if !IsNil(o.Type) { + toSerialize["type"] = o.Type + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentials) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentials := _IdentityCredentials{} + + err = json.Unmarshal(data, &varIdentityCredentials) + + if err != nil { + return err + } + + *o = IdentityCredentials(varIdentityCredentials) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + delete(additionalProperties, "created_at") + delete(additionalProperties, "identifiers") + delete(additionalProperties, "type") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentials struct { + value *IdentityCredentials + isSet bool +} + +func (v NullableIdentityCredentials) Get() *IdentityCredentials { + return v.value +} + +func (v *NullableIdentityCredentials) Set(val *IdentityCredentials) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentials) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentials) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentials(val *IdentityCredentials) *NullableIdentityCredentials { + return &NullableIdentityCredentials{value: val, isSet: true} +} + +func (v NullableIdentityCredentials) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentials) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_code.go b/pkg/httpclient/model_identity_credentials_code.go new file mode 100644 index 0000000..40e6d9a --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_code.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityCredentialsCode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsCode{} + +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code +type IdentityCredentialsCode struct { + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsCode IdentityCredentialsCode + +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode 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 NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode 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 NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || IsNil(o.Addresses) { + var ret []IdentityCredentialsCodeAddress + return ret + } + return o.Addresses +} + +// 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) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || IsNil(o.Addresses) { + return nil, false + } + return o.Addresses, true +} + +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && !IsNil(o.Addresses) { + return true + } + + return false +} + +// 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsCode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Addresses) { + toSerialize["addresses"] = o.Addresses + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsCode) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsCode := _IdentityCredentialsCode{} + + err = json.Unmarshal(data, &varIdentityCredentialsCode) + + if err != nil { + return err + } + + *o = IdentityCredentialsCode(varIdentityCredentialsCode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "addresses") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode + isSet bool +} + +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { + return v.value +} + +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_code_address.go b/pkg/httpclient/model_identity_credentials_code_address.go new file mode 100644 index 0000000..dc6dc73 --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_code_address.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityCredentialsCodeAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsCodeAddress{} + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsCodeAddress IdentityCredentialsCodeAddress + +// 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 || IsNil(o.Address) { + 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 || IsNil(o.Address) { + 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 && !IsNil(o.Address) { + 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 || IsNil(o.Channel) { + 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 || IsNil(o.Channel) { + 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 && !IsNil(o.Channel) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsCodeAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Address) { + toSerialize["address"] = o.Address + } + if !IsNil(o.Channel) { + toSerialize["channel"] = o.Channel + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsCodeAddress) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsCodeAddress := _IdentityCredentialsCodeAddress{} + + err = json.Unmarshal(data, &varIdentityCredentialsCodeAddress) + + if err != nil { + return err + } + + *o = IdentityCredentialsCodeAddress(varIdentityCredentialsCodeAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "address") + delete(additionalProperties, "channel") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_identity_credentials_lookup_secret_code.go b/pkg/httpclient/model_identity_credentials_lookup_secret_code.go new file mode 100644 index 0000000..cc5dd45 --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_lookup_secret_code.go @@ -0,0 +1,193 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentialsLookupSecretCode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsLookupSecretCode{} + +// IdentityCredentialsLookupSecretCode Recovery codes can be used once and are invalidated after use. +type IdentityCredentialsLookupSecretCode struct { + // A recovery code + Code *string `json:"code,omitempty"` + UsedAt *time.Time `json:"used_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsLookupSecretCode IdentityCredentialsLookupSecretCode + +// NewIdentityCredentialsLookupSecretCode instantiates a new IdentityCredentialsLookupSecretCode 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 NewIdentityCredentialsLookupSecretCode() *IdentityCredentialsLookupSecretCode { + this := IdentityCredentialsLookupSecretCode{} + return &this +} + +// NewIdentityCredentialsLookupSecretCodeWithDefaults instantiates a new IdentityCredentialsLookupSecretCode 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 NewIdentityCredentialsLookupSecretCodeWithDefaults() *IdentityCredentialsLookupSecretCode { + this := IdentityCredentialsLookupSecretCode{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *IdentityCredentialsLookupSecretCode) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsLookupSecretCode) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *IdentityCredentialsLookupSecretCode) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *IdentityCredentialsLookupSecretCode) SetCode(v string) { + o.Code = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise. +func (o *IdentityCredentialsLookupSecretCode) GetUsedAt() time.Time { + if o == nil || IsNil(o.UsedAt) { + var ret time.Time + return ret + } + return *o.UsedAt +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsLookupSecretCode) GetUsedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UsedAt) { + return nil, false + } + return o.UsedAt, true +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsLookupSecretCode) HasUsedAt() bool { + if o != nil && !IsNil(o.UsedAt) { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given time.Time and assigns it to the UsedAt field. +func (o *IdentityCredentialsLookupSecretCode) SetUsedAt(v time.Time) { + o.UsedAt = &v +} + +func (o IdentityCredentialsLookupSecretCode) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsLookupSecretCode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.UsedAt) { + toSerialize["used_at"] = o.UsedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsLookupSecretCode) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsLookupSecretCode := _IdentityCredentialsLookupSecretCode{} + + err = json.Unmarshal(data, &varIdentityCredentialsLookupSecretCode) + + if err != nil { + return err + } + + *o = IdentityCredentialsLookupSecretCode(varIdentityCredentialsLookupSecretCode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "used_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsLookupSecretCode struct { + value *IdentityCredentialsLookupSecretCode + isSet bool +} + +func (v NullableIdentityCredentialsLookupSecretCode) Get() *IdentityCredentialsLookupSecretCode { + return v.value +} + +func (v *NullableIdentityCredentialsLookupSecretCode) Set(val *IdentityCredentialsLookupSecretCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsLookupSecretCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsLookupSecretCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsLookupSecretCode(val *IdentityCredentialsLookupSecretCode) *NullableIdentityCredentialsLookupSecretCode { + return &NullableIdentityCredentialsLookupSecretCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsLookupSecretCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsLookupSecretCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_oidc.go b/pkg/httpclient/model_identity_credentials_oidc.go new file mode 100644 index 0000000..452cb6c --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_oidc.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityCredentialsOidc type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsOidc{} + +// IdentityCredentialsOidc struct for IdentityCredentialsOidc +type IdentityCredentialsOidc struct { + Providers []IdentityCredentialsOidcProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsOidc IdentityCredentialsOidc + +// NewIdentityCredentialsOidc instantiates a new IdentityCredentialsOidc 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 NewIdentityCredentialsOidc() *IdentityCredentialsOidc { + this := IdentityCredentialsOidc{} + return &this +} + +// NewIdentityCredentialsOidcWithDefaults instantiates a new IdentityCredentialsOidc 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 NewIdentityCredentialsOidcWithDefaults() *IdentityCredentialsOidc { + this := IdentityCredentialsOidc{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityCredentialsOidc) GetProviders() []IdentityCredentialsOidcProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityCredentialsOidcProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidc) GetProvidersOk() ([]IdentityCredentialsOidcProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityCredentialsOidc) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityCredentialsOidcProvider and assigns it to the Providers field. +func (o *IdentityCredentialsOidc) SetProviders(v []IdentityCredentialsOidcProvider) { + o.Providers = v +} + +func (o IdentityCredentialsOidc) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsOidc) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsOidc) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsOidc := _IdentityCredentialsOidc{} + + err = json.Unmarshal(data, &varIdentityCredentialsOidc) + + if err != nil { + return err + } + + *o = IdentityCredentialsOidc(varIdentityCredentialsOidc) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsOidc struct { + value *IdentityCredentialsOidc + isSet bool +} + +func (v NullableIdentityCredentialsOidc) Get() *IdentityCredentialsOidc { + return v.value +} + +func (v *NullableIdentityCredentialsOidc) Set(val *IdentityCredentialsOidc) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOidc) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOidc) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOidc(val *IdentityCredentialsOidc) *NullableIdentityCredentialsOidc { + return &NullableIdentityCredentialsOidc{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOidc) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOidc) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_oidc_provider.go b/pkg/httpclient/model_identity_credentials_oidc_provider.go new file mode 100644 index 0000000..ce1d28b --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_oidc_provider.go @@ -0,0 +1,376 @@ +/* +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" +) + +// checks if the IdentityCredentialsOidcProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsOidcProvider{} + +// IdentityCredentialsOidcProvider struct for IdentityCredentialsOidcProvider +type IdentityCredentialsOidcProvider struct { + InitialAccessToken *string `json:"initial_access_token,omitempty"` + InitialIdToken *string `json:"initial_id_token,omitempty"` + InitialRefreshToken *string `json:"initial_refresh_token,omitempty"` + Organization *string `json:"organization,omitempty"` + Provider *string `json:"provider,omitempty"` + Subject *string `json:"subject,omitempty"` + UseAutoLink *bool `json:"use_auto_link,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsOidcProvider IdentityCredentialsOidcProvider + +// NewIdentityCredentialsOidcProvider instantiates a new IdentityCredentialsOidcProvider 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 NewIdentityCredentialsOidcProvider() *IdentityCredentialsOidcProvider { + this := IdentityCredentialsOidcProvider{} + return &this +} + +// NewIdentityCredentialsOidcProviderWithDefaults instantiates a new IdentityCredentialsOidcProvider 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 NewIdentityCredentialsOidcProviderWithDefaults() *IdentityCredentialsOidcProvider { + this := IdentityCredentialsOidcProvider{} + return &this +} + +// GetInitialAccessToken returns the InitialAccessToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialAccessToken() string { + if o == nil || IsNil(o.InitialAccessToken) { + var ret string + return ret + } + return *o.InitialAccessToken +} + +// GetInitialAccessTokenOk returns a tuple with the InitialAccessToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialAccessTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialAccessToken) { + return nil, false + } + return o.InitialAccessToken, true +} + +// HasInitialAccessToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialAccessToken() bool { + if o != nil && !IsNil(o.InitialAccessToken) { + return true + } + + return false +} + +// SetInitialAccessToken gets a reference to the given string and assigns it to the InitialAccessToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialAccessToken(v string) { + o.InitialAccessToken = &v +} + +// GetInitialIdToken returns the InitialIdToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialIdToken() string { + if o == nil || IsNil(o.InitialIdToken) { + var ret string + return ret + } + return *o.InitialIdToken +} + +// GetInitialIdTokenOk returns a tuple with the InitialIdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialIdToken) { + return nil, false + } + return o.InitialIdToken, true +} + +// HasInitialIdToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialIdToken() bool { + if o != nil && !IsNil(o.InitialIdToken) { + return true + } + + return false +} + +// SetInitialIdToken gets a reference to the given string and assigns it to the InitialIdToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialIdToken(v string) { + o.InitialIdToken = &v +} + +// GetInitialRefreshToken returns the InitialRefreshToken field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetInitialRefreshToken() string { + if o == nil || IsNil(o.InitialRefreshToken) { + var ret string + return ret + } + return *o.InitialRefreshToken +} + +// GetInitialRefreshTokenOk returns a tuple with the InitialRefreshToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetInitialRefreshTokenOk() (*string, bool) { + if o == nil || IsNil(o.InitialRefreshToken) { + return nil, false + } + return o.InitialRefreshToken, true +} + +// HasInitialRefreshToken returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasInitialRefreshToken() bool { + if o != nil && !IsNil(o.InitialRefreshToken) { + return true + } + + return false +} + +// SetInitialRefreshToken gets a reference to the given string and assigns it to the InitialRefreshToken field. +func (o *IdentityCredentialsOidcProvider) SetInitialRefreshToken(v string) { + o.InitialRefreshToken = &v +} + +// GetOrganization returns the Organization field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization) { + var ret string + return ret + } + return *o.Organization +} + +// GetOrganizationOk returns a tuple with the Organization field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetOrganizationOk() (*string, bool) { + if o == nil || IsNil(o.Organization) { + return nil, false + } + return o.Organization, true +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasOrganization() bool { + if o != nil && !IsNil(o.Organization) { + return true + } + + return false +} + +// SetOrganization gets a reference to the given string and assigns it to the Organization field. +func (o *IdentityCredentialsOidcProvider) SetOrganization(v string) { + o.Organization = &v +} + +// GetProvider returns the Provider field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetProvider() string { + if o == nil || IsNil(o.Provider) { + var ret string + return ret + } + return *o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetProviderOk() (*string, bool) { + if o == nil || IsNil(o.Provider) { + return nil, false + } + return o.Provider, true +} + +// HasProvider returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasProvider() bool { + if o != nil && !IsNil(o.Provider) { + return true + } + + return false +} + +// SetProvider gets a reference to the given string and assigns it to the Provider field. +func (o *IdentityCredentialsOidcProvider) SetProvider(v string) { + o.Provider = &v +} + +// GetSubject returns the Subject field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetSubject() string { + if o == nil || IsNil(o.Subject) { + var ret string + return ret + } + return *o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetSubjectOk() (*string, bool) { + if o == nil || IsNil(o.Subject) { + return nil, false + } + return o.Subject, true +} + +// HasSubject returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasSubject() bool { + if o != nil && !IsNil(o.Subject) { + return true + } + + return false +} + +// SetSubject gets a reference to the given string and assigns it to the Subject field. +func (o *IdentityCredentialsOidcProvider) SetSubject(v string) { + o.Subject = &v +} + +// GetUseAutoLink returns the UseAutoLink field value if set, zero value otherwise. +func (o *IdentityCredentialsOidcProvider) GetUseAutoLink() bool { + if o == nil || IsNil(o.UseAutoLink) { + var ret bool + return ret + } + return *o.UseAutoLink +} + +// GetUseAutoLinkOk returns a tuple with the UseAutoLink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOidcProvider) GetUseAutoLinkOk() (*bool, bool) { + if o == nil || IsNil(o.UseAutoLink) { + return nil, false + } + return o.UseAutoLink, true +} + +// HasUseAutoLink returns a boolean if a field has been set. +func (o *IdentityCredentialsOidcProvider) HasUseAutoLink() bool { + if o != nil && !IsNil(o.UseAutoLink) { + return true + } + + return false +} + +// SetUseAutoLink gets a reference to the given bool and assigns it to the UseAutoLink field. +func (o *IdentityCredentialsOidcProvider) SetUseAutoLink(v bool) { + o.UseAutoLink = &v +} + +func (o IdentityCredentialsOidcProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsOidcProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.InitialAccessToken) { + toSerialize["initial_access_token"] = o.InitialAccessToken + } + if !IsNil(o.InitialIdToken) { + toSerialize["initial_id_token"] = o.InitialIdToken + } + if !IsNil(o.InitialRefreshToken) { + toSerialize["initial_refresh_token"] = o.InitialRefreshToken + } + if !IsNil(o.Organization) { + toSerialize["organization"] = o.Organization + } + if !IsNil(o.Provider) { + toSerialize["provider"] = o.Provider + } + if !IsNil(o.Subject) { + toSerialize["subject"] = o.Subject + } + if !IsNil(o.UseAutoLink) { + toSerialize["use_auto_link"] = o.UseAutoLink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsOidcProvider) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsOidcProvider := _IdentityCredentialsOidcProvider{} + + err = json.Unmarshal(data, &varIdentityCredentialsOidcProvider) + + if err != nil { + return err + } + + *o = IdentityCredentialsOidcProvider(varIdentityCredentialsOidcProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "initial_access_token") + delete(additionalProperties, "initial_id_token") + delete(additionalProperties, "initial_refresh_token") + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + delete(additionalProperties, "use_auto_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsOidcProvider struct { + value *IdentityCredentialsOidcProvider + isSet bool +} + +func (v NullableIdentityCredentialsOidcProvider) Get() *IdentityCredentialsOidcProvider { + return v.value +} + +func (v *NullableIdentityCredentialsOidcProvider) Set(val *IdentityCredentialsOidcProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOidcProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOidcProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOidcProvider(val *IdentityCredentialsOidcProvider) *NullableIdentityCredentialsOidcProvider { + return &NullableIdentityCredentialsOidcProvider{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOidcProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOidcProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_password.go b/pkg/httpclient/model_identity_credentials_password.go new file mode 100644 index 0000000..a8e0830 --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_password.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the IdentityCredentialsPassword type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsPassword{} + +// IdentityCredentialsPassword struct for IdentityCredentialsPassword +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"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsPassword IdentityCredentialsPassword + +// NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword 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 NewIdentityCredentialsPassword() *IdentityCredentialsPassword { + this := IdentityCredentialsPassword{} + return &this +} + +// NewIdentityCredentialsPasswordWithDefaults instantiates a new IdentityCredentialsPassword 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 NewIdentityCredentialsPasswordWithDefaults() *IdentityCredentialsPassword { + this := IdentityCredentialsPassword{} + return &this +} + +// GetHashedPassword returns the HashedPassword field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetHashedPassword() string { + if o == nil || IsNil(o.HashedPassword) { + var ret string + return ret + } + return *o.HashedPassword +} + +// GetHashedPasswordOk returns a tuple with the HashedPassword field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetHashedPasswordOk() (*string, bool) { + if o == nil || IsNil(o.HashedPassword) { + return nil, false + } + return o.HashedPassword, true +} + +// HasHashedPassword returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasHashedPassword() bool { + if o != nil && !IsNil(o.HashedPassword) { + return true + } + + return false +} + +// SetHashedPassword gets a reference to the given string and assigns it to the HashedPassword field. +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 || IsNil(o.UsePasswordMigrationHook) { + 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 || IsNil(o.UsePasswordMigrationHook) { + 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 && !IsNil(o.UsePasswordMigrationHook) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsPassword) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.HashedPassword) { + toSerialize["hashed_password"] = o.HashedPassword + } + if !IsNil(o.UsePasswordMigrationHook) { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsPassword) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsPassword := _IdentityCredentialsPassword{} + + err = json.Unmarshal(data, &varIdentityCredentialsPassword) + + if err != nil { + return err + } + + *o = IdentityCredentialsPassword(varIdentityCredentialsPassword) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "hashed_password") + delete(additionalProperties, "use_password_migration_hook") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsPassword struct { + value *IdentityCredentialsPassword + isSet bool +} + +func (v NullableIdentityCredentialsPassword) Get() *IdentityCredentialsPassword { + return v.value +} + +func (v *NullableIdentityCredentialsPassword) Set(val *IdentityCredentialsPassword) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsPassword) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsPassword) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsPassword(val *IdentityCredentialsPassword) *NullableIdentityCredentialsPassword { + return &NullableIdentityCredentialsPassword{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsPassword) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsPassword) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_web_authn.go b/pkg/httpclient/model_identity_credentials_web_authn.go new file mode 100644 index 0000000..978c36e --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_web_authn.go @@ -0,0 +1,488 @@ +/* +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" + "time" +) + +// checks if the IdentityCredentialsWebAuthn type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthn{} + +// IdentityCredentialsWebAuthn struct for IdentityCredentialsWebAuthn +type IdentityCredentialsWebAuthn struct { + AddedAt *time.Time `json:"added_at,omitempty"` + Attestation *IdentityCredentialsWebAuthnAttestation `json:"attestation,omitempty"` + AttestationType *string `json:"attestation_type,omitempty"` + Authenticator *IdentityCredentialsWebAuthnAuthenticator `json:"authenticator,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + Flags *IdentityCredentialsWebAuthnFlags `json:"flags,omitempty"` + Id []int32 `json:"id,omitempty"` + IsPasswordless *bool `json:"is_passwordless,omitempty"` + PublicKey []int32 `json:"public_key,omitempty"` + Transport []string `json:"transport,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthn IdentityCredentialsWebAuthn + +// NewIdentityCredentialsWebAuthn instantiates a new IdentityCredentialsWebAuthn 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 NewIdentityCredentialsWebAuthn() *IdentityCredentialsWebAuthn { + this := IdentityCredentialsWebAuthn{} + return &this +} + +// NewIdentityCredentialsWebAuthnWithDefaults instantiates a new IdentityCredentialsWebAuthn 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 NewIdentityCredentialsWebAuthnWithDefaults() *IdentityCredentialsWebAuthn { + this := IdentityCredentialsWebAuthn{} + return &this +} + +// GetAddedAt returns the AddedAt field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAddedAt() time.Time { + if o == nil || IsNil(o.AddedAt) { + var ret time.Time + return ret + } + return *o.AddedAt +} + +// GetAddedAtOk returns a tuple with the AddedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAddedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.AddedAt) { + return nil, false + } + return o.AddedAt, true +} + +// HasAddedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAddedAt() bool { + if o != nil && !IsNil(o.AddedAt) { + return true + } + + return false +} + +// SetAddedAt gets a reference to the given time.Time and assigns it to the AddedAt field. +func (o *IdentityCredentialsWebAuthn) SetAddedAt(v time.Time) { + o.AddedAt = &v +} + +// GetAttestation returns the Attestation field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAttestation() IdentityCredentialsWebAuthnAttestation { + if o == nil || IsNil(o.Attestation) { + var ret IdentityCredentialsWebAuthnAttestation + return ret + } + return *o.Attestation +} + +// GetAttestationOk returns a tuple with the Attestation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAttestationOk() (*IdentityCredentialsWebAuthnAttestation, bool) { + if o == nil || IsNil(o.Attestation) { + return nil, false + } + return o.Attestation, true +} + +// HasAttestation returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAttestation() bool { + if o != nil && !IsNil(o.Attestation) { + return true + } + + return false +} + +// SetAttestation gets a reference to the given IdentityCredentialsWebAuthnAttestation and assigns it to the Attestation field. +func (o *IdentityCredentialsWebAuthn) SetAttestation(v IdentityCredentialsWebAuthnAttestation) { + o.Attestation = &v +} + +// GetAttestationType returns the AttestationType field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAttestationType() string { + if o == nil || IsNil(o.AttestationType) { + var ret string + return ret + } + return *o.AttestationType +} + +// GetAttestationTypeOk returns a tuple with the AttestationType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAttestationTypeOk() (*string, bool) { + if o == nil || IsNil(o.AttestationType) { + return nil, false + } + return o.AttestationType, true +} + +// HasAttestationType returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAttestationType() bool { + if o != nil && !IsNil(o.AttestationType) { + return true + } + + return false +} + +// SetAttestationType gets a reference to the given string and assigns it to the AttestationType field. +func (o *IdentityCredentialsWebAuthn) SetAttestationType(v string) { + o.AttestationType = &v +} + +// GetAuthenticator returns the Authenticator field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetAuthenticator() IdentityCredentialsWebAuthnAuthenticator { + if o == nil || IsNil(o.Authenticator) { + var ret IdentityCredentialsWebAuthnAuthenticator + return ret + } + return *o.Authenticator +} + +// GetAuthenticatorOk returns a tuple with the Authenticator field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetAuthenticatorOk() (*IdentityCredentialsWebAuthnAuthenticator, bool) { + if o == nil || IsNil(o.Authenticator) { + return nil, false + } + return o.Authenticator, true +} + +// HasAuthenticator returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasAuthenticator() bool { + if o != nil && !IsNil(o.Authenticator) { + return true + } + + return false +} + +// SetAuthenticator gets a reference to the given IdentityCredentialsWebAuthnAuthenticator and assigns it to the Authenticator field. +func (o *IdentityCredentialsWebAuthn) SetAuthenticator(v IdentityCredentialsWebAuthnAuthenticator) { + o.Authenticator = &v +} + +// GetDisplayName returns the DisplayName field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetDisplayName() string { + if o == nil || IsNil(o.DisplayName) { + var ret string + return ret + } + return *o.DisplayName +} + +// GetDisplayNameOk returns a tuple with the DisplayName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetDisplayNameOk() (*string, bool) { + if o == nil || IsNil(o.DisplayName) { + return nil, false + } + return o.DisplayName, true +} + +// HasDisplayName returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasDisplayName() bool { + if o != nil && !IsNil(o.DisplayName) { + return true + } + + return false +} + +// SetDisplayName gets a reference to the given string and assigns it to the DisplayName field. +func (o *IdentityCredentialsWebAuthn) SetDisplayName(v string) { + o.DisplayName = &v +} + +// GetFlags returns the Flags field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetFlags() IdentityCredentialsWebAuthnFlags { + if o == nil || IsNil(o.Flags) { + var ret IdentityCredentialsWebAuthnFlags + return ret + } + return *o.Flags +} + +// GetFlagsOk returns a tuple with the Flags field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetFlagsOk() (*IdentityCredentialsWebAuthnFlags, bool) { + if o == nil || IsNil(o.Flags) { + return nil, false + } + return o.Flags, true +} + +// HasFlags returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasFlags() bool { + if o != nil && !IsNil(o.Flags) { + return true + } + + return false +} + +// SetFlags gets a reference to the given IdentityCredentialsWebAuthnFlags and assigns it to the Flags field. +func (o *IdentityCredentialsWebAuthn) SetFlags(v IdentityCredentialsWebAuthnFlags) { + o.Flags = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetId() []int32 { + if o == nil || IsNil(o.Id) { + var ret []int32 + return ret + } + return o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetIdOk() ([]int32, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given []int32 and assigns it to the Id field. +func (o *IdentityCredentialsWebAuthn) SetId(v []int32) { + o.Id = v +} + +// GetIsPasswordless returns the IsPasswordless field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetIsPasswordless() bool { + if o == nil || IsNil(o.IsPasswordless) { + var ret bool + return ret + } + return *o.IsPasswordless +} + +// GetIsPasswordlessOk returns a tuple with the IsPasswordless field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetIsPasswordlessOk() (*bool, bool) { + if o == nil || IsNil(o.IsPasswordless) { + return nil, false + } + return o.IsPasswordless, true +} + +// HasIsPasswordless returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasIsPasswordless() bool { + if o != nil && !IsNil(o.IsPasswordless) { + return true + } + + return false +} + +// SetIsPasswordless gets a reference to the given bool and assigns it to the IsPasswordless field. +func (o *IdentityCredentialsWebAuthn) SetIsPasswordless(v bool) { + o.IsPasswordless = &v +} + +// GetPublicKey returns the PublicKey field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetPublicKey() []int32 { + if o == nil || IsNil(o.PublicKey) { + var ret []int32 + return ret + } + return o.PublicKey +} + +// GetPublicKeyOk returns a tuple with the PublicKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetPublicKeyOk() ([]int32, bool) { + if o == nil || IsNil(o.PublicKey) { + return nil, false + } + return o.PublicKey, true +} + +// HasPublicKey returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasPublicKey() bool { + if o != nil && !IsNil(o.PublicKey) { + return true + } + + return false +} + +// SetPublicKey gets a reference to the given []int32 and assigns it to the PublicKey field. +func (o *IdentityCredentialsWebAuthn) SetPublicKey(v []int32) { + o.PublicKey = v +} + +// GetTransport returns the Transport field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthn) GetTransport() []string { + if o == nil || IsNil(o.Transport) { + var ret []string + return ret + } + return o.Transport +} + +// GetTransportOk returns a tuple with the Transport field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthn) GetTransportOk() ([]string, bool) { + if o == nil || IsNil(o.Transport) { + return nil, false + } + return o.Transport, true +} + +// HasTransport returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthn) HasTransport() bool { + if o != nil && !IsNil(o.Transport) { + return true + } + + return false +} + +// SetTransport gets a reference to the given []string and assigns it to the Transport field. +func (o *IdentityCredentialsWebAuthn) SetTransport(v []string) { + o.Transport = v +} + +func (o IdentityCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthn) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AddedAt) { + toSerialize["added_at"] = o.AddedAt + } + if !IsNil(o.Attestation) { + toSerialize["attestation"] = o.Attestation + } + if !IsNil(o.AttestationType) { + toSerialize["attestation_type"] = o.AttestationType + } + if !IsNil(o.Authenticator) { + toSerialize["authenticator"] = o.Authenticator + } + if !IsNil(o.DisplayName) { + toSerialize["display_name"] = o.DisplayName + } + if !IsNil(o.Flags) { + toSerialize["flags"] = o.Flags + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.IsPasswordless) { + toSerialize["is_passwordless"] = o.IsPasswordless + } + if !IsNil(o.PublicKey) { + toSerialize["public_key"] = o.PublicKey + } + if !IsNil(o.Transport) { + toSerialize["transport"] = o.Transport + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthn) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthn := _IdentityCredentialsWebAuthn{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthn) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthn(varIdentityCredentialsWebAuthn) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "added_at") + delete(additionalProperties, "attestation") + delete(additionalProperties, "attestation_type") + delete(additionalProperties, "authenticator") + delete(additionalProperties, "display_name") + delete(additionalProperties, "flags") + delete(additionalProperties, "id") + delete(additionalProperties, "is_passwordless") + delete(additionalProperties, "public_key") + delete(additionalProperties, "transport") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthn struct { + value *IdentityCredentialsWebAuthn + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthn) Get() *IdentityCredentialsWebAuthn { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthn) Set(val *IdentityCredentialsWebAuthn) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthn) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthn) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthn(val *IdentityCredentialsWebAuthn) *NullableIdentityCredentialsWebAuthn { + return &NullableIdentityCredentialsWebAuthn{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthn) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_web_authn_attestation.go b/pkg/httpclient/model_identity_credentials_web_authn_attestation.go new file mode 100644 index 0000000..2aa83d0 --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_web_authn_attestation.go @@ -0,0 +1,302 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnAttestation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnAttestation{} + +// IdentityCredentialsWebAuthnAttestation struct for IdentityCredentialsWebAuthnAttestation +type IdentityCredentialsWebAuthnAttestation struct { + AuthenticatorData []int32 `json:"authenticator_data,omitempty"` + ClientDataJSON []int32 `json:"client_dataJSON,omitempty"` + ClientDataHash []int32 `json:"client_data_hash,omitempty"` + Object []int32 `json:"object,omitempty"` + PublicKeyAlgorithm *int64 `json:"public_key_algorithm,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnAttestation IdentityCredentialsWebAuthnAttestation + +// NewIdentityCredentialsWebAuthnAttestation instantiates a new IdentityCredentialsWebAuthnAttestation 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 NewIdentityCredentialsWebAuthnAttestation() *IdentityCredentialsWebAuthnAttestation { + this := IdentityCredentialsWebAuthnAttestation{} + return &this +} + +// NewIdentityCredentialsWebAuthnAttestationWithDefaults instantiates a new IdentityCredentialsWebAuthnAttestation 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 NewIdentityCredentialsWebAuthnAttestationWithDefaults() *IdentityCredentialsWebAuthnAttestation { + this := IdentityCredentialsWebAuthnAttestation{} + return &this +} + +// GetAuthenticatorData returns the AuthenticatorData field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetAuthenticatorData() []int32 { + if o == nil || IsNil(o.AuthenticatorData) { + var ret []int32 + return ret + } + return o.AuthenticatorData +} + +// GetAuthenticatorDataOk returns a tuple with the AuthenticatorData field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetAuthenticatorDataOk() ([]int32, bool) { + if o == nil || IsNil(o.AuthenticatorData) { + return nil, false + } + return o.AuthenticatorData, true +} + +// HasAuthenticatorData returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasAuthenticatorData() bool { + if o != nil && !IsNil(o.AuthenticatorData) { + return true + } + + return false +} + +// SetAuthenticatorData gets a reference to the given []int32 and assigns it to the AuthenticatorData field. +func (o *IdentityCredentialsWebAuthnAttestation) SetAuthenticatorData(v []int32) { + o.AuthenticatorData = v +} + +// GetClientDataJSON returns the ClientDataJSON field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataJSON() []int32 { + if o == nil || IsNil(o.ClientDataJSON) { + var ret []int32 + return ret + } + return o.ClientDataJSON +} + +// GetClientDataJSONOk returns a tuple with the ClientDataJSON field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataJSONOk() ([]int32, bool) { + if o == nil || IsNil(o.ClientDataJSON) { + return nil, false + } + return o.ClientDataJSON, true +} + +// HasClientDataJSON returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasClientDataJSON() bool { + if o != nil && !IsNil(o.ClientDataJSON) { + return true + } + + return false +} + +// SetClientDataJSON gets a reference to the given []int32 and assigns it to the ClientDataJSON field. +func (o *IdentityCredentialsWebAuthnAttestation) SetClientDataJSON(v []int32) { + o.ClientDataJSON = v +} + +// GetClientDataHash returns the ClientDataHash field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataHash() []int32 { + if o == nil || IsNil(o.ClientDataHash) { + var ret []int32 + return ret + } + return o.ClientDataHash +} + +// GetClientDataHashOk returns a tuple with the ClientDataHash field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetClientDataHashOk() ([]int32, bool) { + if o == nil || IsNil(o.ClientDataHash) { + return nil, false + } + return o.ClientDataHash, true +} + +// HasClientDataHash returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasClientDataHash() bool { + if o != nil && !IsNil(o.ClientDataHash) { + return true + } + + return false +} + +// SetClientDataHash gets a reference to the given []int32 and assigns it to the ClientDataHash field. +func (o *IdentityCredentialsWebAuthnAttestation) SetClientDataHash(v []int32) { + o.ClientDataHash = v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetObject() []int32 { + if o == nil || IsNil(o.Object) { + var ret []int32 + return ret + } + return o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetObjectOk() ([]int32, bool) { + if o == nil || IsNil(o.Object) { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasObject() bool { + if o != nil && !IsNil(o.Object) { + return true + } + + return false +} + +// SetObject gets a reference to the given []int32 and assigns it to the Object field. +func (o *IdentityCredentialsWebAuthnAttestation) SetObject(v []int32) { + o.Object = v +} + +// GetPublicKeyAlgorithm returns the PublicKeyAlgorithm field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAttestation) GetPublicKeyAlgorithm() int64 { + if o == nil || IsNil(o.PublicKeyAlgorithm) { + var ret int64 + return ret + } + return *o.PublicKeyAlgorithm +} + +// GetPublicKeyAlgorithmOk returns a tuple with the PublicKeyAlgorithm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAttestation) GetPublicKeyAlgorithmOk() (*int64, bool) { + if o == nil || IsNil(o.PublicKeyAlgorithm) { + return nil, false + } + return o.PublicKeyAlgorithm, true +} + +// HasPublicKeyAlgorithm returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAttestation) HasPublicKeyAlgorithm() bool { + if o != nil && !IsNil(o.PublicKeyAlgorithm) { + return true + } + + return false +} + +// SetPublicKeyAlgorithm gets a reference to the given int64 and assigns it to the PublicKeyAlgorithm field. +func (o *IdentityCredentialsWebAuthnAttestation) SetPublicKeyAlgorithm(v int64) { + o.PublicKeyAlgorithm = &v +} + +func (o IdentityCredentialsWebAuthnAttestation) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnAttestation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AuthenticatorData) { + toSerialize["authenticator_data"] = o.AuthenticatorData + } + if !IsNil(o.ClientDataJSON) { + toSerialize["client_dataJSON"] = o.ClientDataJSON + } + if !IsNil(o.ClientDataHash) { + toSerialize["client_data_hash"] = o.ClientDataHash + } + if !IsNil(o.Object) { + toSerialize["object"] = o.Object + } + if !IsNil(o.PublicKeyAlgorithm) { + toSerialize["public_key_algorithm"] = o.PublicKeyAlgorithm + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnAttestation) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnAttestation := _IdentityCredentialsWebAuthnAttestation{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnAttestation) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnAttestation(varIdentityCredentialsWebAuthnAttestation) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "authenticator_data") + delete(additionalProperties, "client_dataJSON") + delete(additionalProperties, "client_data_hash") + delete(additionalProperties, "object") + delete(additionalProperties, "public_key_algorithm") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnAttestation struct { + value *IdentityCredentialsWebAuthnAttestation + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) Get() *IdentityCredentialsWebAuthnAttestation { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) Set(val *IdentityCredentialsWebAuthnAttestation) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnAttestation(val *IdentityCredentialsWebAuthnAttestation) *NullableIdentityCredentialsWebAuthnAttestation { + return &NullableIdentityCredentialsWebAuthnAttestation{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnAttestation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnAttestation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_web_authn_authenticator.go b/pkg/httpclient/model_identity_credentials_web_authn_authenticator.go new file mode 100644 index 0000000..b7ee96f --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_web_authn_authenticator.go @@ -0,0 +1,228 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnAuthenticator type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnAuthenticator{} + +// IdentityCredentialsWebAuthnAuthenticator struct for IdentityCredentialsWebAuthnAuthenticator +type IdentityCredentialsWebAuthnAuthenticator struct { + Aaguid []int32 `json:"aaguid,omitempty"` + CloneWarning *bool `json:"clone_warning,omitempty"` + SignCount *int32 `json:"sign_count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnAuthenticator IdentityCredentialsWebAuthnAuthenticator + +// NewIdentityCredentialsWebAuthnAuthenticator instantiates a new IdentityCredentialsWebAuthnAuthenticator 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 NewIdentityCredentialsWebAuthnAuthenticator() *IdentityCredentialsWebAuthnAuthenticator { + this := IdentityCredentialsWebAuthnAuthenticator{} + return &this +} + +// NewIdentityCredentialsWebAuthnAuthenticatorWithDefaults instantiates a new IdentityCredentialsWebAuthnAuthenticator 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 NewIdentityCredentialsWebAuthnAuthenticatorWithDefaults() *IdentityCredentialsWebAuthnAuthenticator { + this := IdentityCredentialsWebAuthnAuthenticator{} + return &this +} + +// GetAaguid returns the Aaguid field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetAaguid() []int32 { + if o == nil || IsNil(o.Aaguid) { + var ret []int32 + return ret + } + return o.Aaguid +} + +// GetAaguidOk returns a tuple with the Aaguid field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetAaguidOk() ([]int32, bool) { + if o == nil || IsNil(o.Aaguid) { + return nil, false + } + return o.Aaguid, true +} + +// HasAaguid returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasAaguid() bool { + if o != nil && !IsNil(o.Aaguid) { + return true + } + + return false +} + +// SetAaguid gets a reference to the given []int32 and assigns it to the Aaguid field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetAaguid(v []int32) { + o.Aaguid = v +} + +// GetCloneWarning returns the CloneWarning field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetCloneWarning() bool { + if o == nil || IsNil(o.CloneWarning) { + var ret bool + return ret + } + return *o.CloneWarning +} + +// GetCloneWarningOk returns a tuple with the CloneWarning field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetCloneWarningOk() (*bool, bool) { + if o == nil || IsNil(o.CloneWarning) { + return nil, false + } + return o.CloneWarning, true +} + +// HasCloneWarning returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasCloneWarning() bool { + if o != nil && !IsNil(o.CloneWarning) { + return true + } + + return false +} + +// SetCloneWarning gets a reference to the given bool and assigns it to the CloneWarning field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetCloneWarning(v bool) { + o.CloneWarning = &v +} + +// GetSignCount returns the SignCount field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetSignCount() int32 { + if o == nil || IsNil(o.SignCount) { + var ret int32 + return ret + } + return *o.SignCount +} + +// GetSignCountOk returns a tuple with the SignCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) GetSignCountOk() (*int32, bool) { + if o == nil || IsNil(o.SignCount) { + return nil, false + } + return o.SignCount, true +} + +// HasSignCount returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnAuthenticator) HasSignCount() bool { + if o != nil && !IsNil(o.SignCount) { + return true + } + + return false +} + +// SetSignCount gets a reference to the given int32 and assigns it to the SignCount field. +func (o *IdentityCredentialsWebAuthnAuthenticator) SetSignCount(v int32) { + o.SignCount = &v +} + +func (o IdentityCredentialsWebAuthnAuthenticator) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnAuthenticator) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Aaguid) { + toSerialize["aaguid"] = o.Aaguid + } + if !IsNil(o.CloneWarning) { + toSerialize["clone_warning"] = o.CloneWarning + } + if !IsNil(o.SignCount) { + toSerialize["sign_count"] = o.SignCount + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnAuthenticator) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnAuthenticator := _IdentityCredentialsWebAuthnAuthenticator{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnAuthenticator) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnAuthenticator(varIdentityCredentialsWebAuthnAuthenticator) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "aaguid") + delete(additionalProperties, "clone_warning") + delete(additionalProperties, "sign_count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnAuthenticator struct { + value *IdentityCredentialsWebAuthnAuthenticator + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) Get() *IdentityCredentialsWebAuthnAuthenticator { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) Set(val *IdentityCredentialsWebAuthnAuthenticator) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnAuthenticator(val *IdentityCredentialsWebAuthnAuthenticator) *NullableIdentityCredentialsWebAuthnAuthenticator { + return &NullableIdentityCredentialsWebAuthnAuthenticator{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnAuthenticator) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnAuthenticator) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_credentials_web_authn_flags.go b/pkg/httpclient/model_identity_credentials_web_authn_flags.go new file mode 100644 index 0000000..3100f0a --- /dev/null +++ b/pkg/httpclient/model_identity_credentials_web_authn_flags.go @@ -0,0 +1,265 @@ +/* +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" +) + +// checks if the IdentityCredentialsWebAuthnFlags type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityCredentialsWebAuthnFlags{} + +// IdentityCredentialsWebAuthnFlags struct for IdentityCredentialsWebAuthnFlags +type IdentityCredentialsWebAuthnFlags struct { + BackupEligible *bool `json:"backup_eligible,omitempty"` + BackupState *bool `json:"backup_state,omitempty"` + UserPresent *bool `json:"user_present,omitempty"` + UserVerified *bool `json:"user_verified,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityCredentialsWebAuthnFlags IdentityCredentialsWebAuthnFlags + +// NewIdentityCredentialsWebAuthnFlags instantiates a new IdentityCredentialsWebAuthnFlags 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 NewIdentityCredentialsWebAuthnFlags() *IdentityCredentialsWebAuthnFlags { + this := IdentityCredentialsWebAuthnFlags{} + return &this +} + +// NewIdentityCredentialsWebAuthnFlagsWithDefaults instantiates a new IdentityCredentialsWebAuthnFlags 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 NewIdentityCredentialsWebAuthnFlagsWithDefaults() *IdentityCredentialsWebAuthnFlags { + this := IdentityCredentialsWebAuthnFlags{} + return &this +} + +// GetBackupEligible returns the BackupEligible field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupEligible() bool { + if o == nil || IsNil(o.BackupEligible) { + var ret bool + return ret + } + return *o.BackupEligible +} + +// GetBackupEligibleOk returns a tuple with the BackupEligible field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupEligibleOk() (*bool, bool) { + if o == nil || IsNil(o.BackupEligible) { + return nil, false + } + return o.BackupEligible, true +} + +// HasBackupEligible returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasBackupEligible() bool { + if o != nil && !IsNil(o.BackupEligible) { + return true + } + + return false +} + +// SetBackupEligible gets a reference to the given bool and assigns it to the BackupEligible field. +func (o *IdentityCredentialsWebAuthnFlags) SetBackupEligible(v bool) { + o.BackupEligible = &v +} + +// GetBackupState returns the BackupState field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupState() bool { + if o == nil || IsNil(o.BackupState) { + var ret bool + return ret + } + return *o.BackupState +} + +// GetBackupStateOk returns a tuple with the BackupState field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetBackupStateOk() (*bool, bool) { + if o == nil || IsNil(o.BackupState) { + return nil, false + } + return o.BackupState, true +} + +// HasBackupState returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasBackupState() bool { + if o != nil && !IsNil(o.BackupState) { + return true + } + + return false +} + +// SetBackupState gets a reference to the given bool and assigns it to the BackupState field. +func (o *IdentityCredentialsWebAuthnFlags) SetBackupState(v bool) { + o.BackupState = &v +} + +// GetUserPresent returns the UserPresent field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetUserPresent() bool { + if o == nil || IsNil(o.UserPresent) { + var ret bool + return ret + } + return *o.UserPresent +} + +// GetUserPresentOk returns a tuple with the UserPresent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetUserPresentOk() (*bool, bool) { + if o == nil || IsNil(o.UserPresent) { + return nil, false + } + return o.UserPresent, true +} + +// HasUserPresent returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasUserPresent() bool { + if o != nil && !IsNil(o.UserPresent) { + return true + } + + return false +} + +// SetUserPresent gets a reference to the given bool and assigns it to the UserPresent field. +func (o *IdentityCredentialsWebAuthnFlags) SetUserPresent(v bool) { + o.UserPresent = &v +} + +// GetUserVerified returns the UserVerified field value if set, zero value otherwise. +func (o *IdentityCredentialsWebAuthnFlags) GetUserVerified() bool { + if o == nil || IsNil(o.UserVerified) { + var ret bool + return ret + } + return *o.UserVerified +} + +// GetUserVerifiedOk returns a tuple with the UserVerified field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsWebAuthnFlags) GetUserVerifiedOk() (*bool, bool) { + if o == nil || IsNil(o.UserVerified) { + return nil, false + } + return o.UserVerified, true +} + +// HasUserVerified returns a boolean if a field has been set. +func (o *IdentityCredentialsWebAuthnFlags) HasUserVerified() bool { + if o != nil && !IsNil(o.UserVerified) { + return true + } + + return false +} + +// SetUserVerified gets a reference to the given bool and assigns it to the UserVerified field. +func (o *IdentityCredentialsWebAuthnFlags) SetUserVerified(v bool) { + o.UserVerified = &v +} + +func (o IdentityCredentialsWebAuthnFlags) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityCredentialsWebAuthnFlags) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.BackupEligible) { + toSerialize["backup_eligible"] = o.BackupEligible + } + if !IsNil(o.BackupState) { + toSerialize["backup_state"] = o.BackupState + } + if !IsNil(o.UserPresent) { + toSerialize["user_present"] = o.UserPresent + } + if !IsNil(o.UserVerified) { + toSerialize["user_verified"] = o.UserVerified + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityCredentialsWebAuthnFlags) UnmarshalJSON(data []byte) (err error) { + varIdentityCredentialsWebAuthnFlags := _IdentityCredentialsWebAuthnFlags{} + + err = json.Unmarshal(data, &varIdentityCredentialsWebAuthnFlags) + + if err != nil { + return err + } + + *o = IdentityCredentialsWebAuthnFlags(varIdentityCredentialsWebAuthnFlags) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "backup_eligible") + delete(additionalProperties, "backup_state") + delete(additionalProperties, "user_present") + delete(additionalProperties, "user_verified") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityCredentialsWebAuthnFlags struct { + value *IdentityCredentialsWebAuthnFlags + isSet bool +} + +func (v NullableIdentityCredentialsWebAuthnFlags) Get() *IdentityCredentialsWebAuthnFlags { + return v.value +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) Set(val *IdentityCredentialsWebAuthnFlags) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsWebAuthnFlags) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsWebAuthnFlags(val *IdentityCredentialsWebAuthnFlags) *NullableIdentityCredentialsWebAuthnFlags { + return &NullableIdentityCredentialsWebAuthnFlags{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsWebAuthnFlags) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsWebAuthnFlags) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_patch.go b/pkg/httpclient/model_identity_patch.go new file mode 100644 index 0000000..1dc2eb4 --- /dev/null +++ b/pkg/httpclient/model_identity_patch.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityPatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityPatch{} + +// IdentityPatch Payload for patching an identity +type IdentityPatch struct { + Create *CreateIdentityBody `json:"create,omitempty"` + // The ID of this patch. The patch ID is optional. If specified, the ID will be returned in the response, so consumers of this API can correlate the response with the patch. + PatchId *string `json:"patch_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityPatch IdentityPatch + +// NewIdentityPatch instantiates a new IdentityPatch 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 NewIdentityPatch() *IdentityPatch { + this := IdentityPatch{} + return &this +} + +// NewIdentityPatchWithDefaults instantiates a new IdentityPatch 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 NewIdentityPatchWithDefaults() *IdentityPatch { + this := IdentityPatch{} + return &this +} + +// GetCreate returns the Create field value if set, zero value otherwise. +func (o *IdentityPatch) GetCreate() CreateIdentityBody { + if o == nil || IsNil(o.Create) { + var ret CreateIdentityBody + return ret + } + return *o.Create +} + +// GetCreateOk returns a tuple with the Create field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatch) GetCreateOk() (*CreateIdentityBody, bool) { + if o == nil || IsNil(o.Create) { + return nil, false + } + return o.Create, true +} + +// HasCreate returns a boolean if a field has been set. +func (o *IdentityPatch) HasCreate() bool { + if o != nil && !IsNil(o.Create) { + return true + } + + return false +} + +// SetCreate gets a reference to the given CreateIdentityBody and assigns it to the Create field. +func (o *IdentityPatch) SetCreate(v CreateIdentityBody) { + o.Create = &v +} + +// GetPatchId returns the PatchId field value if set, zero value otherwise. +func (o *IdentityPatch) GetPatchId() string { + if o == nil || IsNil(o.PatchId) { + var ret string + return ret + } + return *o.PatchId +} + +// GetPatchIdOk returns a tuple with the PatchId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatch) GetPatchIdOk() (*string, bool) { + if o == nil || IsNil(o.PatchId) { + return nil, false + } + return o.PatchId, true +} + +// HasPatchId returns a boolean if a field has been set. +func (o *IdentityPatch) HasPatchId() bool { + if o != nil && !IsNil(o.PatchId) { + return true + } + + return false +} + +// SetPatchId gets a reference to the given string and assigns it to the PatchId field. +func (o *IdentityPatch) SetPatchId(v string) { + o.PatchId = &v +} + +func (o IdentityPatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityPatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Create) { + toSerialize["create"] = o.Create + } + if !IsNil(o.PatchId) { + toSerialize["patch_id"] = o.PatchId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityPatch) UnmarshalJSON(data []byte) (err error) { + varIdentityPatch := _IdentityPatch{} + + err = json.Unmarshal(data, &varIdentityPatch) + + if err != nil { + return err + } + + *o = IdentityPatch(varIdentityPatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "create") + delete(additionalProperties, "patch_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityPatch struct { + value *IdentityPatch + isSet bool +} + +func (v NullableIdentityPatch) Get() *IdentityPatch { + return v.value +} + +func (v *NullableIdentityPatch) Set(val *IdentityPatch) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityPatch) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityPatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityPatch(val *IdentityPatch) *NullableIdentityPatch { + return &NullableIdentityPatch{value: val, isSet: true} +} + +func (v NullableIdentityPatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityPatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_patch_response.go b/pkg/httpclient/model_identity_patch_response.go new file mode 100644 index 0000000..4ad5104 --- /dev/null +++ b/pkg/httpclient/model_identity_patch_response.go @@ -0,0 +1,270 @@ +/* +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" +) + +// checks if the IdentityPatchResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityPatchResponse{} + +// IdentityPatchResponse Response for a single identity patch +type IdentityPatchResponse struct { + // The action for this specific patch create ActionCreate Create this identity. error ActionError Error indicates that the patch failed. + Action *string `json:"action,omitempty"` + // From https://go.dev/wiki/CodeReviewComments#receiver-type: > Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer. > If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, > prefer a pointer receiver, as it will make the intention clearer to the reader. > Don’t mix receiver types. Choose either pointers or struct types for all available methods. + 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. + PatchId *string `json:"patch_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityPatchResponse IdentityPatchResponse + +// NewIdentityPatchResponse instantiates a new IdentityPatchResponse 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 NewIdentityPatchResponse() *IdentityPatchResponse { + this := IdentityPatchResponse{} + return &this +} + +// NewIdentityPatchResponseWithDefaults instantiates a new IdentityPatchResponse 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 NewIdentityPatchResponseWithDefaults() *IdentityPatchResponse { + this := IdentityPatchResponse{} + return &this +} + +// GetAction returns the Action field value if set, zero value otherwise. +func (o *IdentityPatchResponse) GetAction() string { + if o == nil || IsNil(o.Action) { + var ret string + return ret + } + return *o.Action +} + +// GetActionOk returns a tuple with the Action field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetActionOk() (*string, bool) { + if o == nil || IsNil(o.Action) { + return nil, false + } + return o.Action, true +} + +// HasAction returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasAction() bool { + if o != nil && !IsNil(o.Action) { + return true + } + + return false +} + +// SetAction gets a reference to the given string and assigns it to the Action field. +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 || IsNil(o.Error) { + 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 && !IsNil(o.Error) { + 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 || IsNil(o.Identity) { + var ret string + return ret + } + return *o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetIdentityOk() (*string, bool) { + if o == nil || IsNil(o.Identity) { + return nil, false + } + return o.Identity, true +} + +// HasIdentity returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasIdentity() bool { + if o != nil && !IsNil(o.Identity) { + return true + } + + return false +} + +// SetIdentity gets a reference to the given string and assigns it to the Identity field. +func (o *IdentityPatchResponse) SetIdentity(v string) { + o.Identity = &v +} + +// GetPatchId returns the PatchId field value if set, zero value otherwise. +func (o *IdentityPatchResponse) GetPatchId() string { + if o == nil || IsNil(o.PatchId) { + var ret string + return ret + } + return *o.PatchId +} + +// GetPatchIdOk returns a tuple with the PatchId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityPatchResponse) GetPatchIdOk() (*string, bool) { + if o == nil || IsNil(o.PatchId) { + return nil, false + } + return o.PatchId, true +} + +// HasPatchId returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasPatchId() bool { + if o != nil && !IsNil(o.PatchId) { + return true + } + + return false +} + +// SetPatchId gets a reference to the given string and assigns it to the PatchId field. +func (o *IdentityPatchResponse) SetPatchId(v string) { + o.PatchId = &v +} + +func (o IdentityPatchResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityPatchResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Action) { + toSerialize["action"] = o.Action + } + if o.Error != nil { + toSerialize["error"] = o.Error + } + if !IsNil(o.Identity) { + toSerialize["identity"] = o.Identity + } + if !IsNil(o.PatchId) { + toSerialize["patch_id"] = o.PatchId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityPatchResponse) UnmarshalJSON(data []byte) (err error) { + varIdentityPatchResponse := _IdentityPatchResponse{} + + err = json.Unmarshal(data, &varIdentityPatchResponse) + + if err != nil { + return err + } + + *o = IdentityPatchResponse(varIdentityPatchResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "error") + delete(additionalProperties, "identity") + delete(additionalProperties, "patch_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityPatchResponse struct { + value *IdentityPatchResponse + isSet bool +} + +func (v NullableIdentityPatchResponse) Get() *IdentityPatchResponse { + return v.value +} + +func (v *NullableIdentityPatchResponse) Set(val *IdentityPatchResponse) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityPatchResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityPatchResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityPatchResponse(val *IdentityPatchResponse) *NullableIdentityPatchResponse { + return &NullableIdentityPatchResponse{value: val, isSet: true} +} + +func (v NullableIdentityPatchResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityPatchResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_schema_container.go b/pkg/httpclient/model_identity_schema_container.go new file mode 100644 index 0000000..b3e4cf3 --- /dev/null +++ b/pkg/httpclient/model_identity_schema_container.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the IdentitySchemaContainer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentitySchemaContainer{} + +// IdentitySchemaContainer An Identity JSON Schema Container +type IdentitySchemaContainer struct { + // The ID of the Identity JSON Schema + Id string `json:"id"` + // The actual Identity JSON Schema + Schema map[string]interface{} `json:"schema"` + AdditionalProperties map[string]interface{} +} + +type _IdentitySchemaContainer IdentitySchemaContainer + +// NewIdentitySchemaContainer instantiates a new IdentitySchemaContainer 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 NewIdentitySchemaContainer(id string, schema map[string]interface{}) *IdentitySchemaContainer { + this := IdentitySchemaContainer{} + this.Id = id + this.Schema = schema + return &this +} + +// NewIdentitySchemaContainerWithDefaults instantiates a new IdentitySchemaContainer 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 NewIdentitySchemaContainerWithDefaults() *IdentitySchemaContainer { + this := IdentitySchemaContainer{} + return &this +} + +// GetId returns the Id field value +func (o *IdentitySchemaContainer) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *IdentitySchemaContainer) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *IdentitySchemaContainer) SetId(v string) { + o.Id = v +} + +// GetSchema returns the Schema field value +func (o *IdentitySchemaContainer) GetSchema() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Schema +} + +// GetSchemaOk returns a tuple with the Schema field value +// and a boolean to check if the value has been set. +func (o *IdentitySchemaContainer) GetSchemaOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Schema, true +} + +// SetSchema sets field value +func (o *IdentitySchemaContainer) SetSchema(v map[string]interface{}) { + o.Schema = v +} + +func (o IdentitySchemaContainer) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentitySchemaContainer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["schema"] = o.Schema + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentitySchemaContainer) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "schema", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentitySchemaContainer := _IdentitySchemaContainer{} + + err = json.Unmarshal(data, &varIdentitySchemaContainer) + + if err != nil { + return err + } + + *o = IdentitySchemaContainer(varIdentitySchemaContainer) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "schema") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentitySchemaContainer struct { + value *IdentitySchemaContainer + isSet bool +} + +func (v NullableIdentitySchemaContainer) Get() *IdentitySchemaContainer { + return v.value +} + +func (v *NullableIdentitySchemaContainer) Set(val *IdentitySchemaContainer) { + v.value = val + v.isSet = true +} + +func (v NullableIdentitySchemaContainer) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentitySchemaContainer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentitySchemaContainer(val *IdentitySchemaContainer) *NullableIdentitySchemaContainer { + return &NullableIdentitySchemaContainer{value: val, isSet: true} +} + +func (v NullableIdentitySchemaContainer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentitySchemaContainer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials.go b/pkg/httpclient/model_identity_with_credentials.go new file mode 100644 index 0000000..9d188ef --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials.go @@ -0,0 +1,376 @@ +/* +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" +) + +// checks if the IdentityWithCredentials type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentials{} + +// IdentityWithCredentials Create Identity and Import Credentials +type IdentityWithCredentials struct { + LookupSecret *AdminIdentityImportCredentialsLookupSecret `json:"lookup_secret,omitempty"` + Oidc *IdentityWithCredentialsOidc `json:"oidc,omitempty"` + Passkey *IdentityWithCredentialsPasskey `json:"passkey,omitempty"` + Password *IdentityWithCredentialsPassword `json:"password,omitempty"` + Saml *IdentityWithCredentialsSaml `json:"saml,omitempty"` + Totp *IdentityWithCredentialsTotp `json:"totp,omitempty"` + Webauthn *IdentityWithCredentialsWebAuthn `json:"webauthn,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentials IdentityWithCredentials + +// NewIdentityWithCredentials instantiates a new IdentityWithCredentials 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 NewIdentityWithCredentials() *IdentityWithCredentials { + this := IdentityWithCredentials{} + return &this +} + +// NewIdentityWithCredentialsWithDefaults instantiates a new IdentityWithCredentials 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 NewIdentityWithCredentialsWithDefaults() *IdentityWithCredentials { + this := IdentityWithCredentials{} + return &this +} + +// GetLookupSecret returns the LookupSecret field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetLookupSecret() AdminIdentityImportCredentialsLookupSecret { + if o == nil || IsNil(o.LookupSecret) { + var ret AdminIdentityImportCredentialsLookupSecret + return ret + } + return *o.LookupSecret +} + +// GetLookupSecretOk returns a tuple with the LookupSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetLookupSecretOk() (*AdminIdentityImportCredentialsLookupSecret, bool) { + if o == nil || IsNil(o.LookupSecret) { + return nil, false + } + return o.LookupSecret, true +} + +// HasLookupSecret returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasLookupSecret() bool { + if o != nil && !IsNil(o.LookupSecret) { + return true + } + + return false +} + +// SetLookupSecret gets a reference to the given AdminIdentityImportCredentialsLookupSecret and assigns it to the LookupSecret field. +func (o *IdentityWithCredentials) SetLookupSecret(v AdminIdentityImportCredentialsLookupSecret) { + o.LookupSecret = &v +} + +// GetOidc returns the Oidc field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetOidc() IdentityWithCredentialsOidc { + if o == nil || IsNil(o.Oidc) { + var ret IdentityWithCredentialsOidc + return ret + } + return *o.Oidc +} + +// GetOidcOk returns a tuple with the Oidc field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetOidcOk() (*IdentityWithCredentialsOidc, bool) { + if o == nil || IsNil(o.Oidc) { + return nil, false + } + return o.Oidc, true +} + +// HasOidc returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasOidc() bool { + if o != nil && !IsNil(o.Oidc) { + return true + } + + return false +} + +// SetOidc gets a reference to the given IdentityWithCredentialsOidc and assigns it to the Oidc field. +func (o *IdentityWithCredentials) SetOidc(v IdentityWithCredentialsOidc) { + o.Oidc = &v +} + +// GetPasskey returns the Passkey field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetPasskey() IdentityWithCredentialsPasskey { + if o == nil || IsNil(o.Passkey) { + var ret IdentityWithCredentialsPasskey + return ret + } + return *o.Passkey +} + +// GetPasskeyOk returns a tuple with the Passkey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetPasskeyOk() (*IdentityWithCredentialsPasskey, bool) { + if o == nil || IsNil(o.Passkey) { + return nil, false + } + return o.Passkey, true +} + +// HasPasskey returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasPasskey() bool { + if o != nil && !IsNil(o.Passkey) { + return true + } + + return false +} + +// SetPasskey gets a reference to the given IdentityWithCredentialsPasskey and assigns it to the Passkey field. +func (o *IdentityWithCredentials) SetPasskey(v IdentityWithCredentialsPasskey) { + o.Passkey = &v +} + +// GetPassword returns the Password field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetPassword() IdentityWithCredentialsPassword { + if o == nil || IsNil(o.Password) { + var ret IdentityWithCredentialsPassword + return ret + } + return *o.Password +} + +// GetPasswordOk returns a tuple with the Password field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetPasswordOk() (*IdentityWithCredentialsPassword, bool) { + if o == nil || IsNil(o.Password) { + return nil, false + } + return o.Password, true +} + +// HasPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasPassword() bool { + if o != nil && !IsNil(o.Password) { + return true + } + + return false +} + +// SetPassword gets a reference to the given IdentityWithCredentialsPassword and assigns it to the Password field. +func (o *IdentityWithCredentials) SetPassword(v IdentityWithCredentialsPassword) { + o.Password = &v +} + +// GetSaml returns the Saml field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetSaml() IdentityWithCredentialsSaml { + if o == nil || IsNil(o.Saml) { + var ret IdentityWithCredentialsSaml + return ret + } + return *o.Saml +} + +// GetSamlOk returns a tuple with the Saml field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetSamlOk() (*IdentityWithCredentialsSaml, bool) { + if o == nil || IsNil(o.Saml) { + return nil, false + } + return o.Saml, true +} + +// HasSaml returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasSaml() bool { + if o != nil && !IsNil(o.Saml) { + return true + } + + return false +} + +// SetSaml gets a reference to the given IdentityWithCredentialsSaml and assigns it to the Saml field. +func (o *IdentityWithCredentials) SetSaml(v IdentityWithCredentialsSaml) { + o.Saml = &v +} + +// GetTotp returns the Totp field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetTotp() IdentityWithCredentialsTotp { + if o == nil || IsNil(o.Totp) { + var ret IdentityWithCredentialsTotp + return ret + } + return *o.Totp +} + +// GetTotpOk returns a tuple with the Totp field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetTotpOk() (*IdentityWithCredentialsTotp, bool) { + if o == nil || IsNil(o.Totp) { + return nil, false + } + return o.Totp, true +} + +// HasTotp returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasTotp() bool { + if o != nil && !IsNil(o.Totp) { + return true + } + + return false +} + +// SetTotp gets a reference to the given IdentityWithCredentialsTotp and assigns it to the Totp field. +func (o *IdentityWithCredentials) SetTotp(v IdentityWithCredentialsTotp) { + o.Totp = &v +} + +// GetWebauthn returns the Webauthn field value if set, zero value otherwise. +func (o *IdentityWithCredentials) GetWebauthn() IdentityWithCredentialsWebAuthn { + if o == nil || IsNil(o.Webauthn) { + var ret IdentityWithCredentialsWebAuthn + return ret + } + return *o.Webauthn +} + +// GetWebauthnOk returns a tuple with the Webauthn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentials) GetWebauthnOk() (*IdentityWithCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Webauthn) { + return nil, false + } + return o.Webauthn, true +} + +// HasWebauthn returns a boolean if a field has been set. +func (o *IdentityWithCredentials) HasWebauthn() bool { + if o != nil && !IsNil(o.Webauthn) { + return true + } + + return false +} + +// SetWebauthn gets a reference to the given IdentityWithCredentialsWebAuthn and assigns it to the Webauthn field. +func (o *IdentityWithCredentials) SetWebauthn(v IdentityWithCredentialsWebAuthn) { + o.Webauthn = &v +} + +func (o IdentityWithCredentials) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentials) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.LookupSecret) { + toSerialize["lookup_secret"] = o.LookupSecret + } + if !IsNil(o.Oidc) { + toSerialize["oidc"] = o.Oidc + } + if !IsNil(o.Passkey) { + toSerialize["passkey"] = o.Passkey + } + if !IsNil(o.Password) { + toSerialize["password"] = o.Password + } + if !IsNil(o.Saml) { + toSerialize["saml"] = o.Saml + } + if !IsNil(o.Totp) { + toSerialize["totp"] = o.Totp + } + if !IsNil(o.Webauthn) { + toSerialize["webauthn"] = o.Webauthn + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentials) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentials := _IdentityWithCredentials{} + + err = json.Unmarshal(data, &varIdentityWithCredentials) + + if err != nil { + return err + } + + *o = IdentityWithCredentials(varIdentityWithCredentials) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "lookup_secret") + delete(additionalProperties, "oidc") + delete(additionalProperties, "passkey") + delete(additionalProperties, "password") + delete(additionalProperties, "saml") + delete(additionalProperties, "totp") + delete(additionalProperties, "webauthn") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentials struct { + value *IdentityWithCredentials + isSet bool +} + +func (v NullableIdentityWithCredentials) Get() *IdentityWithCredentials { + return v.value +} + +func (v *NullableIdentityWithCredentials) Set(val *IdentityWithCredentials) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentials) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentials) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentials(val *IdentityWithCredentials) *NullableIdentityWithCredentials { + return &NullableIdentityWithCredentials{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentials) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentials) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_oidc.go b/pkg/httpclient/model_identity_with_credentials_oidc.go new file mode 100644 index 0000000..307b9ee --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_oidc.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsOidc type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidc{} + +// IdentityWithCredentialsOidc Create Identity and Import Social Sign In Credentials +type IdentityWithCredentialsOidc struct { + Config *IdentityWithCredentialsOidcConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidc IdentityWithCredentialsOidc + +// NewIdentityWithCredentialsOidc instantiates a new IdentityWithCredentialsOidc 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 NewIdentityWithCredentialsOidc() *IdentityWithCredentialsOidc { + this := IdentityWithCredentialsOidc{} + return &this +} + +// NewIdentityWithCredentialsOidcWithDefaults instantiates a new IdentityWithCredentialsOidc 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 NewIdentityWithCredentialsOidcWithDefaults() *IdentityWithCredentialsOidc { + this := IdentityWithCredentialsOidc{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidc) GetConfig() IdentityWithCredentialsOidcConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsOidcConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidc) GetConfigOk() (*IdentityWithCredentialsOidcConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidc) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsOidcConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsOidc) SetConfig(v IdentityWithCredentialsOidcConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsOidc) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidc) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidc) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsOidc := _IdentityWithCredentialsOidc{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidc) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidc(varIdentityWithCredentialsOidc) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidc struct { + value *IdentityWithCredentialsOidc + isSet bool +} + +func (v NullableIdentityWithCredentialsOidc) Get() *IdentityWithCredentialsOidc { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidc) Set(val *IdentityWithCredentialsOidc) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidc) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidc) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidc(val *IdentityWithCredentialsOidc) *NullableIdentityWithCredentialsOidc { + return &NullableIdentityWithCredentialsOidc{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidc) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidc) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_oidc_config.go b/pkg/httpclient/model_identity_with_credentials_oidc_config.go new file mode 100644 index 0000000..ffe6f89 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_oidc_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsOidcConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidcConfig{} + +// IdentityWithCredentialsOidcConfig struct for IdentityWithCredentialsOidcConfig +type IdentityWithCredentialsOidcConfig struct { + // A list of OpenID Connect Providers + Providers []IdentityWithCredentialsOidcConfigProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidcConfig IdentityWithCredentialsOidcConfig + +// NewIdentityWithCredentialsOidcConfig instantiates a new IdentityWithCredentialsOidcConfig 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 NewIdentityWithCredentialsOidcConfig() *IdentityWithCredentialsOidcConfig { + this := IdentityWithCredentialsOidcConfig{} + return &this +} + +// NewIdentityWithCredentialsOidcConfigWithDefaults instantiates a new IdentityWithCredentialsOidcConfig 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 NewIdentityWithCredentialsOidcConfigWithDefaults() *IdentityWithCredentialsOidcConfig { + this := IdentityWithCredentialsOidcConfig{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidcConfig) GetProviders() []IdentityWithCredentialsOidcConfigProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityWithCredentialsOidcConfigProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfig) GetProvidersOk() ([]IdentityWithCredentialsOidcConfigProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfig) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityWithCredentialsOidcConfigProvider and assigns it to the Providers field. +func (o *IdentityWithCredentialsOidcConfig) SetProviders(v []IdentityWithCredentialsOidcConfigProvider) { + o.Providers = v +} + +func (o IdentityWithCredentialsOidcConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidcConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidcConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsOidcConfig := _IdentityWithCredentialsOidcConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidcConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidcConfig(varIdentityWithCredentialsOidcConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidcConfig struct { + value *IdentityWithCredentialsOidcConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsOidcConfig) Get() *IdentityWithCredentialsOidcConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidcConfig) Set(val *IdentityWithCredentialsOidcConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidcConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidcConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidcConfig(val *IdentityWithCredentialsOidcConfig) *NullableIdentityWithCredentialsOidcConfig { + return &NullableIdentityWithCredentialsOidcConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidcConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidcConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_oidc_config_provider.go b/pkg/httpclient/model_identity_with_credentials_oidc_config_provider.go new file mode 100644 index 0000000..ec9f565 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_oidc_config_provider.go @@ -0,0 +1,284 @@ +/* +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" + "fmt" +) + +// checks if the IdentityWithCredentialsOidcConfigProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsOidcConfigProvider{} + +// IdentityWithCredentialsOidcConfigProvider Create Identity and Import Social Sign In Credentials Configuration +type IdentityWithCredentialsOidcConfigProvider struct { + Organization NullableString `json:"organization,omitempty"` + // The OpenID Connect provider to link the subject to. Usually something like `google` or `github`. + Provider string `json:"provider"` + // The subject (`sub`) of the OpenID Connect connection. Usually the `sub` field of the ID Token. + Subject string `json:"subject"` + // If set, this credential allows the user to sign in using the OpenID Connect provider without setting the subject first. + UseAutoLink *bool `json:"use_auto_link,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsOidcConfigProvider IdentityWithCredentialsOidcConfigProvider + +// NewIdentityWithCredentialsOidcConfigProvider instantiates a new IdentityWithCredentialsOidcConfigProvider 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 NewIdentityWithCredentialsOidcConfigProvider(provider string, subject string) *IdentityWithCredentialsOidcConfigProvider { + this := IdentityWithCredentialsOidcConfigProvider{} + this.Provider = provider + this.Subject = subject + return &this +} + +// NewIdentityWithCredentialsOidcConfigProviderWithDefaults instantiates a new IdentityWithCredentialsOidcConfigProvider 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 NewIdentityWithCredentialsOidcConfigProviderWithDefaults() *IdentityWithCredentialsOidcConfigProvider { + this := IdentityWithCredentialsOidcConfigProvider{} + return &this +} + +// GetOrganization returns the Organization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityWithCredentialsOidcConfigProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization.Get()) { + var ret string + return ret + } + return *o.Organization.Get() +} + +// GetOrganizationOk returns a tuple with the Organization 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 *IdentityWithCredentialsOidcConfigProvider) GetOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Organization.Get(), o.Organization.IsSet() +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) HasOrganization() bool { + if o != nil && o.Organization.IsSet() { + return true + } + + return false +} + +// SetOrganization gets a reference to the given NullableString and assigns it to the Organization field. +func (o *IdentityWithCredentialsOidcConfigProvider) SetOrganization(v string) { + o.Organization.Set(&v) +} + +// SetOrganizationNil sets the value for Organization to be an explicit nil +func (o *IdentityWithCredentialsOidcConfigProvider) SetOrganizationNil() { + o.Organization.Set(nil) +} + +// UnsetOrganization ensures that no value is present for Organization, not even an explicit nil +func (o *IdentityWithCredentialsOidcConfigProvider) UnsetOrganization() { + o.Organization.Unset() +} + +// GetProvider returns the Provider field value +func (o *IdentityWithCredentialsOidcConfigProvider) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *IdentityWithCredentialsOidcConfigProvider) SetProvider(v string) { + o.Provider = v +} + +// GetSubject returns the Subject field value +func (o *IdentityWithCredentialsOidcConfigProvider) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *IdentityWithCredentialsOidcConfigProvider) SetSubject(v string) { + o.Subject = v +} + +// GetUseAutoLink returns the UseAutoLink field value if set, zero value otherwise. +func (o *IdentityWithCredentialsOidcConfigProvider) GetUseAutoLink() bool { + if o == nil || IsNil(o.UseAutoLink) { + var ret bool + return ret + } + return *o.UseAutoLink +} + +// GetUseAutoLinkOk returns a tuple with the UseAutoLink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) GetUseAutoLinkOk() (*bool, bool) { + if o == nil || IsNil(o.UseAutoLink) { + return nil, false + } + return o.UseAutoLink, true +} + +// HasUseAutoLink returns a boolean if a field has been set. +func (o *IdentityWithCredentialsOidcConfigProvider) HasUseAutoLink() bool { + if o != nil && !IsNil(o.UseAutoLink) { + return true + } + + return false +} + +// SetUseAutoLink gets a reference to the given bool and assigns it to the UseAutoLink field. +func (o *IdentityWithCredentialsOidcConfigProvider) SetUseAutoLink(v bool) { + o.UseAutoLink = &v +} + +func (o IdentityWithCredentialsOidcConfigProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsOidcConfigProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Organization.IsSet() { + toSerialize["organization"] = o.Organization.Get() + } + toSerialize["provider"] = o.Provider + toSerialize["subject"] = o.Subject + if !IsNil(o.UseAutoLink) { + toSerialize["use_auto_link"] = o.UseAutoLink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsOidcConfigProvider) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider", + "subject", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentityWithCredentialsOidcConfigProvider := _IdentityWithCredentialsOidcConfigProvider{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsOidcConfigProvider) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsOidcConfigProvider(varIdentityWithCredentialsOidcConfigProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + delete(additionalProperties, "use_auto_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsOidcConfigProvider struct { + value *IdentityWithCredentialsOidcConfigProvider + isSet bool +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) Get() *IdentityWithCredentialsOidcConfigProvider { + return v.value +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) Set(val *IdentityWithCredentialsOidcConfigProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsOidcConfigProvider(val *IdentityWithCredentialsOidcConfigProvider) *NullableIdentityWithCredentialsOidcConfigProvider { + return &NullableIdentityWithCredentialsOidcConfigProvider{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsOidcConfigProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsOidcConfigProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_passkey.go b/pkg/httpclient/model_identity_with_credentials_passkey.go new file mode 100644 index 0000000..2e8bf99 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_passkey.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasskey type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasskey{} + +// IdentityWithCredentialsPasskey Create Identity and Import Passkey Credentials +type IdentityWithCredentialsPasskey struct { + Config *IdentityWithCredentialsPasskeyConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasskey IdentityWithCredentialsPasskey + +// NewIdentityWithCredentialsPasskey instantiates a new IdentityWithCredentialsPasskey 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 NewIdentityWithCredentialsPasskey() *IdentityWithCredentialsPasskey { + this := IdentityWithCredentialsPasskey{} + return &this +} + +// NewIdentityWithCredentialsPasskeyWithDefaults instantiates a new IdentityWithCredentialsPasskey 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 NewIdentityWithCredentialsPasskeyWithDefaults() *IdentityWithCredentialsPasskey { + this := IdentityWithCredentialsPasskey{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskey) GetConfig() IdentityWithCredentialsPasskeyConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsPasskeyConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskey) GetConfigOk() (*IdentityWithCredentialsPasskeyConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskey) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsPasskeyConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsPasskey) SetConfig(v IdentityWithCredentialsPasskeyConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsPasskey) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasskey) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasskey) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasskey := _IdentityWithCredentialsPasskey{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasskey) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasskey(varIdentityWithCredentialsPasskey) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasskey struct { + value *IdentityWithCredentialsPasskey + isSet bool +} + +func (v NullableIdentityWithCredentialsPasskey) Get() *IdentityWithCredentialsPasskey { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasskey) Set(val *IdentityWithCredentialsPasskey) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasskey) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasskey) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasskey(val *IdentityWithCredentialsPasskey) *NullableIdentityWithCredentialsPasskey { + return &NullableIdentityWithCredentialsPasskey{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasskey) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasskey) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_passkey_config.go b/pkg/httpclient/model_identity_with_credentials_passkey_config.go new file mode 100644 index 0000000..a3de891 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_passkey_config.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasskeyConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasskeyConfig{} + +// IdentityWithCredentialsPasskeyConfig Create Identity and Import Passkey Credentials Configuration +type IdentityWithCredentialsPasskeyConfig struct { + Credentials []IdentityCredentialsWebAuthn `json:"credentials,omitempty"` + // UserHandle is the user handle of the webauthn credential. + UserHandle []int32 `json:"user_handle,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasskeyConfig IdentityWithCredentialsPasskeyConfig + +// NewIdentityWithCredentialsPasskeyConfig instantiates a new IdentityWithCredentialsPasskeyConfig 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 NewIdentityWithCredentialsPasskeyConfig() *IdentityWithCredentialsPasskeyConfig { + this := IdentityWithCredentialsPasskeyConfig{} + return &this +} + +// NewIdentityWithCredentialsPasskeyConfigWithDefaults instantiates a new IdentityWithCredentialsPasskeyConfig 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 NewIdentityWithCredentialsPasskeyConfigWithDefaults() *IdentityWithCredentialsPasskeyConfig { + this := IdentityWithCredentialsPasskeyConfig{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskeyConfig) GetCredentials() []IdentityCredentialsWebAuthn { + if o == nil || IsNil(o.Credentials) { + var ret []IdentityCredentialsWebAuthn + return ret + } + return o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskeyConfig) GetCredentialsOk() ([]IdentityCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskeyConfig) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given []IdentityCredentialsWebAuthn and assigns it to the Credentials field. +func (o *IdentityWithCredentialsPasskeyConfig) SetCredentials(v []IdentityCredentialsWebAuthn) { + o.Credentials = v +} + +// GetUserHandle returns the UserHandle field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasskeyConfig) GetUserHandle() []int32 { + if o == nil || IsNil(o.UserHandle) { + var ret []int32 + return ret + } + return o.UserHandle +} + +// GetUserHandleOk returns a tuple with the UserHandle field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasskeyConfig) GetUserHandleOk() ([]int32, bool) { + if o == nil || IsNil(o.UserHandle) { + return nil, false + } + return o.UserHandle, true +} + +// HasUserHandle returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasskeyConfig) HasUserHandle() bool { + if o != nil && !IsNil(o.UserHandle) { + return true + } + + return false +} + +// SetUserHandle gets a reference to the given []int32 and assigns it to the UserHandle field. +func (o *IdentityWithCredentialsPasskeyConfig) SetUserHandle(v []int32) { + o.UserHandle = v +} + +func (o IdentityWithCredentialsPasskeyConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasskeyConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.UserHandle) { + toSerialize["user_handle"] = o.UserHandle + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasskeyConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasskeyConfig := _IdentityWithCredentialsPasskeyConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasskeyConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasskeyConfig(varIdentityWithCredentialsPasskeyConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "user_handle") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasskeyConfig struct { + value *IdentityWithCredentialsPasskeyConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) Get() *IdentityWithCredentialsPasskeyConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) Set(val *IdentityWithCredentialsPasskeyConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasskeyConfig(val *IdentityWithCredentialsPasskeyConfig) *NullableIdentityWithCredentialsPasskeyConfig { + return &NullableIdentityWithCredentialsPasskeyConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasskeyConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasskeyConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_password.go b/pkg/httpclient/model_identity_with_credentials_password.go new file mode 100644 index 0000000..adc4b65 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_password.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPassword type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPassword{} + +// IdentityWithCredentialsPassword Create Identity and Import Password Credentials +type IdentityWithCredentialsPassword struct { + Config *IdentityWithCredentialsPasswordConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPassword IdentityWithCredentialsPassword + +// NewIdentityWithCredentialsPassword instantiates a new IdentityWithCredentialsPassword 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 NewIdentityWithCredentialsPassword() *IdentityWithCredentialsPassword { + this := IdentityWithCredentialsPassword{} + return &this +} + +// NewIdentityWithCredentialsPasswordWithDefaults instantiates a new IdentityWithCredentialsPassword 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 NewIdentityWithCredentialsPasswordWithDefaults() *IdentityWithCredentialsPassword { + this := IdentityWithCredentialsPassword{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPassword) GetConfig() IdentityWithCredentialsPasswordConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsPasswordConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPassword) GetConfigOk() (*IdentityWithCredentialsPasswordConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPassword) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsPasswordConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsPassword) SetConfig(v IdentityWithCredentialsPasswordConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsPassword) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPassword) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPassword) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPassword := _IdentityWithCredentialsPassword{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPassword) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPassword(varIdentityWithCredentialsPassword) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPassword struct { + value *IdentityWithCredentialsPassword + isSet bool +} + +func (v NullableIdentityWithCredentialsPassword) Get() *IdentityWithCredentialsPassword { + return v.value +} + +func (v *NullableIdentityWithCredentialsPassword) Set(val *IdentityWithCredentialsPassword) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPassword) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPassword) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPassword(val *IdentityWithCredentialsPassword) *NullableIdentityWithCredentialsPassword { + return &NullableIdentityWithCredentialsPassword{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPassword) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPassword) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_password_config.go b/pkg/httpclient/model_identity_with_credentials_password_config.go new file mode 100644 index 0000000..26ee07b --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_password_config.go @@ -0,0 +1,231 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsPasswordConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsPasswordConfig{} + +// IdentityWithCredentialsPasswordConfig Create Identity and Import Password Credentials Configuration +type IdentityWithCredentialsPasswordConfig struct { + // The hashed password in [PHC format](https://www.ory.com/docs/kratos/manage-identities/import-user-accounts-identities#hashed-passwords) + 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"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsPasswordConfig IdentityWithCredentialsPasswordConfig + +// NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig 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 NewIdentityWithCredentialsPasswordConfig() *IdentityWithCredentialsPasswordConfig { + this := IdentityWithCredentialsPasswordConfig{} + return &this +} + +// NewIdentityWithCredentialsPasswordConfigWithDefaults instantiates a new IdentityWithCredentialsPasswordConfig 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 NewIdentityWithCredentialsPasswordConfigWithDefaults() *IdentityWithCredentialsPasswordConfig { + this := IdentityWithCredentialsPasswordConfig{} + return &this +} + +// GetHashedPassword returns the HashedPassword field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetHashedPassword() string { + if o == nil || IsNil(o.HashedPassword) { + var ret string + return ret + } + return *o.HashedPassword +} + +// GetHashedPasswordOk returns a tuple with the HashedPassword field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetHashedPasswordOk() (*string, bool) { + if o == nil || IsNil(o.HashedPassword) { + return nil, false + } + return o.HashedPassword, true +} + +// HasHashedPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasHashedPassword() bool { + if o != nil && !IsNil(o.HashedPassword) { + return true + } + + return false +} + +// SetHashedPassword gets a reference to the given string and assigns it to the HashedPassword field. +func (o *IdentityWithCredentialsPasswordConfig) SetHashedPassword(v string) { + o.HashedPassword = &v +} + +// GetPassword returns the Password field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetPassword() string { + if o == nil || IsNil(o.Password) { + var ret string + return ret + } + return *o.Password +} + +// GetPasswordOk returns a tuple with the Password field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetPasswordOk() (*string, bool) { + if o == nil || IsNil(o.Password) { + return nil, false + } + return o.Password, true +} + +// HasPassword returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasPassword() bool { + if o != nil && !IsNil(o.Password) { + return true + } + + return false +} + +// SetPassword gets a reference to the given string and assigns it to the Password field. +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 || IsNil(o.UsePasswordMigrationHook) { + 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 || IsNil(o.UsePasswordMigrationHook) { + 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 && !IsNil(o.UsePasswordMigrationHook) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsPasswordConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.HashedPassword) { + toSerialize["hashed_password"] = o.HashedPassword + } + if !IsNil(o.Password) { + toSerialize["password"] = o.Password + } + if !IsNil(o.UsePasswordMigrationHook) { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsPasswordConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsPasswordConfig := _IdentityWithCredentialsPasswordConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsPasswordConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsPasswordConfig(varIdentityWithCredentialsPasswordConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "hashed_password") + delete(additionalProperties, "password") + delete(additionalProperties, "use_password_migration_hook") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsPasswordConfig struct { + value *IdentityWithCredentialsPasswordConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsPasswordConfig) Get() *IdentityWithCredentialsPasswordConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) Set(val *IdentityWithCredentialsPasswordConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsPasswordConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsPasswordConfig(val *IdentityWithCredentialsPasswordConfig) *NullableIdentityWithCredentialsPasswordConfig { + return &NullableIdentityWithCredentialsPasswordConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsPasswordConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_saml.go b/pkg/httpclient/model_identity_with_credentials_saml.go new file mode 100644 index 0000000..5047d6b --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_saml.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsSaml type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSaml{} + +// IdentityWithCredentialsSaml Payload to import SAML credentials +type IdentityWithCredentialsSaml struct { + Config *IdentityWithCredentialsSamlConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSaml IdentityWithCredentialsSaml + +// NewIdentityWithCredentialsSaml instantiates a new IdentityWithCredentialsSaml 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 NewIdentityWithCredentialsSaml() *IdentityWithCredentialsSaml { + this := IdentityWithCredentialsSaml{} + return &this +} + +// NewIdentityWithCredentialsSamlWithDefaults instantiates a new IdentityWithCredentialsSaml 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 NewIdentityWithCredentialsSamlWithDefaults() *IdentityWithCredentialsSaml { + this := IdentityWithCredentialsSaml{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsSaml) GetConfig() IdentityWithCredentialsSamlConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsSamlConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSaml) GetConfigOk() (*IdentityWithCredentialsSamlConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSaml) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsSamlConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsSaml) SetConfig(v IdentityWithCredentialsSamlConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsSaml) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSaml) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSaml) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsSaml := _IdentityWithCredentialsSaml{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSaml) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSaml(varIdentityWithCredentialsSaml) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSaml struct { + value *IdentityWithCredentialsSaml + isSet bool +} + +func (v NullableIdentityWithCredentialsSaml) Get() *IdentityWithCredentialsSaml { + return v.value +} + +func (v *NullableIdentityWithCredentialsSaml) Set(val *IdentityWithCredentialsSaml) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSaml) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSaml) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSaml(val *IdentityWithCredentialsSaml) *NullableIdentityWithCredentialsSaml { + return &NullableIdentityWithCredentialsSaml{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSaml) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSaml) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_saml_config.go b/pkg/httpclient/model_identity_with_credentials_saml_config.go new file mode 100644 index 0000000..5a06532 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_saml_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsSamlConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSamlConfig{} + +// IdentityWithCredentialsSamlConfig Payload of SAML providers +type IdentityWithCredentialsSamlConfig struct { + // A list of SAML Providers + Providers []IdentityWithCredentialsSamlConfigProvider `json:"providers,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSamlConfig IdentityWithCredentialsSamlConfig + +// NewIdentityWithCredentialsSamlConfig instantiates a new IdentityWithCredentialsSamlConfig 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 NewIdentityWithCredentialsSamlConfig() *IdentityWithCredentialsSamlConfig { + this := IdentityWithCredentialsSamlConfig{} + return &this +} + +// NewIdentityWithCredentialsSamlConfigWithDefaults instantiates a new IdentityWithCredentialsSamlConfig 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 NewIdentityWithCredentialsSamlConfigWithDefaults() *IdentityWithCredentialsSamlConfig { + this := IdentityWithCredentialsSamlConfig{} + return &this +} + +// GetProviders returns the Providers field value if set, zero value otherwise. +func (o *IdentityWithCredentialsSamlConfig) GetProviders() []IdentityWithCredentialsSamlConfigProvider { + if o == nil || IsNil(o.Providers) { + var ret []IdentityWithCredentialsSamlConfigProvider + return ret + } + return o.Providers +} + +// GetProvidersOk returns a tuple with the Providers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfig) GetProvidersOk() ([]IdentityWithCredentialsSamlConfigProvider, bool) { + if o == nil || IsNil(o.Providers) { + return nil, false + } + return o.Providers, true +} + +// HasProviders returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSamlConfig) HasProviders() bool { + if o != nil && !IsNil(o.Providers) { + return true + } + + return false +} + +// SetProviders gets a reference to the given []IdentityWithCredentialsSamlConfigProvider and assigns it to the Providers field. +func (o *IdentityWithCredentialsSamlConfig) SetProviders(v []IdentityWithCredentialsSamlConfigProvider) { + o.Providers = v +} + +func (o IdentityWithCredentialsSamlConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSamlConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Providers) { + toSerialize["providers"] = o.Providers + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSamlConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsSamlConfig := _IdentityWithCredentialsSamlConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSamlConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSamlConfig(varIdentityWithCredentialsSamlConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "providers") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSamlConfig struct { + value *IdentityWithCredentialsSamlConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsSamlConfig) Get() *IdentityWithCredentialsSamlConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsSamlConfig) Set(val *IdentityWithCredentialsSamlConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSamlConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSamlConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSamlConfig(val *IdentityWithCredentialsSamlConfig) *NullableIdentityWithCredentialsSamlConfig { + return &NullableIdentityWithCredentialsSamlConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSamlConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSamlConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_saml_config_provider.go b/pkg/httpclient/model_identity_with_credentials_saml_config_provider.go new file mode 100644 index 0000000..4262c83 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_saml_config_provider.go @@ -0,0 +1,246 @@ +/* +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" + "fmt" +) + +// checks if the IdentityWithCredentialsSamlConfigProvider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsSamlConfigProvider{} + +// IdentityWithCredentialsSamlConfigProvider Payload of specific SAML provider +type IdentityWithCredentialsSamlConfigProvider struct { + Organization NullableString `json:"organization,omitempty"` + // The SAML provider to link the subject to. + Provider string `json:"provider"` + // The unique subject of the SAML connection. This value must be immutable at the source. + Subject string `json:"subject"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsSamlConfigProvider IdentityWithCredentialsSamlConfigProvider + +// NewIdentityWithCredentialsSamlConfigProvider instantiates a new IdentityWithCredentialsSamlConfigProvider 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 NewIdentityWithCredentialsSamlConfigProvider(provider string, subject string) *IdentityWithCredentialsSamlConfigProvider { + this := IdentityWithCredentialsSamlConfigProvider{} + this.Provider = provider + this.Subject = subject + return &this +} + +// NewIdentityWithCredentialsSamlConfigProviderWithDefaults instantiates a new IdentityWithCredentialsSamlConfigProvider 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 NewIdentityWithCredentialsSamlConfigProviderWithDefaults() *IdentityWithCredentialsSamlConfigProvider { + this := IdentityWithCredentialsSamlConfigProvider{} + return &this +} + +// GetOrganization returns the Organization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityWithCredentialsSamlConfigProvider) GetOrganization() string { + if o == nil || IsNil(o.Organization.Get()) { + var ret string + return ret + } + return *o.Organization.Get() +} + +// GetOrganizationOk returns a tuple with the Organization 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 *IdentityWithCredentialsSamlConfigProvider) GetOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Organization.Get(), o.Organization.IsSet() +} + +// HasOrganization returns a boolean if a field has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) HasOrganization() bool { + if o != nil && o.Organization.IsSet() { + return true + } + + return false +} + +// SetOrganization gets a reference to the given NullableString and assigns it to the Organization field. +func (o *IdentityWithCredentialsSamlConfigProvider) SetOrganization(v string) { + o.Organization.Set(&v) +} + +// SetOrganizationNil sets the value for Organization to be an explicit nil +func (o *IdentityWithCredentialsSamlConfigProvider) SetOrganizationNil() { + o.Organization.Set(nil) +} + +// UnsetOrganization ensures that no value is present for Organization, not even an explicit nil +func (o *IdentityWithCredentialsSamlConfigProvider) UnsetOrganization() { + o.Organization.Unset() +} + +// GetProvider returns the Provider field value +func (o *IdentityWithCredentialsSamlConfigProvider) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *IdentityWithCredentialsSamlConfigProvider) SetProvider(v string) { + o.Provider = v +} + +// GetSubject returns the Subject field value +func (o *IdentityWithCredentialsSamlConfigProvider) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsSamlConfigProvider) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *IdentityWithCredentialsSamlConfigProvider) SetSubject(v string) { + o.Subject = v +} + +func (o IdentityWithCredentialsSamlConfigProvider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsSamlConfigProvider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Organization.IsSet() { + toSerialize["organization"] = o.Organization.Get() + } + toSerialize["provider"] = o.Provider + toSerialize["subject"] = o.Subject + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsSamlConfigProvider) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider", + "subject", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIdentityWithCredentialsSamlConfigProvider := _IdentityWithCredentialsSamlConfigProvider{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsSamlConfigProvider) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsSamlConfigProvider(varIdentityWithCredentialsSamlConfigProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "subject") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsSamlConfigProvider struct { + value *IdentityWithCredentialsSamlConfigProvider + isSet bool +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) Get() *IdentityWithCredentialsSamlConfigProvider { + return v.value +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) Set(val *IdentityWithCredentialsSamlConfigProvider) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsSamlConfigProvider(val *IdentityWithCredentialsSamlConfigProvider) *NullableIdentityWithCredentialsSamlConfigProvider { + return &NullableIdentityWithCredentialsSamlConfigProvider{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsSamlConfigProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsSamlConfigProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_totp.go b/pkg/httpclient/model_identity_with_credentials_totp.go new file mode 100644 index 0000000..94da1eb --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_totp.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsTotp type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsTotp{} + +// IdentityWithCredentialsTotp Create Identity and Import TOTP 2FA Credentials +type IdentityWithCredentialsTotp struct { + Config *IdentityWithCredentialsTotpConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsTotp IdentityWithCredentialsTotp + +// NewIdentityWithCredentialsTotp instantiates a new IdentityWithCredentialsTotp 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 NewIdentityWithCredentialsTotp() *IdentityWithCredentialsTotp { + this := IdentityWithCredentialsTotp{} + return &this +} + +// NewIdentityWithCredentialsTotpWithDefaults instantiates a new IdentityWithCredentialsTotp 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 NewIdentityWithCredentialsTotpWithDefaults() *IdentityWithCredentialsTotp { + this := IdentityWithCredentialsTotp{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsTotp) GetConfig() IdentityWithCredentialsTotpConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsTotpConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsTotp) GetConfigOk() (*IdentityWithCredentialsTotpConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsTotp) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsTotpConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsTotp) SetConfig(v IdentityWithCredentialsTotpConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsTotp) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsTotp) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsTotp) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsTotp := _IdentityWithCredentialsTotp{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsTotp) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsTotp(varIdentityWithCredentialsTotp) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsTotp struct { + value *IdentityWithCredentialsTotp + isSet bool +} + +func (v NullableIdentityWithCredentialsTotp) Get() *IdentityWithCredentialsTotp { + return v.value +} + +func (v *NullableIdentityWithCredentialsTotp) Set(val *IdentityWithCredentialsTotp) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsTotp) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsTotp) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsTotp(val *IdentityWithCredentialsTotp) *NullableIdentityWithCredentialsTotp { + return &NullableIdentityWithCredentialsTotp{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsTotp) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsTotp) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_totp_config.go b/pkg/httpclient/model_identity_with_credentials_totp_config.go new file mode 100644 index 0000000..d84498d --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_totp_config.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsTotpConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsTotpConfig{} + +// IdentityWithCredentialsTotpConfig Create Identity and Import TOTP 2FA Credentials Configuration +type IdentityWithCredentialsTotpConfig struct { + // TOTPURL is the TOTP URL For more details see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + TotpUrl *string `json:"totp_url,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsTotpConfig IdentityWithCredentialsTotpConfig + +// NewIdentityWithCredentialsTotpConfig instantiates a new IdentityWithCredentialsTotpConfig 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 NewIdentityWithCredentialsTotpConfig() *IdentityWithCredentialsTotpConfig { + this := IdentityWithCredentialsTotpConfig{} + return &this +} + +// NewIdentityWithCredentialsTotpConfigWithDefaults instantiates a new IdentityWithCredentialsTotpConfig 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 NewIdentityWithCredentialsTotpConfigWithDefaults() *IdentityWithCredentialsTotpConfig { + this := IdentityWithCredentialsTotpConfig{} + return &this +} + +// GetTotpUrl returns the TotpUrl field value if set, zero value otherwise. +func (o *IdentityWithCredentialsTotpConfig) GetTotpUrl() string { + if o == nil || IsNil(o.TotpUrl) { + var ret string + return ret + } + return *o.TotpUrl +} + +// GetTotpUrlOk returns a tuple with the TotpUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsTotpConfig) GetTotpUrlOk() (*string, bool) { + if o == nil || IsNil(o.TotpUrl) { + return nil, false + } + return o.TotpUrl, true +} + +// HasTotpUrl returns a boolean if a field has been set. +func (o *IdentityWithCredentialsTotpConfig) HasTotpUrl() bool { + if o != nil && !IsNil(o.TotpUrl) { + return true + } + + return false +} + +// SetTotpUrl gets a reference to the given string and assigns it to the TotpUrl field. +func (o *IdentityWithCredentialsTotpConfig) SetTotpUrl(v string) { + o.TotpUrl = &v +} + +func (o IdentityWithCredentialsTotpConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsTotpConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.TotpUrl) { + toSerialize["totp_url"] = o.TotpUrl + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsTotpConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsTotpConfig := _IdentityWithCredentialsTotpConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsTotpConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsTotpConfig(varIdentityWithCredentialsTotpConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "totp_url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsTotpConfig struct { + value *IdentityWithCredentialsTotpConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsTotpConfig) Get() *IdentityWithCredentialsTotpConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsTotpConfig) Set(val *IdentityWithCredentialsTotpConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsTotpConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsTotpConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsTotpConfig(val *IdentityWithCredentialsTotpConfig) *NullableIdentityWithCredentialsTotpConfig { + return &NullableIdentityWithCredentialsTotpConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsTotpConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsTotpConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_web_authn.go b/pkg/httpclient/model_identity_with_credentials_web_authn.go new file mode 100644 index 0000000..6f4a70d --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_web_authn.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsWebAuthn type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsWebAuthn{} + +// IdentityWithCredentialsWebAuthn Create Identity and Import WebAuthn Credentials +type IdentityWithCredentialsWebAuthn struct { + Config *IdentityWithCredentialsWebAuthnConfig `json:"config,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsWebAuthn IdentityWithCredentialsWebAuthn + +// NewIdentityWithCredentialsWebAuthn instantiates a new IdentityWithCredentialsWebAuthn 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 NewIdentityWithCredentialsWebAuthn() *IdentityWithCredentialsWebAuthn { + this := IdentityWithCredentialsWebAuthn{} + return &this +} + +// NewIdentityWithCredentialsWebAuthnWithDefaults instantiates a new IdentityWithCredentialsWebAuthn 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 NewIdentityWithCredentialsWebAuthnWithDefaults() *IdentityWithCredentialsWebAuthn { + this := IdentityWithCredentialsWebAuthn{} + return &this +} + +// GetConfig returns the Config field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthn) GetConfig() IdentityWithCredentialsWebAuthnConfig { + if o == nil || IsNil(o.Config) { + var ret IdentityWithCredentialsWebAuthnConfig + return ret + } + return *o.Config +} + +// GetConfigOk returns a tuple with the Config field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthn) GetConfigOk() (*IdentityWithCredentialsWebAuthnConfig, bool) { + if o == nil || IsNil(o.Config) { + return nil, false + } + return o.Config, true +} + +// HasConfig returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthn) HasConfig() bool { + if o != nil && !IsNil(o.Config) { + return true + } + + return false +} + +// SetConfig gets a reference to the given IdentityWithCredentialsWebAuthnConfig and assigns it to the Config field. +func (o *IdentityWithCredentialsWebAuthn) SetConfig(v IdentityWithCredentialsWebAuthnConfig) { + o.Config = &v +} + +func (o IdentityWithCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsWebAuthn) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Config) { + toSerialize["config"] = o.Config + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsWebAuthn) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsWebAuthn := _IdentityWithCredentialsWebAuthn{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsWebAuthn) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsWebAuthn(varIdentityWithCredentialsWebAuthn) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "config") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsWebAuthn struct { + value *IdentityWithCredentialsWebAuthn + isSet bool +} + +func (v NullableIdentityWithCredentialsWebAuthn) Get() *IdentityWithCredentialsWebAuthn { + return v.value +} + +func (v *NullableIdentityWithCredentialsWebAuthn) Set(val *IdentityWithCredentialsWebAuthn) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsWebAuthn) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsWebAuthn) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsWebAuthn(val *IdentityWithCredentialsWebAuthn) *NullableIdentityWithCredentialsWebAuthn { + return &NullableIdentityWithCredentialsWebAuthn{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsWebAuthn) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsWebAuthn) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_identity_with_credentials_web_authn_config.go b/pkg/httpclient/model_identity_with_credentials_web_authn_config.go new file mode 100644 index 0000000..0a61870 --- /dev/null +++ b/pkg/httpclient/model_identity_with_credentials_web_authn_config.go @@ -0,0 +1,192 @@ +/* +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" +) + +// checks if the IdentityWithCredentialsWebAuthnConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IdentityWithCredentialsWebAuthnConfig{} + +// IdentityWithCredentialsWebAuthnConfig Create Identity and Import WebAuthn Credentials Configuration +type IdentityWithCredentialsWebAuthnConfig struct { + Credentials []IdentityCredentialsWebAuthn `json:"credentials,omitempty"` + // UserHandle is the user handle of the webauthn credential. + UserHandle []int32 `json:"user_handle,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _IdentityWithCredentialsWebAuthnConfig IdentityWithCredentialsWebAuthnConfig + +// NewIdentityWithCredentialsWebAuthnConfig instantiates a new IdentityWithCredentialsWebAuthnConfig 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 NewIdentityWithCredentialsWebAuthnConfig() *IdentityWithCredentialsWebAuthnConfig { + this := IdentityWithCredentialsWebAuthnConfig{} + return &this +} + +// NewIdentityWithCredentialsWebAuthnConfigWithDefaults instantiates a new IdentityWithCredentialsWebAuthnConfig 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 NewIdentityWithCredentialsWebAuthnConfigWithDefaults() *IdentityWithCredentialsWebAuthnConfig { + this := IdentityWithCredentialsWebAuthnConfig{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthnConfig) GetCredentials() []IdentityCredentialsWebAuthn { + if o == nil || IsNil(o.Credentials) { + var ret []IdentityCredentialsWebAuthn + return ret + } + return o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) GetCredentialsOk() ([]IdentityCredentialsWebAuthn, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given []IdentityCredentialsWebAuthn and assigns it to the Credentials field. +func (o *IdentityWithCredentialsWebAuthnConfig) SetCredentials(v []IdentityCredentialsWebAuthn) { + o.Credentials = v +} + +// GetUserHandle returns the UserHandle field value if set, zero value otherwise. +func (o *IdentityWithCredentialsWebAuthnConfig) GetUserHandle() []int32 { + if o == nil || IsNil(o.UserHandle) { + var ret []int32 + return ret + } + return o.UserHandle +} + +// GetUserHandleOk returns a tuple with the UserHandle field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) GetUserHandleOk() ([]int32, bool) { + if o == nil || IsNil(o.UserHandle) { + return nil, false + } + return o.UserHandle, true +} + +// HasUserHandle returns a boolean if a field has been set. +func (o *IdentityWithCredentialsWebAuthnConfig) HasUserHandle() bool { + if o != nil && !IsNil(o.UserHandle) { + return true + } + + return false +} + +// SetUserHandle gets a reference to the given []int32 and assigns it to the UserHandle field. +func (o *IdentityWithCredentialsWebAuthnConfig) SetUserHandle(v []int32) { + o.UserHandle = v +} + +func (o IdentityWithCredentialsWebAuthnConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IdentityWithCredentialsWebAuthnConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.UserHandle) { + toSerialize["user_handle"] = o.UserHandle + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IdentityWithCredentialsWebAuthnConfig) UnmarshalJSON(data []byte) (err error) { + varIdentityWithCredentialsWebAuthnConfig := _IdentityWithCredentialsWebAuthnConfig{} + + err = json.Unmarshal(data, &varIdentityWithCredentialsWebAuthnConfig) + + if err != nil { + return err + } + + *o = IdentityWithCredentialsWebAuthnConfig(varIdentityWithCredentialsWebAuthnConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "user_handle") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIdentityWithCredentialsWebAuthnConfig struct { + value *IdentityWithCredentialsWebAuthnConfig + isSet bool +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) Get() *IdentityWithCredentialsWebAuthnConfig { + return v.value +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) Set(val *IdentityWithCredentialsWebAuthnConfig) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityWithCredentialsWebAuthnConfig(val *IdentityWithCredentialsWebAuthnConfig) *NullableIdentityWithCredentialsWebAuthnConfig { + return &NullableIdentityWithCredentialsWebAuthnConfig{value: val, isSet: true} +} + +func (v NullableIdentityWithCredentialsWebAuthnConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityWithCredentialsWebAuthnConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_is_alive_200_response.go b/pkg/httpclient/model_is_alive_200_response.go new file mode 100644 index 0000000..59a8ab5 --- /dev/null +++ b/pkg/httpclient/model_is_alive_200_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the IsAlive200Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IsAlive200Response{} + +// IsAlive200Response struct for IsAlive200Response +type IsAlive200Response struct { + // Always \"ok\". + Status string `json:"status"` + AdditionalProperties map[string]interface{} +} + +type _IsAlive200Response IsAlive200Response + +// NewIsAlive200Response instantiates a new IsAlive200Response 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 NewIsAlive200Response(status string) *IsAlive200Response { + this := IsAlive200Response{} + this.Status = status + return &this +} + +// NewIsAlive200ResponseWithDefaults instantiates a new IsAlive200Response 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 NewIsAlive200ResponseWithDefaults() *IsAlive200Response { + this := IsAlive200Response{} + return &this +} + +// GetStatus returns the Status field value +func (o *IsAlive200Response) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *IsAlive200Response) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *IsAlive200Response) SetStatus(v string) { + o.Status = v +} + +func (o IsAlive200Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IsAlive200Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["status"] = o.Status + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IsAlive200Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "status", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIsAlive200Response := _IsAlive200Response{} + + err = json.Unmarshal(data, &varIsAlive200Response) + + if err != nil { + return err + } + + *o = IsAlive200Response(varIsAlive200Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "status") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIsAlive200Response struct { + value *IsAlive200Response + isSet bool +} + +func (v NullableIsAlive200Response) Get() *IsAlive200Response { + return v.value +} + +func (v *NullableIsAlive200Response) Set(val *IsAlive200Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsAlive200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsAlive200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsAlive200Response(val *IsAlive200Response) *NullableIsAlive200Response { + return &NullableIsAlive200Response{value: val, isSet: true} +} + +func (v NullableIsAlive200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsAlive200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_is_ready_503_response.go b/pkg/httpclient/model_is_ready_503_response.go new file mode 100644 index 0000000..ed05af1 --- /dev/null +++ b/pkg/httpclient/model_is_ready_503_response.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the IsReady503Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &IsReady503Response{} + +// IsReady503Response struct for IsReady503Response +type IsReady503Response struct { + // Errors contains a list of errors that caused the not ready status. + Errors map[string]string `json:"errors"` + AdditionalProperties map[string]interface{} +} + +type _IsReady503Response IsReady503Response + +// NewIsReady503Response instantiates a new IsReady503Response 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 NewIsReady503Response(errors map[string]string) *IsReady503Response { + this := IsReady503Response{} + this.Errors = errors + return &this +} + +// NewIsReady503ResponseWithDefaults instantiates a new IsReady503Response 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 NewIsReady503ResponseWithDefaults() *IsReady503Response { + this := IsReady503Response{} + return &this +} + +// GetErrors returns the Errors field value +func (o *IsReady503Response) GetErrors() map[string]string { + if o == nil { + var ret map[string]string + return ret + } + + return o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value +// and a boolean to check if the value has been set. +func (o *IsReady503Response) GetErrorsOk() (*map[string]string, bool) { + if o == nil { + return nil, false + } + return &o.Errors, true +} + +// SetErrors sets field value +func (o *IsReady503Response) SetErrors(v map[string]string) { + o.Errors = v +} + +func (o IsReady503Response) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o IsReady503Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["errors"] = o.Errors + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *IsReady503Response) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "errors", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varIsReady503Response := _IsReady503Response{} + + err = json.Unmarshal(data, &varIsReady503Response) + + if err != nil { + return err + } + + *o = IsReady503Response(varIsReady503Response) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "errors") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableIsReady503Response struct { + value *IsReady503Response + isSet bool +} + +func (v NullableIsReady503Response) Get() *IsReady503Response { + return v.value +} + +func (v *NullableIsReady503Response) Set(val *IsReady503Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsReady503Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsReady503Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsReady503Response(val *IsReady503Response) *NullableIsReady503Response { + return &NullableIsReady503Response{value: val, isSet: true} +} + +func (v NullableIsReady503Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsReady503Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_json_patch.go b/pkg/httpclient/model_json_patch.go new file mode 100644 index 0000000..111265f --- /dev/null +++ b/pkg/httpclient/model_json_patch.go @@ -0,0 +1,275 @@ +/* +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" + "fmt" +) + +// checks if the JsonPatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &JsonPatch{} + +// JsonPatch A JSONPatch document as defined by RFC 6902 +type JsonPatch struct { + // This field is used together with operation \"move\" and uses JSON Pointer notation. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + From *string `json:"from,omitempty"` + // The operation to be performed. One of \"add\", \"remove\", \"replace\", \"move\", \"copy\", or \"test\". + Op string `json:"op"` + // The path to the target path. Uses JSON pointer notation. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + Path string `json:"path"` + // The value to be used within the operations. Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + Value interface{} `json:"value,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _JsonPatch JsonPatch + +// NewJsonPatch instantiates a new JsonPatch 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 NewJsonPatch(op string, path string) *JsonPatch { + this := JsonPatch{} + this.Op = op + this.Path = path + return &this +} + +// NewJsonPatchWithDefaults instantiates a new JsonPatch 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 NewJsonPatchWithDefaults() *JsonPatch { + this := JsonPatch{} + return &this +} + +// GetFrom returns the From field value if set, zero value otherwise. +func (o *JsonPatch) GetFrom() string { + if o == nil || IsNil(o.From) { + var ret string + return ret + } + return *o.From +} + +// GetFromOk returns a tuple with the From field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetFromOk() (*string, bool) { + if o == nil || IsNil(o.From) { + return nil, false + } + return o.From, true +} + +// HasFrom returns a boolean if a field has been set. +func (o *JsonPatch) HasFrom() bool { + if o != nil && !IsNil(o.From) { + return true + } + + return false +} + +// SetFrom gets a reference to the given string and assigns it to the From field. +func (o *JsonPatch) SetFrom(v string) { + o.From = &v +} + +// GetOp returns the Op field value +func (o *JsonPatch) GetOp() string { + if o == nil { + var ret string + return ret + } + + return o.Op +} + +// GetOpOk returns a tuple with the Op field value +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetOpOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Op, true +} + +// SetOp sets field value +func (o *JsonPatch) SetOp(v string) { + o.Op = v +} + +// GetPath returns the Path field value +func (o *JsonPatch) GetPath() string { + if o == nil { + var ret string + return ret + } + + return o.Path +} + +// GetPathOk returns a tuple with the Path field value +// and a boolean to check if the value has been set. +func (o *JsonPatch) GetPathOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Path, true +} + +// SetPath sets field value +func (o *JsonPatch) SetPath(v string) { + o.Path = v +} + +// GetValue returns the Value field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *JsonPatch) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value 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 *JsonPatch) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *JsonPatch) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given interface{} and assigns it to the Value field. +func (o *JsonPatch) SetValue(v interface{}) { + o.Value = v +} + +func (o JsonPatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o JsonPatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.From) { + toSerialize["from"] = o.From + } + toSerialize["op"] = o.Op + toSerialize["path"] = o.Path + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *JsonPatch) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "op", + "path", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varJsonPatch := _JsonPatch{} + + err = json.Unmarshal(data, &varJsonPatch) + + if err != nil { + return err + } + + *o = JsonPatch(varJsonPatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "from") + delete(additionalProperties, "op") + delete(additionalProperties, "path") + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableJsonPatch struct { + value *JsonPatch + isSet bool +} + +func (v NullableJsonPatch) Get() *JsonPatch { + return v.value +} + +func (v *NullableJsonPatch) Set(val *JsonPatch) { + v.value = val + v.isSet = true +} + +func (v NullableJsonPatch) IsSet() bool { + return v.isSet +} + +func (v *NullableJsonPatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableJsonPatch(val *JsonPatch) *NullableJsonPatch { + return &NullableJsonPatch{value: val, isSet: true} +} + +func (v NullableJsonPatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableJsonPatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_key_state.go b/pkg/httpclient/model_key_state.go new file mode 100644 index 0000000..b8ea1c2 --- /dev/null +++ b/pkg/httpclient/model_key_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// KeyState Only a \"confirmed\" key can be used to authenticate. A \"locked\" key was disabled after too many wrong-PIN attempts; rotating its pin_secret unlocks it. +type KeyState string + +// List of KeyState +const ( + KEYSTATE_INITIAL KeyState = "initial" + KEYSTATE_CONFIRMED KeyState = "confirmed" + KEYSTATE_LOCKED KeyState = "locked" +) + +// All allowed values of KeyState enum +var AllowedKeyStateEnumValues = []KeyState{ + "initial", + "confirmed", + "locked", +} + +func (v *KeyState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := KeyState(value) + for _, existing := range AllowedKeyStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid KeyState", value) +} + +// NewKeyStateFromValue returns a pointer to a valid KeyState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewKeyStateFromValue(v string) (*KeyState, error) { + ev := KeyState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for KeyState: valid values are %v", v, AllowedKeyStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v KeyState) IsValid() bool { + for _, existing := range AllowedKeyStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to KeyState value +func (v KeyState) Ptr() *KeyState { + return &v +} + +type NullableKeyState struct { + value *KeyState + isSet bool +} + +func (v NullableKeyState) Get() *KeyState { + return v.value +} + +func (v *NullableKeyState) Set(val *KeyState) { + v.value = val + v.isSet = true +} + +func (v NullableKeyState) IsSet() bool { + return v.isSet +} + +func (v *NullableKeyState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableKeyState(val *KeyState) *NullableKeyState { + return &NullableKeyState{value: val, isSet: true} +} + +func (v NullableKeyState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableKeyState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow.go b/pkg/httpclient/model_login_flow.go new file mode 100644 index 0000000..d71db3c --- /dev/null +++ b/pkg/httpclient/model_login_flow.go @@ -0,0 +1,853 @@ +/* +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" + "fmt" + "time" +) + +// checks if the LoginFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlow{} + +// 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // 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"` + // ID represents the flow's unique ID. When performing the login flow, this represents the id in the login UI's query parameter: http:///?flow= + Id string `json:"id"` + // IdentitySchema optionally holds the ID of the identity schema that is used for this flow. This value can be set by the user when creating the flow and should be retained when the flow is saved or converted to another flow. + IdentitySchema *string `json:"identity_schema,omitempty"` + // IssuedAt is the time (UTC) when the flow started. + IssuedAt time.Time `json:"issued_at"` + // Ory OAuth 2.0 Login Challenge. This value is set using the `login_challenge` query parameter of the registration and login endpoints. If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + Oauth2LoginChallenge *string `json:"oauth2_login_challenge,omitempty"` + Oauth2LoginRequest *OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // Refresh stores whether this login flow should enforce re-authentication. + Refresh *bool `json:"refresh,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + RequestedAal *AuthenticatorAssuranceLevel `json:"requested_aal,omitempty"` + // ReturnTo contains the requested return_to URL. + ReturnTo *string `json:"return_to,omitempty"` + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. + 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"` + TestContext *LoginFlowTestContext `json:"test_context,omitempty"` + // 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"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlow LoginFlow + +// NewLoginFlow instantiates a new LoginFlow 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 NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { + this := LoginFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewLoginFlowWithDefaults instantiates a new LoginFlow 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 NewLoginFlowWithDefaults() *LoginFlow { + this := LoginFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *LoginFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *LoginFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *LoginFlow) SetActive(v string) { + o.Active = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *LoginFlow) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *LoginFlow) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *LoginFlow) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *LoginFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *LoginFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *LoginFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *LoginFlow) SetId(v string) { + o.Id = v +} + +// GetIdentitySchema returns the IdentitySchema field value if set, zero value otherwise. +func (o *LoginFlow) GetIdentitySchema() string { + if o == nil || IsNil(o.IdentitySchema) { + var ret string + return ret + } + return *o.IdentitySchema +} + +// GetIdentitySchemaOk returns a tuple with the IdentitySchema field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIdentitySchemaOk() (*string, bool) { + if o == nil || IsNil(o.IdentitySchema) { + return nil, false + } + return o.IdentitySchema, true +} + +// HasIdentitySchema returns a boolean if a field has been set. +func (o *LoginFlow) HasIdentitySchema() bool { + if o != nil && !IsNil(o.IdentitySchema) { + return true + } + + return false +} + +// SetIdentitySchema gets a reference to the given string and assigns it to the IdentitySchema field. +func (o *LoginFlow) SetIdentitySchema(v string) { + o.IdentitySchema = &v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *LoginFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *LoginFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOauth2LoginChallenge returns the Oauth2LoginChallenge field value if set, zero value otherwise. +func (o *LoginFlow) GetOauth2LoginChallenge() string { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + var ret string + return ret + } + return *o.Oauth2LoginChallenge +} + +// GetOauth2LoginChallengeOk returns a tuple with the Oauth2LoginChallenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetOauth2LoginChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + return nil, false + } + return o.Oauth2LoginChallenge, true +} + +// HasOauth2LoginChallenge returns a boolean if a field has been set. +func (o *LoginFlow) HasOauth2LoginChallenge() bool { + if o != nil && !IsNil(o.Oauth2LoginChallenge) { + return true + } + + return false +} + +// SetOauth2LoginChallenge gets a reference to the given string and assigns it to the Oauth2LoginChallenge field. +func (o *LoginFlow) SetOauth2LoginChallenge(v string) { + o.Oauth2LoginChallenge = &v +} + +// GetOauth2LoginRequest returns the Oauth2LoginRequest field value if set, zero value otherwise. +func (o *LoginFlow) GetOauth2LoginRequest() OAuth2LoginRequest { + if o == nil || IsNil(o.Oauth2LoginRequest) { + var ret OAuth2LoginRequest + return ret + } + return *o.Oauth2LoginRequest +} + +// GetOauth2LoginRequestOk returns a tuple with the Oauth2LoginRequest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetOauth2LoginRequestOk() (*OAuth2LoginRequest, bool) { + if o == nil || IsNil(o.Oauth2LoginRequest) { + return nil, false + } + return o.Oauth2LoginRequest, true +} + +// HasOauth2LoginRequest returns a boolean if a field has been set. +func (o *LoginFlow) HasOauth2LoginRequest() bool { + if o != nil && !IsNil(o.Oauth2LoginRequest) { + return true + } + + return false +} + +// SetOauth2LoginRequest gets a reference to the given OAuth2LoginRequest and assigns it to the Oauth2LoginRequest field. +func (o *LoginFlow) SetOauth2LoginRequest(v OAuth2LoginRequest) { + o.Oauth2LoginRequest = &v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *LoginFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *LoginFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *LoginFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *LoginFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *LoginFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *LoginFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRefresh returns the Refresh field value if set, zero value otherwise. +func (o *LoginFlow) GetRefresh() bool { + if o == nil || IsNil(o.Refresh) { + var ret bool + return ret + } + return *o.Refresh +} + +// GetRefreshOk returns a tuple with the Refresh field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRefreshOk() (*bool, bool) { + if o == nil || IsNil(o.Refresh) { + return nil, false + } + return o.Refresh, true +} + +// HasRefresh returns a boolean if a field has been set. +func (o *LoginFlow) HasRefresh() bool { + if o != nil && !IsNil(o.Refresh) { + return true + } + + return false +} + +// SetRefresh gets a reference to the given bool and assigns it to the Refresh field. +func (o *LoginFlow) SetRefresh(v bool) { + o.Refresh = &v +} + +// GetRequestUrl returns the RequestUrl field value +func (o *LoginFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *LoginFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetRequestedAal returns the RequestedAal field value if set, zero value otherwise. +func (o *LoginFlow) GetRequestedAal() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.RequestedAal) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.RequestedAal +} + +// GetRequestedAalOk returns a tuple with the RequestedAal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetRequestedAalOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.RequestedAal) { + return nil, false + } + return o.RequestedAal, true +} + +// HasRequestedAal returns a boolean if a field has been set. +func (o *LoginFlow) HasRequestedAal() bool { + if o != nil && !IsNil(o.RequestedAal) { + return true + } + + return false +} + +// SetRequestedAal gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the RequestedAal field. +func (o *LoginFlow) SetRequestedAal(v AuthenticatorAssuranceLevel) { + o.RequestedAal = &v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *LoginFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *LoginFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *LoginFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetSessionTokenExchangeCode returns the SessionTokenExchangeCode field value if set, zero value otherwise. +func (o *LoginFlow) GetSessionTokenExchangeCode() string { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + var ret string + return ret + } + return *o.SessionTokenExchangeCode +} + +// GetSessionTokenExchangeCodeOk returns a tuple with the SessionTokenExchangeCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetSessionTokenExchangeCodeOk() (*string, bool) { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + return nil, false + } + return o.SessionTokenExchangeCode, true +} + +// HasSessionTokenExchangeCode returns a boolean if a field has been set. +func (o *LoginFlow) HasSessionTokenExchangeCode() bool { + if o != nil && !IsNil(o.SessionTokenExchangeCode) { + return true + } + + return false +} + +// SetSessionTokenExchangeCode gets a reference to the given string and assigns it to the SessionTokenExchangeCode field. +func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { + o.SessionTokenExchangeCode = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + +// GetTestContext returns the TestContext field value if set, zero value otherwise. +func (o *LoginFlow) GetTestContext() LoginFlowTestContext { + if o == nil || IsNil(o.TestContext) { + var ret LoginFlowTestContext + return ret + } + return *o.TestContext +} + +// GetTestContextOk returns a tuple with the TestContext field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTestContextOk() (*LoginFlowTestContext, bool) { + if o == nil || IsNil(o.TestContext) { + return nil, false + } + return o.TestContext, true +} + +// HasTestContext returns a boolean if a field has been set. +func (o *LoginFlow) HasTestContext() bool { + if o != nil && !IsNil(o.TestContext) { + return true + } + + return false +} + +// SetTestContext gets a reference to the given LoginFlowTestContext and assigns it to the TestContext field. +func (o *LoginFlow) SetTestContext(v LoginFlowTestContext) { + o.TestContext = &v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *LoginFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *LoginFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *LoginFlow) SetUi(v UiContainer) { + o.Ui = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *LoginFlow) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *LoginFlow) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *LoginFlow) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +func (o LoginFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + if !IsNil(o.IdentitySchema) { + toSerialize["identity_schema"] = o.IdentitySchema + } + toSerialize["issued_at"] = o.IssuedAt + if !IsNil(o.Oauth2LoginChallenge) { + toSerialize["oauth2_login_challenge"] = o.Oauth2LoginChallenge + } + if !IsNil(o.Oauth2LoginRequest) { + toSerialize["oauth2_login_request"] = o.Oauth2LoginRequest + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + if !IsNil(o.Refresh) { + toSerialize["refresh"] = o.Refresh + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.RequestedAal) { + toSerialize["requested_aal"] = o.RequestedAal + } + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if !IsNil(o.SessionTokenExchangeCode) { + toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TestContext) { + toSerialize["test_context"] = o.TestContext + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLoginFlow := _LoginFlow{} + + err = json.Unmarshal(data, &varLoginFlow) + + if err != nil { + return err + } + + *o = LoginFlow(varLoginFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "created_at") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity_schema") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "oauth2_login_challenge") + delete(additionalProperties, "oauth2_login_request") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "refresh") + delete(additionalProperties, "request_url") + delete(additionalProperties, "requested_aal") + delete(additionalProperties, "return_to") + delete(additionalProperties, "session_token_exchange_code") + delete(additionalProperties, "state") + delete(additionalProperties, "test_context") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlow struct { + value *LoginFlow + isSet bool +} + +func (v NullableLoginFlow) Get() *LoginFlow { + return v.value +} + +func (v *NullableLoginFlow) Set(val *LoginFlow) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlow(val *LoginFlow) *NullableLoginFlow { + return &NullableLoginFlow{value: val, isSet: true} +} + +func (v NullableLoginFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow_state.go b/pkg/httpclient/model_login_flow_state.go new file mode 100644 index 0000000..b5c2a1a --- /dev/null +++ b/pkg/httpclient/model_login_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +// All allowed values of LoginFlowState enum +var AllowedLoginFlowStateEnumValues = []LoginFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range AllowedLoginFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// NewLoginFlowStateFromValue returns a pointer to a valid LoginFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewLoginFlowStateFromValue(v string) (*LoginFlowState, error) { + ev := LoginFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for LoginFlowState: valid values are %v", v, AllowedLoginFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v LoginFlowState) IsValid() bool { + for _, existing := range AllowedLoginFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to loginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow_test_context.go b/pkg/httpclient/model_login_flow_test_context.go new file mode 100644 index 0000000..30d4079 --- /dev/null +++ b/pkg/httpclient/model_login_flow_test_context.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the LoginFlowTestContext type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestContext{} + +// LoginFlowTestContext Admin-test extension of a login flow. Populated only for flows created by the admin test endpoint; included in the flow's API response so the admin UI can render the pre-scoped provider and (once captured) the debug round-trip result. +type LoginFlowTestContext struct { + DebugPayload *LoginFlowTestDebugPayload `json:"debug_payload,omitempty"` + // The ID of the OIDC provider this test flow targets. + ProviderId string `json:"provider_id"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestContext LoginFlowTestContext + +// NewLoginFlowTestContext instantiates a new LoginFlowTestContext 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 NewLoginFlowTestContext(providerId string) *LoginFlowTestContext { + this := LoginFlowTestContext{} + this.ProviderId = providerId + return &this +} + +// NewLoginFlowTestContextWithDefaults instantiates a new LoginFlowTestContext 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 NewLoginFlowTestContextWithDefaults() *LoginFlowTestContext { + this := LoginFlowTestContext{} + return &this +} + +// GetDebugPayload returns the DebugPayload field value if set, zero value otherwise. +func (o *LoginFlowTestContext) GetDebugPayload() LoginFlowTestDebugPayload { + if o == nil || IsNil(o.DebugPayload) { + var ret LoginFlowTestDebugPayload + return ret + } + return *o.DebugPayload +} + +// GetDebugPayloadOk returns a tuple with the DebugPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestContext) GetDebugPayloadOk() (*LoginFlowTestDebugPayload, bool) { + if o == nil || IsNil(o.DebugPayload) { + return nil, false + } + return o.DebugPayload, true +} + +// HasDebugPayload returns a boolean if a field has been set. +func (o *LoginFlowTestContext) HasDebugPayload() bool { + if o != nil && !IsNil(o.DebugPayload) { + return true + } + + return false +} + +// SetDebugPayload gets a reference to the given LoginFlowTestDebugPayload and assigns it to the DebugPayload field. +func (o *LoginFlowTestContext) SetDebugPayload(v LoginFlowTestDebugPayload) { + o.DebugPayload = &v +} + +// GetProviderId returns the ProviderId field value +func (o *LoginFlowTestContext) GetProviderId() string { + if o == nil { + var ret string + return ret + } + + return o.ProviderId +} + +// GetProviderIdOk returns a tuple with the ProviderId field value +// and a boolean to check if the value has been set. +func (o *LoginFlowTestContext) GetProviderIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ProviderId, true +} + +// SetProviderId sets field value +func (o *LoginFlowTestContext) SetProviderId(v string) { + o.ProviderId = v +} + +func (o LoginFlowTestContext) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestContext) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DebugPayload) { + toSerialize["debug_payload"] = o.DebugPayload + } + toSerialize["provider_id"] = o.ProviderId + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestContext) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "provider_id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLoginFlowTestContext := _LoginFlowTestContext{} + + err = json.Unmarshal(data, &varLoginFlowTestContext) + + if err != nil { + return err + } + + *o = LoginFlowTestContext(varLoginFlowTestContext) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "debug_payload") + delete(additionalProperties, "provider_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestContext struct { + value *LoginFlowTestContext + isSet bool +} + +func (v NullableLoginFlowTestContext) Get() *LoginFlowTestContext { + return v.value +} + +func (v *NullableLoginFlowTestContext) Set(val *LoginFlowTestContext) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestContext) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestContext) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestContext(val *LoginFlowTestContext) *NullableLoginFlowTestContext { + return &NullableLoginFlowTestContext{value: val, isSet: true} +} + +func (v NullableLoginFlowTestContext) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestContext) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow_test_debug_payload.go b/pkg/httpclient/model_login_flow_test_debug_payload.go new file mode 100644 index 0000000..3756c2f --- /dev/null +++ b/pkg/httpclient/model_login_flow_test_debug_payload.go @@ -0,0 +1,420 @@ +/* +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" +) + +// checks if the LoginFlowTestDebugPayload type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestDebugPayload{} + +// LoginFlowTestDebugPayload Contains the parsed claims, the Jsonnet mapper input and output, and any schema validation errors. Bearer tokens (id_token, access_token, refresh_token) are intentionally excluded to limit the blast radius of the debug payload leaking through audit logs or admin browsers. +type LoginFlowTestDebugPayload struct { + Error *LoginFlowTestStepError `json:"error,omitempty"` + // Claims extracted from the ID token. + IdTokenClaims map[string]interface{} `json:"id_token_claims,omitempty"` + // Input JSON that was fed into the Jsonnet mapper. + JsonnetInput map[string]interface{} `json:"jsonnet_input,omitempty"` + // URL of the Jsonnet mapper that was executed on the claims. + JsonnetMapperUrl *string `json:"jsonnet_mapper_url,omitempty"` + // Output JSON returned by the Jsonnet mapper. + JsonnetOutput map[string]interface{} `json:"jsonnet_output,omitempty"` + // Anything the Jsonnet mapper wrote to standard error. + JsonnetStderr *string `json:"jsonnet_stderr,omitempty"` + // Identity-schema validation errors produced from the mapped traits. + SchemaValidationErrors []LoginFlowTestSchemaValidationError `json:"schema_validation_errors,omitempty"` + // Claims returned from the provider's userinfo endpoint, if any. + Userinfo map[string]interface{} `json:"userinfo,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestDebugPayload LoginFlowTestDebugPayload + +// NewLoginFlowTestDebugPayload instantiates a new LoginFlowTestDebugPayload 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 NewLoginFlowTestDebugPayload() *LoginFlowTestDebugPayload { + this := LoginFlowTestDebugPayload{} + return &this +} + +// NewLoginFlowTestDebugPayloadWithDefaults instantiates a new LoginFlowTestDebugPayload 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 NewLoginFlowTestDebugPayloadWithDefaults() *LoginFlowTestDebugPayload { + this := LoginFlowTestDebugPayload{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetError() LoginFlowTestStepError { + if o == nil || IsNil(o.Error) { + var ret LoginFlowTestStepError + 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. +func (o *LoginFlowTestDebugPayload) GetErrorOk() (*LoginFlowTestStepError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given LoginFlowTestStepError and assigns it to the Error field. +func (o *LoginFlowTestDebugPayload) SetError(v LoginFlowTestStepError) { + o.Error = &v +} + +// GetIdTokenClaims returns the IdTokenClaims field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetIdTokenClaims() map[string]interface{} { + if o == nil || IsNil(o.IdTokenClaims) { + var ret map[string]interface{} + return ret + } + return o.IdTokenClaims +} + +// GetIdTokenClaimsOk returns a tuple with the IdTokenClaims field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetIdTokenClaimsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.IdTokenClaims) { + return map[string]interface{}{}, false + } + return o.IdTokenClaims, true +} + +// HasIdTokenClaims returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasIdTokenClaims() bool { + if o != nil && !IsNil(o.IdTokenClaims) { + return true + } + + return false +} + +// SetIdTokenClaims gets a reference to the given map[string]interface{} and assigns it to the IdTokenClaims field. +func (o *LoginFlowTestDebugPayload) SetIdTokenClaims(v map[string]interface{}) { + o.IdTokenClaims = v +} + +// GetJsonnetInput returns the JsonnetInput field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetInput() map[string]interface{} { + if o == nil || IsNil(o.JsonnetInput) { + var ret map[string]interface{} + return ret + } + return o.JsonnetInput +} + +// GetJsonnetInputOk returns a tuple with the JsonnetInput field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetInputOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.JsonnetInput) { + return map[string]interface{}{}, false + } + return o.JsonnetInput, true +} + +// HasJsonnetInput returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetInput() bool { + if o != nil && !IsNil(o.JsonnetInput) { + return true + } + + return false +} + +// SetJsonnetInput gets a reference to the given map[string]interface{} and assigns it to the JsonnetInput field. +func (o *LoginFlowTestDebugPayload) SetJsonnetInput(v map[string]interface{}) { + o.JsonnetInput = v +} + +// GetJsonnetMapperUrl returns the JsonnetMapperUrl field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetMapperUrl() string { + if o == nil || IsNil(o.JsonnetMapperUrl) { + var ret string + return ret + } + return *o.JsonnetMapperUrl +} + +// GetJsonnetMapperUrlOk returns a tuple with the JsonnetMapperUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetMapperUrlOk() (*string, bool) { + if o == nil || IsNil(o.JsonnetMapperUrl) { + return nil, false + } + return o.JsonnetMapperUrl, true +} + +// HasJsonnetMapperUrl returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetMapperUrl() bool { + if o != nil && !IsNil(o.JsonnetMapperUrl) { + return true + } + + return false +} + +// SetJsonnetMapperUrl gets a reference to the given string and assigns it to the JsonnetMapperUrl field. +func (o *LoginFlowTestDebugPayload) SetJsonnetMapperUrl(v string) { + o.JsonnetMapperUrl = &v +} + +// GetJsonnetOutput returns the JsonnetOutput field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetOutput() map[string]interface{} { + if o == nil || IsNil(o.JsonnetOutput) { + var ret map[string]interface{} + return ret + } + return o.JsonnetOutput +} + +// GetJsonnetOutputOk returns a tuple with the JsonnetOutput field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetOutputOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.JsonnetOutput) { + return map[string]interface{}{}, false + } + return o.JsonnetOutput, true +} + +// HasJsonnetOutput returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetOutput() bool { + if o != nil && !IsNil(o.JsonnetOutput) { + return true + } + + return false +} + +// SetJsonnetOutput gets a reference to the given map[string]interface{} and assigns it to the JsonnetOutput field. +func (o *LoginFlowTestDebugPayload) SetJsonnetOutput(v map[string]interface{}) { + o.JsonnetOutput = v +} + +// GetJsonnetStderr returns the JsonnetStderr field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetJsonnetStderr() string { + if o == nil || IsNil(o.JsonnetStderr) { + var ret string + return ret + } + return *o.JsonnetStderr +} + +// GetJsonnetStderrOk returns a tuple with the JsonnetStderr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetJsonnetStderrOk() (*string, bool) { + if o == nil || IsNil(o.JsonnetStderr) { + return nil, false + } + return o.JsonnetStderr, true +} + +// HasJsonnetStderr returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasJsonnetStderr() bool { + if o != nil && !IsNil(o.JsonnetStderr) { + return true + } + + return false +} + +// SetJsonnetStderr gets a reference to the given string and assigns it to the JsonnetStderr field. +func (o *LoginFlowTestDebugPayload) SetJsonnetStderr(v string) { + o.JsonnetStderr = &v +} + +// GetSchemaValidationErrors returns the SchemaValidationErrors field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetSchemaValidationErrors() []LoginFlowTestSchemaValidationError { + if o == nil || IsNil(o.SchemaValidationErrors) { + var ret []LoginFlowTestSchemaValidationError + return ret + } + return o.SchemaValidationErrors +} + +// GetSchemaValidationErrorsOk returns a tuple with the SchemaValidationErrors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetSchemaValidationErrorsOk() ([]LoginFlowTestSchemaValidationError, bool) { + if o == nil || IsNil(o.SchemaValidationErrors) { + return nil, false + } + return o.SchemaValidationErrors, true +} + +// HasSchemaValidationErrors returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasSchemaValidationErrors() bool { + if o != nil && !IsNil(o.SchemaValidationErrors) { + return true + } + + return false +} + +// SetSchemaValidationErrors gets a reference to the given []LoginFlowTestSchemaValidationError and assigns it to the SchemaValidationErrors field. +func (o *LoginFlowTestDebugPayload) SetSchemaValidationErrors(v []LoginFlowTestSchemaValidationError) { + o.SchemaValidationErrors = v +} + +// GetUserinfo returns the Userinfo field value if set, zero value otherwise. +func (o *LoginFlowTestDebugPayload) GetUserinfo() map[string]interface{} { + if o == nil || IsNil(o.Userinfo) { + var ret map[string]interface{} + return ret + } + return o.Userinfo +} + +// GetUserinfoOk returns a tuple with the Userinfo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestDebugPayload) GetUserinfoOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Userinfo) { + return map[string]interface{}{}, false + } + return o.Userinfo, true +} + +// HasUserinfo returns a boolean if a field has been set. +func (o *LoginFlowTestDebugPayload) HasUserinfo() bool { + if o != nil && !IsNil(o.Userinfo) { + return true + } + + return false +} + +// SetUserinfo gets a reference to the given map[string]interface{} and assigns it to the Userinfo field. +func (o *LoginFlowTestDebugPayload) SetUserinfo(v map[string]interface{}) { + o.Userinfo = v +} + +func (o LoginFlowTestDebugPayload) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestDebugPayload) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.IdTokenClaims) { + toSerialize["id_token_claims"] = o.IdTokenClaims + } + if !IsNil(o.JsonnetInput) { + toSerialize["jsonnet_input"] = o.JsonnetInput + } + if !IsNil(o.JsonnetMapperUrl) { + toSerialize["jsonnet_mapper_url"] = o.JsonnetMapperUrl + } + if !IsNil(o.JsonnetOutput) { + toSerialize["jsonnet_output"] = o.JsonnetOutput + } + if !IsNil(o.JsonnetStderr) { + toSerialize["jsonnet_stderr"] = o.JsonnetStderr + } + if !IsNil(o.SchemaValidationErrors) { + toSerialize["schema_validation_errors"] = o.SchemaValidationErrors + } + if !IsNil(o.Userinfo) { + toSerialize["userinfo"] = o.Userinfo + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestDebugPayload) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestDebugPayload := _LoginFlowTestDebugPayload{} + + err = json.Unmarshal(data, &varLoginFlowTestDebugPayload) + + if err != nil { + return err + } + + *o = LoginFlowTestDebugPayload(varLoginFlowTestDebugPayload) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "id_token_claims") + delete(additionalProperties, "jsonnet_input") + delete(additionalProperties, "jsonnet_mapper_url") + delete(additionalProperties, "jsonnet_output") + delete(additionalProperties, "jsonnet_stderr") + delete(additionalProperties, "schema_validation_errors") + delete(additionalProperties, "userinfo") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestDebugPayload struct { + value *LoginFlowTestDebugPayload + isSet bool +} + +func (v NullableLoginFlowTestDebugPayload) Get() *LoginFlowTestDebugPayload { + return v.value +} + +func (v *NullableLoginFlowTestDebugPayload) Set(val *LoginFlowTestDebugPayload) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestDebugPayload) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestDebugPayload) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestDebugPayload(val *LoginFlowTestDebugPayload) *NullableLoginFlowTestDebugPayload { + return &NullableLoginFlowTestDebugPayload{value: val, isSet: true} +} + +func (v NullableLoginFlowTestDebugPayload) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestDebugPayload) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow_test_schema_validation_error.go b/pkg/httpclient/model_login_flow_test_schema_validation_error.go new file mode 100644 index 0000000..a505ca2 --- /dev/null +++ b/pkg/httpclient/model_login_flow_test_schema_validation_error.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the LoginFlowTestSchemaValidationError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestSchemaValidationError{} + +// LoginFlowTestSchemaValidationError One identity-schema validation failure recorded while evaluating the traits produced by the Jsonnet mapper. +type LoginFlowTestSchemaValidationError struct { + // Human-readable description of the validation failure. + Message *string `json:"message,omitempty"` + // JSON pointer to the field that failed validation. + Path *string `json:"path,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestSchemaValidationError LoginFlowTestSchemaValidationError + +// NewLoginFlowTestSchemaValidationError instantiates a new LoginFlowTestSchemaValidationError 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 NewLoginFlowTestSchemaValidationError() *LoginFlowTestSchemaValidationError { + this := LoginFlowTestSchemaValidationError{} + return &this +} + +// NewLoginFlowTestSchemaValidationErrorWithDefaults instantiates a new LoginFlowTestSchemaValidationError 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 NewLoginFlowTestSchemaValidationErrorWithDefaults() *LoginFlowTestSchemaValidationError { + this := LoginFlowTestSchemaValidationError{} + return &this +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *LoginFlowTestSchemaValidationError) GetMessage() string { + if o == nil || IsNil(o.Message) { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestSchemaValidationError) GetMessageOk() (*string, bool) { + if o == nil || IsNil(o.Message) { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *LoginFlowTestSchemaValidationError) HasMessage() bool { + if o != nil && !IsNil(o.Message) { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *LoginFlowTestSchemaValidationError) SetMessage(v string) { + o.Message = &v +} + +// GetPath returns the Path field value if set, zero value otherwise. +func (o *LoginFlowTestSchemaValidationError) GetPath() string { + if o == nil || IsNil(o.Path) { + var ret string + return ret + } + return *o.Path +} + +// GetPathOk returns a tuple with the Path field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestSchemaValidationError) GetPathOk() (*string, bool) { + if o == nil || IsNil(o.Path) { + return nil, false + } + return o.Path, true +} + +// HasPath returns a boolean if a field has been set. +func (o *LoginFlowTestSchemaValidationError) HasPath() bool { + if o != nil && !IsNil(o.Path) { + return true + } + + return false +} + +// SetPath gets a reference to the given string and assigns it to the Path field. +func (o *LoginFlowTestSchemaValidationError) SetPath(v string) { + o.Path = &v +} + +func (o LoginFlowTestSchemaValidationError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestSchemaValidationError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Message) { + toSerialize["message"] = o.Message + } + if !IsNil(o.Path) { + toSerialize["path"] = o.Path + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestSchemaValidationError) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestSchemaValidationError := _LoginFlowTestSchemaValidationError{} + + err = json.Unmarshal(data, &varLoginFlowTestSchemaValidationError) + + if err != nil { + return err + } + + *o = LoginFlowTestSchemaValidationError(varLoginFlowTestSchemaValidationError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "message") + delete(additionalProperties, "path") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestSchemaValidationError struct { + value *LoginFlowTestSchemaValidationError + isSet bool +} + +func (v NullableLoginFlowTestSchemaValidationError) Get() *LoginFlowTestSchemaValidationError { + return v.value +} + +func (v *NullableLoginFlowTestSchemaValidationError) Set(val *LoginFlowTestSchemaValidationError) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestSchemaValidationError) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestSchemaValidationError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestSchemaValidationError(val *LoginFlowTestSchemaValidationError) *NullableLoginFlowTestSchemaValidationError { + return &NullableLoginFlowTestSchemaValidationError{value: val, isSet: true} +} + +func (v NullableLoginFlowTestSchemaValidationError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestSchemaValidationError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_login_flow_test_step_error.go b/pkg/httpclient/model_login_flow_test_step_error.go new file mode 100644 index 0000000..5604020 --- /dev/null +++ b/pkg/httpclient/model_login_flow_test_step_error.go @@ -0,0 +1,231 @@ +/* +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" +) + +// checks if the LoginFlowTestStepError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LoginFlowTestStepError{} + +// LoginFlowTestStepError Populated when any step (token exchange, claims decode, Jsonnet evaluation, schema validation) cannot complete. +type LoginFlowTestStepError struct { + // Human-readable message describing the failure. + Message *string `json:"message,omitempty"` + // Short classification of the failure cause (for example, \"access_denied\" or \"traits_invalid\"). + Reason *string `json:"reason,omitempty"` + // Machine-readable identifier of the failed step (for example, \"token_exchange\" or \"schema_validate\"). + Step *string `json:"step,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _LoginFlowTestStepError LoginFlowTestStepError + +// NewLoginFlowTestStepError instantiates a new LoginFlowTestStepError 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 NewLoginFlowTestStepError() *LoginFlowTestStepError { + this := LoginFlowTestStepError{} + return &this +} + +// NewLoginFlowTestStepErrorWithDefaults instantiates a new LoginFlowTestStepError 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 NewLoginFlowTestStepErrorWithDefaults() *LoginFlowTestStepError { + this := LoginFlowTestStepError{} + return &this +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetMessage() string { + if o == nil || IsNil(o.Message) { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetMessageOk() (*string, bool) { + if o == nil || IsNil(o.Message) { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasMessage() bool { + if o != nil && !IsNil(o.Message) { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *LoginFlowTestStepError) SetMessage(v string) { + o.Message = &v +} + +// GetReason returns the Reason field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetReason() string { + if o == nil || IsNil(o.Reason) { + var ret string + return ret + } + return *o.Reason +} + +// GetReasonOk returns a tuple with the Reason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetReasonOk() (*string, bool) { + if o == nil || IsNil(o.Reason) { + return nil, false + } + return o.Reason, true +} + +// HasReason returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasReason() bool { + if o != nil && !IsNil(o.Reason) { + return true + } + + return false +} + +// SetReason gets a reference to the given string and assigns it to the Reason field. +func (o *LoginFlowTestStepError) SetReason(v string) { + o.Reason = &v +} + +// GetStep returns the Step field value if set, zero value otherwise. +func (o *LoginFlowTestStepError) GetStep() string { + if o == nil || IsNil(o.Step) { + var ret string + return ret + } + return *o.Step +} + +// GetStepOk returns a tuple with the Step field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlowTestStepError) GetStepOk() (*string, bool) { + if o == nil || IsNil(o.Step) { + return nil, false + } + return o.Step, true +} + +// HasStep returns a boolean if a field has been set. +func (o *LoginFlowTestStepError) HasStep() bool { + if o != nil && !IsNil(o.Step) { + return true + } + + return false +} + +// SetStep gets a reference to the given string and assigns it to the Step field. +func (o *LoginFlowTestStepError) SetStep(v string) { + o.Step = &v +} + +func (o LoginFlowTestStepError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LoginFlowTestStepError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Message) { + toSerialize["message"] = o.Message + } + if !IsNil(o.Reason) { + toSerialize["reason"] = o.Reason + } + if !IsNil(o.Step) { + toSerialize["step"] = o.Step + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LoginFlowTestStepError) UnmarshalJSON(data []byte) (err error) { + varLoginFlowTestStepError := _LoginFlowTestStepError{} + + err = json.Unmarshal(data, &varLoginFlowTestStepError) + + if err != nil { + return err + } + + *o = LoginFlowTestStepError(varLoginFlowTestStepError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "message") + delete(additionalProperties, "reason") + delete(additionalProperties, "step") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLoginFlowTestStepError struct { + value *LoginFlowTestStepError + isSet bool +} + +func (v NullableLoginFlowTestStepError) Get() *LoginFlowTestStepError { + return v.value +} + +func (v *NullableLoginFlowTestStepError) Set(val *LoginFlowTestStepError) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowTestStepError) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowTestStepError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowTestStepError(val *LoginFlowTestStepError) *NullableLoginFlowTestStepError { + return &NullableLoginFlowTestStepError{value: val, isSet: true} +} + +func (v NullableLoginFlowTestStepError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowTestStepError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_logout_flow.go b/pkg/httpclient/model_logout_flow.go new file mode 100644 index 0000000..8823e51 --- /dev/null +++ b/pkg/httpclient/model_logout_flow.go @@ -0,0 +1,198 @@ +/* +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" + "fmt" +) + +// checks if the LogoutFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LogoutFlow{} + +// LogoutFlow Logout Flow +type LogoutFlow struct { + // LogoutToken can be used to perform logout using AJAX. + LogoutToken string `json:"logout_token"` + // LogoutURL can be opened in a browser to sign the user out. format: uri + LogoutUrl string `json:"logout_url"` + AdditionalProperties map[string]interface{} +} + +type _LogoutFlow LogoutFlow + +// NewLogoutFlow instantiates a new LogoutFlow 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 NewLogoutFlow(logoutToken string, logoutUrl string) *LogoutFlow { + this := LogoutFlow{} + this.LogoutToken = logoutToken + this.LogoutUrl = logoutUrl + return &this +} + +// NewLogoutFlowWithDefaults instantiates a new LogoutFlow 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 NewLogoutFlowWithDefaults() *LogoutFlow { + this := LogoutFlow{} + return &this +} + +// GetLogoutToken returns the LogoutToken field value +func (o *LogoutFlow) GetLogoutToken() string { + if o == nil { + var ret string + return ret + } + + return o.LogoutToken +} + +// GetLogoutTokenOk returns a tuple with the LogoutToken field value +// and a boolean to check if the value has been set. +func (o *LogoutFlow) GetLogoutTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LogoutToken, true +} + +// SetLogoutToken sets field value +func (o *LogoutFlow) SetLogoutToken(v string) { + o.LogoutToken = v +} + +// GetLogoutUrl returns the LogoutUrl field value +func (o *LogoutFlow) GetLogoutUrl() string { + if o == nil { + var ret string + return ret + } + + return o.LogoutUrl +} + +// GetLogoutUrlOk returns a tuple with the LogoutUrl field value +// and a boolean to check if the value has been set. +func (o *LogoutFlow) GetLogoutUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LogoutUrl, true +} + +// SetLogoutUrl sets field value +func (o *LogoutFlow) SetLogoutUrl(v string) { + o.LogoutUrl = v +} + +func (o LogoutFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LogoutFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["logout_token"] = o.LogoutToken + toSerialize["logout_url"] = o.LogoutUrl + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *LogoutFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "logout_token", + "logout_url", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varLogoutFlow := _LogoutFlow{} + + err = json.Unmarshal(data, &varLogoutFlow) + + if err != nil { + return err + } + + *o = LogoutFlow(varLogoutFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "logout_token") + delete(additionalProperties, "logout_url") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableLogoutFlow struct { + value *LogoutFlow + isSet bool +} + +func (v NullableLogoutFlow) Get() *LogoutFlow { + return v.value +} + +func (v *NullableLogoutFlow) Set(val *LogoutFlow) { + v.value = val + v.isSet = true +} + +func (v NullableLogoutFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableLogoutFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLogoutFlow(val *LogoutFlow) *NullableLogoutFlow { + return &NullableLogoutFlow{value: val, isSet: true} +} + +func (v NullableLogoutFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLogoutFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_manage_sessions_body.go b/pkg/httpclient/model_manage_sessions_body.go new file mode 100644 index 0000000..2af0e88 --- /dev/null +++ b/pkg/httpclient/model_manage_sessions_body.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the ManageSessionsBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ManageSessionsBody{} + +// ManageSessionsBody Body for the bulk session management endpoint. Exactly one of `identities` or `sessions` must be provided. To operate on every session in the network, pass `identities: [\"*\"]` — the wildcard must appear alone, never mixed with explicit IDs. +type ManageSessionsBody struct { + // Action to perform on the matching sessions. disable ManageSessionsActionDisable delete ManageSessionsActionDelete + Action string `json:"action"` + // Identity IDs whose sessions should be disabled or deleted, or `[\"*\"]` to operate on every session in the network. Mutually exclusive with `sessions`. + Identities []string `json:"identities,omitempty"` + // Session IDs to disable or delete. Mutually exclusive with `identities`. The wildcard `[\"*\"]` is not accepted in this field — pass `identities: [\"*\"]` to scope the operation to every session in the network. + Sessions []string `json:"sessions,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ManageSessionsBody ManageSessionsBody + +// NewManageSessionsBody instantiates a new ManageSessionsBody 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 NewManageSessionsBody(action string) *ManageSessionsBody { + this := ManageSessionsBody{} + this.Action = action + return &this +} + +// NewManageSessionsBodyWithDefaults instantiates a new ManageSessionsBody 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 NewManageSessionsBodyWithDefaults() *ManageSessionsBody { + this := ManageSessionsBody{} + return &this +} + +// GetAction returns the Action field value +func (o *ManageSessionsBody) 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 *ManageSessionsBody) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ManageSessionsBody) SetAction(v string) { + o.Action = v +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *ManageSessionsBody) GetIdentities() []string { + if o == nil || IsNil(o.Identities) { + var ret []string + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsBody) GetIdentitiesOk() ([]string, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *ManageSessionsBody) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []string and assigns it to the Identities field. +func (o *ManageSessionsBody) SetIdentities(v []string) { + o.Identities = v +} + +// GetSessions returns the Sessions field value if set, zero value otherwise. +func (o *ManageSessionsBody) GetSessions() []string { + if o == nil || IsNil(o.Sessions) { + var ret []string + return ret + } + return o.Sessions +} + +// GetSessionsOk returns a tuple with the Sessions field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsBody) GetSessionsOk() ([]string, bool) { + if o == nil || IsNil(o.Sessions) { + return nil, false + } + return o.Sessions, true +} + +// HasSessions returns a boolean if a field has been set. +func (o *ManageSessionsBody) HasSessions() bool { + if o != nil && !IsNil(o.Sessions) { + return true + } + + return false +} + +// SetSessions gets a reference to the given []string and assigns it to the Sessions field. +func (o *ManageSessionsBody) SetSessions(v []string) { + o.Sessions = v +} + +func (o ManageSessionsBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ManageSessionsBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + if !IsNil(o.Sessions) { + toSerialize["sessions"] = o.Sessions + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ManageSessionsBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varManageSessionsBody := _ManageSessionsBody{} + + err = json.Unmarshal(data, &varManageSessionsBody) + + if err != nil { + return err + } + + *o = ManageSessionsBody(varManageSessionsBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "identities") + delete(additionalProperties, "sessions") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableManageSessionsBody struct { + value *ManageSessionsBody + isSet bool +} + +func (v NullableManageSessionsBody) Get() *ManageSessionsBody { + return v.value +} + +func (v *NullableManageSessionsBody) Set(val *ManageSessionsBody) { + v.value = val + v.isSet = true +} + +func (v NullableManageSessionsBody) IsSet() bool { + return v.isSet +} + +func (v *NullableManageSessionsBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableManageSessionsBody(val *ManageSessionsBody) *NullableManageSessionsBody { + return &NullableManageSessionsBody{value: val, isSet: true} +} + +func (v NullableManageSessionsBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableManageSessionsBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_manage_sessions_response.go b/pkg/httpclient/model_manage_sessions_response.go new file mode 100644 index 0000000..698ec2c --- /dev/null +++ b/pkg/httpclient/model_manage_sessions_response.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the ManageSessionsResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ManageSessionsResponse{} + +// ManageSessionsResponse Response body for the bulk session management endpoint. Reports how many rows the call processed and, for the wildcard variant, whether the network still has matching rows left. Explicit-IDs requests always return `more: false`. Wildcard callers drain the network by re-issuing the same request while `more` is `true`. +type ManageSessionsResponse struct { + // True when the call reached the per-call batch limit and additional matching rows may remain. Always false for explicit-IDs requests. + More *bool `json:"more,omitempty"` + // Number of sessions processed in this call. For `disable`, counts only sessions that were active before the call (already-inactive sessions are skipped). For `delete`, counts every matching row removed. + Processed *int64 `json:"processed,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _ManageSessionsResponse ManageSessionsResponse + +// NewManageSessionsResponse instantiates a new ManageSessionsResponse 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 NewManageSessionsResponse() *ManageSessionsResponse { + this := ManageSessionsResponse{} + return &this +} + +// NewManageSessionsResponseWithDefaults instantiates a new ManageSessionsResponse 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 NewManageSessionsResponseWithDefaults() *ManageSessionsResponse { + this := ManageSessionsResponse{} + return &this +} + +// GetMore returns the More field value if set, zero value otherwise. +func (o *ManageSessionsResponse) GetMore() bool { + if o == nil || IsNil(o.More) { + var ret bool + return ret + } + return *o.More +} + +// GetMoreOk returns a tuple with the More field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsResponse) GetMoreOk() (*bool, bool) { + if o == nil || IsNil(o.More) { + return nil, false + } + return o.More, true +} + +// HasMore returns a boolean if a field has been set. +func (o *ManageSessionsResponse) HasMore() bool { + if o != nil && !IsNil(o.More) { + return true + } + + return false +} + +// SetMore gets a reference to the given bool and assigns it to the More field. +func (o *ManageSessionsResponse) SetMore(v bool) { + o.More = &v +} + +// GetProcessed returns the Processed field value if set, zero value otherwise. +func (o *ManageSessionsResponse) GetProcessed() int64 { + if o == nil || IsNil(o.Processed) { + var ret int64 + return ret + } + return *o.Processed +} + +// GetProcessedOk returns a tuple with the Processed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManageSessionsResponse) GetProcessedOk() (*int64, bool) { + if o == nil || IsNil(o.Processed) { + return nil, false + } + return o.Processed, true +} + +// HasProcessed returns a boolean if a field has been set. +func (o *ManageSessionsResponse) HasProcessed() bool { + if o != nil && !IsNil(o.Processed) { + return true + } + + return false +} + +// SetProcessed gets a reference to the given int64 and assigns it to the Processed field. +func (o *ManageSessionsResponse) SetProcessed(v int64) { + o.Processed = &v +} + +func (o ManageSessionsResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ManageSessionsResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.More) { + toSerialize["more"] = o.More + } + if !IsNil(o.Processed) { + toSerialize["processed"] = o.Processed + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *ManageSessionsResponse) UnmarshalJSON(data []byte) (err error) { + varManageSessionsResponse := _ManageSessionsResponse{} + + err = json.Unmarshal(data, &varManageSessionsResponse) + + if err != nil { + return err + } + + *o = ManageSessionsResponse(varManageSessionsResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "more") + delete(additionalProperties, "processed") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableManageSessionsResponse struct { + value *ManageSessionsResponse + isSet bool +} + +func (v NullableManageSessionsResponse) Get() *ManageSessionsResponse { + return v.value +} + +func (v *NullableManageSessionsResponse) Set(val *ManageSessionsResponse) { + v.value = val + v.isSet = true +} + +func (v NullableManageSessionsResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableManageSessionsResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableManageSessionsResponse(val *ManageSessionsResponse) *NullableManageSessionsResponse { + return &NullableManageSessionsResponse{value: val, isSet: true} +} + +func (v NullableManageSessionsResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableManageSessionsResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_message.go b/pkg/httpclient/model_message.go new file mode 100644 index 0000000..5dbfffd --- /dev/null +++ b/pkg/httpclient/model_message.go @@ -0,0 +1,507 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Message type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Message{} + +// Message struct for Message +type Message struct { + Body string `json:"body"` + Channel *string `json:"channel,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at"` + // Dispatches store information about the attempts of delivering a message May contain an error if any happened, or just the `success` state. + Dispatches []MessageDispatch `json:"dispatches,omitempty"` + Id string `json:"id"` + Recipient string `json:"recipient"` + SendCount int64 `json:"send_count"` + Status CourierMessageStatus `json:"status"` + Subject string `json:"subject"` + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid verifiable_address_changed TypeVerifiableAddressChanged authenticator_key_added TypeAuthenticatorKeyAdded + TemplateType string `json:"template_type"` + Type CourierMessageType `json:"type"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at"` + AdditionalProperties map[string]interface{} +} + +type _Message Message + +// NewMessage instantiates a new Message 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 NewMessage(body string, createdAt time.Time, id string, recipient string, sendCount int64, status CourierMessageStatus, subject string, templateType string, type_ CourierMessageType, updatedAt time.Time) *Message { + this := Message{} + this.Body = body + this.CreatedAt = createdAt + this.Id = id + this.Recipient = recipient + this.SendCount = sendCount + this.Status = status + this.Subject = subject + this.TemplateType = templateType + this.Type = type_ + this.UpdatedAt = updatedAt + return &this +} + +// NewMessageWithDefaults instantiates a new Message 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 NewMessageWithDefaults() *Message { + this := Message{} + return &this +} + +// GetBody returns the Body field value +func (o *Message) GetBody() string { + if o == nil { + var ret string + return ret + } + + return o.Body +} + +// GetBodyOk returns a tuple with the Body field value +// and a boolean to check if the value has been set. +func (o *Message) GetBodyOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Body, true +} + +// SetBody sets field value +func (o *Message) SetBody(v string) { + o.Body = v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *Message) GetChannel() string { + if o == nil || IsNil(o.Channel) { + 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 *Message) GetChannelOk() (*string, bool) { + if o == nil || IsNil(o.Channel) { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *Message) HasChannel() bool { + if o != nil && !IsNil(o.Channel) { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *Message) SetChannel(v string) { + o.Channel = &v +} + +// GetCreatedAt returns the CreatedAt field value +func (o *Message) GetCreatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value +// and a boolean to check if the value has been set. +func (o *Message) GetCreatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.CreatedAt, true +} + +// SetCreatedAt sets field value +func (o *Message) SetCreatedAt(v time.Time) { + o.CreatedAt = v +} + +// GetDispatches returns the Dispatches field value if set, zero value otherwise. +func (o *Message) GetDispatches() []MessageDispatch { + if o == nil || IsNil(o.Dispatches) { + var ret []MessageDispatch + return ret + } + return o.Dispatches +} + +// GetDispatchesOk returns a tuple with the Dispatches field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Message) GetDispatchesOk() ([]MessageDispatch, bool) { + if o == nil || IsNil(o.Dispatches) { + return nil, false + } + return o.Dispatches, true +} + +// HasDispatches returns a boolean if a field has been set. +func (o *Message) HasDispatches() bool { + if o != nil && !IsNil(o.Dispatches) { + return true + } + + return false +} + +// SetDispatches gets a reference to the given []MessageDispatch and assigns it to the Dispatches field. +func (o *Message) SetDispatches(v []MessageDispatch) { + o.Dispatches = v +} + +// GetId returns the Id field value +func (o *Message) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Message) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Message) SetId(v string) { + o.Id = v +} + +// GetRecipient returns the Recipient field value +func (o *Message) GetRecipient() string { + if o == nil { + var ret string + return ret + } + + return o.Recipient +} + +// GetRecipientOk returns a tuple with the Recipient field value +// and a boolean to check if the value has been set. +func (o *Message) GetRecipientOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Recipient, true +} + +// SetRecipient sets field value +func (o *Message) SetRecipient(v string) { + o.Recipient = v +} + +// GetSendCount returns the SendCount field value +func (o *Message) GetSendCount() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.SendCount +} + +// GetSendCountOk returns a tuple with the SendCount field value +// and a boolean to check if the value has been set. +func (o *Message) GetSendCountOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.SendCount, true +} + +// SetSendCount sets field value +func (o *Message) SetSendCount(v int64) { + o.SendCount = v +} + +// GetStatus returns the Status field value +func (o *Message) GetStatus() CourierMessageStatus { + if o == nil { + var ret CourierMessageStatus + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *Message) GetStatusOk() (*CourierMessageStatus, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *Message) SetStatus(v CourierMessageStatus) { + o.Status = v +} + +// GetSubject returns the Subject field value +func (o *Message) GetSubject() string { + if o == nil { + var ret string + return ret + } + + return o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value +// and a boolean to check if the value has been set. +func (o *Message) GetSubjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Subject, true +} + +// SetSubject sets field value +func (o *Message) SetSubject(v string) { + o.Subject = v +} + +// GetTemplateType returns the TemplateType field value +func (o *Message) GetTemplateType() string { + if o == nil { + var ret string + return ret + } + + return o.TemplateType +} + +// GetTemplateTypeOk returns a tuple with the TemplateType field value +// and a boolean to check if the value has been set. +func (o *Message) GetTemplateTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.TemplateType, true +} + +// SetTemplateType sets field value +func (o *Message) SetTemplateType(v string) { + o.TemplateType = v +} + +// GetType returns the Type field value +func (o *Message) GetType() CourierMessageType { + if o == nil { + var ret CourierMessageType + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *Message) GetTypeOk() (*CourierMessageType, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *Message) SetType(v CourierMessageType) { + o.Type = v +} + +// GetUpdatedAt returns the UpdatedAt field value +func (o *Message) GetUpdatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value +// and a boolean to check if the value has been set. +func (o *Message) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.UpdatedAt, true +} + +// SetUpdatedAt sets field value +func (o *Message) SetUpdatedAt(v time.Time) { + o.UpdatedAt = v +} + +func (o Message) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Message) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["body"] = o.Body + if !IsNil(o.Channel) { + toSerialize["channel"] = o.Channel + } + toSerialize["created_at"] = o.CreatedAt + if !IsNil(o.Dispatches) { + toSerialize["dispatches"] = o.Dispatches + } + toSerialize["id"] = o.Id + toSerialize["recipient"] = o.Recipient + toSerialize["send_count"] = o.SendCount + toSerialize["status"] = o.Status + toSerialize["subject"] = o.Subject + toSerialize["template_type"] = o.TemplateType + toSerialize["type"] = o.Type + toSerialize["updated_at"] = o.UpdatedAt + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Message) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "body", + "created_at", + "id", + "recipient", + "send_count", + "status", + "subject", + "template_type", + "type", + "updated_at", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varMessage := _Message{} + + err = json.Unmarshal(data, &varMessage) + + if err != nil { + return err + } + + *o = Message(varMessage) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "body") + delete(additionalProperties, "channel") + delete(additionalProperties, "created_at") + delete(additionalProperties, "dispatches") + delete(additionalProperties, "id") + delete(additionalProperties, "recipient") + delete(additionalProperties, "send_count") + delete(additionalProperties, "status") + delete(additionalProperties, "subject") + delete(additionalProperties, "template_type") + delete(additionalProperties, "type") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableMessage struct { + value *Message + isSet bool +} + +func (v NullableMessage) Get() *Message { + return v.value +} + +func (v *NullableMessage) Set(val *Message) { + v.value = val + v.isSet = true +} + +func (v NullableMessage) IsSet() bool { + return v.isSet +} + +func (v *NullableMessage) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMessage(val *Message) *NullableMessage { + return &NullableMessage{value: val, isSet: true} +} + +func (v NullableMessage) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMessage) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_message_dispatch.go b/pkg/httpclient/model_message_dispatch.go new file mode 100644 index 0000000..a7a118c --- /dev/null +++ b/pkg/httpclient/model_message_dispatch.go @@ -0,0 +1,326 @@ +/* +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" + "fmt" + "time" +) + +// checks if the MessageDispatch type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &MessageDispatch{} + +// MessageDispatch MessageDispatch represents an attempt of sending a courier message It contains the status of the attempt (failed or successful) and the error if any occured +type MessageDispatch struct { + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at"` + Error map[string]interface{} `json:"error,omitempty"` + // The ID of this message dispatch + Id string `json:"id"` + // The ID of the message being dispatched + MessageId string `json:"message_id"` + // The status of this dispatch Either \"failed\" or \"success\" failed CourierMessageDispatchStatusFailed success CourierMessageDispatchStatusSuccess + Status string `json:"status"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at"` + AdditionalProperties map[string]interface{} +} + +type _MessageDispatch MessageDispatch + +// NewMessageDispatch instantiates a new MessageDispatch 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 NewMessageDispatch(createdAt time.Time, id string, messageId string, status string, updatedAt time.Time) *MessageDispatch { + this := MessageDispatch{} + this.CreatedAt = createdAt + this.Id = id + this.MessageId = messageId + this.Status = status + this.UpdatedAt = updatedAt + return &this +} + +// NewMessageDispatchWithDefaults instantiates a new MessageDispatch 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 NewMessageDispatchWithDefaults() *MessageDispatch { + this := MessageDispatch{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value +func (o *MessageDispatch) GetCreatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetCreatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.CreatedAt, true +} + +// SetCreatedAt sets field value +func (o *MessageDispatch) SetCreatedAt(v time.Time) { + o.CreatedAt = v +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *MessageDispatch) GetError() map[string]interface{} { + if o == nil || IsNil(o.Error) { + var ret map[string]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. +func (o *MessageDispatch) GetErrorOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Error) { + return map[string]interface{}{}, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *MessageDispatch) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given map[string]interface{} and assigns it to the Error field. +func (o *MessageDispatch) SetError(v map[string]interface{}) { + o.Error = v +} + +// GetId returns the Id field value +func (o *MessageDispatch) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *MessageDispatch) SetId(v string) { + o.Id = v +} + +// GetMessageId returns the MessageId field value +func (o *MessageDispatch) GetMessageId() string { + if o == nil { + var ret string + return ret + } + + return o.MessageId +} + +// GetMessageIdOk returns a tuple with the MessageId field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetMessageIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.MessageId, true +} + +// SetMessageId sets field value +func (o *MessageDispatch) SetMessageId(v string) { + o.MessageId = v +} + +// GetStatus returns the Status field value +func (o *MessageDispatch) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *MessageDispatch) SetStatus(v string) { + o.Status = v +} + +// GetUpdatedAt returns the UpdatedAt field value +func (o *MessageDispatch) GetUpdatedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value +// and a boolean to check if the value has been set. +func (o *MessageDispatch) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.UpdatedAt, true +} + +// SetUpdatedAt sets field value +func (o *MessageDispatch) SetUpdatedAt(v time.Time) { + o.UpdatedAt = v +} + +func (o MessageDispatch) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o MessageDispatch) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["created_at"] = o.CreatedAt + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["id"] = o.Id + toSerialize["message_id"] = o.MessageId + toSerialize["status"] = o.Status + toSerialize["updated_at"] = o.UpdatedAt + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *MessageDispatch) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "created_at", + "id", + "message_id", + "status", + "updated_at", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varMessageDispatch := _MessageDispatch{} + + err = json.Unmarshal(data, &varMessageDispatch) + + if err != nil { + return err + } + + *o = MessageDispatch(varMessageDispatch) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "error") + delete(additionalProperties, "id") + delete(additionalProperties, "message_id") + delete(additionalProperties, "status") + delete(additionalProperties, "updated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableMessageDispatch struct { + value *MessageDispatch + isSet bool +} + +func (v NullableMessageDispatch) Get() *MessageDispatch { + return v.value +} + +func (v *NullableMessageDispatch) Set(val *MessageDispatch) { + v.value = val + v.isSet = true +} + +func (v NullableMessageDispatch) IsSet() bool { + return v.isSet +} + +func (v *NullableMessageDispatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMessageDispatch(val *MessageDispatch) *NullableMessageDispatch { + return &NullableMessageDispatch{value: val, isSet: true} +} + +func (v NullableMessageDispatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMessageDispatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_needs_privileged_session_error.go b/pkg/httpclient/model_needs_privileged_session_error.go new file mode 100644 index 0000000..6b26e35 --- /dev/null +++ b/pkg/httpclient/model_needs_privileged_session_error.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the NeedsPrivilegedSessionError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &NeedsPrivilegedSessionError{} + +// NeedsPrivilegedSessionError struct for NeedsPrivilegedSessionError +type NeedsPrivilegedSessionError struct { + Error *GenericError `json:"error,omitempty"` + // Points to where to redirect the user to next. + RedirectBrowserTo string `json:"redirect_browser_to"` + AdditionalProperties map[string]interface{} +} + +type _NeedsPrivilegedSessionError NeedsPrivilegedSessionError + +// NewNeedsPrivilegedSessionError instantiates a new NeedsPrivilegedSessionError 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 NewNeedsPrivilegedSessionError(redirectBrowserTo string) *NeedsPrivilegedSessionError { + this := NeedsPrivilegedSessionError{} + this.RedirectBrowserTo = redirectBrowserTo + return &this +} + +// NewNeedsPrivilegedSessionErrorWithDefaults instantiates a new NeedsPrivilegedSessionError 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 NewNeedsPrivilegedSessionErrorWithDefaults() *NeedsPrivilegedSessionError { + this := NeedsPrivilegedSessionError{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *NeedsPrivilegedSessionError) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *NeedsPrivilegedSessionError) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *NeedsPrivilegedSessionError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *NeedsPrivilegedSessionError) SetError(v GenericError) { + o.Error = &v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value +func (o *NeedsPrivilegedSessionError) 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 *NeedsPrivilegedSessionError) GetRedirectBrowserToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RedirectBrowserTo, true +} + +// SetRedirectBrowserTo sets field value +func (o *NeedsPrivilegedSessionError) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = v +} + +func (o NeedsPrivilegedSessionError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o NeedsPrivilegedSessionError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *NeedsPrivilegedSessionError) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "redirect_browser_to", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varNeedsPrivilegedSessionError := _NeedsPrivilegedSessionError{} + + err = json.Unmarshal(data, &varNeedsPrivilegedSessionError) + + if err != nil { + return err + } + + *o = NeedsPrivilegedSessionError(varNeedsPrivilegedSessionError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "redirect_browser_to") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableNeedsPrivilegedSessionError struct { + value *NeedsPrivilegedSessionError + isSet bool +} + +func (v NullableNeedsPrivilegedSessionError) Get() *NeedsPrivilegedSessionError { + return v.value +} + +func (v *NullableNeedsPrivilegedSessionError) Set(val *NeedsPrivilegedSessionError) { + v.value = val + v.isSet = true +} + +func (v NullableNeedsPrivilegedSessionError) IsSet() bool { + return v.isSet +} + +func (v *NullableNeedsPrivilegedSessionError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableNeedsPrivilegedSessionError(val *NeedsPrivilegedSessionError) *NullableNeedsPrivilegedSessionError { + return &NullableNeedsPrivilegedSessionError{value: val, isSet: true} +} + +func (v NullableNeedsPrivilegedSessionError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableNeedsPrivilegedSessionError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_o_auth2_client.go b/pkg/httpclient/model_o_auth2_client.go new file mode 100644 index 0000000..f731a0e --- /dev/null +++ b/pkg/httpclient/model_o_auth2_client.go @@ -0,0 +1,1935 @@ +/* +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" + "time" +) + +// checks if the OAuth2Client type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2Client{} + +// OAuth2Client struct for OAuth2Client +type OAuth2Client struct { + // 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"` + Audience []string `json:"audience,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantAccessTokenLifespan *string `json:"authorization_code_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantIdTokenLifespan *string `json:"authorization_code_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + AuthorizationCodeGrantRefreshTokenLifespan *string `json:"authorization_code_grant_refresh_token_lifespan,omitempty"` + // OpenID Connect Back-Channel Logout Session Required Boolean value specifying whether the RP requires that a sid (session ID) Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. + BackchannelLogoutSessionRequired *bool `json:"backchannel_logout_session_required,omitempty"` + // OpenID Connect Back-Channel Logout URI RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. + BackchannelLogoutUri *string `json:"backchannel_logout_uri,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ClientCredentialsGrantAccessTokenLifespan *string `json:"client_credentials_grant_access_token_lifespan,omitempty"` + // OAuth 2.0 Client ID The ID is immutable. If no ID is provided, a UUID4 will be generated. + ClientId *string `json:"client_id,omitempty"` + // OAuth 2.0 Client Name The human-readable name of the client to be presented to the end-user during authorization. + ClientName *string `json:"client_name,omitempty"` + // OAuth 2.0 Client Secret The secret will be included in the create request as cleartext, and then never again. The secret is kept in hashed format and is not recoverable once lost. + ClientSecret *string `json:"client_secret,omitempty"` + // OAuth 2.0 Client Secret Expires At The field is currently not supported and its value is always 0. + ClientSecretExpiresAt *int64 `json:"client_secret_expires_at,omitempty"` + // OAuth 2.0 Client URI ClientURI is a URL string of a web page providing information about the client. If present, the server SHOULD display this URL to the end-user in a clickable fashion. + ClientUri *string `json:"client_uri,omitempty"` + Contacts []string `json:"contacts,omitempty"` + // OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. + CreatedAt *time.Time `json:"created_at,omitempty"` + // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. + FrontchannelLogoutSessionRequired *bool `json:"frontchannel_logout_session_required,omitempty"` + // OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. + FrontchannelLogoutUri *string `json:"frontchannel_logout_uri,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ImplicitGrantAccessTokenLifespan *string `json:"implicit_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + ImplicitGrantIdTokenLifespan *string `json:"implicit_grant_id_token_lifespan,omitempty"` + // OAuth 2.0 Client JSON Web Key Set Client's JSON Web Key Set [JWK] document, passed by value. The semantics of the jwks parameter are the same as the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri parameter, for instance, by native applications that might not have a location to host the contents of the JWK Set. If a Client can use jwks_uri, it MUST NOT use jwks. One significant downside of jwks is that it does not enable key rotation (which jwks_uri does, as described in Section 10 of OpenID Connect Core 1.0 [OpenID.Core]). The jwks_uri and jwks parameters MUST NOT be used together. + Jwks interface{} `json:"jwks,omitempty"` + // OAuth 2.0 Client JSON Web Key Set URL URL for the Client's JSON Web Key Set [JWK] document. If the Client signs requests to the Server, it contains the signing key(s) the Server uses to validate signatures from the Client. The JWK Set MAY also contain the Client's encryption keys(s), which are used by the Server to encrypt responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage. Although some algorithms allow the same key to be used for both signatures and encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match those in the certificate. + JwksUri *string `json:"jwks_uri,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + JwtBearerGrantAccessTokenLifespan *string `json:"jwt_bearer_grant_access_token_lifespan,omitempty"` + // OAuth 2.0 Client Logo URI A URL string referencing the client's logo. + LogoUri *string `json:"logo_uri,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + // OAuth 2.0 Client Owner Owner is a string identifying the owner of the OAuth 2.0 Client. + Owner *string `json:"owner,omitempty"` + // OAuth 2.0 Client Policy URI PolicyURI is a URL string that points to a human-readable privacy policy document that describes how the deployment organization collects, uses, retains, and discloses personal data. + PolicyUri *string `json:"policy_uri,omitempty"` + PostLogoutRedirectUris []string `json:"post_logout_redirect_uris,omitempty"` + RedirectUris []string `json:"redirect_uris,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantAccessTokenLifespan *string `json:"refresh_token_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantIdTokenLifespan *string `json:"refresh_token_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + RefreshTokenGrantRefreshTokenLifespan *string `json:"refresh_token_grant_refresh_token_lifespan,omitempty"` + // OpenID Connect Dynamic Client Registration Access Token RegistrationAccessToken can be used to update, get, or delete the OAuth2 Client. It is sent when creating a client using Dynamic Client Registration. + RegistrationAccessToken *string `json:"registration_access_token,omitempty"` + // OpenID Connect Dynamic Client Registration URL RegistrationClientURI is the URL used to update, get, or delete the OAuth2 Client. + RegistrationClientUri *string `json:"registration_client_uri,omitempty"` + // OpenID Connect Request Object Signing Algorithm JWS [JWS] alg algorithm [JWA] that MUST be used for signing Request Objects sent to the OP. All Request Objects from this Client MUST be rejected, if not signed with this algorithm. + RequestObjectSigningAlg *string `json:"request_object_signing_alg,omitempty"` + RequestUris []string `json:"request_uris,omitempty"` + ResponseTypes []string `json:"response_types,omitempty"` + // OAuth 2.0 Client Scope Scope is a string containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. + Scope *string `json:"scope,omitempty"` + // OpenID Connect Sector Identifier URI URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. The URL references a file with a single JSON array of redirect_uri values. + SectorIdentifierUri *string `json:"sector_identifier_uri,omitempty"` + // SkipConsent skips the consent screen for this client. This field can only be set from the admin API. + SkipConsent *bool `json:"skip_consent,omitempty"` + // SkipLogoutConsent skips the logout consent screen for this client. This field can only be set from the admin API. + SkipLogoutConsent *bool `json:"skip_logout_consent,omitempty"` + // OpenID Connect Subject Type The `subject_types_supported` Discovery parameter contains a list of the supported subject_type values for this server. Valid types include `pairwise` and `public`. + SubjectType *string `json:"subject_type,omitempty"` + // OAuth 2.0 Token Endpoint Authentication Method Requested Client Authentication method for the Token Endpoint. The options are: `client_secret_basic`: (default) Send `client_id` and `client_secret` as `application/x-www-form-urlencoded` encoded in the HTTP Authorization header. `client_secret_post`: Send `client_id` and `client_secret` as `application/x-www-form-urlencoded` in the HTTP body. `private_key_jwt`: Use JSON Web Tokens to authenticate the client. `none`: Used for public clients (native apps, mobile apps) which can not have secrets. + TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"` + // OAuth 2.0 Token Endpoint Signing Algorithm Requested Client Authentication signing algorithm for the Token Endpoint. + TokenEndpointAuthSigningAlg *string `json:"token_endpoint_auth_signing_alg,omitempty"` + // OAuth 2.0 Client Terms of Service URI A URL string pointing to a human-readable terms of service document for the client that describes a contractual relationship between the end-user and the client that the end-user accepts when authorizing the client. + TosUri *string `json:"tos_uri,omitempty"` + // OAuth 2.0 Client Last Update Date UpdatedAt returns the timestamp of the last update. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // OpenID Connect Request Userinfo Signed Response Algorithm JWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses. If this is specified, the response will be JWT [JWT] serialized, and signed using JWS. The default, if omitted, is for the UserInfo Response to return the Claims as a UTF-8 encoded JSON object using the application/json content-type. + UserinfoSignedResponseAlg *string `json:"userinfo_signed_response_alg,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2Client OAuth2Client + +// NewOAuth2Client instantiates a new OAuth2Client 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 NewOAuth2Client() *OAuth2Client { + this := OAuth2Client{} + return &this +} + +// NewOAuth2ClientWithDefaults instantiates a new OAuth2Client 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 NewOAuth2ClientWithDefaults() *OAuth2Client { + this := OAuth2Client{} + return &this +} + +// GetAccessTokenStrategy returns the AccessTokenStrategy field value if set, zero value otherwise. +func (o *OAuth2Client) GetAccessTokenStrategy() string { + if o == nil || IsNil(o.AccessTokenStrategy) { + var ret string + return ret + } + return *o.AccessTokenStrategy +} + +// GetAccessTokenStrategyOk returns a tuple with the AccessTokenStrategy field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAccessTokenStrategyOk() (*string, bool) { + if o == nil || IsNil(o.AccessTokenStrategy) { + return nil, false + } + return o.AccessTokenStrategy, true +} + +// HasAccessTokenStrategy returns a boolean if a field has been set. +func (o *OAuth2Client) HasAccessTokenStrategy() bool { + if o != nil && !IsNil(o.AccessTokenStrategy) { + return true + } + + return false +} + +// SetAccessTokenStrategy gets a reference to the given string and assigns it to the AccessTokenStrategy field. +func (o *OAuth2Client) SetAccessTokenStrategy(v string) { + o.AccessTokenStrategy = &v +} + +// GetAllowedCorsOrigins returns the AllowedCorsOrigins field value if set, zero value otherwise. +func (o *OAuth2Client) GetAllowedCorsOrigins() []string { + if o == nil || IsNil(o.AllowedCorsOrigins) { + var ret []string + return ret + } + return o.AllowedCorsOrigins +} + +// GetAllowedCorsOriginsOk returns a tuple with the AllowedCorsOrigins field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAllowedCorsOriginsOk() ([]string, bool) { + if o == nil || IsNil(o.AllowedCorsOrigins) { + return nil, false + } + return o.AllowedCorsOrigins, true +} + +// HasAllowedCorsOrigins returns a boolean if a field has been set. +func (o *OAuth2Client) HasAllowedCorsOrigins() bool { + if o != nil && !IsNil(o.AllowedCorsOrigins) { + return true + } + + return false +} + +// SetAllowedCorsOrigins gets a reference to the given []string and assigns it to the AllowedCorsOrigins field. +func (o *OAuth2Client) SetAllowedCorsOrigins(v []string) { + o.AllowedCorsOrigins = v +} + +// GetAudience returns the Audience field value if set, zero value otherwise. +func (o *OAuth2Client) GetAudience() []string { + if o == nil || IsNil(o.Audience) { + var ret []string + return ret + } + return o.Audience +} + +// GetAudienceOk returns a tuple with the Audience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.Audience) { + return nil, false + } + return o.Audience, true +} + +// HasAudience returns a boolean if a field has been set. +func (o *OAuth2Client) HasAudience() bool { + if o != nil && !IsNil(o.Audience) { + return true + } + + return false +} + +// SetAudience gets a reference to the given []string and assigns it to the Audience field. +func (o *OAuth2Client) SetAudience(v []string) { + o.Audience = v +} + +// GetAuthorizationCodeGrantAccessTokenLifespan returns the AuthorizationCodeGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantAccessTokenLifespan +} + +// GetAuthorizationCodeGrantAccessTokenLifespanOk returns a tuple with the AuthorizationCodeGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantAccessTokenLifespan, true +} + +// HasAuthorizationCodeGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantAccessTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantAccessTokenLifespan(v string) { + o.AuthorizationCodeGrantAccessTokenLifespan = &v +} + +// GetAuthorizationCodeGrantIdTokenLifespan returns the AuthorizationCodeGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantIdTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantIdTokenLifespan +} + +// GetAuthorizationCodeGrantIdTokenLifespanOk returns a tuple with the AuthorizationCodeGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantIdTokenLifespan, true +} + +// HasAuthorizationCodeGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantIdTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantIdTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantIdTokenLifespan(v string) { + o.AuthorizationCodeGrantIdTokenLifespan = &v +} + +// GetAuthorizationCodeGrantRefreshTokenLifespan returns the AuthorizationCodeGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetAuthorizationCodeGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.AuthorizationCodeGrantRefreshTokenLifespan +} + +// GetAuthorizationCodeGrantRefreshTokenLifespanOk returns a tuple with the AuthorizationCodeGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAuthorizationCodeGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + return nil, false + } + return o.AuthorizationCodeGrantRefreshTokenLifespan, true +} + +// HasAuthorizationCodeGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasAuthorizationCodeGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetAuthorizationCodeGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the AuthorizationCodeGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetAuthorizationCodeGrantRefreshTokenLifespan(v string) { + o.AuthorizationCodeGrantRefreshTokenLifespan = &v +} + +// GetBackchannelLogoutSessionRequired returns the BackchannelLogoutSessionRequired field value if set, zero value otherwise. +func (o *OAuth2Client) GetBackchannelLogoutSessionRequired() bool { + if o == nil || IsNil(o.BackchannelLogoutSessionRequired) { + var ret bool + return ret + } + return *o.BackchannelLogoutSessionRequired +} + +// GetBackchannelLogoutSessionRequiredOk returns a tuple with the BackchannelLogoutSessionRequired field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetBackchannelLogoutSessionRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.BackchannelLogoutSessionRequired) { + return nil, false + } + return o.BackchannelLogoutSessionRequired, true +} + +// HasBackchannelLogoutSessionRequired returns a boolean if a field has been set. +func (o *OAuth2Client) HasBackchannelLogoutSessionRequired() bool { + if o != nil && !IsNil(o.BackchannelLogoutSessionRequired) { + return true + } + + return false +} + +// SetBackchannelLogoutSessionRequired gets a reference to the given bool and assigns it to the BackchannelLogoutSessionRequired field. +func (o *OAuth2Client) SetBackchannelLogoutSessionRequired(v bool) { + o.BackchannelLogoutSessionRequired = &v +} + +// GetBackchannelLogoutUri returns the BackchannelLogoutUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetBackchannelLogoutUri() string { + if o == nil || IsNil(o.BackchannelLogoutUri) { + var ret string + return ret + } + return *o.BackchannelLogoutUri +} + +// GetBackchannelLogoutUriOk returns a tuple with the BackchannelLogoutUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetBackchannelLogoutUriOk() (*string, bool) { + if o == nil || IsNil(o.BackchannelLogoutUri) { + return nil, false + } + return o.BackchannelLogoutUri, true +} + +// HasBackchannelLogoutUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasBackchannelLogoutUri() bool { + if o != nil && !IsNil(o.BackchannelLogoutUri) { + return true + } + + return false +} + +// SetBackchannelLogoutUri gets a reference to the given string and assigns it to the BackchannelLogoutUri field. +func (o *OAuth2Client) SetBackchannelLogoutUri(v string) { + o.BackchannelLogoutUri = &v +} + +// GetClientCredentialsGrantAccessTokenLifespan returns the ClientCredentialsGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientCredentialsGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.ClientCredentialsGrantAccessTokenLifespan +} + +// GetClientCredentialsGrantAccessTokenLifespanOk returns a tuple with the ClientCredentialsGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientCredentialsGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + return nil, false + } + return o.ClientCredentialsGrantAccessTokenLifespan, true +} + +// HasClientCredentialsGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientCredentialsGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetClientCredentialsGrantAccessTokenLifespan gets a reference to the given string and assigns it to the ClientCredentialsGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetClientCredentialsGrantAccessTokenLifespan(v string) { + o.ClientCredentialsGrantAccessTokenLifespan = &v +} + +// GetClientId returns the ClientId field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientId() string { + if o == nil || IsNil(o.ClientId) { + var ret string + return ret + } + return *o.ClientId +} + +// GetClientIdOk returns a tuple with the ClientId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientId) { + return nil, false + } + return o.ClientId, true +} + +// HasClientId returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientId() bool { + if o != nil && !IsNil(o.ClientId) { + return true + } + + return false +} + +// SetClientId gets a reference to the given string and assigns it to the ClientId field. +func (o *OAuth2Client) SetClientId(v string) { + o.ClientId = &v +} + +// GetClientName returns the ClientName field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientName() string { + if o == nil || IsNil(o.ClientName) { + var ret string + return ret + } + return *o.ClientName +} + +// GetClientNameOk returns a tuple with the ClientName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientNameOk() (*string, bool) { + if o == nil || IsNil(o.ClientName) { + return nil, false + } + return o.ClientName, true +} + +// HasClientName returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientName() bool { + if o != nil && !IsNil(o.ClientName) { + return true + } + + return false +} + +// SetClientName gets a reference to the given string and assigns it to the ClientName field. +func (o *OAuth2Client) SetClientName(v string) { + o.ClientName = &v +} + +// GetClientSecret returns the ClientSecret field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientSecret() string { + if o == nil || IsNil(o.ClientSecret) { + var ret string + return ret + } + return *o.ClientSecret +} + +// GetClientSecretOk returns a tuple with the ClientSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientSecretOk() (*string, bool) { + if o == nil || IsNil(o.ClientSecret) { + return nil, false + } + return o.ClientSecret, true +} + +// HasClientSecret returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientSecret() bool { + if o != nil && !IsNil(o.ClientSecret) { + return true + } + + return false +} + +// SetClientSecret gets a reference to the given string and assigns it to the ClientSecret field. +func (o *OAuth2Client) SetClientSecret(v string) { + o.ClientSecret = &v +} + +// GetClientSecretExpiresAt returns the ClientSecretExpiresAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientSecretExpiresAt() int64 { + if o == nil || IsNil(o.ClientSecretExpiresAt) { + var ret int64 + return ret + } + return *o.ClientSecretExpiresAt +} + +// GetClientSecretExpiresAtOk returns a tuple with the ClientSecretExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientSecretExpiresAtOk() (*int64, bool) { + if o == nil || IsNil(o.ClientSecretExpiresAt) { + return nil, false + } + return o.ClientSecretExpiresAt, true +} + +// HasClientSecretExpiresAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientSecretExpiresAt() bool { + if o != nil && !IsNil(o.ClientSecretExpiresAt) { + return true + } + + return false +} + +// SetClientSecretExpiresAt gets a reference to the given int64 and assigns it to the ClientSecretExpiresAt field. +func (o *OAuth2Client) SetClientSecretExpiresAt(v int64) { + o.ClientSecretExpiresAt = &v +} + +// GetClientUri returns the ClientUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetClientUri() string { + if o == nil || IsNil(o.ClientUri) { + var ret string + return ret + } + return *o.ClientUri +} + +// GetClientUriOk returns a tuple with the ClientUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetClientUriOk() (*string, bool) { + if o == nil || IsNil(o.ClientUri) { + return nil, false + } + return o.ClientUri, true +} + +// HasClientUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasClientUri() bool { + if o != nil && !IsNil(o.ClientUri) { + return true + } + + return false +} + +// SetClientUri gets a reference to the given string and assigns it to the ClientUri field. +func (o *OAuth2Client) SetClientUri(v string) { + o.ClientUri = &v +} + +// GetContacts returns the Contacts field value if set, zero value otherwise. +func (o *OAuth2Client) GetContacts() []string { + if o == nil || IsNil(o.Contacts) { + var ret []string + return ret + } + return o.Contacts +} + +// GetContactsOk returns a tuple with the Contacts field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetContactsOk() ([]string, bool) { + if o == nil || IsNil(o.Contacts) { + return nil, false + } + return o.Contacts, true +} + +// HasContacts returns a boolean if a field has been set. +func (o *OAuth2Client) HasContacts() bool { + if o != nil && !IsNil(o.Contacts) { + return true + } + + return false +} + +// SetContacts gets a reference to the given []string and assigns it to the Contacts field. +func (o *OAuth2Client) SetContacts(v []string) { + o.Contacts = v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *OAuth2Client) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetFrontchannelLogoutSessionRequired returns the FrontchannelLogoutSessionRequired field value if set, zero value otherwise. +func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool { + if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { + var ret bool + return ret + } + return *o.FrontchannelLogoutSessionRequired +} + +// GetFrontchannelLogoutSessionRequiredOk returns a tuple with the FrontchannelLogoutSessionRequired field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetFrontchannelLogoutSessionRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { + return nil, false + } + return o.FrontchannelLogoutSessionRequired, true +} + +// HasFrontchannelLogoutSessionRequired returns a boolean if a field has been set. +func (o *OAuth2Client) HasFrontchannelLogoutSessionRequired() bool { + if o != nil && !IsNil(o.FrontchannelLogoutSessionRequired) { + return true + } + + return false +} + +// SetFrontchannelLogoutSessionRequired gets a reference to the given bool and assigns it to the FrontchannelLogoutSessionRequired field. +func (o *OAuth2Client) SetFrontchannelLogoutSessionRequired(v bool) { + o.FrontchannelLogoutSessionRequired = &v +} + +// GetFrontchannelLogoutUri returns the FrontchannelLogoutUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetFrontchannelLogoutUri() string { + if o == nil || IsNil(o.FrontchannelLogoutUri) { + var ret string + return ret + } + return *o.FrontchannelLogoutUri +} + +// GetFrontchannelLogoutUriOk returns a tuple with the FrontchannelLogoutUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetFrontchannelLogoutUriOk() (*string, bool) { + if o == nil || IsNil(o.FrontchannelLogoutUri) { + return nil, false + } + return o.FrontchannelLogoutUri, true +} + +// HasFrontchannelLogoutUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasFrontchannelLogoutUri() bool { + if o != nil && !IsNil(o.FrontchannelLogoutUri) { + return true + } + + return false +} + +// SetFrontchannelLogoutUri gets a reference to the given string and assigns it to the FrontchannelLogoutUri field. +func (o *OAuth2Client) SetFrontchannelLogoutUri(v string) { + o.FrontchannelLogoutUri = &v +} + +// GetGrantTypes returns the GrantTypes field value if set, zero value otherwise. +func (o *OAuth2Client) GetGrantTypes() []string { + if o == nil || IsNil(o.GrantTypes) { + var ret []string + return ret + } + return o.GrantTypes +} + +// GetGrantTypesOk returns a tuple with the GrantTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetGrantTypesOk() ([]string, bool) { + if o == nil || IsNil(o.GrantTypes) { + return nil, false + } + return o.GrantTypes, true +} + +// HasGrantTypes returns a boolean if a field has been set. +func (o *OAuth2Client) HasGrantTypes() bool { + if o != nil && !IsNil(o.GrantTypes) { + return true + } + + return false +} + +// SetGrantTypes gets a reference to the given []string and assigns it to the GrantTypes field. +func (o *OAuth2Client) SetGrantTypes(v []string) { + o.GrantTypes = v +} + +// GetImplicitGrantAccessTokenLifespan returns the ImplicitGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetImplicitGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.ImplicitGrantAccessTokenLifespan +} + +// GetImplicitGrantAccessTokenLifespanOk returns a tuple with the ImplicitGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetImplicitGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { + return nil, false + } + return o.ImplicitGrantAccessTokenLifespan, true +} + +// HasImplicitGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasImplicitGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.ImplicitGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetImplicitGrantAccessTokenLifespan gets a reference to the given string and assigns it to the ImplicitGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetImplicitGrantAccessTokenLifespan(v string) { + o.ImplicitGrantAccessTokenLifespan = &v +} + +// GetImplicitGrantIdTokenLifespan returns the ImplicitGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetImplicitGrantIdTokenLifespan() string { + if o == nil || IsNil(o.ImplicitGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.ImplicitGrantIdTokenLifespan +} + +// GetImplicitGrantIdTokenLifespanOk returns a tuple with the ImplicitGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetImplicitGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.ImplicitGrantIdTokenLifespan) { + return nil, false + } + return o.ImplicitGrantIdTokenLifespan, true +} + +// HasImplicitGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasImplicitGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.ImplicitGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetImplicitGrantIdTokenLifespan gets a reference to the given string and assigns it to the ImplicitGrantIdTokenLifespan field. +func (o *OAuth2Client) SetImplicitGrantIdTokenLifespan(v string) { + o.ImplicitGrantIdTokenLifespan = &v +} + +// GetJwks returns the Jwks field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *OAuth2Client) GetJwks() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Jwks +} + +// GetJwksOk returns a tuple with the Jwks 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 *OAuth2Client) GetJwksOk() (*interface{}, bool) { + if o == nil || IsNil(o.Jwks) { + return nil, false + } + return &o.Jwks, true +} + +// HasJwks returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwks() bool { + if o != nil && !IsNil(o.Jwks) { + return true + } + + return false +} + +// SetJwks gets a reference to the given interface{} and assigns it to the Jwks field. +func (o *OAuth2Client) SetJwks(v interface{}) { + o.Jwks = v +} + +// GetJwksUri returns the JwksUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetJwksUri() string { + if o == nil || IsNil(o.JwksUri) { + var ret string + return ret + } + return *o.JwksUri +} + +// GetJwksUriOk returns a tuple with the JwksUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetJwksUriOk() (*string, bool) { + if o == nil || IsNil(o.JwksUri) { + return nil, false + } + return o.JwksUri, true +} + +// HasJwksUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwksUri() bool { + if o != nil && !IsNil(o.JwksUri) { + return true + } + + return false +} + +// SetJwksUri gets a reference to the given string and assigns it to the JwksUri field. +func (o *OAuth2Client) SetJwksUri(v string) { + o.JwksUri = &v +} + +// GetJwtBearerGrantAccessTokenLifespan returns the JwtBearerGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetJwtBearerGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.JwtBearerGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.JwtBearerGrantAccessTokenLifespan +} + +// GetJwtBearerGrantAccessTokenLifespanOk returns a tuple with the JwtBearerGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetJwtBearerGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.JwtBearerGrantAccessTokenLifespan) { + return nil, false + } + return o.JwtBearerGrantAccessTokenLifespan, true +} + +// HasJwtBearerGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasJwtBearerGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.JwtBearerGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetJwtBearerGrantAccessTokenLifespan gets a reference to the given string and assigns it to the JwtBearerGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetJwtBearerGrantAccessTokenLifespan(v string) { + o.JwtBearerGrantAccessTokenLifespan = &v +} + +// GetLogoUri returns the LogoUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetLogoUri() string { + if o == nil || IsNil(o.LogoUri) { + var ret string + return ret + } + return *o.LogoUri +} + +// GetLogoUriOk returns a tuple with the LogoUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetLogoUriOk() (*string, bool) { + if o == nil || IsNil(o.LogoUri) { + return nil, false + } + return o.LogoUri, true +} + +// HasLogoUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasLogoUri() bool { + if o != nil && !IsNil(o.LogoUri) { + return true + } + + return false +} + +// SetLogoUri gets a reference to the given string and assigns it to the LogoUri field. +func (o *OAuth2Client) SetLogoUri(v string) { + o.LogoUri = &v +} + +// GetMetadata returns the Metadata field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *OAuth2Client) GetMetadata() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Metadata +} + +// GetMetadataOk returns a tuple with the Metadata 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 *OAuth2Client) GetMetadataOk() (*interface{}, bool) { + if o == nil || IsNil(o.Metadata) { + return nil, false + } + return &o.Metadata, true +} + +// HasMetadata returns a boolean if a field has been set. +func (o *OAuth2Client) HasMetadata() bool { + if o != nil && !IsNil(o.Metadata) { + return true + } + + return false +} + +// SetMetadata gets a reference to the given interface{} and assigns it to the Metadata field. +func (o *OAuth2Client) SetMetadata(v interface{}) { + o.Metadata = v +} + +// GetOwner returns the Owner field value if set, zero value otherwise. +func (o *OAuth2Client) GetOwner() string { + if o == nil || IsNil(o.Owner) { + var ret string + return ret + } + return *o.Owner +} + +// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetOwnerOk() (*string, bool) { + if o == nil || IsNil(o.Owner) { + return nil, false + } + return o.Owner, true +} + +// HasOwner returns a boolean if a field has been set. +func (o *OAuth2Client) HasOwner() bool { + if o != nil && !IsNil(o.Owner) { + return true + } + + return false +} + +// SetOwner gets a reference to the given string and assigns it to the Owner field. +func (o *OAuth2Client) SetOwner(v string) { + o.Owner = &v +} + +// GetPolicyUri returns the PolicyUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetPolicyUri() string { + if o == nil || IsNil(o.PolicyUri) { + var ret string + return ret + } + return *o.PolicyUri +} + +// GetPolicyUriOk returns a tuple with the PolicyUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetPolicyUriOk() (*string, bool) { + if o == nil || IsNil(o.PolicyUri) { + return nil, false + } + return o.PolicyUri, true +} + +// HasPolicyUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasPolicyUri() bool { + if o != nil && !IsNil(o.PolicyUri) { + return true + } + + return false +} + +// SetPolicyUri gets a reference to the given string and assigns it to the PolicyUri field. +func (o *OAuth2Client) SetPolicyUri(v string) { + o.PolicyUri = &v +} + +// GetPostLogoutRedirectUris returns the PostLogoutRedirectUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetPostLogoutRedirectUris() []string { + if o == nil || IsNil(o.PostLogoutRedirectUris) { + var ret []string + return ret + } + return o.PostLogoutRedirectUris +} + +// GetPostLogoutRedirectUrisOk returns a tuple with the PostLogoutRedirectUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetPostLogoutRedirectUrisOk() ([]string, bool) { + if o == nil || IsNil(o.PostLogoutRedirectUris) { + return nil, false + } + return o.PostLogoutRedirectUris, true +} + +// HasPostLogoutRedirectUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasPostLogoutRedirectUris() bool { + if o != nil && !IsNil(o.PostLogoutRedirectUris) { + return true + } + + return false +} + +// SetPostLogoutRedirectUris gets a reference to the given []string and assigns it to the PostLogoutRedirectUris field. +func (o *OAuth2Client) SetPostLogoutRedirectUris(v []string) { + o.PostLogoutRedirectUris = v +} + +// GetRedirectUris returns the RedirectUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetRedirectUris() []string { + if o == nil || IsNil(o.RedirectUris) { + var ret []string + return ret + } + return o.RedirectUris +} + +// GetRedirectUrisOk returns a tuple with the RedirectUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRedirectUrisOk() ([]string, bool) { + if o == nil || IsNil(o.RedirectUris) { + return nil, false + } + return o.RedirectUris, true +} + +// HasRedirectUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasRedirectUris() bool { + if o != nil && !IsNil(o.RedirectUris) { + return true + } + + return false +} + +// SetRedirectUris gets a reference to the given []string and assigns it to the RedirectUris field. +func (o *OAuth2Client) SetRedirectUris(v []string) { + o.RedirectUris = v +} + +// GetRefreshTokenGrantAccessTokenLifespan returns the RefreshTokenGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantAccessTokenLifespan +} + +// GetRefreshTokenGrantAccessTokenLifespanOk returns a tuple with the RefreshTokenGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantAccessTokenLifespan, true +} + +// HasRefreshTokenGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantAccessTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantAccessTokenLifespan(v string) { + o.RefreshTokenGrantAccessTokenLifespan = &v +} + +// GetRefreshTokenGrantIdTokenLifespan returns the RefreshTokenGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantIdTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantIdTokenLifespan +} + +// GetRefreshTokenGrantIdTokenLifespanOk returns a tuple with the RefreshTokenGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantIdTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantIdTokenLifespan, true +} + +// HasRefreshTokenGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantIdTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantIdTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantIdTokenLifespan(v string) { + o.RefreshTokenGrantIdTokenLifespan = &v +} + +// GetRefreshTokenGrantRefreshTokenLifespan returns the RefreshTokenGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetRefreshTokenGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.RefreshTokenGrantRefreshTokenLifespan +} + +// GetRefreshTokenGrantRefreshTokenLifespanOk returns a tuple with the RefreshTokenGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRefreshTokenGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + return nil, false + } + return o.RefreshTokenGrantRefreshTokenLifespan, true +} + +// HasRefreshTokenGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasRefreshTokenGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetRefreshTokenGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the RefreshTokenGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetRefreshTokenGrantRefreshTokenLifespan(v string) { + o.RefreshTokenGrantRefreshTokenLifespan = &v +} + +// GetRegistrationAccessToken returns the RegistrationAccessToken field value if set, zero value otherwise. +func (o *OAuth2Client) GetRegistrationAccessToken() string { + if o == nil || IsNil(o.RegistrationAccessToken) { + var ret string + return ret + } + return *o.RegistrationAccessToken +} + +// GetRegistrationAccessTokenOk returns a tuple with the RegistrationAccessToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRegistrationAccessTokenOk() (*string, bool) { + if o == nil || IsNil(o.RegistrationAccessToken) { + return nil, false + } + return o.RegistrationAccessToken, true +} + +// HasRegistrationAccessToken returns a boolean if a field has been set. +func (o *OAuth2Client) HasRegistrationAccessToken() bool { + if o != nil && !IsNil(o.RegistrationAccessToken) { + return true + } + + return false +} + +// SetRegistrationAccessToken gets a reference to the given string and assigns it to the RegistrationAccessToken field. +func (o *OAuth2Client) SetRegistrationAccessToken(v string) { + o.RegistrationAccessToken = &v +} + +// GetRegistrationClientUri returns the RegistrationClientUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetRegistrationClientUri() string { + if o == nil || IsNil(o.RegistrationClientUri) { + var ret string + return ret + } + return *o.RegistrationClientUri +} + +// GetRegistrationClientUriOk returns a tuple with the RegistrationClientUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRegistrationClientUriOk() (*string, bool) { + if o == nil || IsNil(o.RegistrationClientUri) { + return nil, false + } + return o.RegistrationClientUri, true +} + +// HasRegistrationClientUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasRegistrationClientUri() bool { + if o != nil && !IsNil(o.RegistrationClientUri) { + return true + } + + return false +} + +// SetRegistrationClientUri gets a reference to the given string and assigns it to the RegistrationClientUri field. +func (o *OAuth2Client) SetRegistrationClientUri(v string) { + o.RegistrationClientUri = &v +} + +// GetRequestObjectSigningAlg returns the RequestObjectSigningAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetRequestObjectSigningAlg() string { + if o == nil || IsNil(o.RequestObjectSigningAlg) { + var ret string + return ret + } + return *o.RequestObjectSigningAlg +} + +// GetRequestObjectSigningAlgOk returns a tuple with the RequestObjectSigningAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRequestObjectSigningAlgOk() (*string, bool) { + if o == nil || IsNil(o.RequestObjectSigningAlg) { + return nil, false + } + return o.RequestObjectSigningAlg, true +} + +// HasRequestObjectSigningAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasRequestObjectSigningAlg() bool { + if o != nil && !IsNil(o.RequestObjectSigningAlg) { + return true + } + + return false +} + +// SetRequestObjectSigningAlg gets a reference to the given string and assigns it to the RequestObjectSigningAlg field. +func (o *OAuth2Client) SetRequestObjectSigningAlg(v string) { + o.RequestObjectSigningAlg = &v +} + +// GetRequestUris returns the RequestUris field value if set, zero value otherwise. +func (o *OAuth2Client) GetRequestUris() []string { + if o == nil || IsNil(o.RequestUris) { + var ret []string + return ret + } + return o.RequestUris +} + +// GetRequestUrisOk returns a tuple with the RequestUris field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetRequestUrisOk() ([]string, bool) { + if o == nil || IsNil(o.RequestUris) { + return nil, false + } + return o.RequestUris, true +} + +// HasRequestUris returns a boolean if a field has been set. +func (o *OAuth2Client) HasRequestUris() bool { + if o != nil && !IsNil(o.RequestUris) { + return true + } + + return false +} + +// SetRequestUris gets a reference to the given []string and assigns it to the RequestUris field. +func (o *OAuth2Client) SetRequestUris(v []string) { + o.RequestUris = v +} + +// GetResponseTypes returns the ResponseTypes field value if set, zero value otherwise. +func (o *OAuth2Client) GetResponseTypes() []string { + if o == nil || IsNil(o.ResponseTypes) { + var ret []string + return ret + } + return o.ResponseTypes +} + +// GetResponseTypesOk returns a tuple with the ResponseTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetResponseTypesOk() ([]string, bool) { + if o == nil || IsNil(o.ResponseTypes) { + return nil, false + } + return o.ResponseTypes, true +} + +// HasResponseTypes returns a boolean if a field has been set. +func (o *OAuth2Client) HasResponseTypes() bool { + if o != nil && !IsNil(o.ResponseTypes) { + return true + } + + return false +} + +// SetResponseTypes gets a reference to the given []string and assigns it to the ResponseTypes field. +func (o *OAuth2Client) SetResponseTypes(v []string) { + o.ResponseTypes = v +} + +// GetScope returns the Scope field value if set, zero value otherwise. +func (o *OAuth2Client) GetScope() string { + if o == nil || IsNil(o.Scope) { + var ret string + return ret + } + return *o.Scope +} + +// GetScopeOk returns a tuple with the Scope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetScopeOk() (*string, bool) { + if o == nil || IsNil(o.Scope) { + return nil, false + } + return o.Scope, true +} + +// HasScope returns a boolean if a field has been set. +func (o *OAuth2Client) HasScope() bool { + if o != nil && !IsNil(o.Scope) { + return true + } + + return false +} + +// SetScope gets a reference to the given string and assigns it to the Scope field. +func (o *OAuth2Client) SetScope(v string) { + o.Scope = &v +} + +// GetSectorIdentifierUri returns the SectorIdentifierUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetSectorIdentifierUri() string { + if o == nil || IsNil(o.SectorIdentifierUri) { + var ret string + return ret + } + return *o.SectorIdentifierUri +} + +// GetSectorIdentifierUriOk returns a tuple with the SectorIdentifierUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSectorIdentifierUriOk() (*string, bool) { + if o == nil || IsNil(o.SectorIdentifierUri) { + return nil, false + } + return o.SectorIdentifierUri, true +} + +// HasSectorIdentifierUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasSectorIdentifierUri() bool { + if o != nil && !IsNil(o.SectorIdentifierUri) { + return true + } + + return false +} + +// SetSectorIdentifierUri gets a reference to the given string and assigns it to the SectorIdentifierUri field. +func (o *OAuth2Client) SetSectorIdentifierUri(v string) { + o.SectorIdentifierUri = &v +} + +// GetSkipConsent returns the SkipConsent field value if set, zero value otherwise. +func (o *OAuth2Client) GetSkipConsent() bool { + if o == nil || IsNil(o.SkipConsent) { + var ret bool + return ret + } + return *o.SkipConsent +} + +// GetSkipConsentOk returns a tuple with the SkipConsent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSkipConsentOk() (*bool, bool) { + if o == nil || IsNil(o.SkipConsent) { + return nil, false + } + return o.SkipConsent, true +} + +// HasSkipConsent returns a boolean if a field has been set. +func (o *OAuth2Client) HasSkipConsent() bool { + if o != nil && !IsNil(o.SkipConsent) { + return true + } + + return false +} + +// SetSkipConsent gets a reference to the given bool and assigns it to the SkipConsent field. +func (o *OAuth2Client) SetSkipConsent(v bool) { + o.SkipConsent = &v +} + +// GetSkipLogoutConsent returns the SkipLogoutConsent field value if set, zero value otherwise. +func (o *OAuth2Client) GetSkipLogoutConsent() bool { + if o == nil || IsNil(o.SkipLogoutConsent) { + var ret bool + return ret + } + return *o.SkipLogoutConsent +} + +// GetSkipLogoutConsentOk returns a tuple with the SkipLogoutConsent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSkipLogoutConsentOk() (*bool, bool) { + if o == nil || IsNil(o.SkipLogoutConsent) { + return nil, false + } + return o.SkipLogoutConsent, true +} + +// HasSkipLogoutConsent returns a boolean if a field has been set. +func (o *OAuth2Client) HasSkipLogoutConsent() bool { + if o != nil && !IsNil(o.SkipLogoutConsent) { + return true + } + + return false +} + +// SetSkipLogoutConsent gets a reference to the given bool and assigns it to the SkipLogoutConsent field. +func (o *OAuth2Client) SetSkipLogoutConsent(v bool) { + o.SkipLogoutConsent = &v +} + +// GetSubjectType returns the SubjectType field value if set, zero value otherwise. +func (o *OAuth2Client) GetSubjectType() string { + if o == nil || IsNil(o.SubjectType) { + var ret string + return ret + } + return *o.SubjectType +} + +// GetSubjectTypeOk returns a tuple with the SubjectType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetSubjectTypeOk() (*string, bool) { + if o == nil || IsNil(o.SubjectType) { + return nil, false + } + return o.SubjectType, true +} + +// HasSubjectType returns a boolean if a field has been set. +func (o *OAuth2Client) HasSubjectType() bool { + if o != nil && !IsNil(o.SubjectType) { + return true + } + + return false +} + +// SetSubjectType gets a reference to the given string and assigns it to the SubjectType field. +func (o *OAuth2Client) SetSubjectType(v string) { + o.SubjectType = &v +} + +// GetTokenEndpointAuthMethod returns the TokenEndpointAuthMethod field value if set, zero value otherwise. +func (o *OAuth2Client) GetTokenEndpointAuthMethod() string { + if o == nil || IsNil(o.TokenEndpointAuthMethod) { + var ret string + return ret + } + return *o.TokenEndpointAuthMethod +} + +// GetTokenEndpointAuthMethodOk returns a tuple with the TokenEndpointAuthMethod field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTokenEndpointAuthMethodOk() (*string, bool) { + if o == nil || IsNil(o.TokenEndpointAuthMethod) { + return nil, false + } + return o.TokenEndpointAuthMethod, true +} + +// HasTokenEndpointAuthMethod returns a boolean if a field has been set. +func (o *OAuth2Client) HasTokenEndpointAuthMethod() bool { + if o != nil && !IsNil(o.TokenEndpointAuthMethod) { + return true + } + + return false +} + +// SetTokenEndpointAuthMethod gets a reference to the given string and assigns it to the TokenEndpointAuthMethod field. +func (o *OAuth2Client) SetTokenEndpointAuthMethod(v string) { + o.TokenEndpointAuthMethod = &v +} + +// GetTokenEndpointAuthSigningAlg returns the TokenEndpointAuthSigningAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetTokenEndpointAuthSigningAlg() string { + if o == nil || IsNil(o.TokenEndpointAuthSigningAlg) { + var ret string + return ret + } + return *o.TokenEndpointAuthSigningAlg +} + +// GetTokenEndpointAuthSigningAlgOk returns a tuple with the TokenEndpointAuthSigningAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTokenEndpointAuthSigningAlgOk() (*string, bool) { + if o == nil || IsNil(o.TokenEndpointAuthSigningAlg) { + return nil, false + } + return o.TokenEndpointAuthSigningAlg, true +} + +// HasTokenEndpointAuthSigningAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasTokenEndpointAuthSigningAlg() bool { + if o != nil && !IsNil(o.TokenEndpointAuthSigningAlg) { + return true + } + + return false +} + +// SetTokenEndpointAuthSigningAlg gets a reference to the given string and assigns it to the TokenEndpointAuthSigningAlg field. +func (o *OAuth2Client) SetTokenEndpointAuthSigningAlg(v string) { + o.TokenEndpointAuthSigningAlg = &v +} + +// GetTosUri returns the TosUri field value if set, zero value otherwise. +func (o *OAuth2Client) GetTosUri() string { + if o == nil || IsNil(o.TosUri) { + var ret string + return ret + } + return *o.TosUri +} + +// GetTosUriOk returns a tuple with the TosUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetTosUriOk() (*string, bool) { + if o == nil || IsNil(o.TosUri) { + return nil, false + } + return o.TosUri, true +} + +// HasTosUri returns a boolean if a field has been set. +func (o *OAuth2Client) HasTosUri() bool { + if o != nil && !IsNil(o.TosUri) { + return true + } + + return false +} + +// SetTosUri gets a reference to the given string and assigns it to the TosUri field. +func (o *OAuth2Client) SetTosUri(v string) { + o.TosUri = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *OAuth2Client) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *OAuth2Client) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *OAuth2Client) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetUserinfoSignedResponseAlg returns the UserinfoSignedResponseAlg field value if set, zero value otherwise. +func (o *OAuth2Client) GetUserinfoSignedResponseAlg() string { + if o == nil || IsNil(o.UserinfoSignedResponseAlg) { + var ret string + return ret + } + return *o.UserinfoSignedResponseAlg +} + +// GetUserinfoSignedResponseAlgOk returns a tuple with the UserinfoSignedResponseAlg field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetUserinfoSignedResponseAlgOk() (*string, bool) { + if o == nil || IsNil(o.UserinfoSignedResponseAlg) { + return nil, false + } + return o.UserinfoSignedResponseAlg, true +} + +// HasUserinfoSignedResponseAlg returns a boolean if a field has been set. +func (o *OAuth2Client) HasUserinfoSignedResponseAlg() bool { + if o != nil && !IsNil(o.UserinfoSignedResponseAlg) { + return true + } + + return false +} + +// SetUserinfoSignedResponseAlg gets a reference to the given string and assigns it to the UserinfoSignedResponseAlg field. +func (o *OAuth2Client) SetUserinfoSignedResponseAlg(v string) { + o.UserinfoSignedResponseAlg = &v +} + +func (o OAuth2Client) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2Client) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AccessTokenStrategy) { + toSerialize["access_token_strategy"] = o.AccessTokenStrategy + } + if !IsNil(o.AllowedCorsOrigins) { + toSerialize["allowed_cors_origins"] = o.AllowedCorsOrigins + } + if !IsNil(o.Audience) { + toSerialize["audience"] = o.Audience + } + if !IsNil(o.AuthorizationCodeGrantAccessTokenLifespan) { + toSerialize["authorization_code_grant_access_token_lifespan"] = o.AuthorizationCodeGrantAccessTokenLifespan + } + if !IsNil(o.AuthorizationCodeGrantIdTokenLifespan) { + toSerialize["authorization_code_grant_id_token_lifespan"] = o.AuthorizationCodeGrantIdTokenLifespan + } + if !IsNil(o.AuthorizationCodeGrantRefreshTokenLifespan) { + toSerialize["authorization_code_grant_refresh_token_lifespan"] = o.AuthorizationCodeGrantRefreshTokenLifespan + } + if !IsNil(o.BackchannelLogoutSessionRequired) { + toSerialize["backchannel_logout_session_required"] = o.BackchannelLogoutSessionRequired + } + if !IsNil(o.BackchannelLogoutUri) { + toSerialize["backchannel_logout_uri"] = o.BackchannelLogoutUri + } + if !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { + toSerialize["client_credentials_grant_access_token_lifespan"] = o.ClientCredentialsGrantAccessTokenLifespan + } + if !IsNil(o.ClientId) { + toSerialize["client_id"] = o.ClientId + } + if !IsNil(o.ClientName) { + toSerialize["client_name"] = o.ClientName + } + if !IsNil(o.ClientSecret) { + toSerialize["client_secret"] = o.ClientSecret + } + if !IsNil(o.ClientSecretExpiresAt) { + toSerialize["client_secret_expires_at"] = o.ClientSecretExpiresAt + } + if !IsNil(o.ClientUri) { + toSerialize["client_uri"] = o.ClientUri + } + if !IsNil(o.Contacts) { + toSerialize["contacts"] = o.Contacts + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.FrontchannelLogoutSessionRequired) { + toSerialize["frontchannel_logout_session_required"] = o.FrontchannelLogoutSessionRequired + } + if !IsNil(o.FrontchannelLogoutUri) { + toSerialize["frontchannel_logout_uri"] = o.FrontchannelLogoutUri + } + if !IsNil(o.GrantTypes) { + toSerialize["grant_types"] = o.GrantTypes + } + if !IsNil(o.ImplicitGrantAccessTokenLifespan) { + toSerialize["implicit_grant_access_token_lifespan"] = o.ImplicitGrantAccessTokenLifespan + } + if !IsNil(o.ImplicitGrantIdTokenLifespan) { + toSerialize["implicit_grant_id_token_lifespan"] = o.ImplicitGrantIdTokenLifespan + } + if o.Jwks != nil { + toSerialize["jwks"] = o.Jwks + } + if !IsNil(o.JwksUri) { + toSerialize["jwks_uri"] = o.JwksUri + } + if !IsNil(o.JwtBearerGrantAccessTokenLifespan) { + toSerialize["jwt_bearer_grant_access_token_lifespan"] = o.JwtBearerGrantAccessTokenLifespan + } + if !IsNil(o.LogoUri) { + toSerialize["logo_uri"] = o.LogoUri + } + if o.Metadata != nil { + toSerialize["metadata"] = o.Metadata + } + if !IsNil(o.Owner) { + toSerialize["owner"] = o.Owner + } + if !IsNil(o.PolicyUri) { + toSerialize["policy_uri"] = o.PolicyUri + } + if !IsNil(o.PostLogoutRedirectUris) { + toSerialize["post_logout_redirect_uris"] = o.PostLogoutRedirectUris + } + if !IsNil(o.RedirectUris) { + toSerialize["redirect_uris"] = o.RedirectUris + } + if !IsNil(o.RefreshTokenGrantAccessTokenLifespan) { + toSerialize["refresh_token_grant_access_token_lifespan"] = o.RefreshTokenGrantAccessTokenLifespan + } + if !IsNil(o.RefreshTokenGrantIdTokenLifespan) { + toSerialize["refresh_token_grant_id_token_lifespan"] = o.RefreshTokenGrantIdTokenLifespan + } + if !IsNil(o.RefreshTokenGrantRefreshTokenLifespan) { + toSerialize["refresh_token_grant_refresh_token_lifespan"] = o.RefreshTokenGrantRefreshTokenLifespan + } + if !IsNil(o.RegistrationAccessToken) { + toSerialize["registration_access_token"] = o.RegistrationAccessToken + } + if !IsNil(o.RegistrationClientUri) { + toSerialize["registration_client_uri"] = o.RegistrationClientUri + } + if !IsNil(o.RequestObjectSigningAlg) { + toSerialize["request_object_signing_alg"] = o.RequestObjectSigningAlg + } + if !IsNil(o.RequestUris) { + toSerialize["request_uris"] = o.RequestUris + } + if !IsNil(o.ResponseTypes) { + toSerialize["response_types"] = o.ResponseTypes + } + if !IsNil(o.Scope) { + toSerialize["scope"] = o.Scope + } + if !IsNil(o.SectorIdentifierUri) { + toSerialize["sector_identifier_uri"] = o.SectorIdentifierUri + } + if !IsNil(o.SkipConsent) { + toSerialize["skip_consent"] = o.SkipConsent + } + if !IsNil(o.SkipLogoutConsent) { + toSerialize["skip_logout_consent"] = o.SkipLogoutConsent + } + if !IsNil(o.SubjectType) { + toSerialize["subject_type"] = o.SubjectType + } + if !IsNil(o.TokenEndpointAuthMethod) { + toSerialize["token_endpoint_auth_method"] = o.TokenEndpointAuthMethod + } + if !IsNil(o.TokenEndpointAuthSigningAlg) { + toSerialize["token_endpoint_auth_signing_alg"] = o.TokenEndpointAuthSigningAlg + } + if !IsNil(o.TosUri) { + toSerialize["tos_uri"] = o.TosUri + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.UserinfoSignedResponseAlg) { + toSerialize["userinfo_signed_response_alg"] = o.UserinfoSignedResponseAlg + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2Client) UnmarshalJSON(data []byte) (err error) { + varOAuth2Client := _OAuth2Client{} + + err = json.Unmarshal(data, &varOAuth2Client) + + if err != nil { + return err + } + + *o = OAuth2Client(varOAuth2Client) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "access_token_strategy") + delete(additionalProperties, "allowed_cors_origins") + delete(additionalProperties, "audience") + delete(additionalProperties, "authorization_code_grant_access_token_lifespan") + delete(additionalProperties, "authorization_code_grant_id_token_lifespan") + delete(additionalProperties, "authorization_code_grant_refresh_token_lifespan") + delete(additionalProperties, "backchannel_logout_session_required") + delete(additionalProperties, "backchannel_logout_uri") + delete(additionalProperties, "client_credentials_grant_access_token_lifespan") + delete(additionalProperties, "client_id") + delete(additionalProperties, "client_name") + delete(additionalProperties, "client_secret") + delete(additionalProperties, "client_secret_expires_at") + delete(additionalProperties, "client_uri") + delete(additionalProperties, "contacts") + delete(additionalProperties, "created_at") + delete(additionalProperties, "frontchannel_logout_session_required") + delete(additionalProperties, "frontchannel_logout_uri") + delete(additionalProperties, "grant_types") + delete(additionalProperties, "implicit_grant_access_token_lifespan") + delete(additionalProperties, "implicit_grant_id_token_lifespan") + delete(additionalProperties, "jwks") + delete(additionalProperties, "jwks_uri") + delete(additionalProperties, "jwt_bearer_grant_access_token_lifespan") + delete(additionalProperties, "logo_uri") + delete(additionalProperties, "metadata") + delete(additionalProperties, "owner") + delete(additionalProperties, "policy_uri") + delete(additionalProperties, "post_logout_redirect_uris") + delete(additionalProperties, "redirect_uris") + delete(additionalProperties, "refresh_token_grant_access_token_lifespan") + delete(additionalProperties, "refresh_token_grant_id_token_lifespan") + delete(additionalProperties, "refresh_token_grant_refresh_token_lifespan") + delete(additionalProperties, "registration_access_token") + delete(additionalProperties, "registration_client_uri") + delete(additionalProperties, "request_object_signing_alg") + delete(additionalProperties, "request_uris") + delete(additionalProperties, "response_types") + delete(additionalProperties, "scope") + delete(additionalProperties, "sector_identifier_uri") + delete(additionalProperties, "skip_consent") + delete(additionalProperties, "skip_logout_consent") + delete(additionalProperties, "subject_type") + delete(additionalProperties, "token_endpoint_auth_method") + delete(additionalProperties, "token_endpoint_auth_signing_alg") + delete(additionalProperties, "tos_uri") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "userinfo_signed_response_alg") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2Client struct { + value *OAuth2Client + isSet bool +} + +func (v NullableOAuth2Client) Get() *OAuth2Client { + return v.value +} + +func (v *NullableOAuth2Client) Set(val *OAuth2Client) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2Client) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2Client) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2Client(val *OAuth2Client) *NullableOAuth2Client { + return &NullableOAuth2Client{value: val, isSet: true} +} + +func (v NullableOAuth2Client) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2Client) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_o_auth2_consent_request_open_id_connect_context.go b/pkg/httpclient/model_o_auth2_consent_request_open_id_connect_context.go new file mode 100644 index 0000000..038f235 --- /dev/null +++ b/pkg/httpclient/model_o_auth2_consent_request_open_id_connect_context.go @@ -0,0 +1,307 @@ +/* +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" +) + +// checks if the OAuth2ConsentRequestOpenIDConnectContext type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2ConsentRequestOpenIDConnectContext{} + +// OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext +type OAuth2ConsentRequestOpenIDConnectContext struct { + // 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. + Display *string `json:"display,omitempty"` + // IDTokenHintClaims are the claims of the ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. + IdTokenHintClaims map[string]interface{} `json:"id_token_hint_claims,omitempty"` + // LoginHint hints about the login identifier the End-User might use to log in (if necessary). This hint can be used by an RP if it first asks the End-User for their e-mail address (or other identifier) and then wants to pass that value as a hint to the discovered authorization service. This value MAY also be a phone number in the format specified for the phone_number Claim. The use of this parameter is optional. + LoginHint *string `json:"login_hint,omitempty"` + // UILocales is the End-User'id preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value \\\"fr-CA fr en\\\" represents a preference for French as spoken in Canada, then French (without a region designation), followed by English (without a region designation). An error SHOULD NOT result if some or all of the requested locales are not supported by the OpenID Provider. + UiLocales []string `json:"ui_locales,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext + +// NewOAuth2ConsentRequestOpenIDConnectContext instantiates a new OAuth2ConsentRequestOpenIDConnectContext 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 NewOAuth2ConsentRequestOpenIDConnectContext() *OAuth2ConsentRequestOpenIDConnectContext { + this := OAuth2ConsentRequestOpenIDConnectContext{} + return &this +} + +// NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults instantiates a new OAuth2ConsentRequestOpenIDConnectContext 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 NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults() *OAuth2ConsentRequestOpenIDConnectContext { + this := OAuth2ConsentRequestOpenIDConnectContext{} + return &this +} + +// GetAcrValues returns the AcrValues field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValues() []string { + if o == nil || IsNil(o.AcrValues) { + var ret []string + return ret + } + return o.AcrValues +} + +// GetAcrValuesOk returns a tuple with the AcrValues field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValuesOk() ([]string, bool) { + if o == nil || IsNil(o.AcrValues) { + return nil, false + } + return o.AcrValues, true +} + +// HasAcrValues returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasAcrValues() bool { + if o != nil && !IsNil(o.AcrValues) { + return true + } + + return false +} + +// SetAcrValues gets a reference to the given []string and assigns it to the AcrValues field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetAcrValues(v []string) { + o.AcrValues = v +} + +// GetDisplay returns the Display field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetDisplay() string { + if o == nil || IsNil(o.Display) { + var ret string + return ret + } + return *o.Display +} + +// GetDisplayOk returns a tuple with the Display field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetDisplayOk() (*string, bool) { + if o == nil || IsNil(o.Display) { + return nil, false + } + return o.Display, true +} + +// HasDisplay returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasDisplay() bool { + if o != nil && !IsNil(o.Display) { + return true + } + + return false +} + +// SetDisplay gets a reference to the given string and assigns it to the Display field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetDisplay(v string) { + o.Display = &v +} + +// GetIdTokenHintClaims returns the IdTokenHintClaims field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetIdTokenHintClaims() map[string]interface{} { + if o == nil || IsNil(o.IdTokenHintClaims) { + var ret map[string]interface{} + return ret + } + return o.IdTokenHintClaims +} + +// GetIdTokenHintClaimsOk returns a tuple with the IdTokenHintClaims field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetIdTokenHintClaimsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.IdTokenHintClaims) { + return map[string]interface{}{}, false + } + return o.IdTokenHintClaims, true +} + +// HasIdTokenHintClaims returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasIdTokenHintClaims() bool { + if o != nil && !IsNil(o.IdTokenHintClaims) { + return true + } + + return false +} + +// SetIdTokenHintClaims gets a reference to the given map[string]interface{} and assigns it to the IdTokenHintClaims field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetIdTokenHintClaims(v map[string]interface{}) { + o.IdTokenHintClaims = v +} + +// GetLoginHint returns the LoginHint field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetLoginHint() string { + if o == nil || IsNil(o.LoginHint) { + var ret string + return ret + } + return *o.LoginHint +} + +// GetLoginHintOk returns a tuple with the LoginHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetLoginHintOk() (*string, bool) { + if o == nil || IsNil(o.LoginHint) { + return nil, false + } + return o.LoginHint, true +} + +// HasLoginHint returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasLoginHint() bool { + if o != nil && !IsNil(o.LoginHint) { + return true + } + + return false +} + +// SetLoginHint gets a reference to the given string and assigns it to the LoginHint field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetLoginHint(v string) { + o.LoginHint = &v +} + +// GetUiLocales returns the UiLocales field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetUiLocales() []string { + if o == nil || IsNil(o.UiLocales) { + var ret []string + return ret + } + return o.UiLocales +} + +// GetUiLocalesOk returns a tuple with the UiLocales field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetUiLocalesOk() ([]string, bool) { + if o == nil || IsNil(o.UiLocales) { + return nil, false + } + return o.UiLocales, true +} + +// HasUiLocales returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasUiLocales() bool { + if o != nil && !IsNil(o.UiLocales) { + return true + } + + return false +} + +// SetUiLocales gets a reference to the given []string and assigns it to the UiLocales field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetUiLocales(v []string) { + o.UiLocales = v +} + +func (o OAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2ConsentRequestOpenIDConnectContext) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.AcrValues) { + toSerialize["acr_values"] = o.AcrValues + } + if !IsNil(o.Display) { + toSerialize["display"] = o.Display + } + if !IsNil(o.IdTokenHintClaims) { + toSerialize["id_token_hint_claims"] = o.IdTokenHintClaims + } + if !IsNil(o.LoginHint) { + toSerialize["login_hint"] = o.LoginHint + } + if !IsNil(o.UiLocales) { + toSerialize["ui_locales"] = o.UiLocales + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2ConsentRequestOpenIDConnectContext) UnmarshalJSON(data []byte) (err error) { + varOAuth2ConsentRequestOpenIDConnectContext := _OAuth2ConsentRequestOpenIDConnectContext{} + + err = json.Unmarshal(data, &varOAuth2ConsentRequestOpenIDConnectContext) + + if err != nil { + return err + } + + *o = OAuth2ConsentRequestOpenIDConnectContext(varOAuth2ConsentRequestOpenIDConnectContext) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "acr_values") + delete(additionalProperties, "display") + delete(additionalProperties, "id_token_hint_claims") + delete(additionalProperties, "login_hint") + delete(additionalProperties, "ui_locales") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2ConsentRequestOpenIDConnectContext struct { + value *OAuth2ConsentRequestOpenIDConnectContext + isSet bool +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) Get() *OAuth2ConsentRequestOpenIDConnectContext { + return v.value +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) Set(val *OAuth2ConsentRequestOpenIDConnectContext) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2ConsentRequestOpenIDConnectContext(val *OAuth2ConsentRequestOpenIDConnectContext) *NullableOAuth2ConsentRequestOpenIDConnectContext { + return &NullableOAuth2ConsentRequestOpenIDConnectContext{value: val, isSet: true} +} + +func (v NullableOAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2ConsentRequestOpenIDConnectContext) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_o_auth2_login_request.go b/pkg/httpclient/model_o_auth2_login_request.go new file mode 100644 index 0000000..ab4b9b6 --- /dev/null +++ b/pkg/httpclient/model_o_auth2_login_request.go @@ -0,0 +1,455 @@ +/* +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" +) + +// checks if the OAuth2LoginRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OAuth2LoginRequest{} + +// OAuth2LoginRequest OAuth2LoginRequest struct for OAuth2LoginRequest +type OAuth2LoginRequest struct { + // 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"` + OidcContext *OAuth2ConsentRequestOpenIDConnectContext `json:"oidc_context,omitempty"` + // RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` + // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \\\"sid\\\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. + SessionId *string `json:"session_id,omitempty"` + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. This feature allows you to update / set session information. + Skip *bool `json:"skip,omitempty"` + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. If this value is set and `skip` is true, you MUST include this subject type when accepting the login request, or the request will fail. + Subject *string `json:"subject,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _OAuth2LoginRequest OAuth2LoginRequest + +// NewOAuth2LoginRequest instantiates a new OAuth2LoginRequest 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 NewOAuth2LoginRequest() *OAuth2LoginRequest { + this := OAuth2LoginRequest{} + return &this +} + +// NewOAuth2LoginRequestWithDefaults instantiates a new OAuth2LoginRequest 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 NewOAuth2LoginRequestWithDefaults() *OAuth2LoginRequest { + this := OAuth2LoginRequest{} + return &this +} + +// GetChallenge returns the Challenge field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetChallenge() string { + if o == nil || IsNil(o.Challenge) { + var ret string + return ret + } + return *o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Challenge) { + return nil, false + } + return o.Challenge, true +} + +// HasChallenge returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasChallenge() bool { + if o != nil && !IsNil(o.Challenge) { + return true + } + + return false +} + +// SetChallenge gets a reference to the given string and assigns it to the Challenge field. +func (o *OAuth2LoginRequest) SetChallenge(v string) { + o.Challenge = &v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *OAuth2LoginRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetOidcContext returns the OidcContext field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetOidcContext() OAuth2ConsentRequestOpenIDConnectContext { + if o == nil || IsNil(o.OidcContext) { + var ret OAuth2ConsentRequestOpenIDConnectContext + return ret + } + return *o.OidcContext +} + +// GetOidcContextOk returns a tuple with the OidcContext field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetOidcContextOk() (*OAuth2ConsentRequestOpenIDConnectContext, bool) { + if o == nil || IsNil(o.OidcContext) { + return nil, false + } + return o.OidcContext, true +} + +// HasOidcContext returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasOidcContext() bool { + if o != nil && !IsNil(o.OidcContext) { + return true + } + + return false +} + +// SetOidcContext gets a reference to the given OAuth2ConsentRequestOpenIDConnectContext and assigns it to the OidcContext field. +func (o *OAuth2LoginRequest) SetOidcContext(v OAuth2ConsentRequestOpenIDConnectContext) { + o.OidcContext = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *OAuth2LoginRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *OAuth2LoginRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *OAuth2LoginRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +// GetSessionId returns the SessionId field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSessionId() string { + if o == nil || IsNil(o.SessionId) { + var ret string + return ret + } + return *o.SessionId +} + +// GetSessionIdOk returns a tuple with the SessionId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSessionIdOk() (*string, bool) { + if o == nil || IsNil(o.SessionId) { + return nil, false + } + return o.SessionId, true +} + +// HasSessionId returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSessionId() bool { + if o != nil && !IsNil(o.SessionId) { + return true + } + + return false +} + +// SetSessionId gets a reference to the given string and assigns it to the SessionId field. +func (o *OAuth2LoginRequest) SetSessionId(v string) { + o.SessionId = &v +} + +// GetSkip returns the Skip field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSkip() bool { + if o == nil || IsNil(o.Skip) { + var ret bool + return ret + } + return *o.Skip +} + +// GetSkipOk returns a tuple with the Skip field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSkipOk() (*bool, bool) { + if o == nil || IsNil(o.Skip) { + return nil, false + } + return o.Skip, true +} + +// HasSkip returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSkip() bool { + if o != nil && !IsNil(o.Skip) { + return true + } + + return false +} + +// SetSkip gets a reference to the given bool and assigns it to the Skip field. +func (o *OAuth2LoginRequest) SetSkip(v bool) { + o.Skip = &v +} + +// GetSubject returns the Subject field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetSubject() string { + if o == nil || IsNil(o.Subject) { + var ret string + return ret + } + return *o.Subject +} + +// GetSubjectOk returns a tuple with the Subject field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetSubjectOk() (*string, bool) { + if o == nil || IsNil(o.Subject) { + return nil, false + } + return o.Subject, true +} + +// HasSubject returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasSubject() bool { + if o != nil && !IsNil(o.Subject) { + return true + } + + return false +} + +// SetSubject gets a reference to the given string and assigns it to the Subject field. +func (o *OAuth2LoginRequest) SetSubject(v string) { + o.Subject = &v +} + +func (o OAuth2LoginRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OAuth2LoginRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Challenge) { + toSerialize["challenge"] = o.Challenge + } + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.OidcContext) { + toSerialize["oidc_context"] = o.OidcContext + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + if !IsNil(o.SessionId) { + toSerialize["session_id"] = o.SessionId + } + if !IsNil(o.Skip) { + toSerialize["skip"] = o.Skip + } + if !IsNil(o.Subject) { + toSerialize["subject"] = o.Subject + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *OAuth2LoginRequest) UnmarshalJSON(data []byte) (err error) { + varOAuth2LoginRequest := _OAuth2LoginRequest{} + + err = json.Unmarshal(data, &varOAuth2LoginRequest) + + if err != nil { + return err + } + + *o = OAuth2LoginRequest(varOAuth2LoginRequest) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "challenge") + delete(additionalProperties, "client") + delete(additionalProperties, "oidc_context") + delete(additionalProperties, "request_url") + delete(additionalProperties, "requested_access_token_audience") + delete(additionalProperties, "requested_scope") + delete(additionalProperties, "session_id") + delete(additionalProperties, "skip") + delete(additionalProperties, "subject") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableOAuth2LoginRequest struct { + value *OAuth2LoginRequest + isSet bool +} + +func (v NullableOAuth2LoginRequest) Get() *OAuth2LoginRequest { + return v.value +} + +func (v *NullableOAuth2LoginRequest) Set(val *OAuth2LoginRequest) { + v.value = val + v.isSet = true +} + +func (v NullableOAuth2LoginRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableOAuth2LoginRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOAuth2LoginRequest(val *OAuth2LoginRequest) *NullableOAuth2LoginRequest { + return &NullableOAuth2LoginRequest{value: val, isSet: true} +} + +func (v NullableOAuth2LoginRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOAuth2LoginRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_patch_identities_body.go b/pkg/httpclient/model_patch_identities_body.go new file mode 100644 index 0000000..251da77 --- /dev/null +++ b/pkg/httpclient/model_patch_identities_body.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the PatchIdentitiesBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PatchIdentitiesBody{} + +// PatchIdentitiesBody Patch Identities Body +type PatchIdentitiesBody struct { + // Identities holds the list of patches to apply required + Identities []IdentityPatch `json:"identities,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _PatchIdentitiesBody PatchIdentitiesBody + +// NewPatchIdentitiesBody instantiates a new PatchIdentitiesBody 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 NewPatchIdentitiesBody() *PatchIdentitiesBody { + this := PatchIdentitiesBody{} + return &this +} + +// NewPatchIdentitiesBodyWithDefaults instantiates a new PatchIdentitiesBody 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 NewPatchIdentitiesBodyWithDefaults() *PatchIdentitiesBody { + this := PatchIdentitiesBody{} + return &this +} + +// GetIdentities returns the Identities field value if set, zero value otherwise. +func (o *PatchIdentitiesBody) GetIdentities() []IdentityPatch { + if o == nil || IsNil(o.Identities) { + var ret []IdentityPatch + return ret + } + return o.Identities +} + +// GetIdentitiesOk returns a tuple with the Identities field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PatchIdentitiesBody) GetIdentitiesOk() ([]IdentityPatch, bool) { + if o == nil || IsNil(o.Identities) { + return nil, false + } + return o.Identities, true +} + +// HasIdentities returns a boolean if a field has been set. +func (o *PatchIdentitiesBody) HasIdentities() bool { + if o != nil && !IsNil(o.Identities) { + return true + } + + return false +} + +// SetIdentities gets a reference to the given []IdentityPatch and assigns it to the Identities field. +func (o *PatchIdentitiesBody) SetIdentities(v []IdentityPatch) { + o.Identities = v +} + +func (o PatchIdentitiesBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PatchIdentitiesBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Identities) { + toSerialize["identities"] = o.Identities + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PatchIdentitiesBody) UnmarshalJSON(data []byte) (err error) { + varPatchIdentitiesBody := _PatchIdentitiesBody{} + + err = json.Unmarshal(data, &varPatchIdentitiesBody) + + if err != nil { + return err + } + + *o = PatchIdentitiesBody(varPatchIdentitiesBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "identities") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePatchIdentitiesBody struct { + value *PatchIdentitiesBody + isSet bool +} + +func (v NullablePatchIdentitiesBody) Get() *PatchIdentitiesBody { + return v.value +} + +func (v *NullablePatchIdentitiesBody) Set(val *PatchIdentitiesBody) { + v.value = val + v.isSet = true +} + +func (v NullablePatchIdentitiesBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePatchIdentitiesBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePatchIdentitiesBody(val *PatchIdentitiesBody) *NullablePatchIdentitiesBody { + return &NullablePatchIdentitiesBody{value: val, isSet: true} +} + +func (v NullablePatchIdentitiesBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePatchIdentitiesBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_perform_native_logout_body.go b/pkg/httpclient/model_perform_native_logout_body.go new file mode 100644 index 0000000..d3a97b4 --- /dev/null +++ b/pkg/httpclient/model_perform_native_logout_body.go @@ -0,0 +1,168 @@ +/* +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" + "fmt" +) + +// checks if the PerformNativeLogoutBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PerformNativeLogoutBody{} + +// PerformNativeLogoutBody Perform Native Logout Request Body +type PerformNativeLogoutBody struct { + // The Session Token Invalidate this session token. + SessionToken string `json:"session_token"` + AdditionalProperties map[string]interface{} +} + +type _PerformNativeLogoutBody PerformNativeLogoutBody + +// NewPerformNativeLogoutBody instantiates a new PerformNativeLogoutBody 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 NewPerformNativeLogoutBody(sessionToken string) *PerformNativeLogoutBody { + this := PerformNativeLogoutBody{} + this.SessionToken = sessionToken + return &this +} + +// NewPerformNativeLogoutBodyWithDefaults instantiates a new PerformNativeLogoutBody 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 NewPerformNativeLogoutBodyWithDefaults() *PerformNativeLogoutBody { + this := PerformNativeLogoutBody{} + return &this +} + +// GetSessionToken returns the SessionToken field value +func (o *PerformNativeLogoutBody) GetSessionToken() string { + if o == nil { + var ret string + return ret + } + + return o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value +// and a boolean to check if the value has been set. +func (o *PerformNativeLogoutBody) GetSessionTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SessionToken, true +} + +// SetSessionToken sets field value +func (o *PerformNativeLogoutBody) SetSessionToken(v string) { + o.SessionToken = v +} + +func (o PerformNativeLogoutBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PerformNativeLogoutBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["session_token"] = o.SessionToken + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PerformNativeLogoutBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session_token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varPerformNativeLogoutBody := _PerformNativeLogoutBody{} + + err = json.Unmarshal(data, &varPerformNativeLogoutBody) + + if err != nil { + return err + } + + *o = PerformNativeLogoutBody(varPerformNativeLogoutBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePerformNativeLogoutBody struct { + value *PerformNativeLogoutBody + isSet bool +} + +func (v NullablePerformNativeLogoutBody) Get() *PerformNativeLogoutBody { + return v.value +} + +func (v *NullablePerformNativeLogoutBody) Set(val *PerformNativeLogoutBody) { + v.value = val + v.isSet = true +} + +func (v NullablePerformNativeLogoutBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePerformNativeLogoutBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePerformNativeLogoutBody(val *PerformNativeLogoutBody) *NullablePerformNativeLogoutBody { + return &NullablePerformNativeLogoutBody{value: val, isSet: true} +} + +func (v NullablePerformNativeLogoutBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePerformNativeLogoutBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_pin_config.go b/pkg/httpclient/model_pin_config.go new file mode 100644 index 0000000..6e16b00 --- /dev/null +++ b/pkg/httpclient/model_pin_config.go @@ -0,0 +1,270 @@ +/* +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" + "time" +) + +// checks if the PINConfig type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PINConfig{} + +// PINConfig The pin_secret field holds only the at-rest ciphertext; the plaintext exists transiently in server memory during verification and is invalidated once the key locks. +type PINConfig struct { + // When the pin_secret was first issued. + CreatedAt *time.Time `json:"created_at,omitempty"` + // The number of consecutive wrong-PIN attempts so far; the key locks when it reaches the configured maximum (pin_max_attempts, default 5). + FailedAttempts *int32 `json:"failed_attempts,omitempty"` + // The at-rest ciphertext of the pin_secret. It never leaves the server and is cleared once the key locks. + PinSecret *string `json:"pin_secret,omitempty"` + // When the pin_secret was last rotated. Omitted if the secret was never rotated. + RotatedAt *time.Time `json:"rotated_at,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _PINConfig PINConfig + +// NewPINConfig instantiates a new PINConfig 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 NewPINConfig() *PINConfig { + this := PINConfig{} + return &this +} + +// NewPINConfigWithDefaults instantiates a new PINConfig 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 NewPINConfigWithDefaults() *PINConfig { + this := PINConfig{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *PINConfig) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *PINConfig) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *PINConfig) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetFailedAttempts returns the FailedAttempts field value if set, zero value otherwise. +func (o *PINConfig) GetFailedAttempts() int32 { + if o == nil || IsNil(o.FailedAttempts) { + var ret int32 + return ret + } + return *o.FailedAttempts +} + +// GetFailedAttemptsOk returns a tuple with the FailedAttempts field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetFailedAttemptsOk() (*int32, bool) { + if o == nil || IsNil(o.FailedAttempts) { + return nil, false + } + return o.FailedAttempts, true +} + +// HasFailedAttempts returns a boolean if a field has been set. +func (o *PINConfig) HasFailedAttempts() bool { + if o != nil && !IsNil(o.FailedAttempts) { + return true + } + + return false +} + +// SetFailedAttempts gets a reference to the given int32 and assigns it to the FailedAttempts field. +func (o *PINConfig) SetFailedAttempts(v int32) { + o.FailedAttempts = &v +} + +// GetPinSecret returns the PinSecret field value if set, zero value otherwise. +func (o *PINConfig) GetPinSecret() string { + if o == nil || IsNil(o.PinSecret) { + var ret string + return ret + } + return *o.PinSecret +} + +// GetPinSecretOk returns a tuple with the PinSecret field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetPinSecretOk() (*string, bool) { + if o == nil || IsNil(o.PinSecret) { + return nil, false + } + return o.PinSecret, true +} + +// HasPinSecret returns a boolean if a field has been set. +func (o *PINConfig) HasPinSecret() bool { + if o != nil && !IsNil(o.PinSecret) { + return true + } + + return false +} + +// SetPinSecret gets a reference to the given string and assigns it to the PinSecret field. +func (o *PINConfig) SetPinSecret(v string) { + o.PinSecret = &v +} + +// GetRotatedAt returns the RotatedAt field value if set, zero value otherwise. +func (o *PINConfig) GetRotatedAt() time.Time { + if o == nil || IsNil(o.RotatedAt) { + var ret time.Time + return ret + } + return *o.RotatedAt +} + +// GetRotatedAtOk returns a tuple with the RotatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PINConfig) GetRotatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.RotatedAt) { + return nil, false + } + return o.RotatedAt, true +} + +// HasRotatedAt returns a boolean if a field has been set. +func (o *PINConfig) HasRotatedAt() bool { + if o != nil && !IsNil(o.RotatedAt) { + return true + } + + return false +} + +// SetRotatedAt gets a reference to the given time.Time and assigns it to the RotatedAt field. +func (o *PINConfig) SetRotatedAt(v time.Time) { + o.RotatedAt = &v +} + +func (o PINConfig) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PINConfig) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.FailedAttempts) { + toSerialize["failed_attempts"] = o.FailedAttempts + } + if !IsNil(o.PinSecret) { + toSerialize["pin_secret"] = o.PinSecret + } + if !IsNil(o.RotatedAt) { + toSerialize["rotated_at"] = o.RotatedAt + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *PINConfig) UnmarshalJSON(data []byte) (err error) { + varPINConfig := _PINConfig{} + + err = json.Unmarshal(data, &varPINConfig) + + if err != nil { + return err + } + + *o = PINConfig(varPINConfig) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "failed_attempts") + delete(additionalProperties, "pin_secret") + delete(additionalProperties, "rotated_at") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullablePINConfig struct { + value *PINConfig + isSet bool +} + +func (v NullablePINConfig) Get() *PINConfig { + return v.value +} + +func (v *NullablePINConfig) Set(val *PINConfig) { + v.value = val + v.isSet = true +} + +func (v NullablePINConfig) IsSet() bool { + return v.isSet +} + +func (v *NullablePINConfig) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePINConfig(val *PINConfig) *NullablePINConfig { + return &NullablePINConfig{value: val, isSet: true} +} + +func (v NullablePINConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePINConfig) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_provider.go b/pkg/httpclient/model_provider.go new file mode 100644 index 0000000..aa4eb6e --- /dev/null +++ b/pkg/httpclient/model_provider.go @@ -0,0 +1,383 @@ +/* +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" +) + +// checks if the Provider type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Provider{} + +// Provider struct for Provider +type Provider struct { + // The RP's client identifier, issued by the IdP. + ClientId *string `json:"client_id,omitempty"` + // A full path of the IdP config file. + ConfigUrl *string `json:"config_url,omitempty"` + // By specifying one of domain_hints values provided by the accounts endpoints, the FedCM dialog selectively shows the specified account. + DomainHint *string `json:"domain_hint,omitempty"` + // Array of strings that specifies the user information (\"name\", \" email\", \"picture\") that RP needs IdP to share with them. Note: Field API is supported by Chrome 132 and later. + Fields []string `json:"fields,omitempty"` + // By specifying one of login_hints values provided by the accounts endpoints, the FedCM dialog selectively shows the specified account. + LoginHint *string `json:"login_hint,omitempty"` + // A random string to ensure the response is issued for this specific request. Prevents replay attacks. + Nonce *string `json:"nonce,omitempty"` + // Custom object that allows to specify additional key-value parameters: scope: A string value containing additional permissions that RP needs to request, for example \" drive.readonly calendar.readonly\" nonce: A random string to ensure the response is issued for this specific request. Prevents replay attacks. Other custom key-value parameters. Note: parameters is supported from Chrome 132. + Parameters *map[string]string `json:"parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Provider Provider + +// NewProvider instantiates a new Provider 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 NewProvider() *Provider { + this := Provider{} + return &this +} + +// NewProviderWithDefaults instantiates a new Provider 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 NewProviderWithDefaults() *Provider { + this := Provider{} + return &this +} + +// GetClientId returns the ClientId field value if set, zero value otherwise. +func (o *Provider) GetClientId() string { + if o == nil || IsNil(o.ClientId) { + var ret string + return ret + } + return *o.ClientId +} + +// GetClientIdOk returns a tuple with the ClientId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetClientIdOk() (*string, bool) { + if o == nil || IsNil(o.ClientId) { + return nil, false + } + return o.ClientId, true +} + +// HasClientId returns a boolean if a field has been set. +func (o *Provider) HasClientId() bool { + if o != nil && !IsNil(o.ClientId) { + return true + } + + return false +} + +// SetClientId gets a reference to the given string and assigns it to the ClientId field. +func (o *Provider) SetClientId(v string) { + o.ClientId = &v +} + +// GetConfigUrl returns the ConfigUrl field value if set, zero value otherwise. +func (o *Provider) GetConfigUrl() string { + if o == nil || IsNil(o.ConfigUrl) { + var ret string + return ret + } + return *o.ConfigUrl +} + +// GetConfigUrlOk returns a tuple with the ConfigUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetConfigUrlOk() (*string, bool) { + if o == nil || IsNil(o.ConfigUrl) { + return nil, false + } + return o.ConfigUrl, true +} + +// HasConfigUrl returns a boolean if a field has been set. +func (o *Provider) HasConfigUrl() bool { + if o != nil && !IsNil(o.ConfigUrl) { + return true + } + + return false +} + +// SetConfigUrl gets a reference to the given string and assigns it to the ConfigUrl field. +func (o *Provider) SetConfigUrl(v string) { + o.ConfigUrl = &v +} + +// GetDomainHint returns the DomainHint field value if set, zero value otherwise. +func (o *Provider) GetDomainHint() string { + if o == nil || IsNil(o.DomainHint) { + var ret string + return ret + } + return *o.DomainHint +} + +// GetDomainHintOk returns a tuple with the DomainHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetDomainHintOk() (*string, bool) { + if o == nil || IsNil(o.DomainHint) { + return nil, false + } + return o.DomainHint, true +} + +// HasDomainHint returns a boolean if a field has been set. +func (o *Provider) HasDomainHint() bool { + if o != nil && !IsNil(o.DomainHint) { + return true + } + + return false +} + +// SetDomainHint gets a reference to the given string and assigns it to the DomainHint field. +func (o *Provider) SetDomainHint(v string) { + o.DomainHint = &v +} + +// GetFields returns the Fields field value if set, zero value otherwise. +func (o *Provider) GetFields() []string { + if o == nil || IsNil(o.Fields) { + var ret []string + return ret + } + return o.Fields +} + +// GetFieldsOk returns a tuple with the Fields field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetFieldsOk() ([]string, bool) { + if o == nil || IsNil(o.Fields) { + return nil, false + } + return o.Fields, true +} + +// HasFields returns a boolean if a field has been set. +func (o *Provider) HasFields() bool { + if o != nil && !IsNil(o.Fields) { + return true + } + + return false +} + +// SetFields gets a reference to the given []string and assigns it to the Fields field. +func (o *Provider) SetFields(v []string) { + o.Fields = v +} + +// GetLoginHint returns the LoginHint field value if set, zero value otherwise. +func (o *Provider) GetLoginHint() string { + if o == nil || IsNil(o.LoginHint) { + var ret string + return ret + } + return *o.LoginHint +} + +// GetLoginHintOk returns a tuple with the LoginHint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetLoginHintOk() (*string, bool) { + if o == nil || IsNil(o.LoginHint) { + return nil, false + } + return o.LoginHint, true +} + +// HasLoginHint returns a boolean if a field has been set. +func (o *Provider) HasLoginHint() bool { + if o != nil && !IsNil(o.LoginHint) { + return true + } + + return false +} + +// SetLoginHint gets a reference to the given string and assigns it to the LoginHint field. +func (o *Provider) SetLoginHint(v string) { + o.LoginHint = &v +} + +// GetNonce returns the Nonce field value if set, zero value otherwise. +func (o *Provider) GetNonce() string { + if o == nil || IsNil(o.Nonce) { + var ret string + return ret + } + return *o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetNonceOk() (*string, bool) { + if o == nil || IsNil(o.Nonce) { + return nil, false + } + return o.Nonce, true +} + +// HasNonce returns a boolean if a field has been set. +func (o *Provider) HasNonce() bool { + if o != nil && !IsNil(o.Nonce) { + return true + } + + return false +} + +// SetNonce gets a reference to the given string and assigns it to the Nonce field. +func (o *Provider) SetNonce(v string) { + o.Nonce = &v +} + +// GetParameters returns the Parameters field value if set, zero value otherwise. +func (o *Provider) GetParameters() map[string]string { + if o == nil || IsNil(o.Parameters) { + var ret map[string]string + return ret + } + return *o.Parameters +} + +// GetParametersOk returns a tuple with the Parameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Provider) GetParametersOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Parameters) { + return nil, false + } + return o.Parameters, true +} + +// HasParameters returns a boolean if a field has been set. +func (o *Provider) HasParameters() bool { + if o != nil && !IsNil(o.Parameters) { + return true + } + + return false +} + +// SetParameters gets a reference to the given map[string]string and assigns it to the Parameters field. +func (o *Provider) SetParameters(v map[string]string) { + o.Parameters = &v +} + +func (o Provider) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Provider) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ClientId) { + toSerialize["client_id"] = o.ClientId + } + if !IsNil(o.ConfigUrl) { + toSerialize["config_url"] = o.ConfigUrl + } + if !IsNil(o.DomainHint) { + toSerialize["domain_hint"] = o.DomainHint + } + if !IsNil(o.Fields) { + toSerialize["fields"] = o.Fields + } + if !IsNil(o.LoginHint) { + toSerialize["login_hint"] = o.LoginHint + } + if !IsNil(o.Nonce) { + toSerialize["nonce"] = o.Nonce + } + if !IsNil(o.Parameters) { + toSerialize["parameters"] = o.Parameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Provider) UnmarshalJSON(data []byte) (err error) { + varProvider := _Provider{} + + err = json.Unmarshal(data, &varProvider) + + if err != nil { + return err + } + + *o = Provider(varProvider) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "client_id") + delete(additionalProperties, "config_url") + delete(additionalProperties, "domain_hint") + delete(additionalProperties, "fields") + delete(additionalProperties, "login_hint") + delete(additionalProperties, "nonce") + delete(additionalProperties, "parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableProvider struct { + value *Provider + isSet bool +} + +func (v NullableProvider) Get() *Provider { + return v.value +} + +func (v *NullableProvider) Set(val *Provider) { + v.value = val + v.isSet = true +} + +func (v NullableProvider) IsSet() bool { + return v.isSet +} + +func (v *NullableProvider) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableProvider(val *Provider) *NullableProvider { + return &NullableProvider{value: val, isSet: true} +} + +func (v NullableProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableProvider) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_recovery_code_for_identity.go b/pkg/httpclient/model_recovery_code_for_identity.go new file mode 100644 index 0000000..1c4f9ba --- /dev/null +++ b/pkg/httpclient/model_recovery_code_for_identity.go @@ -0,0 +1,237 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryCodeForIdentity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryCodeForIdentity{} + +// RecoveryCodeForIdentity Used when an administrator creates a recovery code for an identity. +type RecoveryCodeForIdentity struct { + // Expires At is the timestamp of when the recovery flow expires The timestamp when the recovery code expires. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // RecoveryCode is the code that can be used to recover the account + RecoveryCode string `json:"recovery_code"` + // RecoveryLink with flow This link opens the recovery UI with an empty `code` field. + RecoveryLink string `json:"recovery_link"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryCodeForIdentity RecoveryCodeForIdentity + +// NewRecoveryCodeForIdentity instantiates a new RecoveryCodeForIdentity 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 NewRecoveryCodeForIdentity(recoveryCode string, recoveryLink string) *RecoveryCodeForIdentity { + this := RecoveryCodeForIdentity{} + this.RecoveryCode = recoveryCode + this.RecoveryLink = recoveryLink + return &this +} + +// NewRecoveryCodeForIdentityWithDefaults instantiates a new RecoveryCodeForIdentity 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 NewRecoveryCodeForIdentityWithDefaults() *RecoveryCodeForIdentity { + this := RecoveryCodeForIdentity{} + return &this +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *RecoveryCodeForIdentity) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *RecoveryCodeForIdentity) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *RecoveryCodeForIdentity) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetRecoveryCode returns the RecoveryCode field value +func (o *RecoveryCodeForIdentity) GetRecoveryCode() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryCode +} + +// GetRecoveryCodeOk returns a tuple with the RecoveryCode field value +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetRecoveryCodeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryCode, true +} + +// SetRecoveryCode sets field value +func (o *RecoveryCodeForIdentity) SetRecoveryCode(v string) { + o.RecoveryCode = v +} + +// GetRecoveryLink returns the RecoveryLink field value +func (o *RecoveryCodeForIdentity) GetRecoveryLink() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryLink +} + +// GetRecoveryLinkOk returns a tuple with the RecoveryLink field value +// and a boolean to check if the value has been set. +func (o *RecoveryCodeForIdentity) GetRecoveryLinkOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryLink, true +} + +// SetRecoveryLink sets field value +func (o *RecoveryCodeForIdentity) SetRecoveryLink(v string) { + o.RecoveryLink = v +} + +func (o RecoveryCodeForIdentity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryCodeForIdentity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["recovery_code"] = o.RecoveryCode + toSerialize["recovery_link"] = o.RecoveryLink + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryCodeForIdentity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "recovery_code", + "recovery_link", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryCodeForIdentity := _RecoveryCodeForIdentity{} + + err = json.Unmarshal(data, &varRecoveryCodeForIdentity) + + if err != nil { + return err + } + + *o = RecoveryCodeForIdentity(varRecoveryCodeForIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_at") + delete(additionalProperties, "recovery_code") + delete(additionalProperties, "recovery_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryCodeForIdentity struct { + value *RecoveryCodeForIdentity + isSet bool +} + +func (v NullableRecoveryCodeForIdentity) Get() *RecoveryCodeForIdentity { + return v.value +} + +func (v *NullableRecoveryCodeForIdentity) Set(val *RecoveryCodeForIdentity) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryCodeForIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryCodeForIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryCodeForIdentity(val *RecoveryCodeForIdentity) *NullableRecoveryCodeForIdentity { + return &NullableRecoveryCodeForIdentity{value: val, isSet: true} +} + +func (v NullableRecoveryCodeForIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryCodeForIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_recovery_flow.go b/pkg/httpclient/model_recovery_flow.go new file mode 100644 index 0000000..4440920 --- /dev/null +++ b/pkg/httpclient/model_recovery_flow.go @@ -0,0 +1,504 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryFlow{} + +// RecoveryFlow This request is used when an identity wants to recover their account. We recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery) +type RecoveryFlow struct { + // Active, if set, contains the recovery method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // Contains possible actions that could follow this flow + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting, a new request has to be initiated. + ExpiresAt time.Time `json:"expires_at"` + // ID represents the request's unique ID. When performing the recovery flow, this represents the id in the recovery ui's query parameter: http://?request= + Id string `json:"id"` + // IssuedAt is the time (UTC) when the request occurred. + IssuedAt time.Time `json:"issued_at"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryFlow RecoveryFlow + +// NewRecoveryFlow instantiates a new RecoveryFlow 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 NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { + this := RecoveryFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewRecoveryFlowWithDefaults instantiates a new RecoveryFlow 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 NewRecoveryFlowWithDefaults() *RecoveryFlow { + this := RecoveryFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *RecoveryFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *RecoveryFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *RecoveryFlow) SetActive(v string) { + o.Active = &v +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *RecoveryFlow) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *RecoveryFlow) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *RecoveryFlow) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *RecoveryFlow) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *RecoveryFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *RecoveryFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *RecoveryFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *RecoveryFlow) SetId(v string) { + o.Id = v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *RecoveryFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *RecoveryFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetRequestUrl returns the RequestUrl field value +func (o *RecoveryFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *RecoveryFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *RecoveryFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *RecoveryFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *RecoveryFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *RecoveryFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *RecoveryFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *RecoveryFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o RecoveryFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + toSerialize["issued_at"] = o.IssuedAt + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryFlow := _RecoveryFlow{} + + err = json.Unmarshal(data, &varRecoveryFlow) + + if err != nil { + return err + } + + *o = RecoveryFlow(varRecoveryFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "continue_with") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryFlow struct { + value *RecoveryFlow + isSet bool +} + +func (v NullableRecoveryFlow) Get() *RecoveryFlow { + return v.value +} + +func (v *NullableRecoveryFlow) Set(val *RecoveryFlow) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryFlow(val *RecoveryFlow) *NullableRecoveryFlow { + return &NullableRecoveryFlow{value: val, isSet: true} +} + +func (v NullableRecoveryFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_recovery_flow_state.go b/pkg/httpclient/model_recovery_flow_state.go new file mode 100644 index 0000000..1b52ba6 --- /dev/null +++ b/pkg/httpclient/model_recovery_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + RECOVERYFLOWSTATE_CHOOSE_METHOD RecoveryFlowState = "choose_method" + RECOVERYFLOWSTATE_SENT_EMAIL RecoveryFlowState = "sent_email" + RECOVERYFLOWSTATE_PASSED_CHALLENGE RecoveryFlowState = "passed_challenge" +) + +// All allowed values of RecoveryFlowState enum +var AllowedRecoveryFlowStateEnumValues = []RecoveryFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *RecoveryFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RecoveryFlowState(value) + for _, existing := range AllowedRecoveryFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RecoveryFlowState", value) +} + +// NewRecoveryFlowStateFromValue returns a pointer to a valid RecoveryFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewRecoveryFlowStateFromValue(v string) (*RecoveryFlowState, error) { + ev := RecoveryFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for RecoveryFlowState: valid values are %v", v, AllowedRecoveryFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v RecoveryFlowState) IsValid() bool { + for _, existing := range AllowedRecoveryFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to recoveryFlowState value +func (v RecoveryFlowState) Ptr() *RecoveryFlowState { + return &v +} + +type NullableRecoveryFlowState struct { + value *RecoveryFlowState + isSet bool +} + +func (v NullableRecoveryFlowState) Get() *RecoveryFlowState { + return v.value +} + +func (v *NullableRecoveryFlowState) Set(val *RecoveryFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryFlowState(val *RecoveryFlowState) *NullableRecoveryFlowState { + return &NullableRecoveryFlowState{value: val, isSet: true} +} + +func (v NullableRecoveryFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_recovery_identity_address.go b/pkg/httpclient/model_recovery_identity_address.go new file mode 100644 index 0000000..3bae00d --- /dev/null +++ b/pkg/httpclient/model_recovery_identity_address.go @@ -0,0 +1,358 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryIdentityAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryIdentityAddress{} + +// RecoveryIdentityAddress struct for RecoveryIdentityAddress +type RecoveryIdentityAddress struct { + BreakGlassForOrganization NullableString `json:"break_glass_for_organization,omitempty"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt *time.Time `json:"created_at,omitempty"` + Id *string `json:"id,omitempty"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Value string `json:"value"` + Via string `json:"via"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryIdentityAddress RecoveryIdentityAddress + +// NewRecoveryIdentityAddress instantiates a new RecoveryIdentityAddress 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 NewRecoveryIdentityAddress(value string, via string) *RecoveryIdentityAddress { + this := RecoveryIdentityAddress{} + this.Value = value + this.Via = via + return &this +} + +// NewRecoveryIdentityAddressWithDefaults instantiates a new RecoveryIdentityAddress 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 NewRecoveryIdentityAddressWithDefaults() *RecoveryIdentityAddress { + this := RecoveryIdentityAddress{} + return &this +} + +// GetBreakGlassForOrganization returns the BreakGlassForOrganization field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *RecoveryIdentityAddress) GetBreakGlassForOrganization() string { + if o == nil || IsNil(o.BreakGlassForOrganization.Get()) { + var ret string + return ret + } + return *o.BreakGlassForOrganization.Get() +} + +// GetBreakGlassForOrganizationOk returns a tuple with the BreakGlassForOrganization 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 *RecoveryIdentityAddress) GetBreakGlassForOrganizationOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.BreakGlassForOrganization.Get(), o.BreakGlassForOrganization.IsSet() +} + +// HasBreakGlassForOrganization returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasBreakGlassForOrganization() bool { + if o != nil && o.BreakGlassForOrganization.IsSet() { + return true + } + + return false +} + +// SetBreakGlassForOrganization gets a reference to the given NullableString and assigns it to the BreakGlassForOrganization field. +func (o *RecoveryIdentityAddress) SetBreakGlassForOrganization(v string) { + o.BreakGlassForOrganization.Set(&v) +} + +// SetBreakGlassForOrganizationNil sets the value for BreakGlassForOrganization to be an explicit nil +func (o *RecoveryIdentityAddress) SetBreakGlassForOrganizationNil() { + o.BreakGlassForOrganization.Set(nil) +} + +// UnsetBreakGlassForOrganization ensures that no value is present for BreakGlassForOrganization, not even an explicit nil +func (o *RecoveryIdentityAddress) UnsetBreakGlassForOrganization() { + o.BreakGlassForOrganization.Unset() +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *RecoveryIdentityAddress) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *RecoveryIdentityAddress) SetId(v string) { + o.Id = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *RecoveryIdentityAddress) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *RecoveryIdentityAddress) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *RecoveryIdentityAddress) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetValue returns the Value field value +func (o *RecoveryIdentityAddress) GetValue() string { + if o == nil { + var ret string + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetValueOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *RecoveryIdentityAddress) SetValue(v string) { + o.Value = v +} + +// GetVia returns the Via field value +func (o *RecoveryIdentityAddress) GetVia() string { + if o == nil { + var ret string + return ret + } + + return o.Via +} + +// GetViaOk returns a tuple with the Via field value +// and a boolean to check if the value has been set. +func (o *RecoveryIdentityAddress) GetViaOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Via, true +} + +// SetVia sets field value +func (o *RecoveryIdentityAddress) SetVia(v string) { + o.Via = v +} + +func (o RecoveryIdentityAddress) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryIdentityAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.BreakGlassForOrganization.IsSet() { + toSerialize["break_glass_for_organization"] = o.BreakGlassForOrganization.Get() + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["value"] = o.Value + toSerialize["via"] = o.Via + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryIdentityAddress) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "value", + "via", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryIdentityAddress := _RecoveryIdentityAddress{} + + err = json.Unmarshal(data, &varRecoveryIdentityAddress) + + if err != nil { + return err + } + + *o = RecoveryIdentityAddress(varRecoveryIdentityAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "break_glass_for_organization") + delete(additionalProperties, "created_at") + delete(additionalProperties, "id") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "value") + delete(additionalProperties, "via") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryIdentityAddress struct { + value *RecoveryIdentityAddress + isSet bool +} + +func (v NullableRecoveryIdentityAddress) Get() *RecoveryIdentityAddress { + return v.value +} + +func (v *NullableRecoveryIdentityAddress) Set(val *RecoveryIdentityAddress) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryIdentityAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryIdentityAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryIdentityAddress(val *RecoveryIdentityAddress) *NullableRecoveryIdentityAddress { + return &NullableRecoveryIdentityAddress{value: val, isSet: true} +} + +func (v NullableRecoveryIdentityAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryIdentityAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_recovery_link_for_identity.go b/pkg/httpclient/model_recovery_link_for_identity.go new file mode 100644 index 0000000..60c0143 --- /dev/null +++ b/pkg/httpclient/model_recovery_link_for_identity.go @@ -0,0 +1,207 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RecoveryLinkForIdentity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RecoveryLinkForIdentity{} + +// RecoveryLinkForIdentity Used when an administrator creates a recovery link for an identity. +type RecoveryLinkForIdentity struct { + // Recovery Link Expires At The timestamp when the recovery link expires. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Recovery Link This link can be used to recover the account. + RecoveryLink string `json:"recovery_link"` + AdditionalProperties map[string]interface{} +} + +type _RecoveryLinkForIdentity RecoveryLinkForIdentity + +// NewRecoveryLinkForIdentity instantiates a new RecoveryLinkForIdentity 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 NewRecoveryLinkForIdentity(recoveryLink string) *RecoveryLinkForIdentity { + this := RecoveryLinkForIdentity{} + this.RecoveryLink = recoveryLink + return &this +} + +// NewRecoveryLinkForIdentityWithDefaults instantiates a new RecoveryLinkForIdentity 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 NewRecoveryLinkForIdentityWithDefaults() *RecoveryLinkForIdentity { + this := RecoveryLinkForIdentity{} + return &this +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *RecoveryLinkForIdentity) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryLinkForIdentity) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *RecoveryLinkForIdentity) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *RecoveryLinkForIdentity) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetRecoveryLink returns the RecoveryLink field value +func (o *RecoveryLinkForIdentity) GetRecoveryLink() string { + if o == nil { + var ret string + return ret + } + + return o.RecoveryLink +} + +// GetRecoveryLinkOk returns a tuple with the RecoveryLink field value +// and a boolean to check if the value has been set. +func (o *RecoveryLinkForIdentity) GetRecoveryLinkOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RecoveryLink, true +} + +// SetRecoveryLink sets field value +func (o *RecoveryLinkForIdentity) SetRecoveryLink(v string) { + o.RecoveryLink = v +} + +func (o RecoveryLinkForIdentity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RecoveryLinkForIdentity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["recovery_link"] = o.RecoveryLink + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RecoveryLinkForIdentity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "recovery_link", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRecoveryLinkForIdentity := _RecoveryLinkForIdentity{} + + err = json.Unmarshal(data, &varRecoveryLinkForIdentity) + + if err != nil { + return err + } + + *o = RecoveryLinkForIdentity(varRecoveryLinkForIdentity) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "expires_at") + delete(additionalProperties, "recovery_link") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRecoveryLinkForIdentity struct { + value *RecoveryLinkForIdentity + isSet bool +} + +func (v NullableRecoveryLinkForIdentity) Get() *RecoveryLinkForIdentity { + return v.value +} + +func (v *NullableRecoveryLinkForIdentity) Set(val *RecoveryLinkForIdentity) { + v.value = val + v.isSet = true +} + +func (v NullableRecoveryLinkForIdentity) IsSet() bool { + return v.isSet +} + +func (v *NullableRecoveryLinkForIdentity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecoveryLinkForIdentity(val *RecoveryLinkForIdentity) *NullableRecoveryLinkForIdentity { + return &NullableRecoveryLinkForIdentity{value: val, isSet: true} +} + +func (v NullableRecoveryLinkForIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecoveryLinkForIdentity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_registration_flow.go b/pkg/httpclient/model_registration_flow.go new file mode 100644 index 0000000..b2976e1 --- /dev/null +++ b/pkg/httpclient/model_registration_flow.go @@ -0,0 +1,665 @@ +/* +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" + "fmt" + "time" +) + +// checks if the RegistrationFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &RegistrationFlow{} + +// 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 passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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"` + // ID represents the flow's unique ID. When performing the registration flow, this represents the id in the registration ui's query parameter: http:///?flow= + Id string `json:"id"` + // IdentitySchema optionally holds the ID of the identity schema that is used for this flow. This value can be set by the user when creating the flow and should be retained when the flow is saved or converted to another flow. + IdentitySchema *string `json:"identity_schema,omitempty"` + // IssuedAt is the time (UTC) when the flow occurred. + IssuedAt time.Time `json:"issued_at"` + // Ory OAuth 2.0 Login Challenge. This value is set using the `login_challenge` query parameter of the registration and login endpoints. If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + Oauth2LoginChallenge *string `json:"oauth2_login_challenge,omitempty"` + Oauth2LoginRequest *OAuth2LoginRequest `json:"oauth2_login_request,omitempty"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + ReturnTo *string `json:"return_to,omitempty"` + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. + SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: 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. + State interface{} `json:"state"` + // TransientPayload is used to pass data from the registration to a webhook + 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"` + AdditionalProperties map[string]interface{} +} + +type _RegistrationFlow RegistrationFlow + +// NewRegistrationFlow instantiates a new RegistrationFlow 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 NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { + this := RegistrationFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewRegistrationFlowWithDefaults instantiates a new RegistrationFlow 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 NewRegistrationFlowWithDefaults() *RegistrationFlow { + this := RegistrationFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *RegistrationFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *RegistrationFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *RegistrationFlow) SetActive(v string) { + o.Active = &v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *RegistrationFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *RegistrationFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *RegistrationFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *RegistrationFlow) SetId(v string) { + o.Id = v +} + +// GetIdentitySchema returns the IdentitySchema field value if set, zero value otherwise. +func (o *RegistrationFlow) GetIdentitySchema() string { + if o == nil || IsNil(o.IdentitySchema) { + var ret string + return ret + } + return *o.IdentitySchema +} + +// GetIdentitySchemaOk returns a tuple with the IdentitySchema field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIdentitySchemaOk() (*string, bool) { + if o == nil || IsNil(o.IdentitySchema) { + return nil, false + } + return o.IdentitySchema, true +} + +// HasIdentitySchema returns a boolean if a field has been set. +func (o *RegistrationFlow) HasIdentitySchema() bool { + if o != nil && !IsNil(o.IdentitySchema) { + return true + } + + return false +} + +// SetIdentitySchema gets a reference to the given string and assigns it to the IdentitySchema field. +func (o *RegistrationFlow) SetIdentitySchema(v string) { + o.IdentitySchema = &v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *RegistrationFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *RegistrationFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOauth2LoginChallenge returns the Oauth2LoginChallenge field value if set, zero value otherwise. +func (o *RegistrationFlow) GetOauth2LoginChallenge() string { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + var ret string + return ret + } + return *o.Oauth2LoginChallenge +} + +// GetOauth2LoginChallengeOk returns a tuple with the Oauth2LoginChallenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetOauth2LoginChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Oauth2LoginChallenge) { + return nil, false + } + return o.Oauth2LoginChallenge, true +} + +// HasOauth2LoginChallenge returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOauth2LoginChallenge() bool { + if o != nil && !IsNil(o.Oauth2LoginChallenge) { + return true + } + + return false +} + +// SetOauth2LoginChallenge gets a reference to the given string and assigns it to the Oauth2LoginChallenge field. +func (o *RegistrationFlow) SetOauth2LoginChallenge(v string) { + o.Oauth2LoginChallenge = &v +} + +// GetOauth2LoginRequest returns the Oauth2LoginRequest field value if set, zero value otherwise. +func (o *RegistrationFlow) GetOauth2LoginRequest() OAuth2LoginRequest { + if o == nil || IsNil(o.Oauth2LoginRequest) { + var ret OAuth2LoginRequest + return ret + } + return *o.Oauth2LoginRequest +} + +// GetOauth2LoginRequestOk returns a tuple with the Oauth2LoginRequest field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetOauth2LoginRequestOk() (*OAuth2LoginRequest, bool) { + if o == nil || IsNil(o.Oauth2LoginRequest) { + return nil, false + } + return o.Oauth2LoginRequest, true +} + +// HasOauth2LoginRequest returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOauth2LoginRequest() bool { + if o != nil && !IsNil(o.Oauth2LoginRequest) { + return true + } + + return false +} + +// SetOauth2LoginRequest gets a reference to the given OAuth2LoginRequest and assigns it to the Oauth2LoginRequest field. +func (o *RegistrationFlow) SetOauth2LoginRequest(v OAuth2LoginRequest) { + o.Oauth2LoginRequest = &v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *RegistrationFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *RegistrationFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *RegistrationFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *RegistrationFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *RegistrationFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *RegistrationFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRequestUrl returns the RequestUrl field value +func (o *RegistrationFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *RegistrationFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *RegistrationFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *RegistrationFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *RegistrationFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetSessionTokenExchangeCode returns the SessionTokenExchangeCode field value if set, zero value otherwise. +func (o *RegistrationFlow) GetSessionTokenExchangeCode() string { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + var ret string + return ret + } + return *o.SessionTokenExchangeCode +} + +// GetSessionTokenExchangeCodeOk returns a tuple with the SessionTokenExchangeCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetSessionTokenExchangeCodeOk() (*string, bool) { + if o == nil || IsNil(o.SessionTokenExchangeCode) { + return nil, false + } + return o.SessionTokenExchangeCode, true +} + +// HasSessionTokenExchangeCode returns a boolean if a field has been set. +func (o *RegistrationFlow) HasSessionTokenExchangeCode() bool { + if o != nil && !IsNil(o.SessionTokenExchangeCode) { + return true + } + + return false +} + +// SetSessionTokenExchangeCode gets a reference to the given string and assigns it to the SessionTokenExchangeCode field. +func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { + o.SessionTokenExchangeCode = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *RegistrationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RegistrationFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RegistrationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetType returns the Type field value +func (o *RegistrationFlow) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *RegistrationFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *RegistrationFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *RegistrationFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *RegistrationFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o RegistrationFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o RegistrationFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + if !IsNil(o.IdentitySchema) { + toSerialize["identity_schema"] = o.IdentitySchema + } + toSerialize["issued_at"] = o.IssuedAt + if !IsNil(o.Oauth2LoginChallenge) { + toSerialize["oauth2_login_challenge"] = o.Oauth2LoginChallenge + } + if !IsNil(o.Oauth2LoginRequest) { + toSerialize["oauth2_login_request"] = o.Oauth2LoginRequest + } + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if !IsNil(o.SessionTokenExchangeCode) { + toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *RegistrationFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varRegistrationFlow := _RegistrationFlow{} + + err = json.Unmarshal(data, &varRegistrationFlow) + + if err != nil { + return err + } + + *o = RegistrationFlow(varRegistrationFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity_schema") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "oauth2_login_challenge") + delete(additionalProperties, "oauth2_login_request") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "session_token_exchange_code") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableRegistrationFlow struct { + value *RegistrationFlow + isSet bool +} + +func (v NullableRegistrationFlow) Get() *RegistrationFlow { + return v.value +} + +func (v *NullableRegistrationFlow) Set(val *RegistrationFlow) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlow(val *RegistrationFlow) *NullableRegistrationFlow { + return &NullableRegistrationFlow{value: val, isSet: true} +} + +func (v NullableRegistrationFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_registration_flow_state.go b/pkg/httpclient/model_registration_flow_state.go new file mode 100644 index 0000000..2211c6a --- /dev/null +++ b/pkg/httpclient/model_registration_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +// All allowed values of RegistrationFlowState enum +var AllowedRegistrationFlowStateEnumValues = []RegistrationFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range AllowedRegistrationFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// NewRegistrationFlowStateFromValue returns a pointer to a valid RegistrationFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewRegistrationFlowStateFromValue(v string) (*RegistrationFlowState, error) { + ev := RegistrationFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for RegistrationFlowState: valid values are %v", v, AllowedRegistrationFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v RegistrationFlowState) IsValid() bool { + for _, existing := range AllowedRegistrationFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to registrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_self_service_flow_expired_error.go b/pkg/httpclient/model_self_service_flow_expired_error.go new file mode 100644 index 0000000..9878b0f --- /dev/null +++ b/pkg/httpclient/model_self_service_flow_expired_error.go @@ -0,0 +1,269 @@ +/* +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" + "time" +) + +// checks if the SelfServiceFlowExpiredError type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SelfServiceFlowExpiredError{} + +// SelfServiceFlowExpiredError Is sent when a flow is expired +type SelfServiceFlowExpiredError struct { + Error *GenericError `json:"error,omitempty"` + // When the flow has expired + ExpiredAt *time.Time `json:"expired_at,omitempty"` + // A Duration represents the elapsed time between two instants as an int64 nanosecond count. The representation limits the largest representable duration to approximately 290 years. + Since *int64 `json:"since,omitempty"` + // The flow ID that should be used for the new flow as it contains the correct messages. + UseFlowId *string `json:"use_flow_id,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SelfServiceFlowExpiredError SelfServiceFlowExpiredError + +// NewSelfServiceFlowExpiredError instantiates a new SelfServiceFlowExpiredError 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 NewSelfServiceFlowExpiredError() *SelfServiceFlowExpiredError { + this := SelfServiceFlowExpiredError{} + return &this +} + +// NewSelfServiceFlowExpiredErrorWithDefaults instantiates a new SelfServiceFlowExpiredError 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 NewSelfServiceFlowExpiredErrorWithDefaults() *SelfServiceFlowExpiredError { + this := SelfServiceFlowExpiredError{} + return &this +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetError() GenericError { + if o == nil || IsNil(o.Error) { + var ret GenericError + 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. +func (o *SelfServiceFlowExpiredError) GetErrorOk() (*GenericError, bool) { + if o == nil || IsNil(o.Error) { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasError() bool { + if o != nil && !IsNil(o.Error) { + return true + } + + return false +} + +// SetError gets a reference to the given GenericError and assigns it to the Error field. +func (o *SelfServiceFlowExpiredError) SetError(v GenericError) { + o.Error = &v +} + +// GetExpiredAt returns the ExpiredAt field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetExpiredAt() time.Time { + if o == nil || IsNil(o.ExpiredAt) { + var ret time.Time + return ret + } + return *o.ExpiredAt +} + +// GetExpiredAtOk returns a tuple with the ExpiredAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetExpiredAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiredAt) { + return nil, false + } + return o.ExpiredAt, true +} + +// HasExpiredAt returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasExpiredAt() bool { + if o != nil && !IsNil(o.ExpiredAt) { + return true + } + + return false +} + +// SetExpiredAt gets a reference to the given time.Time and assigns it to the ExpiredAt field. +func (o *SelfServiceFlowExpiredError) SetExpiredAt(v time.Time) { + o.ExpiredAt = &v +} + +// GetSince returns the Since field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetSince() int64 { + if o == nil || IsNil(o.Since) { + var ret int64 + return ret + } + return *o.Since +} + +// GetSinceOk returns a tuple with the Since field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetSinceOk() (*int64, bool) { + if o == nil || IsNil(o.Since) { + return nil, false + } + return o.Since, true +} + +// HasSince returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasSince() bool { + if o != nil && !IsNil(o.Since) { + return true + } + + return false +} + +// SetSince gets a reference to the given int64 and assigns it to the Since field. +func (o *SelfServiceFlowExpiredError) SetSince(v int64) { + o.Since = &v +} + +// GetUseFlowId returns the UseFlowId field value if set, zero value otherwise. +func (o *SelfServiceFlowExpiredError) GetUseFlowId() string { + if o == nil || IsNil(o.UseFlowId) { + var ret string + return ret + } + return *o.UseFlowId +} + +// GetUseFlowIdOk returns a tuple with the UseFlowId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SelfServiceFlowExpiredError) GetUseFlowIdOk() (*string, bool) { + if o == nil || IsNil(o.UseFlowId) { + return nil, false + } + return o.UseFlowId, true +} + +// HasUseFlowId returns a boolean if a field has been set. +func (o *SelfServiceFlowExpiredError) HasUseFlowId() bool { + if o != nil && !IsNil(o.UseFlowId) { + return true + } + + return false +} + +// SetUseFlowId gets a reference to the given string and assigns it to the UseFlowId field. +func (o *SelfServiceFlowExpiredError) SetUseFlowId(v string) { + o.UseFlowId = &v +} + +func (o SelfServiceFlowExpiredError) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SelfServiceFlowExpiredError) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Error) { + toSerialize["error"] = o.Error + } + if !IsNil(o.ExpiredAt) { + toSerialize["expired_at"] = o.ExpiredAt + } + if !IsNil(o.Since) { + toSerialize["since"] = o.Since + } + if !IsNil(o.UseFlowId) { + toSerialize["use_flow_id"] = o.UseFlowId + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SelfServiceFlowExpiredError) UnmarshalJSON(data []byte) (err error) { + varSelfServiceFlowExpiredError := _SelfServiceFlowExpiredError{} + + err = json.Unmarshal(data, &varSelfServiceFlowExpiredError) + + if err != nil { + return err + } + + *o = SelfServiceFlowExpiredError(varSelfServiceFlowExpiredError) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "error") + delete(additionalProperties, "expired_at") + delete(additionalProperties, "since") + delete(additionalProperties, "use_flow_id") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSelfServiceFlowExpiredError struct { + value *SelfServiceFlowExpiredError + isSet bool +} + +func (v NullableSelfServiceFlowExpiredError) Get() *SelfServiceFlowExpiredError { + return v.value +} + +func (v *NullableSelfServiceFlowExpiredError) Set(val *SelfServiceFlowExpiredError) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceFlowExpiredError) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceFlowExpiredError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceFlowExpiredError(val *SelfServiceFlowExpiredError) *NullableSelfServiceFlowExpiredError { + return &NullableSelfServiceFlowExpiredError{value: val, isSet: true} +} + +func (v NullableSelfServiceFlowExpiredError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceFlowExpiredError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_session.go b/pkg/httpclient/model_session.go new file mode 100644 index 0000000..1228e15 --- /dev/null +++ b/pkg/httpclient/model_session.go @@ -0,0 +1,509 @@ +/* +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" + "fmt" + "time" +) + +// checks if the Session type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Session{} + +// Session A Session +type Session struct { + // Active state. If false the session is no longer active. + Active *bool `json:"active,omitempty"` + // The Session Authentication Timestamp When this session was authenticated at. If multi-factor authentication was used this is the time when the last factor was authenticated (e.g. the TOTP code challenge was completed). + AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"` + // A list of authenticators which were used to authenticate the session. + AuthenticationMethods []SessionAuthenticationMethod `json:"authentication_methods,omitempty"` + AuthenticatorAssuranceLevel *AuthenticatorAssuranceLevel `json:"authenticator_assurance_level,omitempty"` + // Devices has history of all endpoints where the session was used + Devices []SessionDevice `json:"devices,omitempty"` + // The Session Expiry When this session expires at. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Session ID + Id string `json:"id"` + Identity *Identity `json:"identity,omitempty"` + // The Session Issuance Timestamp When this session was issued at. Usually equal or close to `authenticated_at`. + IssuedAt *time.Time `json:"issued_at,omitempty"` + // Tokenized is the tokenized (e.g. JWT) version of the session. It is only set when the `tokenize_as` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized *string `json:"tokenized,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Session Session + +// NewSession instantiates a new Session 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 NewSession(id string) *Session { + this := Session{} + this.Id = id + return &this +} + +// NewSessionWithDefaults instantiates a new Session 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 NewSessionWithDefaults() *Session { + this := Session{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *Session) GetActive() bool { + if o == nil || IsNil(o.Active) { + var ret bool + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetActiveOk() (*bool, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *Session) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given bool and assigns it to the Active field. +func (o *Session) SetActive(v bool) { + o.Active = &v +} + +// GetAuthenticatedAt returns the AuthenticatedAt field value if set, zero value otherwise. +func (o *Session) GetAuthenticatedAt() time.Time { + if o == nil || IsNil(o.AuthenticatedAt) { + var ret time.Time + return ret + } + return *o.AuthenticatedAt +} + +// GetAuthenticatedAtOk returns a tuple with the AuthenticatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.AuthenticatedAt) { + return nil, false + } + return o.AuthenticatedAt, true +} + +// HasAuthenticatedAt returns a boolean if a field has been set. +func (o *Session) HasAuthenticatedAt() bool { + if o != nil && !IsNil(o.AuthenticatedAt) { + return true + } + + return false +} + +// SetAuthenticatedAt gets a reference to the given time.Time and assigns it to the AuthenticatedAt field. +func (o *Session) SetAuthenticatedAt(v time.Time) { + o.AuthenticatedAt = &v +} + +// GetAuthenticationMethods returns the AuthenticationMethods field value if set, zero value otherwise. +func (o *Session) GetAuthenticationMethods() []SessionAuthenticationMethod { + if o == nil || IsNil(o.AuthenticationMethods) { + var ret []SessionAuthenticationMethod + return ret + } + return o.AuthenticationMethods +} + +// GetAuthenticationMethodsOk returns a tuple with the AuthenticationMethods field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticationMethodsOk() ([]SessionAuthenticationMethod, bool) { + if o == nil || IsNil(o.AuthenticationMethods) { + return nil, false + } + return o.AuthenticationMethods, true +} + +// HasAuthenticationMethods returns a boolean if a field has been set. +func (o *Session) HasAuthenticationMethods() bool { + if o != nil && !IsNil(o.AuthenticationMethods) { + return true + } + + return false +} + +// SetAuthenticationMethods gets a reference to the given []SessionAuthenticationMethod and assigns it to the AuthenticationMethods field. +func (o *Session) SetAuthenticationMethods(v []SessionAuthenticationMethod) { + o.AuthenticationMethods = v +} + +// GetAuthenticatorAssuranceLevel returns the AuthenticatorAssuranceLevel field value if set, zero value otherwise. +func (o *Session) GetAuthenticatorAssuranceLevel() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.AuthenticatorAssuranceLevel) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.AuthenticatorAssuranceLevel +} + +// GetAuthenticatorAssuranceLevelOk returns a tuple with the AuthenticatorAssuranceLevel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetAuthenticatorAssuranceLevelOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.AuthenticatorAssuranceLevel) { + return nil, false + } + return o.AuthenticatorAssuranceLevel, true +} + +// HasAuthenticatorAssuranceLevel returns a boolean if a field has been set. +func (o *Session) HasAuthenticatorAssuranceLevel() bool { + if o != nil && !IsNil(o.AuthenticatorAssuranceLevel) { + return true + } + + return false +} + +// SetAuthenticatorAssuranceLevel gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the AuthenticatorAssuranceLevel field. +func (o *Session) SetAuthenticatorAssuranceLevel(v AuthenticatorAssuranceLevel) { + o.AuthenticatorAssuranceLevel = &v +} + +// GetDevices returns the Devices field value if set, zero value otherwise. +func (o *Session) GetDevices() []SessionDevice { + if o == nil || IsNil(o.Devices) { + var ret []SessionDevice + return ret + } + return o.Devices +} + +// GetDevicesOk returns a tuple with the Devices field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetDevicesOk() ([]SessionDevice, bool) { + if o == nil || IsNil(o.Devices) { + return nil, false + } + return o.Devices, true +} + +// HasDevices returns a boolean if a field has been set. +func (o *Session) HasDevices() bool { + if o != nil && !IsNil(o.Devices) { + return true + } + + return false +} + +// SetDevices gets a reference to the given []SessionDevice and assigns it to the Devices field. +func (o *Session) SetDevices(v []SessionDevice) { + o.Devices = v +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *Session) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *Session) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *Session) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetId returns the Id field value +func (o *Session) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Session) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Session) SetId(v string) { + o.Id = v +} + +// GetIdentity returns the Identity field value if set, zero value otherwise. +func (o *Session) GetIdentity() Identity { + if o == nil || IsNil(o.Identity) { + var ret Identity + return ret + } + return *o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetIdentityOk() (*Identity, bool) { + if o == nil || IsNil(o.Identity) { + return nil, false + } + return o.Identity, true +} + +// HasIdentity returns a boolean if a field has been set. +func (o *Session) HasIdentity() bool { + if o != nil && !IsNil(o.Identity) { + return true + } + + return false +} + +// SetIdentity gets a reference to the given Identity and assigns it to the Identity field. +func (o *Session) SetIdentity(v Identity) { + o.Identity = &v +} + +// GetIssuedAt returns the IssuedAt field value if set, zero value otherwise. +func (o *Session) GetIssuedAt() time.Time { + if o == nil || IsNil(o.IssuedAt) { + var ret time.Time + return ret + } + return *o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetIssuedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.IssuedAt) { + return nil, false + } + return o.IssuedAt, true +} + +// HasIssuedAt returns a boolean if a field has been set. +func (o *Session) HasIssuedAt() bool { + if o != nil && !IsNil(o.IssuedAt) { + return true + } + + return false +} + +// SetIssuedAt gets a reference to the given time.Time and assigns it to the IssuedAt field. +func (o *Session) SetIssuedAt(v time.Time) { + o.IssuedAt = &v +} + +// GetTokenized returns the Tokenized field value if set, zero value otherwise. +func (o *Session) GetTokenized() string { + if o == nil || IsNil(o.Tokenized) { + var ret string + return ret + } + return *o.Tokenized +} + +// GetTokenizedOk returns a tuple with the Tokenized field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetTokenizedOk() (*string, bool) { + if o == nil || IsNil(o.Tokenized) { + return nil, false + } + return o.Tokenized, true +} + +// HasTokenized returns a boolean if a field has been set. +func (o *Session) HasTokenized() bool { + if o != nil && !IsNil(o.Tokenized) { + return true + } + + return false +} + +// SetTokenized gets a reference to the given string and assigns it to the Tokenized field. +func (o *Session) SetTokenized(v string) { + o.Tokenized = &v +} + +func (o Session) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Session) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.AuthenticatedAt) { + toSerialize["authenticated_at"] = o.AuthenticatedAt + } + if !IsNil(o.AuthenticationMethods) { + toSerialize["authentication_methods"] = o.AuthenticationMethods + } + if !IsNil(o.AuthenticatorAssuranceLevel) { + toSerialize["authenticator_assurance_level"] = o.AuthenticatorAssuranceLevel + } + if !IsNil(o.Devices) { + toSerialize["devices"] = o.Devices + } + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["id"] = o.Id + if !IsNil(o.Identity) { + toSerialize["identity"] = o.Identity + } + if !IsNil(o.IssuedAt) { + toSerialize["issued_at"] = o.IssuedAt + } + if !IsNil(o.Tokenized) { + toSerialize["tokenized"] = o.Tokenized + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Session) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSession := _Session{} + + err = json.Unmarshal(data, &varSession) + + if err != nil { + return err + } + + *o = Session(varSession) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "authenticated_at") + delete(additionalProperties, "authentication_methods") + delete(additionalProperties, "authenticator_assurance_level") + delete(additionalProperties, "devices") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "tokenized") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSession struct { + value *Session + isSet bool +} + +func (v NullableSession) Get() *Session { + return v.value +} + +func (v *NullableSession) Set(val *Session) { + v.value = val + v.isSet = true +} + +func (v NullableSession) IsSet() bool { + return v.isSet +} + +func (v *NullableSession) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSession(val *Session) *NullableSession { + return &NullableSession{value: val, isSet: true} +} + +func (v NullableSession) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSession) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_session_authentication_method.go b/pkg/httpclient/model_session_authentication_method.go new file mode 100644 index 0000000..a72b735 --- /dev/null +++ b/pkg/httpclient/model_session_authentication_method.go @@ -0,0 +1,383 @@ +/* +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" + "time" +) + +// checks if the SessionAuthenticationMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SessionAuthenticationMethod{} + +// SessionAuthenticationMethod A singular authenticator used during authentication / login. +type SessionAuthenticationMethod struct { + Aal *AuthenticatorAssuranceLevel `json:"aal,omitempty"` + // When the authentication challenge was completed. + CompletedAt *time.Time `json:"completed_at,omitempty"` + // The method used in this authenticator. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile saml CredentialsTypeSAML deviceauthn CredentialsTypeDeviceAuthn identifier_first CredentialsTypeIdentifierFirst 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 + Method *string `json:"method,omitempty"` + // The Organization id used for authentication + Organization *string `json:"organization,omitempty"` + // OIDC or SAML provider id used for authentication + Provider *string `json:"provider,omitempty"` + // UpstreamACR is the `acr` claim reported by the upstream OIDC provider, if any. Populated only for OIDC login methods when the upstream ID token contained an `acr` claim. + UpstreamAcr *string `json:"upstream_acr,omitempty"` + // UpstreamAMR is the `amr` claim reported by the upstream OIDC provider, if any. Populated only for OIDC login methods when the upstream ID token contained an `amr` claim. + UpstreamAmr []string `json:"upstream_amr,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SessionAuthenticationMethod SessionAuthenticationMethod + +// NewSessionAuthenticationMethod instantiates a new SessionAuthenticationMethod 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 NewSessionAuthenticationMethod() *SessionAuthenticationMethod { + this := SessionAuthenticationMethod{} + return &this +} + +// NewSessionAuthenticationMethodWithDefaults instantiates a new SessionAuthenticationMethod 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 NewSessionAuthenticationMethodWithDefaults() *SessionAuthenticationMethod { + this := SessionAuthenticationMethod{} + return &this +} + +// GetAal returns the Aal field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetAal() AuthenticatorAssuranceLevel { + if o == nil || IsNil(o.Aal) { + var ret AuthenticatorAssuranceLevel + return ret + } + return *o.Aal +} + +// GetAalOk returns a tuple with the Aal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetAalOk() (*AuthenticatorAssuranceLevel, bool) { + if o == nil || IsNil(o.Aal) { + return nil, false + } + return o.Aal, true +} + +// HasAal returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasAal() bool { + if o != nil && !IsNil(o.Aal) { + return true + } + + return false +} + +// SetAal gets a reference to the given AuthenticatorAssuranceLevel and assigns it to the Aal field. +func (o *SessionAuthenticationMethod) SetAal(v AuthenticatorAssuranceLevel) { + o.Aal = &v +} + +// GetCompletedAt returns the CompletedAt field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetCompletedAt() time.Time { + if o == nil || IsNil(o.CompletedAt) { + var ret time.Time + return ret + } + return *o.CompletedAt +} + +// GetCompletedAtOk returns a tuple with the CompletedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetCompletedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CompletedAt) { + return nil, false + } + return o.CompletedAt, true +} + +// HasCompletedAt returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasCompletedAt() bool { + if o != nil && !IsNil(o.CompletedAt) { + return true + } + + return false +} + +// SetCompletedAt gets a reference to the given time.Time and assigns it to the CompletedAt field. +func (o *SessionAuthenticationMethod) SetCompletedAt(v time.Time) { + o.CompletedAt = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetMethod() string { + if o == nil || IsNil(o.Method) { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetMethodOk() (*string, bool) { + if o == nil || IsNil(o.Method) { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasMethod() bool { + if o != nil && !IsNil(o.Method) { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *SessionAuthenticationMethod) SetMethod(v string) { + o.Method = &v +} + +// GetOrganization returns the Organization field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetOrganization() string { + if o == nil || IsNil(o.Organization) { + var ret string + return ret + } + return *o.Organization +} + +// GetOrganizationOk returns a tuple with the Organization field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetOrganizationOk() (*string, bool) { + if o == nil || IsNil(o.Organization) { + return nil, false + } + return o.Organization, true +} + +// HasOrganization returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasOrganization() bool { + if o != nil && !IsNil(o.Organization) { + return true + } + + return false +} + +// SetOrganization gets a reference to the given string and assigns it to the Organization field. +func (o *SessionAuthenticationMethod) SetOrganization(v string) { + o.Organization = &v +} + +// GetProvider returns the Provider field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetProvider() string { + if o == nil || IsNil(o.Provider) { + var ret string + return ret + } + return *o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetProviderOk() (*string, bool) { + if o == nil || IsNil(o.Provider) { + return nil, false + } + return o.Provider, true +} + +// HasProvider returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasProvider() bool { + if o != nil && !IsNil(o.Provider) { + return true + } + + return false +} + +// SetProvider gets a reference to the given string and assigns it to the Provider field. +func (o *SessionAuthenticationMethod) SetProvider(v string) { + o.Provider = &v +} + +// GetUpstreamAcr returns the UpstreamAcr field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetUpstreamAcr() string { + if o == nil || IsNil(o.UpstreamAcr) { + var ret string + return ret + } + return *o.UpstreamAcr +} + +// GetUpstreamAcrOk returns a tuple with the UpstreamAcr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetUpstreamAcrOk() (*string, bool) { + if o == nil || IsNil(o.UpstreamAcr) { + return nil, false + } + return o.UpstreamAcr, true +} + +// HasUpstreamAcr returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasUpstreamAcr() bool { + if o != nil && !IsNil(o.UpstreamAcr) { + return true + } + + return false +} + +// SetUpstreamAcr gets a reference to the given string and assigns it to the UpstreamAcr field. +func (o *SessionAuthenticationMethod) SetUpstreamAcr(v string) { + o.UpstreamAcr = &v +} + +// GetUpstreamAmr returns the UpstreamAmr field value if set, zero value otherwise. +func (o *SessionAuthenticationMethod) GetUpstreamAmr() []string { + if o == nil || IsNil(o.UpstreamAmr) { + var ret []string + return ret + } + return o.UpstreamAmr +} + +// GetUpstreamAmrOk returns a tuple with the UpstreamAmr field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionAuthenticationMethod) GetUpstreamAmrOk() ([]string, bool) { + if o == nil || IsNil(o.UpstreamAmr) { + return nil, false + } + return o.UpstreamAmr, true +} + +// HasUpstreamAmr returns a boolean if a field has been set. +func (o *SessionAuthenticationMethod) HasUpstreamAmr() bool { + if o != nil && !IsNil(o.UpstreamAmr) { + return true + } + + return false +} + +// SetUpstreamAmr gets a reference to the given []string and assigns it to the UpstreamAmr field. +func (o *SessionAuthenticationMethod) SetUpstreamAmr(v []string) { + o.UpstreamAmr = v +} + +func (o SessionAuthenticationMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SessionAuthenticationMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Aal) { + toSerialize["aal"] = o.Aal + } + if !IsNil(o.CompletedAt) { + toSerialize["completed_at"] = o.CompletedAt + } + if !IsNil(o.Method) { + toSerialize["method"] = o.Method + } + if !IsNil(o.Organization) { + toSerialize["organization"] = o.Organization + } + if !IsNil(o.Provider) { + toSerialize["provider"] = o.Provider + } + if !IsNil(o.UpstreamAcr) { + toSerialize["upstream_acr"] = o.UpstreamAcr + } + if !IsNil(o.UpstreamAmr) { + toSerialize["upstream_amr"] = o.UpstreamAmr + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SessionAuthenticationMethod) UnmarshalJSON(data []byte) (err error) { + varSessionAuthenticationMethod := _SessionAuthenticationMethod{} + + err = json.Unmarshal(data, &varSessionAuthenticationMethod) + + if err != nil { + return err + } + + *o = SessionAuthenticationMethod(varSessionAuthenticationMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "aal") + delete(additionalProperties, "completed_at") + delete(additionalProperties, "method") + delete(additionalProperties, "organization") + delete(additionalProperties, "provider") + delete(additionalProperties, "upstream_acr") + delete(additionalProperties, "upstream_amr") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSessionAuthenticationMethod struct { + value *SessionAuthenticationMethod + isSet bool +} + +func (v NullableSessionAuthenticationMethod) Get() *SessionAuthenticationMethod { + return v.value +} + +func (v *NullableSessionAuthenticationMethod) Set(val *SessionAuthenticationMethod) { + v.value = val + v.isSet = true +} + +func (v NullableSessionAuthenticationMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableSessionAuthenticationMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSessionAuthenticationMethod(val *SessionAuthenticationMethod) *NullableSessionAuthenticationMethod { + return &NullableSessionAuthenticationMethod{value: val, isSet: true} +} + +func (v NullableSessionAuthenticationMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSessionAuthenticationMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_session_device.go b/pkg/httpclient/model_session_device.go new file mode 100644 index 0000000..3370aed --- /dev/null +++ b/pkg/httpclient/model_session_device.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" +) + +// checks if the SessionDevice type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SessionDevice{} + +// SessionDevice Device corresponding to a Session +type SessionDevice struct { + // Device record ID + Id string `json:"id"` + // IPAddress of the client + IpAddress *string `json:"ip_address,omitempty"` + // Geo Location corresponding to the IP Address + Location *string `json:"location,omitempty"` + // UserAgent of the client + UserAgent *string `json:"user_agent,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _SessionDevice SessionDevice + +// NewSessionDevice instantiates a new SessionDevice 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 NewSessionDevice(id string) *SessionDevice { + this := SessionDevice{} + this.Id = id + return &this +} + +// NewSessionDeviceWithDefaults instantiates a new SessionDevice 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 NewSessionDeviceWithDefaults() *SessionDevice { + this := SessionDevice{} + return &this +} + +// GetId returns the Id field value +func (o *SessionDevice) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *SessionDevice) SetId(v string) { + o.Id = v +} + +// GetIpAddress returns the IpAddress field value if set, zero value otherwise. +func (o *SessionDevice) GetIpAddress() string { + if o == nil || IsNil(o.IpAddress) { + var ret string + return ret + } + return *o.IpAddress +} + +// GetIpAddressOk returns a tuple with the IpAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetIpAddressOk() (*string, bool) { + if o == nil || IsNil(o.IpAddress) { + return nil, false + } + return o.IpAddress, true +} + +// HasIpAddress returns a boolean if a field has been set. +func (o *SessionDevice) HasIpAddress() bool { + if o != nil && !IsNil(o.IpAddress) { + return true + } + + return false +} + +// SetIpAddress gets a reference to the given string and assigns it to the IpAddress field. +func (o *SessionDevice) SetIpAddress(v string) { + o.IpAddress = &v +} + +// GetLocation returns the Location field value if set, zero value otherwise. +func (o *SessionDevice) GetLocation() string { + if o == nil || IsNil(o.Location) { + var ret string + return ret + } + return *o.Location +} + +// GetLocationOk returns a tuple with the Location field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetLocationOk() (*string, bool) { + if o == nil || IsNil(o.Location) { + return nil, false + } + return o.Location, true +} + +// HasLocation returns a boolean if a field has been set. +func (o *SessionDevice) HasLocation() bool { + if o != nil && !IsNil(o.Location) { + return true + } + + return false +} + +// SetLocation gets a reference to the given string and assigns it to the Location field. +func (o *SessionDevice) SetLocation(v string) { + o.Location = &v +} + +// GetUserAgent returns the UserAgent field value if set, zero value otherwise. +func (o *SessionDevice) GetUserAgent() string { + if o == nil || IsNil(o.UserAgent) { + var ret string + return ret + } + return *o.UserAgent +} + +// GetUserAgentOk returns a tuple with the UserAgent field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SessionDevice) GetUserAgentOk() (*string, bool) { + if o == nil || IsNil(o.UserAgent) { + return nil, false + } + return o.UserAgent, true +} + +// HasUserAgent returns a boolean if a field has been set. +func (o *SessionDevice) HasUserAgent() bool { + if o != nil && !IsNil(o.UserAgent) { + return true + } + + return false +} + +// SetUserAgent gets a reference to the given string and assigns it to the UserAgent field. +func (o *SessionDevice) SetUserAgent(v string) { + o.UserAgent = &v +} + +func (o SessionDevice) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SessionDevice) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + if !IsNil(o.IpAddress) { + toSerialize["ip_address"] = o.IpAddress + } + if !IsNil(o.Location) { + toSerialize["location"] = o.Location + } + if !IsNil(o.UserAgent) { + toSerialize["user_agent"] = o.UserAgent + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SessionDevice) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSessionDevice := _SessionDevice{} + + err = json.Unmarshal(data, &varSessionDevice) + + if err != nil { + return err + } + + *o = SessionDevice(varSessionDevice) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "ip_address") + delete(additionalProperties, "location") + delete(additionalProperties, "user_agent") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSessionDevice struct { + value *SessionDevice + isSet bool +} + +func (v NullableSessionDevice) Get() *SessionDevice { + return v.value +} + +func (v *NullableSessionDevice) Set(val *SessionDevice) { + v.value = val + v.isSet = true +} + +func (v NullableSessionDevice) IsSet() bool { + return v.isSet +} + +func (v *NullableSessionDevice) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSessionDevice(val *SessionDevice) *NullableSessionDevice { + return &NullableSessionDevice{value: val, isSet: true} +} + +func (v NullableSessionDevice) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSessionDevice) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_settings_flow.go b/pkg/httpclient/model_settings_flow.go new file mode 100644 index 0000000..5767aee --- /dev/null +++ b/pkg/httpclient/model_settings_flow.go @@ -0,0 +1,581 @@ +/* +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" + "fmt" + "time" +) + +// checks if the SettingsFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SettingsFlow{} + +// SettingsFlow This flow is used when an identity wants to update settings (e.g. profile data, passwords, ...) in a selfservice manner. We recommend reading the [User Settings Documentation](../self-service/flows/user-settings) +type SettingsFlow struct { + // Active, if set, contains the registration method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // 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. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting, a new flow has to be initiated. + ExpiresAt time.Time `json:"expires_at"` + // ID represents the flow's unique ID. When performing the settings flow, this represents the id in the settings ui's query parameter: http://?flow= + Id string `json:"id"` + Identity Identity `json:"identity"` + // IssuedAt is the time (UTC) when the flow occurred. + IssuedAt time.Time `json:"issued_at"` + OrganizationId NullableString `json:"organization_id,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl string `json:"request_url"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _SettingsFlow SettingsFlow + +// NewSettingsFlow instantiates a new SettingsFlow 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 NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { + this := SettingsFlow{} + this.ExpiresAt = expiresAt + this.Id = id + this.Identity = identity + this.IssuedAt = issuedAt + this.RequestUrl = requestUrl + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewSettingsFlowWithDefaults instantiates a new SettingsFlow 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 NewSettingsFlowWithDefaults() *SettingsFlow { + this := SettingsFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *SettingsFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *SettingsFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *SettingsFlow) SetActive(v string) { + o.Active = &v +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SettingsFlow) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *SettingsFlow) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SettingsFlow) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SettingsFlow) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetExpiresAt returns the ExpiresAt field value +func (o *SettingsFlow) GetExpiresAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.ExpiresAt, true +} + +// SetExpiresAt sets field value +func (o *SettingsFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = v +} + +// GetId returns the Id field value +func (o *SettingsFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *SettingsFlow) SetId(v string) { + o.Id = v +} + +// GetIdentity returns the Identity field value +func (o *SettingsFlow) GetIdentity() Identity { + if o == nil { + var ret Identity + return ret + } + + return o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIdentityOk() (*Identity, bool) { + if o == nil { + return nil, false + } + return &o.Identity, true +} + +// SetIdentity sets field value +func (o *SettingsFlow) SetIdentity(v Identity) { + o.Identity = v +} + +// GetIssuedAt returns the IssuedAt field value +func (o *SettingsFlow) GetIssuedAt() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.IssuedAt, true +} + +// SetIssuedAt sets field value +func (o *SettingsFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = v +} + +// GetOrganizationId returns the OrganizationId field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *SettingsFlow) GetOrganizationId() string { + if o == nil || IsNil(o.OrganizationId.Get()) { + var ret string + return ret + } + return *o.OrganizationId.Get() +} + +// GetOrganizationIdOk returns a tuple with the OrganizationId 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 *SettingsFlow) GetOrganizationIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.OrganizationId.Get(), o.OrganizationId.IsSet() +} + +// HasOrganizationId returns a boolean if a field has been set. +func (o *SettingsFlow) HasOrganizationId() bool { + if o != nil && o.OrganizationId.IsSet() { + return true + } + + return false +} + +// SetOrganizationId gets a reference to the given NullableString and assigns it to the OrganizationId field. +func (o *SettingsFlow) SetOrganizationId(v string) { + o.OrganizationId.Set(&v) +} + +// SetOrganizationIdNil sets the value for OrganizationId to be an explicit nil +func (o *SettingsFlow) SetOrganizationIdNil() { + o.OrganizationId.Set(nil) +} + +// UnsetOrganizationId ensures that no value is present for OrganizationId, not even an explicit nil +func (o *SettingsFlow) UnsetOrganizationId() { + o.OrganizationId.Unset() +} + +// GetRequestUrl returns the RequestUrl field value +func (o *SettingsFlow) GetRequestUrl() string { + if o == nil { + var ret string + return ret + } + + return o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetRequestUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RequestUrl, true +} + +// SetRequestUrl sets field value +func (o *SettingsFlow) SetRequestUrl(v string) { + o.RequestUrl = v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *SettingsFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *SettingsFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *SettingsFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *SettingsFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *SettingsFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *SettingsFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o SettingsFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SettingsFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["expires_at"] = o.ExpiresAt + toSerialize["id"] = o.Id + toSerialize["identity"] = o.Identity + toSerialize["issued_at"] = o.IssuedAt + if o.OrganizationId.IsSet() { + toSerialize["organization_id"] = o.OrganizationId.Get() + } + toSerialize["request_url"] = o.RequestUrl + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SettingsFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "expires_at", + "id", + "identity", + "issued_at", + "request_url", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSettingsFlow := _SettingsFlow{} + + err = json.Unmarshal(data, &varSettingsFlow) + + if err != nil { + return err + } + + *o = SettingsFlow(varSettingsFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "continue_with") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "identity") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "organization_id") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSettingsFlow struct { + value *SettingsFlow + isSet bool +} + +func (v NullableSettingsFlow) Get() *SettingsFlow { + return v.value +} + +func (v *NullableSettingsFlow) Set(val *SettingsFlow) { + v.value = val + v.isSet = true +} + +func (v NullableSettingsFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableSettingsFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSettingsFlow(val *SettingsFlow) *NullableSettingsFlow { + return &NullableSettingsFlow{value: val, isSet: true} +} + +func (v NullableSettingsFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSettingsFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_settings_flow_state.go b/pkg/httpclient/model_settings_flow_state.go new file mode 100644 index 0000000..47817bc --- /dev/null +++ b/pkg/httpclient/model_settings_flow_state.go @@ -0,0 +1,111 @@ +/* +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" + "fmt" +) + +// 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 +const ( + SETTINGSFLOWSTATE_SHOW_FORM SettingsFlowState = "show_form" + SETTINGSFLOWSTATE_SUCCESS SettingsFlowState = "success" +) + +// All allowed values of SettingsFlowState enum +var AllowedSettingsFlowStateEnumValues = []SettingsFlowState{ + "show_form", + "success", +} + +func (v *SettingsFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SettingsFlowState(value) + for _, existing := range AllowedSettingsFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SettingsFlowState", value) +} + +// NewSettingsFlowStateFromValue returns a pointer to a valid SettingsFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewSettingsFlowStateFromValue(v string) (*SettingsFlowState, error) { + ev := SettingsFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for SettingsFlowState: valid values are %v", v, AllowedSettingsFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v SettingsFlowState) IsValid() bool { + for _, existing := range AllowedSettingsFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to settingsFlowState value +func (v SettingsFlowState) Ptr() *SettingsFlowState { + return &v +} + +type NullableSettingsFlowState struct { + value *SettingsFlowState + isSet bool +} + +func (v NullableSettingsFlowState) Get() *SettingsFlowState { + return v.value +} + +func (v *NullableSettingsFlowState) Set(val *SettingsFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSettingsFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSettingsFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSettingsFlowState(val *SettingsFlowState) *NullableSettingsFlowState { + return &NullableSettingsFlowState{value: val, isSet: true} +} + +func (v NullableSettingsFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSettingsFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_successful_code_exchange_response.go b/pkg/httpclient/model_successful_code_exchange_response.go new file mode 100644 index 0000000..e1bbaa2 --- /dev/null +++ b/pkg/httpclient/model_successful_code_exchange_response.go @@ -0,0 +1,205 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulCodeExchangeResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulCodeExchangeResponse{} + +// SuccessfulCodeExchangeResponse The Response for Registration Flows via API +type SuccessfulCodeExchangeResponse struct { + 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulCodeExchangeResponse SuccessfulCodeExchangeResponse + +// NewSuccessfulCodeExchangeResponse instantiates a new SuccessfulCodeExchangeResponse 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 NewSuccessfulCodeExchangeResponse(session Session) *SuccessfulCodeExchangeResponse { + this := SuccessfulCodeExchangeResponse{} + this.Session = session + return &this +} + +// NewSuccessfulCodeExchangeResponseWithDefaults instantiates a new SuccessfulCodeExchangeResponse 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 NewSuccessfulCodeExchangeResponseWithDefaults() *SuccessfulCodeExchangeResponse { + this := SuccessfulCodeExchangeResponse{} + return &this +} + +// GetSession returns the Session field value +func (o *SuccessfulCodeExchangeResponse) GetSession() Session { + if o == nil { + var ret Session + return ret + } + + return o.Session +} + +// GetSessionOk returns a tuple with the Session field value +// and a boolean to check if the value has been set. +func (o *SuccessfulCodeExchangeResponse) GetSessionOk() (*Session, bool) { + if o == nil { + return nil, false + } + return &o.Session, true +} + +// SetSession sets field value +func (o *SuccessfulCodeExchangeResponse) SetSession(v Session) { + o.Session = v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulCodeExchangeResponse) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulCodeExchangeResponse) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulCodeExchangeResponse) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulCodeExchangeResponse) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulCodeExchangeResponse) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulCodeExchangeResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["session"] = o.Session + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulCodeExchangeResponse) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulCodeExchangeResponse := _SuccessfulCodeExchangeResponse{} + + err = json.Unmarshal(data, &varSuccessfulCodeExchangeResponse) + + if err != nil { + return err + } + + *o = SuccessfulCodeExchangeResponse(varSuccessfulCodeExchangeResponse) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulCodeExchangeResponse struct { + value *SuccessfulCodeExchangeResponse + isSet bool +} + +func (v NullableSuccessfulCodeExchangeResponse) Get() *SuccessfulCodeExchangeResponse { + return v.value +} + +func (v *NullableSuccessfulCodeExchangeResponse) Set(val *SuccessfulCodeExchangeResponse) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulCodeExchangeResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulCodeExchangeResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulCodeExchangeResponse(val *SuccessfulCodeExchangeResponse) *NullableSuccessfulCodeExchangeResponse { + return &NullableSuccessfulCodeExchangeResponse{value: val, isSet: true} +} + +func (v NullableSuccessfulCodeExchangeResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulCodeExchangeResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_successful_native_login.go b/pkg/httpclient/model_successful_native_login.go new file mode 100644 index 0000000..05bf6b7 --- /dev/null +++ b/pkg/httpclient/model_successful_native_login.go @@ -0,0 +1,243 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulNativeLogin type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulNativeLogin{} + +// SuccessfulNativeLogin The Response for Login Flows via API +type SuccessfulNativeLogin struct { + // 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulNativeLogin SuccessfulNativeLogin + +// NewSuccessfulNativeLogin instantiates a new SuccessfulNativeLogin 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 NewSuccessfulNativeLogin(session Session) *SuccessfulNativeLogin { + this := SuccessfulNativeLogin{} + this.Session = session + return &this +} + +// NewSuccessfulNativeLoginWithDefaults instantiates a new SuccessfulNativeLogin 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 NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { + this := SuccessfulNativeLogin{} + return &this +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 || IsNil(o.ContinueWith) { + 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 && !IsNil(o.ContinueWith) { + 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 { + var ret Session + return ret + } + + return o.Session +} + +// GetSessionOk returns a tuple with the Session field value +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetSessionOk() (*Session, bool) { + if o == nil { + return nil, false + } + return &o.Session, true +} + +// SetSession sets field value +func (o *SuccessfulNativeLogin) SetSession(v Session) { + o.Session = v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulNativeLogin) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulNativeLogin) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["session"] = o.Session + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulNativeLogin) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "session", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulNativeLogin := _SuccessfulNativeLogin{} + + err = json.Unmarshal(data, &varSuccessfulNativeLogin) + + if err != nil { + return err + } + + *o = SuccessfulNativeLogin(varSuccessfulNativeLogin) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "continue_with") + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulNativeLogin struct { + value *SuccessfulNativeLogin + isSet bool +} + +func (v NullableSuccessfulNativeLogin) Get() *SuccessfulNativeLogin { + return v.value +} + +func (v *NullableSuccessfulNativeLogin) Set(val *SuccessfulNativeLogin) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulNativeLogin) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulNativeLogin) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulNativeLogin(val *SuccessfulNativeLogin) *NullableSuccessfulNativeLogin { + return &NullableSuccessfulNativeLogin{value: val, isSet: true} +} + +func (v NullableSuccessfulNativeLogin) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulNativeLogin) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_successful_native_registration.go b/pkg/httpclient/model_successful_native_registration.go new file mode 100644 index 0000000..d12b45a --- /dev/null +++ b/pkg/httpclient/model_successful_native_registration.go @@ -0,0 +1,280 @@ +/* +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" + "fmt" +) + +// checks if the SuccessfulNativeRegistration type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SuccessfulNativeRegistration{} + +// SuccessfulNativeRegistration The Response for Registration Flows via API +type SuccessfulNativeRegistration struct { + // 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"` + Identity Identity `json:"identity"` + Session *Session `json:"session,omitempty"` + // The Session Token This field is only set when the session hook is configured as a post-registration hook. 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"` + AdditionalProperties map[string]interface{} +} + +type _SuccessfulNativeRegistration SuccessfulNativeRegistration + +// NewSuccessfulNativeRegistration instantiates a new SuccessfulNativeRegistration 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 NewSuccessfulNativeRegistration(identity Identity) *SuccessfulNativeRegistration { + this := SuccessfulNativeRegistration{} + this.Identity = identity + return &this +} + +// NewSuccessfulNativeRegistrationWithDefaults instantiates a new SuccessfulNativeRegistration 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 NewSuccessfulNativeRegistrationWithDefaults() *SuccessfulNativeRegistration { + this := SuccessfulNativeRegistration{} + return &this +} + +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetContinueWith() []ContinueWith { + if o == nil || IsNil(o.ContinueWith) { + 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 *SuccessfulNativeRegistration) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || IsNil(o.ContinueWith) { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasContinueWith() bool { + if o != nil && !IsNil(o.ContinueWith) { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeRegistration) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + +// GetIdentity returns the Identity field value +func (o *SuccessfulNativeRegistration) GetIdentity() Identity { + if o == nil { + var ret Identity + return ret + } + + return o.Identity +} + +// GetIdentityOk returns a tuple with the Identity field value +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetIdentityOk() (*Identity, bool) { + if o == nil { + return nil, false + } + return &o.Identity, true +} + +// SetIdentity sets field value +func (o *SuccessfulNativeRegistration) SetIdentity(v Identity) { + o.Identity = v +} + +// GetSession returns the Session field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetSession() Session { + if o == nil || IsNil(o.Session) { + var ret Session + return ret + } + return *o.Session +} + +// GetSessionOk returns a tuple with the Session field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetSessionOk() (*Session, bool) { + if o == nil || IsNil(o.Session) { + return nil, false + } + return o.Session, true +} + +// HasSession returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasSession() bool { + if o != nil && !IsNil(o.Session) { + return true + } + + return false +} + +// SetSession gets a reference to the given Session and assigns it to the Session field. +func (o *SuccessfulNativeRegistration) SetSession(v Session) { + o.Session = &v +} + +// GetSessionToken returns the SessionToken field value if set, zero value otherwise. +func (o *SuccessfulNativeRegistration) GetSessionToken() string { + if o == nil || IsNil(o.SessionToken) { + var ret string + return ret + } + return *o.SessionToken +} + +// GetSessionTokenOk returns a tuple with the SessionToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeRegistration) GetSessionTokenOk() (*string, bool) { + if o == nil || IsNil(o.SessionToken) { + return nil, false + } + return o.SessionToken, true +} + +// HasSessionToken returns a boolean if a field has been set. +func (o *SuccessfulNativeRegistration) HasSessionToken() bool { + if o != nil && !IsNil(o.SessionToken) { + return true + } + + return false +} + +// SetSessionToken gets a reference to the given string and assigns it to the SessionToken field. +func (o *SuccessfulNativeRegistration) SetSessionToken(v string) { + o.SessionToken = &v +} + +func (o SuccessfulNativeRegistration) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SuccessfulNativeRegistration) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.ContinueWith) { + toSerialize["continue_with"] = o.ContinueWith + } + toSerialize["identity"] = o.Identity + if !IsNil(o.Session) { + toSerialize["session"] = o.Session + } + if !IsNil(o.SessionToken) { + toSerialize["session_token"] = o.SessionToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *SuccessfulNativeRegistration) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identity", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSuccessfulNativeRegistration := _SuccessfulNativeRegistration{} + + err = json.Unmarshal(data, &varSuccessfulNativeRegistration) + + if err != nil { + return err + } + + *o = SuccessfulNativeRegistration(varSuccessfulNativeRegistration) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "continue_with") + delete(additionalProperties, "identity") + delete(additionalProperties, "session") + delete(additionalProperties, "session_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableSuccessfulNativeRegistration struct { + value *SuccessfulNativeRegistration + isSet bool +} + +func (v NullableSuccessfulNativeRegistration) Get() *SuccessfulNativeRegistration { + return v.value +} + +func (v *NullableSuccessfulNativeRegistration) Set(val *SuccessfulNativeRegistration) { + v.value = val + v.isSet = true +} + +func (v NullableSuccessfulNativeRegistration) IsSet() bool { + return v.isSet +} + +func (v *NullableSuccessfulNativeRegistration) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSuccessfulNativeRegistration(val *SuccessfulNativeRegistration) *NullableSuccessfulNativeRegistration { + return &NullableSuccessfulNativeRegistration{value: val, isSet: true} +} + +func (v NullableSuccessfulNativeRegistration) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSuccessfulNativeRegistration) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_token_pagination.go b/pkg/httpclient/model_token_pagination.go new file mode 100644 index 0000000..d51af6c --- /dev/null +++ b/pkg/httpclient/model_token_pagination.go @@ -0,0 +1,197 @@ +/* +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" +) + +// checks if the TokenPagination type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TokenPagination{} + +// TokenPagination struct for TokenPagination +type TokenPagination struct { + // Items per page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + PageSize *int64 `json:"page_size,omitempty"` + // Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + PageToken *string `json:"page_token,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _TokenPagination TokenPagination + +// NewTokenPagination instantiates a new TokenPagination 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 NewTokenPagination() *TokenPagination { + this := TokenPagination{} + var pageSize int64 = 250 + this.PageSize = &pageSize + return &this +} + +// NewTokenPaginationWithDefaults instantiates a new TokenPagination 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 NewTokenPaginationWithDefaults() *TokenPagination { + this := TokenPagination{} + var pageSize int64 = 250 + this.PageSize = &pageSize + return &this +} + +// GetPageSize returns the PageSize field value if set, zero value otherwise. +func (o *TokenPagination) GetPageSize() int64 { + if o == nil || IsNil(o.PageSize) { + var ret int64 + return ret + } + return *o.PageSize +} + +// GetPageSizeOk returns a tuple with the PageSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPagination) GetPageSizeOk() (*int64, bool) { + if o == nil || IsNil(o.PageSize) { + return nil, false + } + return o.PageSize, true +} + +// HasPageSize returns a boolean if a field has been set. +func (o *TokenPagination) HasPageSize() bool { + if o != nil && !IsNil(o.PageSize) { + return true + } + + return false +} + +// SetPageSize gets a reference to the given int64 and assigns it to the PageSize field. +func (o *TokenPagination) SetPageSize(v int64) { + o.PageSize = &v +} + +// GetPageToken returns the PageToken field value if set, zero value otherwise. +func (o *TokenPagination) GetPageToken() string { + if o == nil || IsNil(o.PageToken) { + var ret string + return ret + } + return *o.PageToken +} + +// GetPageTokenOk returns a tuple with the PageToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPagination) GetPageTokenOk() (*string, bool) { + if o == nil || IsNil(o.PageToken) { + return nil, false + } + return o.PageToken, true +} + +// HasPageToken returns a boolean if a field has been set. +func (o *TokenPagination) HasPageToken() bool { + if o != nil && !IsNil(o.PageToken) { + return true + } + + return false +} + +// SetPageToken gets a reference to the given string and assigns it to the PageToken field. +func (o *TokenPagination) SetPageToken(v string) { + o.PageToken = &v +} + +func (o TokenPagination) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TokenPagination) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.PageSize) { + toSerialize["page_size"] = o.PageSize + } + if !IsNil(o.PageToken) { + toSerialize["page_token"] = o.PageToken + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *TokenPagination) UnmarshalJSON(data []byte) (err error) { + varTokenPagination := _TokenPagination{} + + err = json.Unmarshal(data, &varTokenPagination) + + if err != nil { + return err + } + + *o = TokenPagination(varTokenPagination) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "page_size") + delete(additionalProperties, "page_token") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableTokenPagination struct { + value *TokenPagination + isSet bool +} + +func (v NullableTokenPagination) Get() *TokenPagination { + return v.value +} + +func (v *NullableTokenPagination) Set(val *TokenPagination) { + v.value = val + v.isSet = true +} + +func (v NullableTokenPagination) IsSet() bool { + return v.isSet +} + +func (v *NullableTokenPagination) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTokenPagination(val *TokenPagination) *NullableTokenPagination { + return &NullableTokenPagination{value: val, isSet: true} +} + +func (v NullableTokenPagination) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTokenPagination) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_token_pagination_headers.go b/pkg/httpclient/model_token_pagination_headers.go new file mode 100644 index 0000000..0bf5689 --- /dev/null +++ b/pkg/httpclient/model_token_pagination_headers.go @@ -0,0 +1,193 @@ +/* +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" +) + +// checks if the TokenPaginationHeaders type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TokenPaginationHeaders{} + +// TokenPaginationHeaders struct for TokenPaginationHeaders +type TokenPaginationHeaders struct { + // The link header contains pagination links. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). in: header + Link *string `json:"link,omitempty"` + // The total number of clients. in: header + XTotalCount *string `json:"x-total-count,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _TokenPaginationHeaders TokenPaginationHeaders + +// NewTokenPaginationHeaders instantiates a new TokenPaginationHeaders 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 NewTokenPaginationHeaders() *TokenPaginationHeaders { + this := TokenPaginationHeaders{} + return &this +} + +// NewTokenPaginationHeadersWithDefaults instantiates a new TokenPaginationHeaders 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 NewTokenPaginationHeadersWithDefaults() *TokenPaginationHeaders { + this := TokenPaginationHeaders{} + return &this +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *TokenPaginationHeaders) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPaginationHeaders) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *TokenPaginationHeaders) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *TokenPaginationHeaders) SetLink(v string) { + o.Link = &v +} + +// GetXTotalCount returns the XTotalCount field value if set, zero value otherwise. +func (o *TokenPaginationHeaders) GetXTotalCount() string { + if o == nil || IsNil(o.XTotalCount) { + var ret string + return ret + } + return *o.XTotalCount +} + +// GetXTotalCountOk returns a tuple with the XTotalCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TokenPaginationHeaders) GetXTotalCountOk() (*string, bool) { + if o == nil || IsNil(o.XTotalCount) { + return nil, false + } + return o.XTotalCount, true +} + +// HasXTotalCount returns a boolean if a field has been set. +func (o *TokenPaginationHeaders) HasXTotalCount() bool { + if o != nil && !IsNil(o.XTotalCount) { + return true + } + + return false +} + +// SetXTotalCount gets a reference to the given string and assigns it to the XTotalCount field. +func (o *TokenPaginationHeaders) SetXTotalCount(v string) { + o.XTotalCount = &v +} + +func (o TokenPaginationHeaders) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TokenPaginationHeaders) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + if !IsNil(o.XTotalCount) { + toSerialize["x-total-count"] = o.XTotalCount + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *TokenPaginationHeaders) UnmarshalJSON(data []byte) (err error) { + varTokenPaginationHeaders := _TokenPaginationHeaders{} + + err = json.Unmarshal(data, &varTokenPaginationHeaders) + + if err != nil { + return err + } + + *o = TokenPaginationHeaders(varTokenPaginationHeaders) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "link") + delete(additionalProperties, "x-total-count") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableTokenPaginationHeaders struct { + value *TokenPaginationHeaders + isSet bool +} + +func (v NullableTokenPaginationHeaders) Get() *TokenPaginationHeaders { + return v.value +} + +func (v *NullableTokenPaginationHeaders) Set(val *TokenPaginationHeaders) { + v.value = val + v.isSet = true +} + +func (v NullableTokenPaginationHeaders) IsSet() bool { + return v.isSet +} + +func (v *NullableTokenPaginationHeaders) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTokenPaginationHeaders(val *TokenPaginationHeaders) *NullableTokenPaginationHeaders { + return &NullableTokenPaginationHeaders{value: val, isSet: true} +} + +func (v NullableTokenPaginationHeaders) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTokenPaginationHeaders) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_container.go b/pkg/httpclient/model_ui_container.go new file mode 100644 index 0000000..25e0ab5 --- /dev/null +++ b/pkg/httpclient/model_ui_container.go @@ -0,0 +1,264 @@ +/* +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" + "fmt" +) + +// checks if the UiContainer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiContainer{} + +// UiContainer Container represents a HTML Form. The container can work with both HTTP Form and JSON requests +type UiContainer struct { + // Action should be used as the form action URL ``. + Action string `json:"action"` + Messages []UiText `json:"messages,omitempty"` + // Method is the form method (e.g. POST) + Method string `json:"method"` + Nodes []UiNode `json:"nodes"` + AdditionalProperties map[string]interface{} +} + +type _UiContainer UiContainer + +// NewUiContainer instantiates a new UiContainer 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 NewUiContainer(action string, method string, nodes []UiNode) *UiContainer { + this := UiContainer{} + this.Action = action + this.Method = method + this.Nodes = nodes + return &this +} + +// NewUiContainerWithDefaults instantiates a new UiContainer 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 NewUiContainerWithDefaults() *UiContainer { + this := UiContainer{} + return &this +} + +// GetAction returns the Action field value +func (o *UiContainer) 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 *UiContainer) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *UiContainer) SetAction(v string) { + o.Action = v +} + +// GetMessages returns the Messages field value if set, zero value otherwise. +func (o *UiContainer) GetMessages() []UiText { + if o == nil || IsNil(o.Messages) { + var ret []UiText + return ret + } + return o.Messages +} + +// GetMessagesOk returns a tuple with the Messages field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiContainer) GetMessagesOk() ([]UiText, bool) { + if o == nil || IsNil(o.Messages) { + return nil, false + } + return o.Messages, true +} + +// HasMessages returns a boolean if a field has been set. +func (o *UiContainer) HasMessages() bool { + if o != nil && !IsNil(o.Messages) { + return true + } + + return false +} + +// SetMessages gets a reference to the given []UiText and assigns it to the Messages field. +func (o *UiContainer) SetMessages(v []UiText) { + o.Messages = v +} + +// GetMethod returns the Method field value +func (o *UiContainer) 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 *UiContainer) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UiContainer) SetMethod(v string) { + o.Method = v +} + +// GetNodes returns the Nodes field value +func (o *UiContainer) GetNodes() []UiNode { + if o == nil { + var ret []UiNode + return ret + } + + return o.Nodes +} + +// GetNodesOk returns a tuple with the Nodes field value +// and a boolean to check if the value has been set. +func (o *UiContainer) GetNodesOk() ([]UiNode, bool) { + if o == nil { + return nil, false + } + return o.Nodes, true +} + +// SetNodes sets field value +func (o *UiContainer) SetNodes(v []UiNode) { + o.Nodes = v +} + +func (o UiContainer) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiContainer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["action"] = o.Action + if !IsNil(o.Messages) { + toSerialize["messages"] = o.Messages + } + toSerialize["method"] = o.Method + toSerialize["nodes"] = o.Nodes + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiContainer) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "action", + "method", + "nodes", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiContainer := _UiContainer{} + + err = json.Unmarshal(data, &varUiContainer) + + if err != nil { + return err + } + + *o = UiContainer(varUiContainer) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "action") + delete(additionalProperties, "messages") + delete(additionalProperties, "method") + delete(additionalProperties, "nodes") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiContainer struct { + value *UiContainer + isSet bool +} + +func (v NullableUiContainer) Get() *UiContainer { + return v.value +} + +func (v *NullableUiContainer) Set(val *UiContainer) { + v.value = val + v.isSet = true +} + +func (v NullableUiContainer) IsSet() bool { + return v.isSet +} + +func (v *NullableUiContainer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiContainer(val *UiContainer) *NullableUiContainer { + return &NullableUiContainer{value: val, isSet: true} +} + +func (v NullableUiContainer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiContainer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node.go b/pkg/httpclient/model_ui_node.go new file mode 100644 index 0000000..ffa5f78 --- /dev/null +++ b/pkg/httpclient/model_ui_node.go @@ -0,0 +1,285 @@ +/* +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" + "fmt" +) + +// checks if the UiNode type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNode{} + +// 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 passkey PasskeyGroup identifier_first IdentifierFirstGroup captcha CaptchaGroup saml SAMLGroup deviceauthn DeviceAuthnGroup + Group string `json:"group"` + Messages []UiText `json:"messages"` + Meta UiNodeMeta `json:"meta"` + // The node's type text Text input Input img Image a Anchor script Script div Division + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiNode UiNode + +// NewUiNode instantiates a new UiNode 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 NewUiNode(attributes UiNodeAttributes, group string, messages []UiText, meta UiNodeMeta, type_ string) *UiNode { + this := UiNode{} + this.Attributes = attributes + this.Group = group + this.Messages = messages + this.Meta = meta + this.Type = type_ + return &this +} + +// NewUiNodeWithDefaults instantiates a new UiNode 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 NewUiNodeWithDefaults() *UiNode { + this := UiNode{} + return &this +} + +// GetAttributes returns the Attributes field value +func (o *UiNode) GetAttributes() UiNodeAttributes { + if o == nil { + var ret UiNodeAttributes + return ret + } + + return o.Attributes +} + +// GetAttributesOk returns a tuple with the Attributes field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetAttributesOk() (*UiNodeAttributes, bool) { + if o == nil { + return nil, false + } + return &o.Attributes, true +} + +// SetAttributes sets field value +func (o *UiNode) SetAttributes(v UiNodeAttributes) { + o.Attributes = v +} + +// GetGroup returns the Group field value +func (o *UiNode) GetGroup() string { + if o == nil { + var ret string + return ret + } + + return o.Group +} + +// GetGroupOk returns a tuple with the Group field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetGroupOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Group, true +} + +// SetGroup sets field value +func (o *UiNode) SetGroup(v string) { + o.Group = v +} + +// GetMessages returns the Messages field value +func (o *UiNode) GetMessages() []UiText { + if o == nil { + var ret []UiText + return ret + } + + return o.Messages +} + +// GetMessagesOk returns a tuple with the Messages field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetMessagesOk() ([]UiText, bool) { + if o == nil { + return nil, false + } + return o.Messages, true +} + +// SetMessages sets field value +func (o *UiNode) SetMessages(v []UiText) { + o.Messages = v +} + +// GetMeta returns the Meta field value +func (o *UiNode) GetMeta() UiNodeMeta { + if o == nil { + var ret UiNodeMeta + return ret + } + + return o.Meta +} + +// GetMetaOk returns a tuple with the Meta field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetMetaOk() (*UiNodeMeta, bool) { + if o == nil { + return nil, false + } + return &o.Meta, true +} + +// SetMeta sets field value +func (o *UiNode) SetMeta(v UiNodeMeta) { + o.Meta = v +} + +// GetType returns the Type field value +func (o *UiNode) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNode) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNode) SetType(v string) { + o.Type = v +} + +func (o UiNode) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNode) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["attributes"] = o.Attributes + toSerialize["group"] = o.Group + toSerialize["messages"] = o.Messages + toSerialize["meta"] = o.Meta + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNode) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "attributes", + "group", + "messages", + "meta", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNode := _UiNode{} + + err = json.Unmarshal(data, &varUiNode) + + if err != nil { + return err + } + + *o = UiNode(varUiNode) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "attributes") + delete(additionalProperties, "group") + delete(additionalProperties, "messages") + delete(additionalProperties, "meta") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNode struct { + value *UiNode + isSet bool +} + +func (v NullableUiNode) Get() *UiNode { + return v.value +} + +func (v *NullableUiNode) Set(val *UiNode) { + v.value = val + v.isSet = true +} + +func (v NullableUiNode) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNode(val *UiNode) *NullableUiNode { + return &NullableUiNode{value: val, isSet: true} +} + +func (v NullableUiNode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_anchor_attributes.go b/pkg/httpclient/model_ui_node_anchor_attributes.go new file mode 100644 index 0000000..4b9da73 --- /dev/null +++ b/pkg/httpclient/model_ui_node_anchor_attributes.go @@ -0,0 +1,257 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeAnchorAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeAnchorAttributes{} + +// UiNodeAnchorAttributes struct for UiNodeAnchorAttributes +type UiNodeAnchorAttributes struct { + // The link's href (destination) URL. format: uri + 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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + Title UiText `json:"title"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeAnchorAttributes UiNodeAnchorAttributes + +// NewUiNodeAnchorAttributes instantiates a new UiNodeAnchorAttributes 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 NewUiNodeAnchorAttributes(href string, id string, nodeType string, title UiText) *UiNodeAnchorAttributes { + this := UiNodeAnchorAttributes{} + this.Href = href + this.Id = id + this.NodeType = nodeType + this.Title = title + return &this +} + +// NewUiNodeAnchorAttributesWithDefaults instantiates a new UiNodeAnchorAttributes 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 NewUiNodeAnchorAttributesWithDefaults() *UiNodeAnchorAttributes { + this := UiNodeAnchorAttributes{} + return &this +} + +// GetHref returns the Href field value +func (o *UiNodeAnchorAttributes) GetHref() string { + if o == nil { + var ret string + return ret + } + + return o.Href +} + +// GetHrefOk returns a tuple with the Href field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetHrefOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Href, true +} + +// SetHref sets field value +func (o *UiNodeAnchorAttributes) SetHref(v string) { + o.Href = v +} + +// GetId returns the Id field value +func (o *UiNodeAnchorAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeAnchorAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeAnchorAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeAnchorAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetTitle returns the Title field value +func (o *UiNodeAnchorAttributes) GetTitle() UiText { + if o == nil { + var ret UiText + return ret + } + + return o.Title +} + +// GetTitleOk returns a tuple with the Title field value +// and a boolean to check if the value has been set. +func (o *UiNodeAnchorAttributes) GetTitleOk() (*UiText, bool) { + if o == nil { + return nil, false + } + return &o.Title, true +} + +// SetTitle sets field value +func (o *UiNodeAnchorAttributes) SetTitle(v UiText) { + o.Title = v +} + +func (o UiNodeAnchorAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeAnchorAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["href"] = o.Href + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["title"] = o.Title + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeAnchorAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "href", + "id", + "node_type", + "title", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeAnchorAttributes := _UiNodeAnchorAttributes{} + + err = json.Unmarshal(data, &varUiNodeAnchorAttributes) + + if err != nil { + return err + } + + *o = UiNodeAnchorAttributes(varUiNodeAnchorAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "href") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "title") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeAnchorAttributes struct { + value *UiNodeAnchorAttributes + isSet bool +} + +func (v NullableUiNodeAnchorAttributes) Get() *UiNodeAnchorAttributes { + return v.value +} + +func (v *NullableUiNodeAnchorAttributes) Set(val *UiNodeAnchorAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeAnchorAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeAnchorAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeAnchorAttributes(val *UiNodeAnchorAttributes) *NullableUiNodeAnchorAttributes { + return &NullableUiNodeAnchorAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeAnchorAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeAnchorAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_attributes.go b/pkg/httpclient/model_ui_node_attributes.go new file mode 100644 index 0000000..4dfcdcc --- /dev/null +++ b/pkg/httpclient/model_ui_node_attributes.go @@ -0,0 +1,354 @@ +/* +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" + "fmt" +) + +// UiNodeAttributes - struct for UiNodeAttributes +type UiNodeAttributes struct { + UiNodeAnchorAttributes *UiNodeAnchorAttributes + UiNodeDivisionAttributes *UiNodeDivisionAttributes + UiNodeImageAttributes *UiNodeImageAttributes + UiNodeInputAttributes *UiNodeInputAttributes + UiNodeScriptAttributes *UiNodeScriptAttributes + UiNodeTextAttributes *UiNodeTextAttributes +} + +// UiNodeAnchorAttributesAsUiNodeAttributes is a convenience function that returns UiNodeAnchorAttributes wrapped in UiNodeAttributes +func UiNodeAnchorAttributesAsUiNodeAttributes(v *UiNodeAnchorAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeAnchorAttributes: v, + } +} + +// UiNodeDivisionAttributesAsUiNodeAttributes is a convenience function that returns UiNodeDivisionAttributes wrapped in UiNodeAttributes +func UiNodeDivisionAttributesAsUiNodeAttributes(v *UiNodeDivisionAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeDivisionAttributes: v, + } +} + +// UiNodeImageAttributesAsUiNodeAttributes is a convenience function that returns UiNodeImageAttributes wrapped in UiNodeAttributes +func UiNodeImageAttributesAsUiNodeAttributes(v *UiNodeImageAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeImageAttributes: v, + } +} + +// UiNodeInputAttributesAsUiNodeAttributes is a convenience function that returns UiNodeInputAttributes wrapped in UiNodeAttributes +func UiNodeInputAttributesAsUiNodeAttributes(v *UiNodeInputAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeInputAttributes: v, + } +} + +// UiNodeScriptAttributesAsUiNodeAttributes is a convenience function that returns UiNodeScriptAttributes wrapped in UiNodeAttributes +func UiNodeScriptAttributesAsUiNodeAttributes(v *UiNodeScriptAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeScriptAttributes: v, + } +} + +// UiNodeTextAttributesAsUiNodeAttributes is a convenience function that returns UiNodeTextAttributes wrapped in UiNodeAttributes +func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttributes { + return UiNodeAttributes{ + UiNodeTextAttributes: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'div' + if jsonDict["node_type"] == "div" { + // try to unmarshal JSON data into UiNodeDivisionAttributes + err = json.Unmarshal(data, &dst.UiNodeDivisionAttributes) + if err == nil { + return nil // data stored in dst.UiNodeDivisionAttributes, return on the first match + } else { + dst.UiNodeDivisionAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeDivisionAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeImageAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } + + // 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 { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // 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 'uiNodeDivisionAttributes' + if jsonDict["node_type"] == "uiNodeDivisionAttributes" { + // try to unmarshal JSON data into UiNodeDivisionAttributes + err = json.Unmarshal(data, &dst.UiNodeDivisionAttributes) + if err == nil { + return nil // data stored in dst.UiNodeDivisionAttributes, return on the first match + } else { + dst.UiNodeDivisionAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeDivisionAttributes: %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 { + dst.UiNodeImageAttributes = nil + return fmt.Errorf("failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UiNodeAttributes) MarshalJSON() ([]byte, error) { + if src.UiNodeAnchorAttributes != nil { + return json.Marshal(&src.UiNodeAnchorAttributes) + } + + if src.UiNodeDivisionAttributes != nil { + return json.Marshal(&src.UiNodeDivisionAttributes) + } + + if src.UiNodeImageAttributes != nil { + return json.Marshal(&src.UiNodeImageAttributes) + } + + if src.UiNodeInputAttributes != nil { + return json.Marshal(&src.UiNodeInputAttributes) + } + + if src.UiNodeScriptAttributes != nil { + return json.Marshal(&src.UiNodeScriptAttributes) + } + + if src.UiNodeTextAttributes != nil { + return json.Marshal(&src.UiNodeTextAttributes) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UiNodeAttributes) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UiNodeAnchorAttributes != nil { + return obj.UiNodeAnchorAttributes + } + + if obj.UiNodeDivisionAttributes != nil { + return obj.UiNodeDivisionAttributes + } + + if obj.UiNodeImageAttributes != nil { + return obj.UiNodeImageAttributes + } + + if obj.UiNodeInputAttributes != nil { + return obj.UiNodeInputAttributes + } + + if obj.UiNodeScriptAttributes != nil { + return obj.UiNodeScriptAttributes + } + + if obj.UiNodeTextAttributes != nil { + return obj.UiNodeTextAttributes + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UiNodeAttributes) GetActualInstanceValue() interface{} { + if obj.UiNodeAnchorAttributes != nil { + return *obj.UiNodeAnchorAttributes + } + + if obj.UiNodeDivisionAttributes != nil { + return *obj.UiNodeDivisionAttributes + } + + if obj.UiNodeImageAttributes != nil { + return *obj.UiNodeImageAttributes + } + + if obj.UiNodeInputAttributes != nil { + return *obj.UiNodeInputAttributes + } + + if obj.UiNodeScriptAttributes != nil { + return *obj.UiNodeScriptAttributes + } + + if obj.UiNodeTextAttributes != nil { + return *obj.UiNodeTextAttributes + } + + // all schemas are nil + return nil +} + +type NullableUiNodeAttributes struct { + value *UiNodeAttributes + isSet bool +} + +func (v NullableUiNodeAttributes) Get() *UiNodeAttributes { + return v.value +} + +func (v *NullableUiNodeAttributes) Set(val *UiNodeAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeAttributes(val *UiNodeAttributes) *NullableUiNodeAttributes { + return &NullableUiNodeAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_division_attributes.go b/pkg/httpclient/model_ui_node_division_attributes.go new file mode 100644 index 0000000..4b84309 --- /dev/null +++ b/pkg/httpclient/model_ui_node_division_attributes.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeDivisionAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeDivisionAttributes{} + +// UiNodeDivisionAttributes Division sections are used for interactive widgets that require a hook in the DOM / view. +type UiNodeDivisionAttributes struct { + // A classname that should be rendered into the DOM. + Class *string `json:"class,omitempty"` + // Data is a map of key-value pairs that are passed to the division. They may be used for `data-...` attributes. + Data *map[string]string `json:"data,omitempty"` + // A unique identifier + Id string `json:"id"` + // NodeType represents this node's type. 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 div Division + NodeType string `json:"node_type"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeDivisionAttributes UiNodeDivisionAttributes + +// NewUiNodeDivisionAttributes instantiates a new UiNodeDivisionAttributes 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 NewUiNodeDivisionAttributes(id string, nodeType string) *UiNodeDivisionAttributes { + this := UiNodeDivisionAttributes{} + this.Id = id + this.NodeType = nodeType + return &this +} + +// NewUiNodeDivisionAttributesWithDefaults instantiates a new UiNodeDivisionAttributes 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 NewUiNodeDivisionAttributesWithDefaults() *UiNodeDivisionAttributes { + this := UiNodeDivisionAttributes{} + return &this +} + +// GetClass returns the Class field value if set, zero value otherwise. +func (o *UiNodeDivisionAttributes) GetClass() string { + if o == nil || IsNil(o.Class) { + var ret string + return ret + } + return *o.Class +} + +// GetClassOk returns a tuple with the Class field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetClassOk() (*string, bool) { + if o == nil || IsNil(o.Class) { + return nil, false + } + return o.Class, true +} + +// HasClass returns a boolean if a field has been set. +func (o *UiNodeDivisionAttributes) HasClass() bool { + if o != nil && !IsNil(o.Class) { + return true + } + + return false +} + +// SetClass gets a reference to the given string and assigns it to the Class field. +func (o *UiNodeDivisionAttributes) SetClass(v string) { + o.Class = &v +} + +// GetData returns the Data field value if set, zero value otherwise. +func (o *UiNodeDivisionAttributes) GetData() map[string]string { + if o == nil || IsNil(o.Data) { + var ret map[string]string + return ret + } + return *o.Data +} + +// GetDataOk returns a tuple with the Data field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetDataOk() (*map[string]string, bool) { + if o == nil || IsNil(o.Data) { + return nil, false + } + return o.Data, true +} + +// HasData returns a boolean if a field has been set. +func (o *UiNodeDivisionAttributes) HasData() bool { + if o != nil && !IsNil(o.Data) { + return true + } + + return false +} + +// SetData gets a reference to the given map[string]string and assigns it to the Data field. +func (o *UiNodeDivisionAttributes) SetData(v map[string]string) { + o.Data = &v +} + +// GetId returns the Id field value +func (o *UiNodeDivisionAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeDivisionAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeDivisionAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeDivisionAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeDivisionAttributes) SetNodeType(v string) { + o.NodeType = v +} + +func (o UiNodeDivisionAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeDivisionAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Class) { + toSerialize["class"] = o.Class + } + if !IsNil(o.Data) { + toSerialize["data"] = o.Data + } + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeDivisionAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "node_type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeDivisionAttributes := _UiNodeDivisionAttributes{} + + err = json.Unmarshal(data, &varUiNodeDivisionAttributes) + + if err != nil { + return err + } + + *o = UiNodeDivisionAttributes(varUiNodeDivisionAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "class") + delete(additionalProperties, "data") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeDivisionAttributes struct { + value *UiNodeDivisionAttributes + isSet bool +} + +func (v NullableUiNodeDivisionAttributes) Get() *UiNodeDivisionAttributes { + return v.value +} + +func (v *NullableUiNodeDivisionAttributes) Set(val *UiNodeDivisionAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeDivisionAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeDivisionAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeDivisionAttributes(val *UiNodeDivisionAttributes) *NullableUiNodeDivisionAttributes { + return &NullableUiNodeDivisionAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeDivisionAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeDivisionAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_image_attributes.go b/pkg/httpclient/model_ui_node_image_attributes.go new file mode 100644 index 0000000..604d823 --- /dev/null +++ b/pkg/httpclient/model_ui_node_image_attributes.go @@ -0,0 +1,288 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeImageAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeImageAttributes{} + +// UiNodeImageAttributes struct for UiNodeImageAttributes +type UiNodeImageAttributes struct { + // Height of the image + 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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + // The image's source URL. format: uri + Src string `json:"src"` + // Width of the image + Width int64 `json:"width"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeImageAttributes UiNodeImageAttributes + +// NewUiNodeImageAttributes instantiates a new UiNodeImageAttributes 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 NewUiNodeImageAttributes(height int64, id string, nodeType string, src string, width int64) *UiNodeImageAttributes { + this := UiNodeImageAttributes{} + this.Height = height + this.Id = id + this.NodeType = nodeType + this.Src = src + this.Width = width + return &this +} + +// NewUiNodeImageAttributesWithDefaults instantiates a new UiNodeImageAttributes 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 NewUiNodeImageAttributesWithDefaults() *UiNodeImageAttributes { + this := UiNodeImageAttributes{} + return &this +} + +// GetHeight returns the Height field value +func (o *UiNodeImageAttributes) GetHeight() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Height +} + +// GetHeightOk returns a tuple with the Height field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetHeightOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Height, true +} + +// SetHeight sets field value +func (o *UiNodeImageAttributes) SetHeight(v int64) { + o.Height = v +} + +// GetId returns the Id field value +func (o *UiNodeImageAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeImageAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeImageAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeImageAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetSrc returns the Src field value +func (o *UiNodeImageAttributes) GetSrc() string { + if o == nil { + var ret string + return ret + } + + return o.Src +} + +// GetSrcOk returns a tuple with the Src field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetSrcOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Src, true +} + +// SetSrc sets field value +func (o *UiNodeImageAttributes) SetSrc(v string) { + o.Src = v +} + +// GetWidth returns the Width field value +func (o *UiNodeImageAttributes) GetWidth() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Width +} + +// GetWidthOk returns a tuple with the Width field value +// and a boolean to check if the value has been set. +func (o *UiNodeImageAttributes) GetWidthOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Width, true +} + +// SetWidth sets field value +func (o *UiNodeImageAttributes) SetWidth(v int64) { + o.Width = v +} + +func (o UiNodeImageAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeImageAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["height"] = o.Height + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["src"] = o.Src + toSerialize["width"] = o.Width + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeImageAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "height", + "id", + "node_type", + "src", + "width", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeImageAttributes := _UiNodeImageAttributes{} + + err = json.Unmarshal(data, &varUiNodeImageAttributes) + + if err != nil { + return err + } + + *o = UiNodeImageAttributes(varUiNodeImageAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "height") + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "src") + delete(additionalProperties, "width") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeImageAttributes struct { + value *UiNodeImageAttributes + isSet bool +} + +func (v NullableUiNodeImageAttributes) Get() *UiNodeImageAttributes { + return v.value +} + +func (v *NullableUiNodeImageAttributes) Set(val *UiNodeImageAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeImageAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeImageAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeImageAttributes(val *UiNodeImageAttributes) *NullableUiNodeImageAttributes { + return &NullableUiNodeImageAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeImageAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeImageAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_input_attributes.go b/pkg/httpclient/model_ui_node_input_attributes.go new file mode 100644 index 0000000..f329565 --- /dev/null +++ b/pkg/httpclient/model_ui_node_input_attributes.go @@ -0,0 +1,676 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeInputAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeInputAttributes{} + +// UiNodeInputAttributes InputAttributes represents the attributes of an input node +type UiNodeInputAttributes struct { + // The autocomplete attribute for the input. email InputAttributeAutocompleteEmail tel InputAttributeAutocompleteTel url InputAttributeAutocompleteUrl current-password InputAttributeAutocompleteCurrentPassword new-password InputAttributeAutocompleteNewPassword one-time-code InputAttributeAutocompleteOneTimeCode username InputAttributeAutocompleteUsername username webauthn InputAttributeAutocompleteUsernameWebauthn + Autocomplete *string `json:"autocomplete,omitempty"` + // Sets the input's disabled field to true or false. + Disabled bool `json:"disabled"` + Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + // 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 allowed values for the input when the underlying JSON schema defines an `enum`. When present, clients should render the field as a select/dropdown rather than a free-form text input. When absent, clients continue to render a plain text input, so this field is backward compatible with UIs that do not look at it. + Options []UiNodeInputAttributesOption `json:"options,omitempty"` + // The input's pattern. + Pattern *string `json:"pattern,omitempty"` + // Mark this input field as required. + Required *bool `json:"required,omitempty"` + // The input's element type. text InputAttributeTypeText password InputAttributeTypePassword number InputAttributeTypeNumber checkbox InputAttributeTypeCheckbox hidden InputAttributeTypeHidden email InputAttributeTypeEmail tel InputAttributeTypeTel submit InputAttributeTypeSubmit button InputAttributeTypeButton datetime-local InputAttributeTypeDateTimeLocal date InputAttributeTypeDate url InputAttributeTypeURI + Type string `json:"type"` + // The input's value. + Value interface{} `json:"value,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeInputAttributes UiNodeInputAttributes + +// NewUiNodeInputAttributes instantiates a new UiNodeInputAttributes 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 NewUiNodeInputAttributes(disabled bool, name string, nodeType string, type_ string) *UiNodeInputAttributes { + this := UiNodeInputAttributes{} + this.Disabled = disabled + this.Name = name + this.NodeType = nodeType + this.Type = type_ + return &this +} + +// NewUiNodeInputAttributesWithDefaults instantiates a new UiNodeInputAttributes 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 NewUiNodeInputAttributesWithDefaults() *UiNodeInputAttributes { + this := UiNodeInputAttributes{} + return &this +} + +// GetAutocomplete returns the Autocomplete field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetAutocomplete() string { + if o == nil || IsNil(o.Autocomplete) { + var ret string + return ret + } + return *o.Autocomplete +} + +// GetAutocompleteOk returns a tuple with the Autocomplete field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetAutocompleteOk() (*string, bool) { + if o == nil || IsNil(o.Autocomplete) { + return nil, false + } + return o.Autocomplete, true +} + +// HasAutocomplete returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasAutocomplete() bool { + if o != nil && !IsNil(o.Autocomplete) { + return true + } + + return false +} + +// SetAutocomplete gets a reference to the given string and assigns it to the Autocomplete field. +func (o *UiNodeInputAttributes) SetAutocomplete(v string) { + o.Autocomplete = &v +} + +// GetDisabled returns the Disabled field value +func (o *UiNodeInputAttributes) GetDisabled() bool { + if o == nil { + var ret bool + return ret + } + + return o.Disabled +} + +// GetDisabledOk returns a tuple with the Disabled field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetDisabledOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Disabled, true +} + +// SetDisabled sets field value +func (o *UiNodeInputAttributes) SetDisabled(v bool) { + o.Disabled = v +} + +// GetLabel returns the Label field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetLabel() UiText { + if o == nil || IsNil(o.Label) { + var ret UiText + return ret + } + return *o.Label +} + +// GetLabelOk returns a tuple with the Label field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetLabelOk() (*UiText, bool) { + if o == nil || IsNil(o.Label) { + return nil, false + } + return o.Label, true +} + +// HasLabel returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasLabel() bool { + if o != nil && !IsNil(o.Label) { + return true + } + + return false +} + +// SetLabel gets a reference to the given UiText and assigns it to the Label field. +func (o *UiNodeInputAttributes) SetLabel(v UiText) { + o.Label = &v +} + +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || IsNil(o.Maxlength) { + var ret int64 + return ret + } + return *o.Maxlength +} + +// 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() (*int64, bool) { + if o == nil || IsNil(o.Maxlength) { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && !IsNil(o.Maxlength) { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + +// GetName returns the Name field value +func (o *UiNodeInputAttributes) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *UiNodeInputAttributes) SetName(v string) { + o.Name = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeInputAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeInputAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetOnclick returns the Onclick field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclick() string { + if o == nil || IsNil(o.Onclick) { + var ret string + return ret + } + return *o.Onclick +} + +// GetOnclickOk returns a tuple with the Onclick field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickOk() (*string, bool) { + if o == nil || IsNil(o.Onclick) { + return nil, false + } + return o.Onclick, true +} + +// HasOnclick returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclick() bool { + if o != nil && !IsNil(o.Onclick) { + return true + } + + return false +} + +// SetOnclick gets a reference to the given string and assigns it to the Onclick field. +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 || IsNil(o.OnclickTrigger) { + 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 || IsNil(o.OnclickTrigger) { + 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 && !IsNil(o.OnclickTrigger) { + 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 || IsNil(o.Onload) { + 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 || IsNil(o.Onload) { + 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 && !IsNil(o.Onload) { + 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 || IsNil(o.OnloadTrigger) { + 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 || IsNil(o.OnloadTrigger) { + 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 && !IsNil(o.OnloadTrigger) { + 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 +} + +// GetOptions returns the Options field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOptions() []UiNodeInputAttributesOption { + if o == nil || IsNil(o.Options) { + var ret []UiNodeInputAttributesOption + return ret + } + return o.Options +} + +// GetOptionsOk returns a tuple with the Options field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOptionsOk() ([]UiNodeInputAttributesOption, bool) { + if o == nil || IsNil(o.Options) { + return nil, false + } + return o.Options, true +} + +// HasOptions returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOptions() bool { + if o != nil && !IsNil(o.Options) { + return true + } + + return false +} + +// SetOptions gets a reference to the given []UiNodeInputAttributesOption and assigns it to the Options field. +func (o *UiNodeInputAttributes) SetOptions(v []UiNodeInputAttributesOption) { + o.Options = v +} + +// GetPattern returns the Pattern field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetPattern() string { + if o == nil || IsNil(o.Pattern) { + var ret string + return ret + } + return *o.Pattern +} + +// GetPatternOk returns a tuple with the Pattern field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetPatternOk() (*string, bool) { + if o == nil || IsNil(o.Pattern) { + return nil, false + } + return o.Pattern, true +} + +// HasPattern returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasPattern() bool { + if o != nil && !IsNil(o.Pattern) { + return true + } + + return false +} + +// SetPattern gets a reference to the given string and assigns it to the Pattern field. +func (o *UiNodeInputAttributes) SetPattern(v string) { + o.Pattern = &v +} + +// GetRequired returns the Required field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetRequired() bool { + if o == nil || IsNil(o.Required) { + var ret bool + return ret + } + return *o.Required +} + +// GetRequiredOk returns a tuple with the Required field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetRequiredOk() (*bool, bool) { + if o == nil || IsNil(o.Required) { + return nil, false + } + return o.Required, true +} + +// HasRequired returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasRequired() bool { + if o != nil && !IsNil(o.Required) { + return true + } + + return false +} + +// SetRequired gets a reference to the given bool and assigns it to the Required field. +func (o *UiNodeInputAttributes) SetRequired(v bool) { + o.Required = &v +} + +// GetType returns the Type field value +func (o *UiNodeInputAttributes) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNodeInputAttributes) SetType(v string) { + o.Type = v +} + +// GetValue returns the Value field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UiNodeInputAttributes) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value 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 *UiNodeInputAttributes) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given interface{} and assigns it to the Value field. +func (o *UiNodeInputAttributes) SetValue(v interface{}) { + o.Value = v +} + +func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeInputAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Autocomplete) { + toSerialize["autocomplete"] = o.Autocomplete + } + toSerialize["disabled"] = o.Disabled + if !IsNil(o.Label) { + toSerialize["label"] = o.Label + } + if !IsNil(o.Maxlength) { + toSerialize["maxlength"] = o.Maxlength + } + toSerialize["name"] = o.Name + toSerialize["node_type"] = o.NodeType + if !IsNil(o.Onclick) { + toSerialize["onclick"] = o.Onclick + } + if !IsNil(o.OnclickTrigger) { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } + if !IsNil(o.Onload) { + toSerialize["onload"] = o.Onload + } + if !IsNil(o.OnloadTrigger) { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } + if !IsNil(o.Options) { + toSerialize["options"] = o.Options + } + if !IsNil(o.Pattern) { + toSerialize["pattern"] = o.Pattern + } + if !IsNil(o.Required) { + toSerialize["required"] = o.Required + } + toSerialize["type"] = o.Type + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeInputAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "disabled", + "name", + "node_type", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeInputAttributes := _UiNodeInputAttributes{} + + err = json.Unmarshal(data, &varUiNodeInputAttributes) + + if err != nil { + return err + } + + *o = UiNodeInputAttributes(varUiNodeInputAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "autocomplete") + delete(additionalProperties, "disabled") + delete(additionalProperties, "label") + delete(additionalProperties, "maxlength") + delete(additionalProperties, "name") + delete(additionalProperties, "node_type") + delete(additionalProperties, "onclick") + delete(additionalProperties, "onclickTrigger") + delete(additionalProperties, "onload") + delete(additionalProperties, "onloadTrigger") + delete(additionalProperties, "options") + delete(additionalProperties, "pattern") + delete(additionalProperties, "required") + delete(additionalProperties, "type") + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeInputAttributes struct { + value *UiNodeInputAttributes + isSet bool +} + +func (v NullableUiNodeInputAttributes) Get() *UiNodeInputAttributes { + return v.value +} + +func (v *NullableUiNodeInputAttributes) Set(val *UiNodeInputAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeInputAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeInputAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeInputAttributes(val *UiNodeInputAttributes) *NullableUiNodeInputAttributes { + return &NullableUiNodeInputAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeInputAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeInputAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_input_attributes_option.go b/pkg/httpclient/model_ui_node_input_attributes_option.go new file mode 100644 index 0000000..6de8af3 --- /dev/null +++ b/pkg/httpclient/model_ui_node_input_attributes_option.go @@ -0,0 +1,172 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeInputAttributesOption type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeInputAttributesOption{} + +// UiNodeInputAttributesOption Represents a single selectable value for an input whose JSON schema defined an `enum`. The value is always a scalar JSON type (string, number, or boolean) serialized verbatim from the schema. When present, clients should render the parent input as a select/dropdown. +type UiNodeInputAttributesOption struct { + // The value that will be submitted when this option is picked. It is serialized verbatim from the JSON schema `enum` entry, so it is always a scalar JSON value (string, number, or boolean). + Value interface{} `json:"value"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeInputAttributesOption UiNodeInputAttributesOption + +// NewUiNodeInputAttributesOption instantiates a new UiNodeInputAttributesOption 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 NewUiNodeInputAttributesOption(value interface{}) *UiNodeInputAttributesOption { + this := UiNodeInputAttributesOption{} + this.Value = value + return &this +} + +// NewUiNodeInputAttributesOptionWithDefaults instantiates a new UiNodeInputAttributesOption 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 NewUiNodeInputAttributesOptionWithDefaults() *UiNodeInputAttributesOption { + this := UiNodeInputAttributesOption{} + return &this +} + +// GetValue returns the Value field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *UiNodeInputAttributesOption) GetValue() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// 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 *UiNodeInputAttributesOption) GetValueOk() (*interface{}, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *UiNodeInputAttributesOption) SetValue(v interface{}) { + o.Value = v +} + +func (o UiNodeInputAttributesOption) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeInputAttributesOption) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if o.Value != nil { + toSerialize["value"] = o.Value + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeInputAttributesOption) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "value", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeInputAttributesOption := _UiNodeInputAttributesOption{} + + err = json.Unmarshal(data, &varUiNodeInputAttributesOption) + + if err != nil { + return err + } + + *o = UiNodeInputAttributesOption(varUiNodeInputAttributesOption) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "value") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeInputAttributesOption struct { + value *UiNodeInputAttributesOption + isSet bool +} + +func (v NullableUiNodeInputAttributesOption) Get() *UiNodeInputAttributesOption { + return v.value +} + +func (v *NullableUiNodeInputAttributesOption) Set(val *UiNodeInputAttributesOption) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeInputAttributesOption) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeInputAttributesOption) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeInputAttributesOption(val *UiNodeInputAttributesOption) *NullableUiNodeInputAttributesOption { + return &NullableUiNodeInputAttributesOption{value: val, isSet: true} +} + +func (v NullableUiNodeInputAttributesOption) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeInputAttributesOption) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_meta.go b/pkg/httpclient/model_ui_node_meta.go new file mode 100644 index 0000000..80b52f0 --- /dev/null +++ b/pkg/httpclient/model_ui_node_meta.go @@ -0,0 +1,154 @@ +/* +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" +) + +// checks if the UiNodeMeta type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeMeta{} + +// UiNodeMeta This might include a label and other information that can optionally be used to render UIs. +type UiNodeMeta struct { + Label *UiText `json:"label,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeMeta UiNodeMeta + +// NewUiNodeMeta instantiates a new UiNodeMeta 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 NewUiNodeMeta() *UiNodeMeta { + this := UiNodeMeta{} + return &this +} + +// NewUiNodeMetaWithDefaults instantiates a new UiNodeMeta 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 NewUiNodeMetaWithDefaults() *UiNodeMeta { + this := UiNodeMeta{} + return &this +} + +// GetLabel returns the Label field value if set, zero value otherwise. +func (o *UiNodeMeta) GetLabel() UiText { + if o == nil || IsNil(o.Label) { + var ret UiText + return ret + } + return *o.Label +} + +// GetLabelOk returns a tuple with the Label field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeMeta) GetLabelOk() (*UiText, bool) { + if o == nil || IsNil(o.Label) { + return nil, false + } + return o.Label, true +} + +// HasLabel returns a boolean if a field has been set. +func (o *UiNodeMeta) HasLabel() bool { + if o != nil && !IsNil(o.Label) { + return true + } + + return false +} + +// SetLabel gets a reference to the given UiText and assigns it to the Label field. +func (o *UiNodeMeta) SetLabel(v UiText) { + o.Label = &v +} + +func (o UiNodeMeta) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeMeta) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Label) { + toSerialize["label"] = o.Label + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeMeta) UnmarshalJSON(data []byte) (err error) { + varUiNodeMeta := _UiNodeMeta{} + + err = json.Unmarshal(data, &varUiNodeMeta) + + if err != nil { + return err + } + + *o = UiNodeMeta(varUiNodeMeta) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "label") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeMeta struct { + value *UiNodeMeta + isSet bool +} + +func (v NullableUiNodeMeta) Get() *UiNodeMeta { + return v.value +} + +func (v *NullableUiNodeMeta) Set(val *UiNodeMeta) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeMeta) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeMeta) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeMeta(val *UiNodeMeta) *NullableUiNodeMeta { + return &NullableUiNodeMeta{value: val, isSet: true} +} + +func (v NullableUiNodeMeta) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeMeta) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_script_attributes.go b/pkg/httpclient/model_ui_node_script_attributes.go new file mode 100644 index 0000000..22b0765 --- /dev/null +++ b/pkg/httpclient/model_ui_node_script_attributes.go @@ -0,0 +1,408 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeScriptAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeScriptAttributes{} + +// UiNodeScriptAttributes struct for UiNodeScriptAttributes +type UiNodeScriptAttributes struct { + // The script async type + Async bool `json:"async"` + // The script cross origin policy + Crossorigin string `json:"crossorigin"` + // A unique identifier + 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\". text Text input Input img Image a Anchor script Script div Division + 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"` + // The script referrer policy + Referrerpolicy string `json:"referrerpolicy"` + // The script source + Src string `json:"src"` + // The script MIME type + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeScriptAttributes UiNodeScriptAttributes + +// NewUiNodeScriptAttributes instantiates a new UiNodeScriptAttributes 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 NewUiNodeScriptAttributes(async bool, crossorigin string, id string, integrity string, nodeType string, nonce string, referrerpolicy string, src string, type_ string) *UiNodeScriptAttributes { + this := UiNodeScriptAttributes{} + this.Async = async + this.Crossorigin = crossorigin + this.Id = id + this.Integrity = integrity + this.NodeType = nodeType + this.Nonce = nonce + this.Referrerpolicy = referrerpolicy + this.Src = src + this.Type = type_ + return &this +} + +// NewUiNodeScriptAttributesWithDefaults instantiates a new UiNodeScriptAttributes 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 NewUiNodeScriptAttributesWithDefaults() *UiNodeScriptAttributes { + this := UiNodeScriptAttributes{} + return &this +} + +// GetAsync returns the Async field value +func (o *UiNodeScriptAttributes) GetAsync() bool { + if o == nil { + var ret bool + return ret + } + + return o.Async +} + +// GetAsyncOk returns a tuple with the Async field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetAsyncOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Async, true +} + +// SetAsync sets field value +func (o *UiNodeScriptAttributes) SetAsync(v bool) { + o.Async = v +} + +// GetCrossorigin returns the Crossorigin field value +func (o *UiNodeScriptAttributes) GetCrossorigin() string { + if o == nil { + var ret string + return ret + } + + return o.Crossorigin +} + +// GetCrossoriginOk returns a tuple with the Crossorigin field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetCrossoriginOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Crossorigin, true +} + +// SetCrossorigin sets field value +func (o *UiNodeScriptAttributes) SetCrossorigin(v string) { + o.Crossorigin = v +} + +// GetId returns the Id field value +func (o *UiNodeScriptAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeScriptAttributes) SetId(v string) { + o.Id = v +} + +// GetIntegrity returns the Integrity field value +func (o *UiNodeScriptAttributes) GetIntegrity() string { + if o == nil { + var ret string + return ret + } + + return o.Integrity +} + +// GetIntegrityOk returns a tuple with the Integrity field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetIntegrityOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Integrity, true +} + +// SetIntegrity sets field value +func (o *UiNodeScriptAttributes) SetIntegrity(v string) { + o.Integrity = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeScriptAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeScriptAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetNonce returns the Nonce field value +func (o *UiNodeScriptAttributes) GetNonce() string { + if o == nil { + var ret string + return ret + } + + return o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetNonceOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Nonce, true +} + +// SetNonce sets field value +func (o *UiNodeScriptAttributes) SetNonce(v string) { + o.Nonce = v +} + +// GetReferrerpolicy returns the Referrerpolicy field value +func (o *UiNodeScriptAttributes) GetReferrerpolicy() string { + if o == nil { + var ret string + return ret + } + + return o.Referrerpolicy +} + +// GetReferrerpolicyOk returns a tuple with the Referrerpolicy field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetReferrerpolicyOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Referrerpolicy, true +} + +// SetReferrerpolicy sets field value +func (o *UiNodeScriptAttributes) SetReferrerpolicy(v string) { + o.Referrerpolicy = v +} + +// GetSrc returns the Src field value +func (o *UiNodeScriptAttributes) GetSrc() string { + if o == nil { + var ret string + return ret + } + + return o.Src +} + +// GetSrcOk returns a tuple with the Src field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetSrcOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Src, true +} + +// SetSrc sets field value +func (o *UiNodeScriptAttributes) SetSrc(v string) { + o.Src = v +} + +// GetType returns the Type field value +func (o *UiNodeScriptAttributes) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiNodeScriptAttributes) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiNodeScriptAttributes) SetType(v string) { + o.Type = v +} + +func (o UiNodeScriptAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeScriptAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["async"] = o.Async + toSerialize["crossorigin"] = o.Crossorigin + toSerialize["id"] = o.Id + toSerialize["integrity"] = o.Integrity + toSerialize["node_type"] = o.NodeType + toSerialize["nonce"] = o.Nonce + toSerialize["referrerpolicy"] = o.Referrerpolicy + toSerialize["src"] = o.Src + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeScriptAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "async", + "crossorigin", + "id", + "integrity", + "node_type", + "nonce", + "referrerpolicy", + "src", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeScriptAttributes := _UiNodeScriptAttributes{} + + err = json.Unmarshal(data, &varUiNodeScriptAttributes) + + if err != nil { + return err + } + + *o = UiNodeScriptAttributes(varUiNodeScriptAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "async") + delete(additionalProperties, "crossorigin") + delete(additionalProperties, "id") + delete(additionalProperties, "integrity") + delete(additionalProperties, "node_type") + delete(additionalProperties, "nonce") + delete(additionalProperties, "referrerpolicy") + delete(additionalProperties, "src") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeScriptAttributes struct { + value *UiNodeScriptAttributes + isSet bool +} + +func (v NullableUiNodeScriptAttributes) Get() *UiNodeScriptAttributes { + return v.value +} + +func (v *NullableUiNodeScriptAttributes) Set(val *UiNodeScriptAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeScriptAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeScriptAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeScriptAttributes(val *UiNodeScriptAttributes) *NullableUiNodeScriptAttributes { + return &NullableUiNodeScriptAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeScriptAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeScriptAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_node_text_attributes.go b/pkg/httpclient/model_ui_node_text_attributes.go new file mode 100644 index 0000000..6c7c3dc --- /dev/null +++ b/pkg/httpclient/model_ui_node_text_attributes.go @@ -0,0 +1,227 @@ +/* +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" + "fmt" +) + +// checks if the UiNodeTextAttributes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiNodeTextAttributes{} + +// UiNodeTextAttributes struct for UiNodeTextAttributes +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\". text Text input Input img Image a Anchor script Script div Division + NodeType string `json:"node_type"` + Text UiText `json:"text"` + AdditionalProperties map[string]interface{} +} + +type _UiNodeTextAttributes UiNodeTextAttributes + +// NewUiNodeTextAttributes instantiates a new UiNodeTextAttributes 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 NewUiNodeTextAttributes(id string, nodeType string, text UiText) *UiNodeTextAttributes { + this := UiNodeTextAttributes{} + this.Id = id + this.NodeType = nodeType + this.Text = text + return &this +} + +// NewUiNodeTextAttributesWithDefaults instantiates a new UiNodeTextAttributes 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 NewUiNodeTextAttributesWithDefaults() *UiNodeTextAttributes { + this := UiNodeTextAttributes{} + return &this +} + +// GetId returns the Id field value +func (o *UiNodeTextAttributes) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiNodeTextAttributes) SetId(v string) { + o.Id = v +} + +// GetNodeType returns the NodeType field value +func (o *UiNodeTextAttributes) GetNodeType() string { + if o == nil { + var ret string + return ret + } + + return o.NodeType +} + +// GetNodeTypeOk returns a tuple with the NodeType field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetNodeTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NodeType, true +} + +// SetNodeType sets field value +func (o *UiNodeTextAttributes) SetNodeType(v string) { + o.NodeType = v +} + +// GetText returns the Text field value +func (o *UiNodeTextAttributes) GetText() UiText { + if o == nil { + var ret UiText + return ret + } + + return o.Text +} + +// GetTextOk returns a tuple with the Text field value +// and a boolean to check if the value has been set. +func (o *UiNodeTextAttributes) GetTextOk() (*UiText, bool) { + if o == nil { + return nil, false + } + return &o.Text, true +} + +// SetText sets field value +func (o *UiNodeTextAttributes) SetText(v UiText) { + o.Text = v +} + +func (o UiNodeTextAttributes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiNodeTextAttributes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["node_type"] = o.NodeType + toSerialize["text"] = o.Text + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiNodeTextAttributes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "node_type", + "text", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiNodeTextAttributes := _UiNodeTextAttributes{} + + err = json.Unmarshal(data, &varUiNodeTextAttributes) + + if err != nil { + return err + } + + *o = UiNodeTextAttributes(varUiNodeTextAttributes) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "id") + delete(additionalProperties, "node_type") + delete(additionalProperties, "text") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiNodeTextAttributes struct { + value *UiNodeTextAttributes + isSet bool +} + +func (v NullableUiNodeTextAttributes) Get() *UiNodeTextAttributes { + return v.value +} + +func (v *NullableUiNodeTextAttributes) Set(val *UiNodeTextAttributes) { + v.value = val + v.isSet = true +} + +func (v NullableUiNodeTextAttributes) IsSet() bool { + return v.isSet +} + +func (v *NullableUiNodeTextAttributes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiNodeTextAttributes(val *UiNodeTextAttributes) *NullableUiNodeTextAttributes { + return &NullableUiNodeTextAttributes{value: val, isSet: true} +} + +func (v NullableUiNodeTextAttributes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiNodeTextAttributes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_ui_text.go b/pkg/httpclient/model_ui_text.go new file mode 100644 index 0000000..e4c93b5 --- /dev/null +++ b/pkg/httpclient/model_ui_text.go @@ -0,0 +1,265 @@ +/* +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" + "fmt" +) + +// checks if the UiText type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UiText{} + +// UiText struct for UiText +type UiText struct { + // The message's context. Useful when customizing messages. + Context map[string]interface{} `json:"context,omitempty"` + Id int64 `json:"id"` + // The message text. Written in american english. + Text string `json:"text"` + // The message type. info Info error Error success Success + Type string `json:"type"` + AdditionalProperties map[string]interface{} +} + +type _UiText UiText + +// NewUiText instantiates a new UiText 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 NewUiText(id int64, text string, type_ string) *UiText { + this := UiText{} + this.Id = id + this.Text = text + this.Type = type_ + return &this +} + +// NewUiTextWithDefaults instantiates a new UiText 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 NewUiTextWithDefaults() *UiText { + this := UiText{} + return &this +} + +// GetContext returns the Context field value if set, zero value otherwise. +func (o *UiText) GetContext() map[string]interface{} { + if o == nil || IsNil(o.Context) { + var ret map[string]interface{} + return ret + } + return o.Context +} + +// GetContextOk returns a tuple with the Context field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiText) GetContextOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Context) { + return map[string]interface{}{}, false + } + return o.Context, true +} + +// HasContext returns a boolean if a field has been set. +func (o *UiText) HasContext() bool { + if o != nil && !IsNil(o.Context) { + return true + } + + return false +} + +// SetContext gets a reference to the given map[string]interface{} and assigns it to the Context field. +func (o *UiText) SetContext(v map[string]interface{}) { + o.Context = v +} + +// GetId returns the Id field value +func (o *UiText) GetId() int64 { + if o == nil { + var ret int64 + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *UiText) GetIdOk() (*int64, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *UiText) SetId(v int64) { + o.Id = v +} + +// GetText returns the Text field value +func (o *UiText) GetText() string { + if o == nil { + var ret string + return ret + } + + return o.Text +} + +// GetTextOk returns a tuple with the Text field value +// and a boolean to check if the value has been set. +func (o *UiText) GetTextOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Text, true +} + +// SetText sets field value +func (o *UiText) SetText(v string) { + o.Text = v +} + +// GetType returns the Type field value +func (o *UiText) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *UiText) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *UiText) SetType(v string) { + o.Type = v +} + +func (o UiText) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UiText) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Context) { + toSerialize["context"] = o.Context + } + toSerialize["id"] = o.Id + toSerialize["text"] = o.Text + toSerialize["type"] = o.Type + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UiText) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "text", + "type", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUiText := _UiText{} + + err = json.Unmarshal(data, &varUiText) + + if err != nil { + return err + } + + *o = UiText(varUiText) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "context") + delete(additionalProperties, "id") + delete(additionalProperties, "text") + delete(additionalProperties, "type") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUiText struct { + value *UiText + isSet bool +} + +func (v NullableUiText) Get() *UiText { + return v.value +} + +func (v *NullableUiText) Set(val *UiText) { + v.value = val + v.isSet = true +} + +func (v NullableUiText) IsSet() bool { + return v.isSet +} + +func (v *NullableUiText) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUiText(val *UiText) *NullableUiText { + return &NullableUiText{value: val, isSet: true} +} + +func (v NullableUiText) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUiText) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_fedcm_flow_body.go b/pkg/httpclient/model_update_fedcm_flow_body.go new file mode 100644 index 0000000..3f1bfdc --- /dev/null +++ b/pkg/httpclient/model_update_fedcm_flow_body.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateFedcmFlowBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateFedcmFlowBody{} + +// UpdateFedcmFlowBody struct for UpdateFedcmFlowBody +type UpdateFedcmFlowBody struct { + // CSRFToken is the anti-CSRF token. + CsrfToken string `json:"csrf_token"` + // Nonce is the nonce that was used in the `navigator.credentials.get` call. If specified, it must match the `nonce` claim in the token. + Nonce *string `json:"nonce,omitempty"` + // Token contains the result of `navigator.credentials.get`. + Token string `json:"token"` + // Transient data to pass along to any webhooks. + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateFedcmFlowBody UpdateFedcmFlowBody + +// NewUpdateFedcmFlowBody instantiates a new UpdateFedcmFlowBody 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 NewUpdateFedcmFlowBody(csrfToken string, token string) *UpdateFedcmFlowBody { + this := UpdateFedcmFlowBody{} + this.CsrfToken = csrfToken + this.Token = token + return &this +} + +// NewUpdateFedcmFlowBodyWithDefaults instantiates a new UpdateFedcmFlowBody 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 NewUpdateFedcmFlowBodyWithDefaults() *UpdateFedcmFlowBody { + this := UpdateFedcmFlowBody{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateFedcmFlowBody) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateFedcmFlowBody) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetNonce returns the Nonce field value if set, zero value otherwise. +func (o *UpdateFedcmFlowBody) GetNonce() string { + if o == nil || IsNil(o.Nonce) { + var ret string + return ret + } + return *o.Nonce +} + +// GetNonceOk returns a tuple with the Nonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetNonceOk() (*string, bool) { + if o == nil || IsNil(o.Nonce) { + return nil, false + } + return o.Nonce, true +} + +// HasNonce returns a boolean if a field has been set. +func (o *UpdateFedcmFlowBody) HasNonce() bool { + if o != nil && !IsNil(o.Nonce) { + return true + } + + return false +} + +// SetNonce gets a reference to the given string and assigns it to the Nonce field. +func (o *UpdateFedcmFlowBody) SetNonce(v string) { + o.Nonce = &v +} + +// GetToken returns the Token field value +func (o *UpdateFedcmFlowBody) GetToken() string { + if o == nil { + var ret string + return ret + } + + return o.Token +} + +// GetTokenOk returns a tuple with the Token field value +// and a boolean to check if the value has been set. +func (o *UpdateFedcmFlowBody) GetTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Token, true +} + +// SetToken sets field value +func (o *UpdateFedcmFlowBody) SetToken(v string) { + o.Token = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateFedcmFlowBody) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateFedcmFlowBody) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateFedcmFlowBody) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateFedcmFlowBody) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateFedcmFlowBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateFedcmFlowBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["csrf_token"] = o.CsrfToken + if !IsNil(o.Nonce) { + toSerialize["nonce"] = o.Nonce + } + toSerialize["token"] = o.Token + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateFedcmFlowBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "csrf_token", + "token", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateFedcmFlowBody := _UpdateFedcmFlowBody{} + + err = json.Unmarshal(data, &varUpdateFedcmFlowBody) + + if err != nil { + return err + } + + *o = UpdateFedcmFlowBody(varUpdateFedcmFlowBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "nonce") + delete(additionalProperties, "token") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateFedcmFlowBody struct { + value *UpdateFedcmFlowBody + isSet bool +} + +func (v NullableUpdateFedcmFlowBody) Get() *UpdateFedcmFlowBody { + return v.value +} + +func (v *NullableUpdateFedcmFlowBody) Set(val *UpdateFedcmFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateFedcmFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateFedcmFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateFedcmFlowBody(val *UpdateFedcmFlowBody) *NullableUpdateFedcmFlowBody { + return &NullableUpdateFedcmFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateFedcmFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateFedcmFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_identity_body.go b/pkg/httpclient/model_update_identity_body.go new file mode 100644 index 0000000..db29fa0 --- /dev/null +++ b/pkg/httpclient/model_update_identity_body.go @@ -0,0 +1,419 @@ +/* +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" + "fmt" +) + +// checks if the UpdateIdentityBody type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateIdentityBody{} + +// UpdateIdentityBody Update Identity Body +type UpdateIdentityBody struct { + Credentials *IdentityWithCredentials `json:"credentials,omitempty"` + // ExternalID is an optional external ID of the identity. This is used to link the identity to an external system. If set, the external ID must be unique across all identities. + ExternalId *string `json:"external_id,omitempty"` + // Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/`. + MetadataAdmin interface{} `json:"metadata_admin,omitempty"` + // Store metadata about the identity which the identity itself can see when calling for example the session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field. + MetadataPublic interface{} `json:"metadata_public,omitempty"` + // Region is the Ory Network region this identity is homed in. Optional; omit to leave the current region unchanged. eu-central EUCentral asia-northeast AsiaNorthEast us-east USEast us-west USWest eu EU asia Asia us US global Global + Region *string `json:"region,omitempty"` + // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set will update the Identity's SchemaID. + SchemaId string `json:"schema_id"` + // State is the identity's state. active StateActive inactive StateInactive + State string `json:"state"` + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_id`. + Traits map[string]interface{} `json:"traits"` + AdditionalProperties map[string]interface{} +} + +type _UpdateIdentityBody UpdateIdentityBody + +// NewUpdateIdentityBody instantiates a new UpdateIdentityBody 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 NewUpdateIdentityBody(schemaId string, state string, traits map[string]interface{}) *UpdateIdentityBody { + this := UpdateIdentityBody{} + this.SchemaId = schemaId + this.State = state + this.Traits = traits + return &this +} + +// NewUpdateIdentityBodyWithDefaults instantiates a new UpdateIdentityBody 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 NewUpdateIdentityBodyWithDefaults() *UpdateIdentityBody { + this := UpdateIdentityBody{} + return &this +} + +// GetCredentials returns the Credentials field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetCredentials() IdentityWithCredentials { + if o == nil || IsNil(o.Credentials) { + var ret IdentityWithCredentials + return ret + } + return *o.Credentials +} + +// GetCredentialsOk returns a tuple with the Credentials field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetCredentialsOk() (*IdentityWithCredentials, bool) { + if o == nil || IsNil(o.Credentials) { + return nil, false + } + return o.Credentials, true +} + +// HasCredentials returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasCredentials() bool { + if o != nil && !IsNil(o.Credentials) { + return true + } + + return false +} + +// SetCredentials gets a reference to the given IdentityWithCredentials and assigns it to the Credentials field. +func (o *UpdateIdentityBody) SetCredentials(v IdentityWithCredentials) { + o.Credentials = &v +} + +// GetExternalId returns the ExternalId field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetExternalId() string { + if o == nil || IsNil(o.ExternalId) { + var ret string + return ret + } + return *o.ExternalId +} + +// GetExternalIdOk returns a tuple with the ExternalId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetExternalIdOk() (*string, bool) { + if o == nil || IsNil(o.ExternalId) { + return nil, false + } + return o.ExternalId, true +} + +// HasExternalId returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasExternalId() bool { + if o != nil && !IsNil(o.ExternalId) { + return true + } + + return false +} + +// SetExternalId gets a reference to the given string and assigns it to the ExternalId field. +func (o *UpdateIdentityBody) SetExternalId(v string) { + o.ExternalId = &v +} + +// GetMetadataAdmin returns the MetadataAdmin field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UpdateIdentityBody) GetMetadataAdmin() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataAdmin +} + +// GetMetadataAdminOk returns a tuple with the MetadataAdmin 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 *UpdateIdentityBody) GetMetadataAdminOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataAdmin) { + return nil, false + } + return &o.MetadataAdmin, true +} + +// HasMetadataAdmin returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasMetadataAdmin() bool { + if o != nil && !IsNil(o.MetadataAdmin) { + return true + } + + return false +} + +// SetMetadataAdmin gets a reference to the given interface{} and assigns it to the MetadataAdmin field. +func (o *UpdateIdentityBody) SetMetadataAdmin(v interface{}) { + o.MetadataAdmin = v +} + +// GetMetadataPublic returns the MetadataPublic field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UpdateIdentityBody) GetMetadataPublic() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.MetadataPublic +} + +// GetMetadataPublicOk returns a tuple with the MetadataPublic 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 *UpdateIdentityBody) GetMetadataPublicOk() (*interface{}, bool) { + if o == nil || IsNil(o.MetadataPublic) { + return nil, false + } + return &o.MetadataPublic, true +} + +// HasMetadataPublic returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasMetadataPublic() bool { + if o != nil && !IsNil(o.MetadataPublic) { + return true + } + + return false +} + +// SetMetadataPublic gets a reference to the given interface{} and assigns it to the MetadataPublic field. +func (o *UpdateIdentityBody) SetMetadataPublic(v interface{}) { + o.MetadataPublic = v +} + +// GetRegion returns the Region field value if set, zero value otherwise. +func (o *UpdateIdentityBody) GetRegion() string { + if o == nil || IsNil(o.Region) { + var ret string + return ret + } + return *o.Region +} + +// GetRegionOk returns a tuple with the Region field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetRegionOk() (*string, bool) { + if o == nil || IsNil(o.Region) { + return nil, false + } + return o.Region, true +} + +// HasRegion returns a boolean if a field has been set. +func (o *UpdateIdentityBody) HasRegion() bool { + if o != nil && !IsNil(o.Region) { + return true + } + + return false +} + +// SetRegion gets a reference to the given string and assigns it to the Region field. +func (o *UpdateIdentityBody) SetRegion(v string) { + o.Region = &v +} + +// GetSchemaId returns the SchemaId field value +func (o *UpdateIdentityBody) GetSchemaId() string { + if o == nil { + var ret string + return ret + } + + return o.SchemaId +} + +// GetSchemaIdOk returns a tuple with the SchemaId field value +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetSchemaIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SchemaId, true +} + +// SetSchemaId sets field value +func (o *UpdateIdentityBody) SetSchemaId(v string) { + o.SchemaId = v +} + +// GetState returns the State field value +func (o *UpdateIdentityBody) GetState() string { + if o == nil { + var ret string + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +func (o *UpdateIdentityBody) GetStateOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *UpdateIdentityBody) SetState(v string) { + o.State = v +} + +// GetTraits returns the Traits field value +func (o *UpdateIdentityBody) 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 *UpdateIdentityBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateIdentityBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +func (o UpdateIdentityBody) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateIdentityBody) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Credentials) { + toSerialize["credentials"] = o.Credentials + } + if !IsNil(o.ExternalId) { + toSerialize["external_id"] = o.ExternalId + } + if o.MetadataAdmin != nil { + toSerialize["metadata_admin"] = o.MetadataAdmin + } + if o.MetadataPublic != nil { + toSerialize["metadata_public"] = o.MetadataPublic + } + if !IsNil(o.Region) { + toSerialize["region"] = o.Region + } + toSerialize["schema_id"] = o.SchemaId + toSerialize["state"] = o.State + toSerialize["traits"] = o.Traits + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateIdentityBody) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "schema_id", + "state", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateIdentityBody := _UpdateIdentityBody{} + + err = json.Unmarshal(data, &varUpdateIdentityBody) + + if err != nil { + return err + } + + *o = UpdateIdentityBody(varUpdateIdentityBody) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "credentials") + delete(additionalProperties, "external_id") + delete(additionalProperties, "metadata_admin") + delete(additionalProperties, "metadata_public") + delete(additionalProperties, "region") + delete(additionalProperties, "schema_id") + delete(additionalProperties, "state") + delete(additionalProperties, "traits") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateIdentityBody struct { + value *UpdateIdentityBody + isSet bool +} + +func (v NullableUpdateIdentityBody) Get() *UpdateIdentityBody { + return v.value +} + +func (v *NullableUpdateIdentityBody) Set(val *UpdateIdentityBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateIdentityBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateIdentityBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateIdentityBody(val *UpdateIdentityBody) *NullableUpdateIdentityBody { + return &NullableUpdateIdentityBody{value: val, isSet: true} +} + +func (v NullableUpdateIdentityBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateIdentityBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_body.go b/pkg/httpclient/model_update_login_flow_body.go new file mode 100644 index 0000000..a917194 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_body.go @@ -0,0 +1,486 @@ +/* +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" + "fmt" +) + +// UpdateLoginFlowBody - struct for UpdateLoginFlowBody +type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod + UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod + UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod + UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod + UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod + UpdateLoginFlowWithSamlMethod *UpdateLoginFlowWithSamlMethod + UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod + UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod +} + +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + +// 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{ + UpdateLoginFlowWithLookupSecretMethod: v, + } +} + +// UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithOidcMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOidcMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateLoginFlowWithPasswordMethod: v, + } +} + +// UpdateLoginFlowWithSamlMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithSamlMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithSamlMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithSamlMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithSamlMethod: v, + } +} + +// UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithTotpMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithTotpMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithTotpMethod: v, + } +} + +// UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithWebAuthnMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithWebAuthnMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateLoginFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithSamlMethod, return on the first match + } else { + dst.UpdateLoginFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateLoginFlowWithSamlMethod' + if jsonDict["method"] == "updateLoginFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithSamlMethod, return on the first match + } else { + dst.UpdateLoginFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithSamlMethod: %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 +func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + + if src.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod) + } + + if src.UpdateLoginFlowWithLookupSecretMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) + } + + if src.UpdateLoginFlowWithOidcMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) + } + + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + + if src.UpdateLoginFlowWithPasswordMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) + } + + if src.UpdateLoginFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithSamlMethod) + } + + if src.UpdateLoginFlowWithTotpMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithTotpMethod) + } + + if src.UpdateLoginFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return obj.UpdateLoginFlowWithIdentifierFirstMethod + } + + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { + return obj.UpdateLoginFlowWithLookupSecretMethod + } + + if obj.UpdateLoginFlowWithOidcMethod != nil { + return obj.UpdateLoginFlowWithOidcMethod + } + + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + + if obj.UpdateLoginFlowWithPasswordMethod != nil { + return obj.UpdateLoginFlowWithPasswordMethod + } + + if obj.UpdateLoginFlowWithSamlMethod != nil { + return obj.UpdateLoginFlowWithSamlMethod + } + + if obj.UpdateLoginFlowWithTotpMethod != nil { + return obj.UpdateLoginFlowWithTotpMethod + } + + if obj.UpdateLoginFlowWithWebAuthnMethod != nil { + return obj.UpdateLoginFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateLoginFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateLoginFlowWithCodeMethod != nil { + return *obj.UpdateLoginFlowWithCodeMethod + } + + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return *obj.UpdateLoginFlowWithIdentifierFirstMethod + } + + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { + return *obj.UpdateLoginFlowWithLookupSecretMethod + } + + if obj.UpdateLoginFlowWithOidcMethod != nil { + return *obj.UpdateLoginFlowWithOidcMethod + } + + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return *obj.UpdateLoginFlowWithPasskeyMethod + } + + if obj.UpdateLoginFlowWithPasswordMethod != nil { + return *obj.UpdateLoginFlowWithPasswordMethod + } + + if obj.UpdateLoginFlowWithSamlMethod != nil { + return *obj.UpdateLoginFlowWithSamlMethod + } + + if obj.UpdateLoginFlowWithTotpMethod != nil { + return *obj.UpdateLoginFlowWithTotpMethod + } + + if obj.UpdateLoginFlowWithWebAuthnMethod != nil { + return *obj.UpdateLoginFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateLoginFlowBody struct { + value *UpdateLoginFlowBody + isSet bool +} + +func (v NullableUpdateLoginFlowBody) Get() *UpdateLoginFlowBody { + return v.value +} + +func (v *NullableUpdateLoginFlowBody) Set(val *UpdateLoginFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowBody(val *UpdateLoginFlowBody) *NullableUpdateLoginFlowBody { + return &NullableUpdateLoginFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_code_method.go b/pkg/httpclient/model_update_login_flow_with_code_method.go new file mode 100644 index 0000000..23aa030 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_code_method.go @@ -0,0 +1,388 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithCodeMethod{} + +// 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 + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithCodeMethod + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod 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 NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod 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 NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || IsNil(o.Address) { + 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 || IsNil(o.Address) { + 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 && !IsNil(o.Address) { + 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 || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || IsNil(o.Identifier) { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || IsNil(o.Identifier) { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && !IsNil(o.Identifier) { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) 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 *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || IsNil(o.Resend) { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || IsNil(o.Resend) { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && !IsNil(o.Resend) { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Address) { + toSerialize["address"] = o.Address + } + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + toSerialize["csrf_token"] = o.CsrfToken + if !IsNil(o.Identifier) { + toSerialize["identifier"] = o.Identifier + } + toSerialize["method"] = o.Method + if !IsNil(o.Resend) { + toSerialize["resend"] = o.Resend + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "csrf_token", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithCodeMethod := _UpdateLoginFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithCodeMethod(varUpdateLoginFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "address") + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "resend") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_identifier_first_method.go b/pkg/httpclient/model_update_login_flow_with_identifier_first_method.go new file mode 100644 index 0000000..405356f --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_identifier_first_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithIdentifierFirstMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithIdentifierFirstMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithIdentifierFirstMethod UpdateLoginFlowWithIdentifierFirstMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithIdentifierFirstMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithIdentifierFirstMethod := _UpdateLoginFlowWithIdentifierFirstMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithIdentifierFirstMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithIdentifierFirstMethod(varUpdateLoginFlowWithIdentifierFirstMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_update_login_flow_with_lookup_secret_method.go b/pkg/httpclient/model_update_login_flow_with_lookup_secret_method.go new file mode 100644 index 0000000..d522cde --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_lookup_secret_method.go @@ -0,0 +1,236 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithLookupSecretMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithLookupSecretMethod{} + +// UpdateLoginFlowWithLookupSecretMethod Update Login Flow with Lookup Secret Method +type UpdateLoginFlowWithLookupSecretMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The lookup secret. + LookupSecret string `json:"lookup_secret"` + // Method should be set to \"lookup_secret\" when logging in using the lookup_secret strategy. + Method string `json:"method"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithLookupSecretMethod + +// NewUpdateLoginFlowWithLookupSecretMethod instantiates a new UpdateLoginFlowWithLookupSecretMethod 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 NewUpdateLoginFlowWithLookupSecretMethod(lookupSecret string, method string) *UpdateLoginFlowWithLookupSecretMethod { + this := UpdateLoginFlowWithLookupSecretMethod{} + this.LookupSecret = lookupSecret + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithLookupSecretMethodWithDefaults instantiates a new UpdateLoginFlowWithLookupSecretMethod 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 NewUpdateLoginFlowWithLookupSecretMethodWithDefaults() *UpdateLoginFlowWithLookupSecretMethod { + this := UpdateLoginFlowWithLookupSecretMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithLookupSecretMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithLookupSecretMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithLookupSecretMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithLookupSecretMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetLookupSecret returns the LookupSecret field value +func (o *UpdateLoginFlowWithLookupSecretMethod) GetLookupSecret() string { + if o == nil { + var ret string + return ret + } + + return o.LookupSecret +} + +// GetLookupSecretOk returns a tuple with the LookupSecret field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithLookupSecretMethod) GetLookupSecretOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.LookupSecret, true +} + +// SetLookupSecret sets field value +func (o *UpdateLoginFlowWithLookupSecretMethod) SetLookupSecret(v string) { + o.LookupSecret = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithLookupSecretMethod) 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 *UpdateLoginFlowWithLookupSecretMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithLookupSecretMethod) SetMethod(v string) { + o.Method = v +} + +func (o UpdateLoginFlowWithLookupSecretMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithLookupSecretMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["lookup_secret"] = o.LookupSecret + toSerialize["method"] = o.Method + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithLookupSecretMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "lookup_secret", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithLookupSecretMethod := _UpdateLoginFlowWithLookupSecretMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithLookupSecretMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithLookupSecretMethod(varUpdateLoginFlowWithLookupSecretMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "lookup_secret") + delete(additionalProperties, "method") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithLookupSecretMethod struct { + value *UpdateLoginFlowWithLookupSecretMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) Get() *UpdateLoginFlowWithLookupSecretMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) Set(val *UpdateLoginFlowWithLookupSecretMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithLookupSecretMethod(val *UpdateLoginFlowWithLookupSecretMethod) *NullableUpdateLoginFlowWithLookupSecretMethod { + return &NullableUpdateLoginFlowWithLookupSecretMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithLookupSecretMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithLookupSecretMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_oidc_method.go b/pkg/httpclient/model_update_login_flow_with_oidc_method.go new file mode 100644 index 0000000..407c50f --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_oidc_method.go @@ -0,0 +1,426 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithOidcMethod{} + +// UpdateLoginFlowWithOidcMethod Update Login Flow with OpenID Connect Method +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 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"` + // Method to use This field must be set to `oidc` when using the oidc method. + Method string `json:"method"` + // The provider to register with + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithOidcMethod + +// NewUpdateLoginFlowWithOidcMethod instantiates a new UpdateLoginFlowWithOidcMethod 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 NewUpdateLoginFlowWithOidcMethod(method string, provider string) *UpdateLoginFlowWithOidcMethod { + this := UpdateLoginFlowWithOidcMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateLoginFlowWithOidcMethodWithDefaults instantiates a new UpdateLoginFlowWithOidcMethod 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 NewUpdateLoginFlowWithOidcMethodWithDefaults() *UpdateLoginFlowWithOidcMethod { + this := UpdateLoginFlowWithOidcMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithOidcMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdToken() string { + if o == nil || IsNil(o.IdToken) { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.IdToken) { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdToken() bool { + if o != nil && !IsNil(o.IdToken) { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || IsNil(o.IdTokenNonce) { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || IsNil(o.IdTokenNonce) { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && !IsNil(o.IdTokenNonce) { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithOidcMethod) 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 *UpdateLoginFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateLoginFlowWithOidcMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateLoginFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.IdToken) { + toSerialize["id_token"] = o.IdToken + } + if !IsNil(o.IdTokenNonce) { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithOidcMethod := _UpdateLoginFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithOidcMethod(varUpdateLoginFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "id_token") + delete(additionalProperties, "id_token_nonce") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithOidcMethod struct { + value *UpdateLoginFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithOidcMethod) Get() *UpdateLoginFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) Set(val *UpdateLoginFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithOidcMethod(val *UpdateLoginFlowWithOidcMethod) *NullableUpdateLoginFlowWithOidcMethod { + return &NullableUpdateLoginFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_passkey_method.go b/pkg/httpclient/model_update_login_flow_with_passkey_method.go new file mode 100644 index 0000000..88277d8 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,244 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithPasskeyMethod UpdateLoginFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyLogin) { + 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 || IsNil(o.PasskeyLogin) { + 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 && !IsNil(o.PasskeyLogin) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyLogin) { + toSerialize["passkey_login"] = o.PasskeyLogin + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithPasskeyMethod := _UpdateLoginFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithPasskeyMethod(varUpdateLoginFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_login") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_update_login_flow_with_password_method.go b/pkg/httpclient/model_update_login_flow_with_password_method.go new file mode 100644 index 0000000..d3491b7 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_password_method.go @@ -0,0 +1,342 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithPasswordMethod{} + +// UpdateLoginFlowWithPasswordMethod Update Login Flow with Password Method +type UpdateLoginFlowWithPasswordMethod 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"` + // The user's password. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithPasswordMethod UpdateLoginFlowWithPasswordMethod + +// NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod 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 NewUpdateLoginFlowWithPasswordMethod(identifier string, method string, password string) *UpdateLoginFlowWithPasswordMethod { + this := UpdateLoginFlowWithPasswordMethod{} + this.Identifier = identifier + this.Method = method + this.Password = password + return &this +} + +// NewUpdateLoginFlowWithPasswordMethodWithDefaults instantiates a new UpdateLoginFlowWithPasswordMethod 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 NewUpdateLoginFlowWithPasswordMethodWithDefaults() *UpdateLoginFlowWithPasswordMethod { + this := UpdateLoginFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithPasswordMethod) 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 *UpdateLoginFlowWithPasswordMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasswordMethod) 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 *UpdateLoginFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateLoginFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +func (o *UpdateLoginFlowWithPasswordMethod) SetPassword(v string) { + o.Password = v +} + +// GetPasswordIdentifier returns the PasswordIdentifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordIdentifier() string { + if o == nil || IsNil(o.PasswordIdentifier) { + var ret string + return ret + } + return *o.PasswordIdentifier +} + +// GetPasswordIdentifierOk returns a tuple with the PasswordIdentifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetPasswordIdentifierOk() (*string, bool) { + if o == nil || IsNil(o.PasswordIdentifier) { + return nil, false + } + return o.PasswordIdentifier, true +} + +// HasPasswordIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasPasswordIdentifier() bool { + if o != nil && !IsNil(o.PasswordIdentifier) { + return true + } + + return false +} + +// SetPasswordIdentifier gets a reference to the given string and assigns it to the PasswordIdentifier field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + if !IsNil(o.PasswordIdentifier) { + toSerialize["password_identifier"] = o.PasswordIdentifier + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + "password", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithPasswordMethod := _UpdateLoginFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithPasswordMethod(varUpdateLoginFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "password_identifier") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithPasswordMethod struct { + value *UpdateLoginFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) Get() *UpdateLoginFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) Set(val *UpdateLoginFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasswordMethod(val *UpdateLoginFlowWithPasswordMethod) *NullableUpdateLoginFlowWithPasswordMethod { + return &NullableUpdateLoginFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_saml_method.go b/pkg/httpclient/model_update_login_flow_with_saml_method.go new file mode 100644 index 0000000..0457a88 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_saml_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithSamlMethod{} + +// UpdateLoginFlowWithSamlMethod Update login flow using SAML +type UpdateLoginFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `saml` when using the saml method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithSamlMethod UpdateLoginFlowWithSamlMethod + +// NewUpdateLoginFlowWithSamlMethod instantiates a new UpdateLoginFlowWithSamlMethod 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 NewUpdateLoginFlowWithSamlMethod(method string, provider string) *UpdateLoginFlowWithSamlMethod { + this := UpdateLoginFlowWithSamlMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateLoginFlowWithSamlMethodWithDefaults instantiates a new UpdateLoginFlowWithSamlMethod 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 NewUpdateLoginFlowWithSamlMethodWithDefaults() *UpdateLoginFlowWithSamlMethod { + this := UpdateLoginFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithSamlMethod) 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 *UpdateLoginFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateLoginFlowWithSamlMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithSamlMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateLoginFlowWithSamlMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateLoginFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateLoginFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithSamlMethod := _UpdateLoginFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithSamlMethod(varUpdateLoginFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithSamlMethod struct { + value *UpdateLoginFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithSamlMethod) Get() *UpdateLoginFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) Set(val *UpdateLoginFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithSamlMethod(val *UpdateLoginFlowWithSamlMethod) *NullableUpdateLoginFlowWithSamlMethod { + return &NullableUpdateLoginFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_totp_method.go b/pkg/httpclient/model_update_login_flow_with_totp_method.go new file mode 100644 index 0000000..e108edf --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_totp_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithTotpMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithTotpMethod{} + +// UpdateLoginFlowWithTotpMethod Update Login Flow with TOTP Method +type UpdateLoginFlowWithTotpMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"totp\" when logging in using the TOTP strategy. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithTotpMethod UpdateLoginFlowWithTotpMethod + +// NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod 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 NewUpdateLoginFlowWithTotpMethod(method string, totpCode string) *UpdateLoginFlowWithTotpMethod { + this := UpdateLoginFlowWithTotpMethod{} + this.Method = method + this.TotpCode = totpCode + return &this +} + +// NewUpdateLoginFlowWithTotpMethodWithDefaults instantiates a new UpdateLoginFlowWithTotpMethod 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 NewUpdateLoginFlowWithTotpMethodWithDefaults() *UpdateLoginFlowWithTotpMethod { + this := UpdateLoginFlowWithTotpMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithTotpMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithTotpMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithTotpMethod) 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 *UpdateLoginFlowWithTotpMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithTotpMethod) SetMethod(v string) { + o.Method = v +} + +// GetTotpCode returns the TotpCode field value +func (o *UpdateLoginFlowWithTotpMethod) GetTotpCode() string { + if o == nil { + var ret string + return ret + } + + return o.TotpCode +} + +// GetTotpCodeOk returns a tuple with the TotpCode field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTotpCodeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.TotpCode, true +} + +// SetTotpCode sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithTotpMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["totp_code"] = o.TotpCode + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithTotpMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "totp_code", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithTotpMethod := _UpdateLoginFlowWithTotpMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithTotpMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithTotpMethod(varUpdateLoginFlowWithTotpMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "totp_code") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithTotpMethod struct { + value *UpdateLoginFlowWithTotpMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithTotpMethod) Get() *UpdateLoginFlowWithTotpMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) Set(val *UpdateLoginFlowWithTotpMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithTotpMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithTotpMethod(val *UpdateLoginFlowWithTotpMethod) *NullableUpdateLoginFlowWithTotpMethod { + return &NullableUpdateLoginFlowWithTotpMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithTotpMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_login_flow_with_web_authn_method.go b/pkg/httpclient/model_update_login_flow_with_web_authn_method.go new file mode 100644 index 0000000..a79dee2 --- /dev/null +++ b/pkg/httpclient/model_update_login_flow_with_web_authn_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateLoginFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateLoginFlowWithWebAuthnMethod{} + +// UpdateLoginFlowWithWebAuthnMethod Update Login Flow with WebAuthn Method +type UpdateLoginFlowWithWebAuthnMethod 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 \"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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateLoginFlowWithWebAuthnMethod UpdateLoginFlowWithWebAuthnMethod + +// NewUpdateLoginFlowWithWebAuthnMethod instantiates a new UpdateLoginFlowWithWebAuthnMethod 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 NewUpdateLoginFlowWithWebAuthnMethod(identifier string, method string) *UpdateLoginFlowWithWebAuthnMethod { + this := UpdateLoginFlowWithWebAuthnMethod{} + this.Identifier = identifier + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateLoginFlowWithWebAuthnMethod 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 NewUpdateLoginFlowWithWebAuthnMethodWithDefaults() *UpdateLoginFlowWithWebAuthnMethod { + this := UpdateLoginFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateLoginFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithWebAuthnMethod) 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 *UpdateLoginFlowWithWebAuthnMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithWebAuthnMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithWebAuthnMethod) 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 *UpdateLoginFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.WebauthnLogin) { + var ret string + return ret + } + return *o.WebauthnLogin +} + +// GetWebauthnLoginOk returns a tuple with the WebauthnLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLoginOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnLogin) { + return nil, false + } + return o.WebauthnLogin, true +} + +// HasWebauthnLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasWebauthnLogin() bool { + if o != nil && !IsNil(o.WebauthnLogin) { + return true + } + + return false +} + +// SetWebauthnLogin gets a reference to the given string and assigns it to the WebauthnLogin field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetWebauthnLogin(v string) { + o.WebauthnLogin = &v +} + +func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateLoginFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["identifier"] = o.Identifier + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnLogin) { + toSerialize["webauthn_login"] = o.WebauthnLogin + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateLoginFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "identifier", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateLoginFlowWithWebAuthnMethod := _UpdateLoginFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateLoginFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateLoginFlowWithWebAuthnMethod(varUpdateLoginFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "identifier") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_login") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateLoginFlowWithWebAuthnMethod struct { + value *UpdateLoginFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) Get() *UpdateLoginFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) Set(val *UpdateLoginFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithWebAuthnMethod(val *UpdateLoginFlowWithWebAuthnMethod) *NullableUpdateLoginFlowWithWebAuthnMethod { + return &NullableUpdateLoginFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_recovery_flow_body.go b/pkg/httpclient/model_update_recovery_flow_body.go new file mode 100644 index 0000000..c226e9e --- /dev/null +++ b/pkg/httpclient/model_update_recovery_flow_body.go @@ -0,0 +1,178 @@ +/* +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" + "fmt" +) + +// UpdateRecoveryFlowBody - Update Recovery Flow Request Body +type UpdateRecoveryFlowBody struct { + UpdateRecoveryFlowWithCodeMethod *UpdateRecoveryFlowWithCodeMethod + UpdateRecoveryFlowWithLinkMethod *UpdateRecoveryFlowWithLinkMethod +} + +// UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody is a convenience function that returns UpdateRecoveryFlowWithCodeMethod wrapped in UpdateRecoveryFlowBody +func UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryFlowWithCodeMethod) UpdateRecoveryFlowBody { + return UpdateRecoveryFlowBody{ + UpdateRecoveryFlowWithCodeMethod: v, + } +} + +// UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody is a convenience function that returns UpdateRecoveryFlowWithLinkMethod wrapped in UpdateRecoveryFlowBody +func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryFlowWithLinkMethod) UpdateRecoveryFlowBody { + return UpdateRecoveryFlowBody{ + UpdateRecoveryFlowWithLinkMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UpdateRecoveryFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRecoveryFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRecoveryFlowWithCodeMethod) + } + + if src.UpdateRecoveryFlowWithLinkMethod != nil { + return json.Marshal(&src.UpdateRecoveryFlowWithLinkMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateRecoveryFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateRecoveryFlowWithCodeMethod != nil { + return obj.UpdateRecoveryFlowWithCodeMethod + } + + if obj.UpdateRecoveryFlowWithLinkMethod != nil { + return obj.UpdateRecoveryFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateRecoveryFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateRecoveryFlowWithCodeMethod != nil { + return *obj.UpdateRecoveryFlowWithCodeMethod + } + + if obj.UpdateRecoveryFlowWithLinkMethod != nil { + return *obj.UpdateRecoveryFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateRecoveryFlowBody struct { + value *UpdateRecoveryFlowBody + isSet bool +} + +func (v NullableUpdateRecoveryFlowBody) Get() *UpdateRecoveryFlowBody { + return v.value +} + +func (v *NullableUpdateRecoveryFlowBody) Set(val *UpdateRecoveryFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowBody(val *UpdateRecoveryFlowBody) *NullableUpdateRecoveryFlowBody { + return &NullableUpdateRecoveryFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_recovery_flow_with_code_method.go b/pkg/httpclient/model_update_recovery_flow_with_code_method.go new file mode 100644 index 0000000..36f06ab --- /dev/null +++ b/pkg/httpclient/model_update_recovery_flow_with_code_method.go @@ -0,0 +1,472 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRecoveryFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRecoveryFlowWithCodeMethod{} + +// UpdateRecoveryFlowWithCodeMethod Update Recovery Flow with Code Method +type UpdateRecoveryFlowWithCodeMethod struct { + // Code from the recovery email If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The email address of the account to recover If the email belongs to a valid account, a recovery email will be sent. If you want to notify the email address if the account does not exist, see the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/account-recovery-password-reset#attempted-recovery-notifications) If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. format: email + 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"` + // A recovery address that is registered for the user. It can be an email, a phone number (to receive the code via SMS), etc. Used in RecoveryV2. + RecoveryAddress *string `json:"recovery_address,omitempty"` + // If there are multiple recovery addresses registered for the user, and the initially provided address is different from the address chosen when the choice (of masked addresses) is presented, then we need to make sure that the user actually knows the full address to avoid information exfiltration, so we ask for the full address. Used in RecoveryV2. + RecoveryConfirmAddress *string `json:"recovery_confirm_address,omitempty"` + // If there are multiple addresses registered for the user, a choice is presented and this field stores the result of this choice. Addresses are 'masked' (never sent in full to the client and shown partially in the UI) since at this point in the recovery flow, the user has not yet proven that it knows the full address and we want to avoid information exfiltration. So for all intents and purposes, the value of this field should be treated as an opaque identifier. Used in RecoveryV2. + RecoverySelectAddress *string `json:"recovery_select_address,omitempty"` + // Set to \"previous\" to go back in the flow, meaningfully. Used in RecoveryV2. + Screen *string `json:"screen,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRecoveryFlowWithCodeMethod UpdateRecoveryFlowWithCodeMethod + +// NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod 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 NewUpdateRecoveryFlowWithCodeMethod(method string) *UpdateRecoveryFlowWithCodeMethod { + this := UpdateRecoveryFlowWithCodeMethod{} + this.Method = method + return &this +} + +// NewUpdateRecoveryFlowWithCodeMethodWithDefaults instantiates a new UpdateRecoveryFlowWithCodeMethod 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 NewUpdateRecoveryFlowWithCodeMethodWithDefaults() *UpdateRecoveryFlowWithCodeMethod { + this := UpdateRecoveryFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRecoveryFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetEmail() string { + if o == nil || IsNil(o.Email) { + var ret string + return ret + } + return *o.Email +} + +// GetEmailOk returns a tuple with the Email field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetEmailOk() (*string, bool) { + if o == nil || IsNil(o.Email) { + return nil, false + } + return o.Email, true +} + +// HasEmail returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasEmail() bool { + if o != nil && !IsNil(o.Email) { + return true + } + + return false +} + +// SetEmail gets a reference to the given string and assigns it to the Email field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetEmail(v string) { + o.Email = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRecoveryFlowWithCodeMethod) 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 *UpdateRecoveryFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetRecoveryAddress returns the RecoveryAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryAddress() string { + if o == nil || IsNil(o.RecoveryAddress) { + var ret string + return ret + } + return *o.RecoveryAddress +} + +// GetRecoveryAddressOk returns a tuple with the RecoveryAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoveryAddress) { + return nil, false + } + return o.RecoveryAddress, true +} + +// HasRecoveryAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoveryAddress() bool { + if o != nil && !IsNil(o.RecoveryAddress) { + return true + } + + return false +} + +// SetRecoveryAddress gets a reference to the given string and assigns it to the RecoveryAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoveryAddress(v string) { + o.RecoveryAddress = &v +} + +// GetRecoveryConfirmAddress returns the RecoveryConfirmAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryConfirmAddress() string { + if o == nil || IsNil(o.RecoveryConfirmAddress) { + var ret string + return ret + } + return *o.RecoveryConfirmAddress +} + +// GetRecoveryConfirmAddressOk returns a tuple with the RecoveryConfirmAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoveryConfirmAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoveryConfirmAddress) { + return nil, false + } + return o.RecoveryConfirmAddress, true +} + +// HasRecoveryConfirmAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoveryConfirmAddress() bool { + if o != nil && !IsNil(o.RecoveryConfirmAddress) { + return true + } + + return false +} + +// SetRecoveryConfirmAddress gets a reference to the given string and assigns it to the RecoveryConfirmAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoveryConfirmAddress(v string) { + o.RecoveryConfirmAddress = &v +} + +// GetRecoverySelectAddress returns the RecoverySelectAddress field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoverySelectAddress() string { + if o == nil || IsNil(o.RecoverySelectAddress) { + var ret string + return ret + } + return *o.RecoverySelectAddress +} + +// GetRecoverySelectAddressOk returns a tuple with the RecoverySelectAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetRecoverySelectAddressOk() (*string, bool) { + if o == nil || IsNil(o.RecoverySelectAddress) { + return nil, false + } + return o.RecoverySelectAddress, true +} + +// HasRecoverySelectAddress returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasRecoverySelectAddress() bool { + if o != nil && !IsNil(o.RecoverySelectAddress) { + return true + } + + return false +} + +// SetRecoverySelectAddress gets a reference to the given string and assigns it to the RecoverySelectAddress field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetRecoverySelectAddress(v string) { + o.RecoverySelectAddress = &v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetScreen() string { + if o == nil || IsNil(o.Screen) { + 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 *UpdateRecoveryFlowWithCodeMethod) GetScreenOk() (*string, bool) { + if o == nil || IsNil(o.Screen) { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasScreen() bool { + if o != nil && !IsNil(o.Screen) { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRecoveryFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Email) { + toSerialize["email"] = o.Email + } + toSerialize["method"] = o.Method + if !IsNil(o.RecoveryAddress) { + toSerialize["recovery_address"] = o.RecoveryAddress + } + if !IsNil(o.RecoveryConfirmAddress) { + toSerialize["recovery_confirm_address"] = o.RecoveryConfirmAddress + } + if !IsNil(o.RecoverySelectAddress) { + toSerialize["recovery_select_address"] = o.RecoverySelectAddress + } + if !IsNil(o.Screen) { + toSerialize["screen"] = o.Screen + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRecoveryFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRecoveryFlowWithCodeMethod := _UpdateRecoveryFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateRecoveryFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateRecoveryFlowWithCodeMethod(varUpdateRecoveryFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "recovery_address") + delete(additionalProperties, "recovery_confirm_address") + delete(additionalProperties, "recovery_select_address") + delete(additionalProperties, "screen") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRecoveryFlowWithCodeMethod struct { + value *UpdateRecoveryFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) Get() *UpdateRecoveryFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) Set(val *UpdateRecoveryFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowWithCodeMethod(val *UpdateRecoveryFlowWithCodeMethod) *NullableUpdateRecoveryFlowWithCodeMethod { + return &NullableUpdateRecoveryFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_recovery_flow_with_link_method.go b/pkg/httpclient/model_update_recovery_flow_with_link_method.go new file mode 100644 index 0000000..a6d63a8 --- /dev/null +++ b/pkg/httpclient/model_update_recovery_flow_with_link_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRecoveryFlowWithLinkMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRecoveryFlowWithLinkMethod{} + +// UpdateRecoveryFlowWithLinkMethod Update Recovery Flow with Link Method +type UpdateRecoveryFlowWithLinkMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Email to Recover Needs to be set when initiating the flow. If the email is a registered recovery email, a recovery link will be sent. If the email is not known, an email with details on what happened will be sent instead. format: email + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRecoveryFlowWithLinkMethod UpdateRecoveryFlowWithLinkMethod + +// NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod 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 NewUpdateRecoveryFlowWithLinkMethod(email string, method string) *UpdateRecoveryFlowWithLinkMethod { + this := UpdateRecoveryFlowWithLinkMethod{} + this.Email = email + this.Method = method + return &this +} + +// NewUpdateRecoveryFlowWithLinkMethodWithDefaults instantiates a new UpdateRecoveryFlowWithLinkMethod 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 NewUpdateRecoveryFlowWithLinkMethodWithDefaults() *UpdateRecoveryFlowWithLinkMethod { + this := UpdateRecoveryFlowWithLinkMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRecoveryFlowWithLinkMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value +func (o *UpdateRecoveryFlowWithLinkMethod) GetEmail() string { + if o == nil { + var ret string + return ret + } + + return o.Email +} + +// GetEmailOk returns a tuple with the Email field value +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetEmailOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Email, true +} + +// SetEmail sets field value +func (o *UpdateRecoveryFlowWithLinkMethod) SetEmail(v string) { + o.Email = v +} + +// GetMethod returns the Method field value +func (o *UpdateRecoveryFlowWithLinkMethod) 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 *UpdateRecoveryFlowWithLinkMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRecoveryFlowWithLinkMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["email"] = o.Email + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRecoveryFlowWithLinkMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "email", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRecoveryFlowWithLinkMethod := _UpdateRecoveryFlowWithLinkMethod{} + + err = json.Unmarshal(data, &varUpdateRecoveryFlowWithLinkMethod) + + if err != nil { + return err + } + + *o = UpdateRecoveryFlowWithLinkMethod(varUpdateRecoveryFlowWithLinkMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRecoveryFlowWithLinkMethod struct { + value *UpdateRecoveryFlowWithLinkMethod + isSet bool +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) Get() *UpdateRecoveryFlowWithLinkMethod { + return v.value +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) Set(val *UpdateRecoveryFlowWithLinkMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRecoveryFlowWithLinkMethod(val *UpdateRecoveryFlowWithLinkMethod) *NullableUpdateRecoveryFlowWithLinkMethod { + return &NullableUpdateRecoveryFlowWithLinkMethod{value: val, isSet: true} +} + +func (v NullableUpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRecoveryFlowWithLinkMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_body.go b/pkg/httpclient/model_update_registration_flow_body.go new file mode 100644 index 0000000..07be3ba --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_body.go @@ -0,0 +1,398 @@ +/* +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" + "fmt" +) + +// UpdateRegistrationFlowBody - Update Registration Request Body +type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod + UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod + UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod + UpdateRegistrationFlowWithSamlMethod *UpdateRegistrationFlowWithSamlMethod + UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod +} + +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + +// UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateRegistrationFlowWithPasswordMethod: v, + } +} + +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + +// UpdateRegistrationFlowWithSamlMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithSamlMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithSamlMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithSamlMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithSamlMethod: v, + } +} + +// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithSamlMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateRegistrationFlowWithSamlMethod' + if jsonDict["method"] == "updateRegistrationFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithSamlMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithSamlMethod: %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 +func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + + if src.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithSamlMethod) + } + + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + + if obj.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return obj.UpdateRegistrationFlowWithSamlMethod + } + + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return obj.UpdateRegistrationFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateRegistrationFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return *obj.UpdateRegistrationFlowWithCodeMethod + } + + if obj.UpdateRegistrationFlowWithOidcMethod != nil { + 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.UpdateRegistrationFlowWithSamlMethod != nil { + return *obj.UpdateRegistrationFlowWithSamlMethod + } + + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { + return *obj.UpdateRegistrationFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateRegistrationFlowBody struct { + value *UpdateRegistrationFlowBody + isSet bool +} + +func (v NullableUpdateRegistrationFlowBody) Get() *UpdateRegistrationFlowBody { + return v.value +} + +func (v *NullableUpdateRegistrationFlowBody) Set(val *UpdateRegistrationFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowBody(val *UpdateRegistrationFlowBody) *NullableUpdateRegistrationFlowBody { + return &NullableUpdateRegistrationFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_with_code_method.go b/pkg/httpclient/model_update_registration_flow_with_code_method.go new file mode 100644 index 0000000..e864d1b --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_code_method.go @@ -0,0 +1,350 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithCodeMethod{} + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithCodeMethod + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod 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 NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod 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 NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) 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 *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || IsNil(o.Resend) { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || IsNil(o.Resend) { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && !IsNil(o.Resend) { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) 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 *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.Resend) { + toSerialize["resend"] = o.Resend + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithCodeMethod := _UpdateRegistrationFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithCodeMethod(varUpdateRegistrationFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "resend") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_with_oidc_method.go b/pkg/httpclient/model_update_registration_flow_with_oidc_method.go new file mode 100644 index 0000000..c8df11b --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_oidc_method.go @@ -0,0 +1,426 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithOidcMethod{} + +// UpdateRegistrationFlowWithOidcMethod Update Registration Flow with OpenID Connect Method +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 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"` + // Method to use This field must be set to `oidc` when using the oidc method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // The identity traits + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithOidcMethod + +// NewUpdateRegistrationFlowWithOidcMethod instantiates a new UpdateRegistrationFlowWithOidcMethod 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 NewUpdateRegistrationFlowWithOidcMethod(method string, provider string) *UpdateRegistrationFlowWithOidcMethod { + this := UpdateRegistrationFlowWithOidcMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateRegistrationFlowWithOidcMethodWithDefaults instantiates a new UpdateRegistrationFlowWithOidcMethod 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 NewUpdateRegistrationFlowWithOidcMethodWithDefaults() *UpdateRegistrationFlowWithOidcMethod { + this := UpdateRegistrationFlowWithOidcMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithOidcMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdToken() string { + if o == nil || IsNil(o.IdToken) { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || IsNil(o.IdToken) { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdToken() bool { + if o != nil && !IsNil(o.IdToken) { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || IsNil(o.IdTokenNonce) { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || IsNil(o.IdTokenNonce) { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && !IsNil(o.IdTokenNonce) { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithOidcMethod) 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 *UpdateRegistrationFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateRegistrationFlowWithOidcMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { + if o == nil || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.IdToken) { + toSerialize["id_token"] = o.IdToken + } + if !IsNil(o.IdTokenNonce) { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithOidcMethod := _UpdateRegistrationFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithOidcMethod(varUpdateRegistrationFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "id_token") + delete(additionalProperties, "id_token_nonce") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithOidcMethod struct { + value *UpdateRegistrationFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) Get() *UpdateRegistrationFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) Set(val *UpdateRegistrationFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithOidcMethod(val *UpdateRegistrationFlowWithOidcMethod) *NullableUpdateRegistrationFlowWithOidcMethod { + return &NullableUpdateRegistrationFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_with_passkey_method.go b/pkg/httpclient/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 0000000..a9a7ee1 --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyRegister) { + 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 || IsNil(o.PasskeyRegister) { + 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 && !IsNil(o.PasskeyRegister) { + 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 map[string]interface{}{}, 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyRegister) { + toSerialize["passkey_register"] = o.PasskeyRegister + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithPasskeyMethod := _UpdateRegistrationFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithPasskeyMethod(varUpdateRegistrationFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_register") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_update_registration_flow_with_password_method.go b/pkg/httpclient/model_update_registration_flow_with_password_method.go new file mode 100644 index 0000000..3aaaf0b --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_password_method.go @@ -0,0 +1,304 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithPasswordMethod{} + +// UpdateRegistrationFlowWithPasswordMethod Update Registration Flow with Password Method +type UpdateRegistrationFlowWithPasswordMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `password` when using the password method. + Method string `json:"method"` + // Password to sign the user up with + Password string `json:"password"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithPasswordMethod + +// NewUpdateRegistrationFlowWithPasswordMethod instantiates a new UpdateRegistrationFlowWithPasswordMethod 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 NewUpdateRegistrationFlowWithPasswordMethod(method string, password string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasswordMethod { + this := UpdateRegistrationFlowWithPasswordMethod{} + this.Method = method + this.Password = password + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasswordMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasswordMethod 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 NewUpdateRegistrationFlowWithPasswordMethodWithDefaults() *UpdateRegistrationFlowWithPasswordMethod { + this := UpdateRegistrationFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasswordMethod) 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 *UpdateRegistrationFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateRegistrationFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetPassword(v string) { + o.Password = v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasswordMethod) 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 *UpdateRegistrationFlowWithPasswordMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasswordMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "password", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithPasswordMethod := _UpdateRegistrationFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithPasswordMethod(varUpdateRegistrationFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithPasswordMethod struct { + value *UpdateRegistrationFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) Get() *UpdateRegistrationFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) Set(val *UpdateRegistrationFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasswordMethod(val *UpdateRegistrationFlowWithPasswordMethod) *NullableUpdateRegistrationFlowWithPasswordMethod { + return &NullableUpdateRegistrationFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_with_profile_method.go b/pkg/httpclient/model_update_registration_flow_with_profile_method.go new file mode 100644 index 0000000..fbd35f3 --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithProfileMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithProfileMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithProfileMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.Screen) { + 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 || IsNil(o.Screen) { + 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 && !IsNil(o.Screen) { + 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 map[string]interface{}{}, 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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithProfileMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.Screen) { + toSerialize["screen"] = o.Screen + } + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithProfileMethod := _UpdateRegistrationFlowWithProfileMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithProfileMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithProfileMethod(varUpdateRegistrationFlowWithProfileMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "screen") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_update_registration_flow_with_saml_method.go b/pkg/httpclient/model_update_registration_flow_with_saml_method.go new file mode 100644 index 0000000..e217676 --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_saml_method.go @@ -0,0 +1,312 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithSamlMethod{} + +// UpdateRegistrationFlowWithSamlMethod Update registration flow using SAML +type UpdateRegistrationFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `saml` when using the saml method. + Method string `json:"method"` + // The provider to register with + Provider string `json:"provider"` + // The identity traits + Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithSamlMethod UpdateRegistrationFlowWithSamlMethod + +// NewUpdateRegistrationFlowWithSamlMethod instantiates a new UpdateRegistrationFlowWithSamlMethod 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 NewUpdateRegistrationFlowWithSamlMethod(method string, provider string) *UpdateRegistrationFlowWithSamlMethod { + this := UpdateRegistrationFlowWithSamlMethod{} + this.Method = method + this.Provider = provider + return &this +} + +// NewUpdateRegistrationFlowWithSamlMethodWithDefaults instantiates a new UpdateRegistrationFlowWithSamlMethod 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 NewUpdateRegistrationFlowWithSamlMethodWithDefaults() *UpdateRegistrationFlowWithSamlMethod { + this := UpdateRegistrationFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithSamlMethod) 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 *UpdateRegistrationFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetProvider returns the Provider field value +func (o *UpdateRegistrationFlowWithSamlMethod) GetProvider() string { + if o == nil { + var ret string + return ret + } + + return o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) GetProviderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Provider, true +} + +// SetProvider sets field value +func (o *UpdateRegistrationFlowWithSamlMethod) SetProvider(v string) { + o.Provider = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["provider"] = o.Provider + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "provider", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithSamlMethod := _UpdateRegistrationFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithSamlMethod(varUpdateRegistrationFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "provider") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithSamlMethod struct { + value *UpdateRegistrationFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) Get() *UpdateRegistrationFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) Set(val *UpdateRegistrationFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithSamlMethod(val *UpdateRegistrationFlowWithSamlMethod) *NullableUpdateRegistrationFlowWithSamlMethod { + return &NullableUpdateRegistrationFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_registration_flow_with_web_authn_method.go b/pkg/httpclient/model_update_registration_flow_with_web_authn_method.go new file mode 100644 index 0000000..3688f8f --- /dev/null +++ b/pkg/httpclient/model_update_registration_flow_with_web_authn_method.go @@ -0,0 +1,350 @@ +/* +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" + "fmt" +) + +// checks if the UpdateRegistrationFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateRegistrationFlowWithWebAuthnMethod{} + +// UpdateRegistrationFlowWithWebAuthnMethod Update Registration Flow with WebAuthn Method +type UpdateRegistrationFlowWithWebAuthnMethod struct { + // CSRFToken is the anti-CSRF token + 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"` + // 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"` + // 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. + WebauthnRegisterDisplayname *string `json:"webauthn_register_displayname,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateRegistrationFlowWithWebAuthnMethod UpdateRegistrationFlowWithWebAuthnMethod + +// NewUpdateRegistrationFlowWithWebAuthnMethod instantiates a new UpdateRegistrationFlowWithWebAuthnMethod 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 NewUpdateRegistrationFlowWithWebAuthnMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithWebAuthnMethod { + this := UpdateRegistrationFlowWithWebAuthnMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateRegistrationFlowWithWebAuthnMethod 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 NewUpdateRegistrationFlowWithWebAuthnMethodWithDefaults() *UpdateRegistrationFlowWithWebAuthnMethod { + this := UpdateRegistrationFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateRegistrationFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegister() string { + if o == nil || IsNil(o.WebauthnRegister) { + var ret string + return ret + } + return *o.WebauthnRegister +} + +// GetWebauthnRegisterOk returns a tuple with the WebauthnRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegister) { + return nil, false + } + return o.WebauthnRegister, true +} + +// HasWebauthnRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasWebauthnRegister() bool { + if o != nil && !IsNil(o.WebauthnRegister) { + return true + } + + return false +} + +// SetWebauthnRegister gets a reference to the given string and assigns it to the WebauthnRegister field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetWebauthnRegister(v string) { + o.WebauthnRegister = &v +} + +// GetWebauthnRegisterDisplayname returns the WebauthnRegisterDisplayname field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterDisplayname() string { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + var ret string + return ret + } + return *o.WebauthnRegisterDisplayname +} + +// GetWebauthnRegisterDisplaynameOk returns a tuple with the WebauthnRegisterDisplayname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) GetWebauthnRegisterDisplaynameOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + return nil, false + } + return o.WebauthnRegisterDisplayname, true +} + +// HasWebauthnRegisterDisplayname returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) HasWebauthnRegisterDisplayname() bool { + if o != nil && !IsNil(o.WebauthnRegisterDisplayname) { + return true + } + + return false +} + +// SetWebauthnRegisterDisplayname gets a reference to the given string and assigns it to the WebauthnRegisterDisplayname field. +func (o *UpdateRegistrationFlowWithWebAuthnMethod) SetWebauthnRegisterDisplayname(v string) { + o.WebauthnRegisterDisplayname = &v +} + +func (o UpdateRegistrationFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateRegistrationFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnRegister) { + toSerialize["webauthn_register"] = o.WebauthnRegister + } + if !IsNil(o.WebauthnRegisterDisplayname) { + toSerialize["webauthn_register_displayname"] = o.WebauthnRegisterDisplayname + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateRegistrationFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateRegistrationFlowWithWebAuthnMethod := _UpdateRegistrationFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateRegistrationFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateRegistrationFlowWithWebAuthnMethod(varUpdateRegistrationFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_register") + delete(additionalProperties, "webauthn_register_displayname") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateRegistrationFlowWithWebAuthnMethod struct { + value *UpdateRegistrationFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) Get() *UpdateRegistrationFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) Set(val *UpdateRegistrationFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithWebAuthnMethod(val *UpdateRegistrationFlowWithWebAuthnMethod) *NullableUpdateRegistrationFlowWithWebAuthnMethod { + return &NullableUpdateRegistrationFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_body.go b/pkg/httpclient/model_update_settings_flow_body.go new file mode 100644 index 0000000..a47984a --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_body.go @@ -0,0 +1,442 @@ +/* +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" + "fmt" +) + +// UpdateSettingsFlowBody - Update Settings Flow Request Body +type UpdateSettingsFlowBody struct { + UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod + UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod + UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod + UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod + UpdateSettingsFlowWithSamlMethod *UpdateSettingsFlowWithSamlMethod + UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod + UpdateSettingsFlowWithWebAuthnMethod *UpdateSettingsFlowWithWebAuthnMethod +} + +// UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithLookupMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithLookupMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithLookupMethod: v, + } +} + +// UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithOidcMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithOidcMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithOidcMethod: v, + } +} + +// 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{ + UpdateSettingsFlowWithPasswordMethod: v, + } +} + +// UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithProfileMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithProfileMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithProfileMethod: v, + } +} + +// UpdateSettingsFlowWithSamlMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithSamlMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithSamlMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithSamlMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithSamlMethod: v, + } +} + +// UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithTotpMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithTotpMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithTotpMethod: v, + } +} + +// UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithWebAuthnMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithWebAuthnMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithWebAuthnMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'saml' + if jsonDict["method"] == "saml" { + // try to unmarshal JSON data into UpdateSettingsFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithSamlMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithSamlMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 'updateSettingsFlowWithSamlMethod' + if jsonDict["method"] == "updateSettingsFlowWithSamlMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithSamlMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithSamlMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithSamlMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithSamlMethod = nil + return fmt.Errorf("failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithSamlMethod: %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 +func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateSettingsFlowWithLookupMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithLookupMethod) + } + + if src.UpdateSettingsFlowWithOidcMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) + } + + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + + if src.UpdateSettingsFlowWithPasswordMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) + } + + if src.UpdateSettingsFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithProfileMethod) + } + + if src.UpdateSettingsFlowWithSamlMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithSamlMethod) + } + + if src.UpdateSettingsFlowWithTotpMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithTotpMethod) + } + + if src.UpdateSettingsFlowWithWebAuthnMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithWebAuthnMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateSettingsFlowWithLookupMethod != nil { + return obj.UpdateSettingsFlowWithLookupMethod + } + + if obj.UpdateSettingsFlowWithOidcMethod != nil { + return obj.UpdateSettingsFlowWithOidcMethod + } + + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + + if obj.UpdateSettingsFlowWithPasswordMethod != nil { + return obj.UpdateSettingsFlowWithPasswordMethod + } + + if obj.UpdateSettingsFlowWithProfileMethod != nil { + return obj.UpdateSettingsFlowWithProfileMethod + } + + if obj.UpdateSettingsFlowWithSamlMethod != nil { + return obj.UpdateSettingsFlowWithSamlMethod + } + + if obj.UpdateSettingsFlowWithTotpMethod != nil { + return obj.UpdateSettingsFlowWithTotpMethod + } + + if obj.UpdateSettingsFlowWithWebAuthnMethod != nil { + return obj.UpdateSettingsFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateSettingsFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateSettingsFlowWithLookupMethod != nil { + return *obj.UpdateSettingsFlowWithLookupMethod + } + + if obj.UpdateSettingsFlowWithOidcMethod != nil { + return *obj.UpdateSettingsFlowWithOidcMethod + } + + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return *obj.UpdateSettingsFlowWithPasskeyMethod + } + + if obj.UpdateSettingsFlowWithPasswordMethod != nil { + return *obj.UpdateSettingsFlowWithPasswordMethod + } + + if obj.UpdateSettingsFlowWithProfileMethod != nil { + return *obj.UpdateSettingsFlowWithProfileMethod + } + + if obj.UpdateSettingsFlowWithSamlMethod != nil { + return *obj.UpdateSettingsFlowWithSamlMethod + } + + if obj.UpdateSettingsFlowWithTotpMethod != nil { + return *obj.UpdateSettingsFlowWithTotpMethod + } + + if obj.UpdateSettingsFlowWithWebAuthnMethod != nil { + return *obj.UpdateSettingsFlowWithWebAuthnMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateSettingsFlowBody struct { + value *UpdateSettingsFlowBody + isSet bool +} + +func (v NullableUpdateSettingsFlowBody) Get() *UpdateSettingsFlowBody { + return v.value +} + +func (v *NullableUpdateSettingsFlowBody) Set(val *UpdateSettingsFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowBody(val *UpdateSettingsFlowBody) *NullableUpdateSettingsFlowBody { + return &NullableUpdateSettingsFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_lookup_method.go b/pkg/httpclient/model_update_settings_flow_with_lookup_method.go new file mode 100644 index 0000000..8354fa0 --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_lookup_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithLookupMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithLookupMethod{} + +// UpdateSettingsFlowWithLookupMethod Update Settings Flow with Lookup Method +type UpdateSettingsFlowWithLookupMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // If set to true will save the regenerated lookup secrets + LookupSecretConfirm *bool `json:"lookup_secret_confirm,omitempty"` + // Disables this method if true. + LookupSecretDisable *bool `json:"lookup_secret_disable,omitempty"` + // If set to true will regenerate the lookup secrets + LookupSecretRegenerate *bool `json:"lookup_secret_regenerate,omitempty"` + // If set to true will reveal the lookup secrets + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithLookupMethod + +// NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod 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 NewUpdateSettingsFlowWithLookupMethod(method string) *UpdateSettingsFlowWithLookupMethod { + this := UpdateSettingsFlowWithLookupMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithLookupMethodWithDefaults instantiates a new UpdateSettingsFlowWithLookupMethod 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 NewUpdateSettingsFlowWithLookupMethodWithDefaults() *UpdateSettingsFlowWithLookupMethod { + this := UpdateSettingsFlowWithLookupMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithLookupMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithLookupMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetLookupSecretConfirm returns the LookupSecretConfirm field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretConfirm() bool { + if o == nil || IsNil(o.LookupSecretConfirm) { + var ret bool + return ret + } + return *o.LookupSecretConfirm +} + +// GetLookupSecretConfirmOk returns a tuple with the LookupSecretConfirm field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretConfirmOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretConfirm) { + return nil, false + } + return o.LookupSecretConfirm, true +} + +// HasLookupSecretConfirm returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretConfirm() bool { + if o != nil && !IsNil(o.LookupSecretConfirm) { + return true + } + + return false +} + +// SetLookupSecretConfirm gets a reference to the given bool and assigns it to the LookupSecretConfirm field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretConfirm(v bool) { + o.LookupSecretConfirm = &v +} + +// GetLookupSecretDisable returns the LookupSecretDisable field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretDisable() bool { + if o == nil || IsNil(o.LookupSecretDisable) { + var ret bool + return ret + } + return *o.LookupSecretDisable +} + +// GetLookupSecretDisableOk returns a tuple with the LookupSecretDisable field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretDisableOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretDisable) { + return nil, false + } + return o.LookupSecretDisable, true +} + +// HasLookupSecretDisable returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretDisable() bool { + if o != nil && !IsNil(o.LookupSecretDisable) { + return true + } + + return false +} + +// SetLookupSecretDisable gets a reference to the given bool and assigns it to the LookupSecretDisable field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretDisable(v bool) { + o.LookupSecretDisable = &v +} + +// GetLookupSecretRegenerate returns the LookupSecretRegenerate field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRegenerate() bool { + if o == nil || IsNil(o.LookupSecretRegenerate) { + var ret bool + return ret + } + return *o.LookupSecretRegenerate +} + +// GetLookupSecretRegenerateOk returns a tuple with the LookupSecretRegenerate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRegenerateOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretRegenerate) { + return nil, false + } + return o.LookupSecretRegenerate, true +} + +// HasLookupSecretRegenerate returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretRegenerate() bool { + if o != nil && !IsNil(o.LookupSecretRegenerate) { + return true + } + + return false +} + +// SetLookupSecretRegenerate gets a reference to the given bool and assigns it to the LookupSecretRegenerate field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretRegenerate(v bool) { + o.LookupSecretRegenerate = &v +} + +// GetLookupSecretReveal returns the LookupSecretReveal field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretReveal() bool { + if o == nil || IsNil(o.LookupSecretReveal) { + var ret bool + return ret + } + return *o.LookupSecretReveal +} + +// GetLookupSecretRevealOk returns a tuple with the LookupSecretReveal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetLookupSecretRevealOk() (*bool, bool) { + if o == nil || IsNil(o.LookupSecretReveal) { + return nil, false + } + return o.LookupSecretReveal, true +} + +// HasLookupSecretReveal returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasLookupSecretReveal() bool { + if o != nil && !IsNil(o.LookupSecretReveal) { + return true + } + + return false +} + +// SetLookupSecretReveal gets a reference to the given bool and assigns it to the LookupSecretReveal field. +func (o *UpdateSettingsFlowWithLookupMethod) SetLookupSecretReveal(v bool) { + o.LookupSecretReveal = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithLookupMethod) 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 *UpdateSettingsFlowWithLookupMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithLookupMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.LookupSecretConfirm) { + toSerialize["lookup_secret_confirm"] = o.LookupSecretConfirm + } + if !IsNil(o.LookupSecretDisable) { + toSerialize["lookup_secret_disable"] = o.LookupSecretDisable + } + if !IsNil(o.LookupSecretRegenerate) { + toSerialize["lookup_secret_regenerate"] = o.LookupSecretRegenerate + } + if !IsNil(o.LookupSecretReveal) { + toSerialize["lookup_secret_reveal"] = o.LookupSecretReveal + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithLookupMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithLookupMethod := _UpdateSettingsFlowWithLookupMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithLookupMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithLookupMethod(varUpdateSettingsFlowWithLookupMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "lookup_secret_confirm") + delete(additionalProperties, "lookup_secret_disable") + delete(additionalProperties, "lookup_secret_regenerate") + delete(additionalProperties, "lookup_secret_reveal") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithLookupMethod struct { + value *UpdateSettingsFlowWithLookupMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) Get() *UpdateSettingsFlowWithLookupMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) Set(val *UpdateSettingsFlowWithLookupMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithLookupMethod(val *UpdateSettingsFlowWithLookupMethod) *NullableUpdateSettingsFlowWithLookupMethod { + return &NullableUpdateSettingsFlowWithLookupMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithLookupMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_oidc_method.go b/pkg/httpclient/model_update_settings_flow_with_oidc_method.go new file mode 100644 index 0000000..1e8cc28 --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_oidc_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithOidcMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithOidcMethod{} + +// UpdateSettingsFlowWithOidcMethod Update Settings Flow with OpenID Connect Method +type UpdateSettingsFlowWithOidcMethod struct { + // Flow ID is the flow's ID. in: query + Flow *string `json:"flow,omitempty"` + // Link this provider Either this or `unlink` must be set. type: string in: body + Link *string `json:"link,omitempty"` + // Method Should be set to profile when trying to update a profile. + 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: `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`. `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithOidcMethod UpdateSettingsFlowWithOidcMethod + +// NewUpdateSettingsFlowWithOidcMethod instantiates a new UpdateSettingsFlowWithOidcMethod 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 NewUpdateSettingsFlowWithOidcMethod(method string) *UpdateSettingsFlowWithOidcMethod { + this := UpdateSettingsFlowWithOidcMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithOidcMethodWithDefaults instantiates a new UpdateSettingsFlowWithOidcMethod 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 NewUpdateSettingsFlowWithOidcMethodWithDefaults() *UpdateSettingsFlowWithOidcMethod { + this := UpdateSettingsFlowWithOidcMethod{} + return &this +} + +// GetFlow returns the Flow field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetFlow() string { + if o == nil || IsNil(o.Flow) { + var ret string + return ret + } + return *o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetFlowOk() (*string, bool) { + if o == nil || IsNil(o.Flow) { + return nil, false + } + return o.Flow, true +} + +// HasFlow returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasFlow() bool { + if o != nil && !IsNil(o.Flow) { + return true + } + + return false +} + +// SetFlow gets a reference to the given string and assigns it to the Flow field. +func (o *UpdateSettingsFlowWithOidcMethod) SetFlow(v string) { + o.Flow = &v +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *UpdateSettingsFlowWithOidcMethod) SetLink(v string) { + o.Link = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithOidcMethod) 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 *UpdateSettingsFlowWithOidcMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithOidcMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.Unlink) { + var ret string + return ret + } + return *o.Unlink +} + +// GetUnlinkOk returns a tuple with the Unlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetUnlinkOk() (*string, bool) { + if o == nil || IsNil(o.Unlink) { + return nil, false + } + return o.Unlink, true +} + +// HasUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasUnlink() bool { + if o != nil && !IsNil(o.Unlink) { + return true + } + + return false +} + +// SetUnlink gets a reference to the given string and assigns it to the Unlink field. +func (o *UpdateSettingsFlowWithOidcMethod) SetUnlink(v string) { + o.Unlink = &v +} + +// GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { + if o == nil || IsNil(o.UpstreamParameters) { + var ret map[string]interface{} + return ret + } + return o.UpstreamParameters +} + +// GetUpstreamParametersOk returns a tuple with the UpstreamParameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetUpstreamParametersOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.UpstreamParameters) { + return map[string]interface{}{}, false + } + return o.UpstreamParameters, true +} + +// HasUpstreamParameters returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasUpstreamParameters() bool { + if o != nil && !IsNil(o.UpstreamParameters) { + return true + } + + return false +} + +// SetUpstreamParameters gets a reference to the given map[string]interface{} and assigns it to the UpstreamParameters field. +func (o *UpdateSettingsFlowWithOidcMethod) SetUpstreamParameters(v map[string]interface{}) { + o.UpstreamParameters = v +} + +func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithOidcMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Flow) { + toSerialize["flow"] = o.Flow + } + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + toSerialize["method"] = o.Method + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.Unlink) { + toSerialize["unlink"] = o.Unlink + } + if !IsNil(o.UpstreamParameters) { + toSerialize["upstream_parameters"] = o.UpstreamParameters + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithOidcMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithOidcMethod := _UpdateSettingsFlowWithOidcMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithOidcMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithOidcMethod(varUpdateSettingsFlowWithOidcMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "flow") + delete(additionalProperties, "link") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "unlink") + delete(additionalProperties, "upstream_parameters") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithOidcMethod struct { + value *UpdateSettingsFlowWithOidcMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) Get() *UpdateSettingsFlowWithOidcMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) Set(val *UpdateSettingsFlowWithOidcMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithOidcMethod(val *UpdateSettingsFlowWithOidcMethod) *NullableUpdateSettingsFlowWithOidcMethod { + return &NullableUpdateSettingsFlowWithOidcMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithOidcMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_passkey_method.go b/pkg/httpclient/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 0000000..1e67672 --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,282 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithPasskeyMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithPasskeyMethod{} + +// 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasskeyMethod + +// 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 || IsNil(o.CsrfToken) { + 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 || IsNil(o.CsrfToken) { + 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 && !IsNil(o.CsrfToken) { + 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 || IsNil(o.PasskeyRemove) { + 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 || IsNil(o.PasskeyRemove) { + 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 && !IsNil(o.PasskeyRemove) { + 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 || IsNil(o.PasskeySettingsRegister) { + 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 || IsNil(o.PasskeySettingsRegister) { + 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 && !IsNil(o.PasskeySettingsRegister) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithPasskeyMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.PasskeyRemove) { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if !IsNil(o.PasskeySettingsRegister) { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithPasskeyMethod := _UpdateSettingsFlowWithPasskeyMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithPasskeyMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithPasskeyMethod(varUpdateSettingsFlowWithPasskeyMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "passkey_remove") + delete(additionalProperties, "passkey_settings_register") + o.AdditionalProperties = additionalProperties + } + + return err +} + +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/pkg/httpclient/model_update_settings_flow_with_password_method.go b/pkg/httpclient/model_update_settings_flow_with_password_method.go new file mode 100644 index 0000000..1ecc2cd --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_password_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithPasswordMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithPasswordMethod{} + +// UpdateSettingsFlowWithPasswordMethod Update Settings Flow with Password Method +type UpdateSettingsFlowWithPasswordMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to password when trying to update a password. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithPasswordMethod + +// NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod 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 NewUpdateSettingsFlowWithPasswordMethod(method string, password string) *UpdateSettingsFlowWithPasswordMethod { + this := UpdateSettingsFlowWithPasswordMethod{} + this.Method = method + this.Password = password + return &this +} + +// NewUpdateSettingsFlowWithPasswordMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasswordMethod 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 NewUpdateSettingsFlowWithPasswordMethodWithDefaults() *UpdateSettingsFlowWithPasswordMethod { + this := UpdateSettingsFlowWithPasswordMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithPasswordMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasswordMethod) 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 *UpdateSettingsFlowWithPasswordMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasswordMethod) SetMethod(v string) { + o.Method = v +} + +// GetPassword returns the Password field value +func (o *UpdateSettingsFlowWithPasswordMethod) GetPassword() string { + if o == nil { + var ret string + return ret + } + + return o.Password +} + +// GetPasswordOk returns a tuple with the Password field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetPasswordOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Password, true +} + +// SetPassword sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithPasswordMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["password"] = o.Password + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithPasswordMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "password", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithPasswordMethod := _UpdateSettingsFlowWithPasswordMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithPasswordMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithPasswordMethod(varUpdateSettingsFlowWithPasswordMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "password") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithPasswordMethod struct { + value *UpdateSettingsFlowWithPasswordMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) Get() *UpdateSettingsFlowWithPasswordMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) Set(val *UpdateSettingsFlowWithPasswordMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasswordMethod(val *UpdateSettingsFlowWithPasswordMethod) *NullableUpdateSettingsFlowWithPasswordMethod { + return &NullableUpdateSettingsFlowWithPasswordMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasswordMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_profile_method.go b/pkg/httpclient/model_update_settings_flow_with_profile_method.go new file mode 100644 index 0000000..14d33eb --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_profile_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithProfileMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithProfileMethod{} + +// UpdateSettingsFlowWithProfileMethod Update Settings Flow with Profile Method +type UpdateSettingsFlowWithProfileMethod 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"` + // 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithProfileMethod + +// NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod 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 NewUpdateSettingsFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateSettingsFlowWithProfileMethod { + this := UpdateSettingsFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateSettingsFlowWithProfileMethodWithDefaults instantiates a new UpdateSettingsFlowWithProfileMethod 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 NewUpdateSettingsFlowWithProfileMethodWithDefaults() *UpdateSettingsFlowWithProfileMethod { + this := UpdateSettingsFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithProfileMethod) 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 *UpdateSettingsFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *UpdateSettingsFlowWithProfileMethod) 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 *UpdateSettingsFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// SetTraits sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithProfileMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + toSerialize["traits"] = o.Traits + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithProfileMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + "traits", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithProfileMethod := _UpdateSettingsFlowWithProfileMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithProfileMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithProfileMethod(varUpdateSettingsFlowWithProfileMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithProfileMethod struct { + value *UpdateSettingsFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) Get() *UpdateSettingsFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) Set(val *UpdateSettingsFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithProfileMethod(val *UpdateSettingsFlowWithProfileMethod) *NullableUpdateSettingsFlowWithProfileMethod { + return &NullableUpdateSettingsFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_saml_method.go b/pkg/httpclient/model_update_settings_flow_with_saml_method.go new file mode 100644 index 0000000..063117a --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_saml_method.go @@ -0,0 +1,396 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithSamlMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithSamlMethod{} + +// UpdateSettingsFlowWithSamlMethod Update settings flow using SAML +type UpdateSettingsFlowWithSamlMethod struct { + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Flow ID is the flow's ID. in: query + Flow *string `json:"flow,omitempty"` + // Link this provider Either this or `unlink` must be set. type: string in: body + Link *string `json:"link,omitempty"` + // Method Should be set to saml when trying to update a profile. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithSamlMethod UpdateSettingsFlowWithSamlMethod + +// NewUpdateSettingsFlowWithSamlMethod instantiates a new UpdateSettingsFlowWithSamlMethod 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 NewUpdateSettingsFlowWithSamlMethod(method string) *UpdateSettingsFlowWithSamlMethod { + this := UpdateSettingsFlowWithSamlMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithSamlMethodWithDefaults instantiates a new UpdateSettingsFlowWithSamlMethod 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 NewUpdateSettingsFlowWithSamlMethodWithDefaults() *UpdateSettingsFlowWithSamlMethod { + this := UpdateSettingsFlowWithSamlMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithSamlMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithSamlMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetFlow returns the Flow field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetFlow() string { + if o == nil || IsNil(o.Flow) { + var ret string + return ret + } + return *o.Flow +} + +// GetFlowOk returns a tuple with the Flow field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetFlowOk() (*string, bool) { + if o == nil || IsNil(o.Flow) { + return nil, false + } + return o.Flow, true +} + +// HasFlow returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasFlow() bool { + if o != nil && !IsNil(o.Flow) { + return true + } + + return false +} + +// SetFlow gets a reference to the given string and assigns it to the Flow field. +func (o *UpdateSettingsFlowWithSamlMethod) SetFlow(v string) { + o.Flow = &v +} + +// GetLink returns the Link field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetLink() string { + if o == nil || IsNil(o.Link) { + var ret string + return ret + } + return *o.Link +} + +// GetLinkOk returns a tuple with the Link field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetLinkOk() (*string, bool) { + if o == nil || IsNil(o.Link) { + return nil, false + } + return o.Link, true +} + +// HasLink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasLink() bool { + if o != nil && !IsNil(o.Link) { + return true + } + + return false +} + +// SetLink gets a reference to the given string and assigns it to the Link field. +func (o *UpdateSettingsFlowWithSamlMethod) SetLink(v string) { + o.Link = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithSamlMethod) 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 *UpdateSettingsFlowWithSamlMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithSamlMethod) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetTraits() map[string]interface{} { + if o == nil || IsNil(o.Traits) { + var ret map[string]interface{} + return ret + } + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Traits) { + return map[string]interface{}{}, false + } + return o.Traits, true +} + +// HasTraits returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasTraits() bool { + if o != nil && !IsNil(o.Traits) { + return true + } + + return false +} + +// SetTraits gets a reference to the given map[string]interface{} and assigns it to the Traits field. +func (o *UpdateSettingsFlowWithSamlMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetTransientPayload() map[string]interface{} { + if o == nil || IsNil(o.TransientPayload) { + 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 *UpdateSettingsFlowWithSamlMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithSamlMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +// GetUnlink returns the Unlink field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithSamlMethod) GetUnlink() string { + if o == nil || IsNil(o.Unlink) { + var ret string + return ret + } + return *o.Unlink +} + +// GetUnlinkOk returns a tuple with the Unlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithSamlMethod) GetUnlinkOk() (*string, bool) { + if o == nil || IsNil(o.Unlink) { + return nil, false + } + return o.Unlink, true +} + +// HasUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithSamlMethod) HasUnlink() bool { + if o != nil && !IsNil(o.Unlink) { + return true + } + + return false +} + +// SetUnlink gets a reference to the given string and assigns it to the Unlink field. +func (o *UpdateSettingsFlowWithSamlMethod) SetUnlink(v string) { + o.Unlink = &v +} + +func (o UpdateSettingsFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithSamlMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Flow) { + toSerialize["flow"] = o.Flow + } + if !IsNil(o.Link) { + toSerialize["link"] = o.Link + } + toSerialize["method"] = o.Method + if !IsNil(o.Traits) { + toSerialize["traits"] = o.Traits + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.Unlink) { + toSerialize["unlink"] = o.Unlink + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithSamlMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithSamlMethod := _UpdateSettingsFlowWithSamlMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithSamlMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithSamlMethod(varUpdateSettingsFlowWithSamlMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "flow") + delete(additionalProperties, "link") + delete(additionalProperties, "method") + delete(additionalProperties, "traits") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "unlink") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithSamlMethod struct { + value *UpdateSettingsFlowWithSamlMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) Get() *UpdateSettingsFlowWithSamlMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) Set(val *UpdateSettingsFlowWithSamlMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithSamlMethod(val *UpdateSettingsFlowWithSamlMethod) *NullableUpdateSettingsFlowWithSamlMethod { + return &NullableUpdateSettingsFlowWithSamlMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithSamlMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithSamlMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_totp_method.go b/pkg/httpclient/model_update_settings_flow_with_totp_method.go new file mode 100644 index 0000000..0e77ab4 --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_totp_method.go @@ -0,0 +1,320 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithTotpMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithTotpMethod{} + +// UpdateSettingsFlowWithTotpMethod Update Settings Flow with TOTP Method +type UpdateSettingsFlowWithTotpMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"totp\" when trying to add, update, or remove a totp pairing. + Method string `json:"method"` + // ValidationTOTP must contain a valid TOTP based on the + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithTotpMethod UpdateSettingsFlowWithTotpMethod + +// NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod 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 NewUpdateSettingsFlowWithTotpMethod(method string) *UpdateSettingsFlowWithTotpMethod { + this := UpdateSettingsFlowWithTotpMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithTotpMethodWithDefaults instantiates a new UpdateSettingsFlowWithTotpMethod 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 NewUpdateSettingsFlowWithTotpMethodWithDefaults() *UpdateSettingsFlowWithTotpMethod { + this := UpdateSettingsFlowWithTotpMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithTotpMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithTotpMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithTotpMethod) 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 *UpdateSettingsFlowWithTotpMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithTotpMethod) SetMethod(v string) { + o.Method = v +} + +// GetTotpCode returns the TotpCode field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpCode() string { + if o == nil || IsNil(o.TotpCode) { + var ret string + return ret + } + return *o.TotpCode +} + +// GetTotpCodeOk returns a tuple with the TotpCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpCodeOk() (*string, bool) { + if o == nil || IsNil(o.TotpCode) { + return nil, false + } + return o.TotpCode, true +} + +// HasTotpCode returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTotpCode() bool { + if o != nil && !IsNil(o.TotpCode) { + return true + } + + return false +} + +// SetTotpCode gets a reference to the given string and assigns it to the TotpCode field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTotpCode(v string) { + o.TotpCode = &v +} + +// GetTotpUnlink returns the TotpUnlink field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpUnlink() bool { + if o == nil || IsNil(o.TotpUnlink) { + var ret bool + return ret + } + return *o.TotpUnlink +} + +// GetTotpUnlinkOk returns a tuple with the TotpUnlink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTotpUnlinkOk() (*bool, bool) { + if o == nil || IsNil(o.TotpUnlink) { + return nil, false + } + return o.TotpUnlink, true +} + +// HasTotpUnlink returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTotpUnlink() bool { + if o != nil && !IsNil(o.TotpUnlink) { + return true + } + + return false +} + +// SetTotpUnlink gets a reference to the given bool and assigns it to the TotpUnlink field. +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithTotpMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.TotpCode) { + toSerialize["totp_code"] = o.TotpCode + } + if !IsNil(o.TotpUnlink) { + toSerialize["totp_unlink"] = o.TotpUnlink + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithTotpMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithTotpMethod := _UpdateSettingsFlowWithTotpMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithTotpMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithTotpMethod(varUpdateSettingsFlowWithTotpMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "totp_code") + delete(additionalProperties, "totp_unlink") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithTotpMethod struct { + value *UpdateSettingsFlowWithTotpMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) Get() *UpdateSettingsFlowWithTotpMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) Set(val *UpdateSettingsFlowWithTotpMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithTotpMethod(val *UpdateSettingsFlowWithTotpMethod) *NullableUpdateSettingsFlowWithTotpMethod { + return &NullableUpdateSettingsFlowWithTotpMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithTotpMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_settings_flow_with_web_authn_method.go b/pkg/httpclient/model_update_settings_flow_with_web_authn_method.go new file mode 100644 index 0000000..549b2e8 --- /dev/null +++ b/pkg/httpclient/model_update_settings_flow_with_web_authn_method.go @@ -0,0 +1,358 @@ +/* +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" + "fmt" +) + +// checks if the UpdateSettingsFlowWithWebAuthnMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateSettingsFlowWithWebAuthnMethod{} + +// UpdateSettingsFlowWithWebAuthnMethod Update Settings Flow with WebAuthn Method +type UpdateSettingsFlowWithWebAuthnMethod struct { + // CSRFToken is the anti-CSRF token + 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. + WebauthnRegisterDisplayname *string `json:"webauthn_register_displayname,omitempty"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + WebauthnRemove *string `json:"webauthn_remove,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _UpdateSettingsFlowWithWebAuthnMethod UpdateSettingsFlowWithWebAuthnMethod + +// NewUpdateSettingsFlowWithWebAuthnMethod instantiates a new UpdateSettingsFlowWithWebAuthnMethod 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 NewUpdateSettingsFlowWithWebAuthnMethod(method string) *UpdateSettingsFlowWithWebAuthnMethod { + this := UpdateSettingsFlowWithWebAuthnMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults instantiates a new UpdateSettingsFlowWithWebAuthnMethod 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 NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults() *UpdateSettingsFlowWithWebAuthnMethod { + this := UpdateSettingsFlowWithWebAuthnMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateSettingsFlowWithWebAuthnMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithWebAuthnMethod) 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 *UpdateSettingsFlowWithWebAuthnMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 || IsNil(o.WebauthnRegister) { + var ret string + return ret + } + return *o.WebauthnRegister +} + +// GetWebauthnRegisterOk returns a tuple with the WebauthnRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegister) { + return nil, false + } + return o.WebauthnRegister, true +} + +// HasWebauthnRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRegister() bool { + if o != nil && !IsNil(o.WebauthnRegister) { + return true + } + + return false +} + +// SetWebauthnRegister gets a reference to the given string and assigns it to the WebauthnRegister field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRegister(v string) { + o.WebauthnRegister = &v +} + +// GetWebauthnRegisterDisplayname returns the WebauthnRegisterDisplayname field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterDisplayname() string { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + var ret string + return ret + } + return *o.WebauthnRegisterDisplayname +} + +// GetWebauthnRegisterDisplaynameOk returns a tuple with the WebauthnRegisterDisplayname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegisterDisplaynameOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRegisterDisplayname) { + return nil, false + } + return o.WebauthnRegisterDisplayname, true +} + +// HasWebauthnRegisterDisplayname returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRegisterDisplayname() bool { + if o != nil && !IsNil(o.WebauthnRegisterDisplayname) { + return true + } + + return false +} + +// SetWebauthnRegisterDisplayname gets a reference to the given string and assigns it to the WebauthnRegisterDisplayname field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRegisterDisplayname(v string) { + o.WebauthnRegisterDisplayname = &v +} + +// GetWebauthnRemove returns the WebauthnRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRemove() string { + if o == nil || IsNil(o.WebauthnRemove) { + var ret string + return ret + } + return *o.WebauthnRemove +} + +// GetWebauthnRemoveOk returns a tuple with the WebauthnRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRemoveOk() (*string, bool) { + if o == nil || IsNil(o.WebauthnRemove) { + return nil, false + } + return o.WebauthnRemove, true +} + +// HasWebauthnRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasWebauthnRemove() bool { + if o != nil && !IsNil(o.WebauthnRemove) { + return true + } + + return false +} + +// SetWebauthnRemove gets a reference to the given string and assigns it to the WebauthnRemove field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetWebauthnRemove(v string) { + o.WebauthnRemove = &v +} + +func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateSettingsFlowWithWebAuthnMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + if !IsNil(o.WebauthnRegister) { + toSerialize["webauthn_register"] = o.WebauthnRegister + } + if !IsNil(o.WebauthnRegisterDisplayname) { + toSerialize["webauthn_register_displayname"] = o.WebauthnRegisterDisplayname + } + if !IsNil(o.WebauthnRemove) { + toSerialize["webauthn_remove"] = o.WebauthnRemove + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateSettingsFlowWithWebAuthnMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateSettingsFlowWithWebAuthnMethod := _UpdateSettingsFlowWithWebAuthnMethod{} + + err = json.Unmarshal(data, &varUpdateSettingsFlowWithWebAuthnMethod) + + if err != nil { + return err + } + + *o = UpdateSettingsFlowWithWebAuthnMethod(varUpdateSettingsFlowWithWebAuthnMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "webauthn_register") + delete(additionalProperties, "webauthn_register_displayname") + delete(additionalProperties, "webauthn_remove") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateSettingsFlowWithWebAuthnMethod struct { + value *UpdateSettingsFlowWithWebAuthnMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) Get() *UpdateSettingsFlowWithWebAuthnMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) Set(val *UpdateSettingsFlowWithWebAuthnMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithWebAuthnMethod(val *UpdateSettingsFlowWithWebAuthnMethod) *NullableUpdateSettingsFlowWithWebAuthnMethod { + return &NullableUpdateSettingsFlowWithWebAuthnMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithWebAuthnMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_verification_flow_body.go b/pkg/httpclient/model_update_verification_flow_body.go new file mode 100644 index 0000000..84f0e40 --- /dev/null +++ b/pkg/httpclient/model_update_verification_flow_body.go @@ -0,0 +1,178 @@ +/* +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" + "fmt" +) + +// UpdateVerificationFlowBody - Update Verification Flow Request Body +type UpdateVerificationFlowBody struct { + UpdateVerificationFlowWithCodeMethod *UpdateVerificationFlowWithCodeMethod + UpdateVerificationFlowWithLinkMethod *UpdateVerificationFlowWithLinkMethod +} + +// UpdateVerificationFlowWithCodeMethodAsUpdateVerificationFlowBody is a convenience function that returns UpdateVerificationFlowWithCodeMethod wrapped in UpdateVerificationFlowBody +func UpdateVerificationFlowWithCodeMethodAsUpdateVerificationFlowBody(v *UpdateVerificationFlowWithCodeMethod) UpdateVerificationFlowBody { + return UpdateVerificationFlowBody{ + UpdateVerificationFlowWithCodeMethod: v, + } +} + +// UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody is a convenience function that returns UpdateVerificationFlowWithLinkMethod wrapped in UpdateVerificationFlowBody +func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateVerificationFlowWithLinkMethod) UpdateVerificationFlowBody { + return UpdateVerificationFlowBody{ + UpdateVerificationFlowWithLinkMethod: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { + var err error + // 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 discriminator 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 { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } + + // 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 { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } + } + + // 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()) + } + } + + // 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 +func (src UpdateVerificationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateVerificationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateVerificationFlowWithCodeMethod) + } + + if src.UpdateVerificationFlowWithLinkMethod != nil { + return json.Marshal(&src.UpdateVerificationFlowWithLinkMethod) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *UpdateVerificationFlowBody) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.UpdateVerificationFlowWithCodeMethod != nil { + return obj.UpdateVerificationFlowWithCodeMethod + } + + if obj.UpdateVerificationFlowWithLinkMethod != nil { + return obj.UpdateVerificationFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj UpdateVerificationFlowBody) GetActualInstanceValue() interface{} { + if obj.UpdateVerificationFlowWithCodeMethod != nil { + return *obj.UpdateVerificationFlowWithCodeMethod + } + + if obj.UpdateVerificationFlowWithLinkMethod != nil { + return *obj.UpdateVerificationFlowWithLinkMethod + } + + // all schemas are nil + return nil +} + +type NullableUpdateVerificationFlowBody struct { + value *UpdateVerificationFlowBody + isSet bool +} + +func (v NullableUpdateVerificationFlowBody) Get() *UpdateVerificationFlowBody { + return v.value +} + +func (v *NullableUpdateVerificationFlowBody) Set(val *UpdateVerificationFlowBody) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowBody) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowBody(val *UpdateVerificationFlowBody) *NullableUpdateVerificationFlowBody { + return &NullableUpdateVerificationFlowBody{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_verification_flow_with_code_method.go b/pkg/httpclient/model_update_verification_flow_with_code_method.go new file mode 100644 index 0000000..63d9008 --- /dev/null +++ b/pkg/httpclient/model_update_verification_flow_with_code_method.go @@ -0,0 +1,320 @@ +/* +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" + "fmt" +) + +// checks if the UpdateVerificationFlowWithCodeMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateVerificationFlowWithCodeMethod{} + +// UpdateVerificationFlowWithCodeMethod struct for UpdateVerificationFlowWithCodeMethod +type UpdateVerificationFlowWithCodeMethod struct { + // Code from the recovery email If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The email address or phone number to verify If the provided address belongs to a valid account, a verification email or SMS will be sent. If you want to notify the email address if the account does not exist, see the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation#attempted-verification-notifications) If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateVerificationFlowWithCodeMethod UpdateVerificationFlowWithCodeMethod + +// NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod 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 NewUpdateVerificationFlowWithCodeMethod(method string) *UpdateVerificationFlowWithCodeMethod { + this := UpdateVerificationFlowWithCodeMethod{} + this.Method = method + return &this +} + +// NewUpdateVerificationFlowWithCodeMethodWithDefaults instantiates a new UpdateVerificationFlowWithCodeMethod 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 NewUpdateVerificationFlowWithCodeMethodWithDefaults() *UpdateVerificationFlowWithCodeMethod { + this := UpdateVerificationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetCode() string { + if o == nil || IsNil(o.Code) { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || IsNil(o.Code) { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasCode() bool { + if o != nil && !IsNil(o.Code) { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateVerificationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateVerificationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateVerificationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetEmail() string { + if o == nil || IsNil(o.Email) { + var ret string + return ret + } + return *o.Email +} + +// GetEmailOk returns a tuple with the Email field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetEmailOk() (*string, bool) { + if o == nil || IsNil(o.Email) { + return nil, false + } + return o.Email, true +} + +// HasEmail returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasEmail() bool { + if o != nil && !IsNil(o.Email) { + return true + } + + return false +} + +// SetEmail gets a reference to the given string and assigns it to the Email field. +func (o *UpdateVerificationFlowWithCodeMethod) SetEmail(v string) { + o.Email = &v +} + +// GetMethod returns the Method field value +func (o *UpdateVerificationFlowWithCodeMethod) 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 *UpdateVerificationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateVerificationFlowWithCodeMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Code) { + toSerialize["code"] = o.Code + } + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + if !IsNil(o.Email) { + toSerialize["email"] = o.Email + } + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateVerificationFlowWithCodeMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateVerificationFlowWithCodeMethod := _UpdateVerificationFlowWithCodeMethod{} + + err = json.Unmarshal(data, &varUpdateVerificationFlowWithCodeMethod) + + if err != nil { + return err + } + + *o = UpdateVerificationFlowWithCodeMethod(varUpdateVerificationFlowWithCodeMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "code") + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateVerificationFlowWithCodeMethod struct { + value *UpdateVerificationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) Get() *UpdateVerificationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) Set(val *UpdateVerificationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowWithCodeMethod(val *UpdateVerificationFlowWithCodeMethod) *NullableUpdateVerificationFlowWithCodeMethod { + return &NullableUpdateVerificationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_update_verification_flow_with_link_method.go b/pkg/httpclient/model_update_verification_flow_with_link_method.go new file mode 100644 index 0000000..aed4593 --- /dev/null +++ b/pkg/httpclient/model_update_verification_flow_with_link_method.go @@ -0,0 +1,274 @@ +/* +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" + "fmt" +) + +// checks if the UpdateVerificationFlowWithLinkMethod type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &UpdateVerificationFlowWithLinkMethod{} + +// UpdateVerificationFlowWithLinkMethod Update Verification Flow with Link Method +type UpdateVerificationFlowWithLinkMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Email to Verify Needs to be set when initiating the flow. If the email is a registered verification email, a verification link will be sent. If the email is not known, a email with details on what happened will be sent instead. format: email + 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"` + AdditionalProperties map[string]interface{} +} + +type _UpdateVerificationFlowWithLinkMethod UpdateVerificationFlowWithLinkMethod + +// NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod 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 NewUpdateVerificationFlowWithLinkMethod(email string, method string) *UpdateVerificationFlowWithLinkMethod { + this := UpdateVerificationFlowWithLinkMethod{} + this.Email = email + this.Method = method + return &this +} + +// NewUpdateVerificationFlowWithLinkMethodWithDefaults instantiates a new UpdateVerificationFlowWithLinkMethod 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 NewUpdateVerificationFlowWithLinkMethodWithDefaults() *UpdateVerificationFlowWithLinkMethod { + this := UpdateVerificationFlowWithLinkMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetCsrfToken() string { + if o == nil || IsNil(o.CsrfToken) { + 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 *UpdateVerificationFlowWithLinkMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || IsNil(o.CsrfToken) { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasCsrfToken() bool { + if o != nil && !IsNil(o.CsrfToken) { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateVerificationFlowWithLinkMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetEmail returns the Email field value +func (o *UpdateVerificationFlowWithLinkMethod) GetEmail() string { + if o == nil { + var ret string + return ret + } + + return o.Email +} + +// GetEmailOk returns a tuple with the Email field value +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetEmailOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Email, true +} + +// SetEmail sets field value +func (o *UpdateVerificationFlowWithLinkMethod) SetEmail(v string) { + o.Email = v +} + +// GetMethod returns the Method field value +func (o *UpdateVerificationFlowWithLinkMethod) 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 *UpdateVerificationFlowWithLinkMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o UpdateVerificationFlowWithLinkMethod) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CsrfToken) { + toSerialize["csrf_token"] = o.CsrfToken + } + toSerialize["email"] = o.Email + toSerialize["method"] = o.Method + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *UpdateVerificationFlowWithLinkMethod) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "email", + "method", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varUpdateVerificationFlowWithLinkMethod := _UpdateVerificationFlowWithLinkMethod{} + + err = json.Unmarshal(data, &varUpdateVerificationFlowWithLinkMethod) + + if err != nil { + return err + } + + *o = UpdateVerificationFlowWithLinkMethod(varUpdateVerificationFlowWithLinkMethod) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "csrf_token") + delete(additionalProperties, "email") + delete(additionalProperties, "method") + delete(additionalProperties, "transient_payload") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableUpdateVerificationFlowWithLinkMethod struct { + value *UpdateVerificationFlowWithLinkMethod + isSet bool +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) Get() *UpdateVerificationFlowWithLinkMethod { + return v.value +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) Set(val *UpdateVerificationFlowWithLinkMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateVerificationFlowWithLinkMethod(val *UpdateVerificationFlowWithLinkMethod) *NullableUpdateVerificationFlowWithLinkMethod { + return &NullableUpdateVerificationFlowWithLinkMethod{value: val, isSet: true} +} + +func (v NullableUpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateVerificationFlowWithLinkMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_user_verification.go b/pkg/httpclient/model_user_verification.go new file mode 100644 index 0000000..289a085 --- /dev/null +++ b/pkg/httpclient/model_user_verification.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// UserVerification \"pin\" means the holder proves knowledge of a PIN via pin_proof at login; \"platform\" means the device gates key use behind platform biometrics or an equivalent lock screen; \"none\" marks a possession-only key, usable as a second factor but never as the sole first factor. The empty value marks a legacy key from before user verification existed; such keys are rejected at login and must be re-enrolled. +type UserVerification string + +// List of UserVerification +const ( + USERVERIFICATION_PIN UserVerification = "pin" + USERVERIFICATION_PLATFORM UserVerification = "platform" + USERVERIFICATION_NONE UserVerification = "none" +) + +// All allowed values of UserVerification enum +var AllowedUserVerificationEnumValues = []UserVerification{ + "pin", + "platform", + "none", +} + +func (v *UserVerification) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := UserVerification(value) + for _, existing := range AllowedUserVerificationEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid UserVerification", value) +} + +// NewUserVerificationFromValue returns a pointer to a valid UserVerification +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewUserVerificationFromValue(v string) (*UserVerification, error) { + ev := UserVerification(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for UserVerification: valid values are %v", v, AllowedUserVerificationEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v UserVerification) IsValid() bool { + for _, existing := range AllowedUserVerificationEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to UserVerification value +func (v UserVerification) Ptr() *UserVerification { + return &v +} + +type NullableUserVerification struct { + value *UserVerification + isSet bool +} + +func (v NullableUserVerification) Get() *UserVerification { + return v.value +} + +func (v *NullableUserVerification) Set(val *UserVerification) { + v.value = val + v.isSet = true +} + +func (v NullableUserVerification) IsSet() bool { + return v.isSet +} + +func (v *NullableUserVerification) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUserVerification(val *UserVerification) *NullableUserVerification { + return &NullableUserVerification{value: val, isSet: true} +} + +func (v NullableUserVerification) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUserVerification) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_verifiable_identity_address.go b/pkg/httpclient/model_verifiable_identity_address.go new file mode 100644 index 0000000..d51bc64 --- /dev/null +++ b/pkg/httpclient/model_verifiable_identity_address.go @@ -0,0 +1,410 @@ +/* +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" + "fmt" + "time" +) + +// checks if the VerifiableIdentityAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerifiableIdentityAddress{} + +// VerifiableIdentityAddress VerifiableAddress is an identity's verifiable address +type VerifiableIdentityAddress struct { + // When this entry was created + CreatedAt *time.Time `json:"created_at,omitempty"` + // The ID + Id *string `json:"id,omitempty"` + // VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema + Status string `json:"status"` + // When this entry was last updated + UpdatedAt *time.Time `json:"updated_at,omitempty"` + // The address value example foo@user.com + Value string `json:"value"` + // Indicates if the address has already been verified + Verified bool `json:"verified"` + VerifiedAt *time.Time `json:"verified_at,omitempty"` + // The delivery method + Via string `json:"via"` + AdditionalProperties map[string]interface{} +} + +type _VerifiableIdentityAddress VerifiableIdentityAddress + +// NewVerifiableIdentityAddress instantiates a new VerifiableIdentityAddress 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 NewVerifiableIdentityAddress(status string, value string, verified bool, via string) *VerifiableIdentityAddress { + this := VerifiableIdentityAddress{} + this.Status = status + this.Value = value + this.Verified = verified + this.Via = via + return &this +} + +// NewVerifiableIdentityAddressWithDefaults instantiates a new VerifiableIdentityAddress 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 NewVerifiableIdentityAddressWithDefaults() *VerifiableIdentityAddress { + this := VerifiableIdentityAddress{} + return &this +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *VerifiableIdentityAddress) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *VerifiableIdentityAddress) SetId(v string) { + o.Id = &v +} + +// GetStatus returns the Status field value +func (o *VerifiableIdentityAddress) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *VerifiableIdentityAddress) SetStatus(v string) { + o.Status = v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *VerifiableIdentityAddress) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetValue returns the Value field value +func (o *VerifiableIdentityAddress) GetValue() string { + if o == nil { + var ret string + return ret + } + + return o.Value +} + +// GetValueOk returns a tuple with the Value field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetValueOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Value, true +} + +// SetValue sets field value +func (o *VerifiableIdentityAddress) SetValue(v string) { + o.Value = v +} + +// GetVerified returns the Verified field value +func (o *VerifiableIdentityAddress) GetVerified() bool { + if o == nil { + var ret bool + return ret + } + + return o.Verified +} + +// GetVerifiedOk returns a tuple with the Verified field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetVerifiedOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Verified, true +} + +// SetVerified sets field value +func (o *VerifiableIdentityAddress) SetVerified(v bool) { + o.Verified = v +} + +// GetVerifiedAt returns the VerifiedAt field value if set, zero value otherwise. +func (o *VerifiableIdentityAddress) GetVerifiedAt() time.Time { + if o == nil || IsNil(o.VerifiedAt) { + var ret time.Time + return ret + } + return *o.VerifiedAt +} + +// GetVerifiedAtOk returns a tuple with the VerifiedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetVerifiedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.VerifiedAt) { + return nil, false + } + return o.VerifiedAt, true +} + +// HasVerifiedAt returns a boolean if a field has been set. +func (o *VerifiableIdentityAddress) HasVerifiedAt() bool { + if o != nil && !IsNil(o.VerifiedAt) { + return true + } + + return false +} + +// SetVerifiedAt gets a reference to the given time.Time and assigns it to the VerifiedAt field. +func (o *VerifiableIdentityAddress) SetVerifiedAt(v time.Time) { + o.VerifiedAt = &v +} + +// GetVia returns the Via field value +func (o *VerifiableIdentityAddress) GetVia() string { + if o == nil { + var ret string + return ret + } + + return o.Via +} + +// GetViaOk returns a tuple with the Via field value +// and a boolean to check if the value has been set. +func (o *VerifiableIdentityAddress) GetViaOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Via, true +} + +// SetVia sets field value +func (o *VerifiableIdentityAddress) SetVia(v string) { + o.Via = v +} + +func (o VerifiableIdentityAddress) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerifiableIdentityAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + toSerialize["status"] = o.Status + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["value"] = o.Value + toSerialize["verified"] = o.Verified + if !IsNil(o.VerifiedAt) { + toSerialize["verified_at"] = o.VerifiedAt + } + toSerialize["via"] = o.Via + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *VerifiableIdentityAddress) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "status", + "value", + "verified", + "via", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varVerifiableIdentityAddress := _VerifiableIdentityAddress{} + + err = json.Unmarshal(data, &varVerifiableIdentityAddress) + + if err != nil { + return err + } + + *o = VerifiableIdentityAddress(varVerifiableIdentityAddress) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "created_at") + delete(additionalProperties, "id") + delete(additionalProperties, "status") + delete(additionalProperties, "updated_at") + delete(additionalProperties, "value") + delete(additionalProperties, "verified") + delete(additionalProperties, "verified_at") + delete(additionalProperties, "via") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVerifiableIdentityAddress struct { + value *VerifiableIdentityAddress + isSet bool +} + +func (v NullableVerifiableIdentityAddress) Get() *VerifiableIdentityAddress { + return v.value +} + +func (v *NullableVerifiableIdentityAddress) Set(val *VerifiableIdentityAddress) { + v.value = val + v.isSet = true +} + +func (v NullableVerifiableIdentityAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableVerifiableIdentityAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerifiableIdentityAddress(val *VerifiableIdentityAddress) *NullableVerifiableIdentityAddress { + return &NullableVerifiableIdentityAddress{value: val, isSet: true} +} + +func (v NullableVerifiableIdentityAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerifiableIdentityAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_verification_flow.go b/pkg/httpclient/model_verification_flow.go new file mode 100644 index 0000000..b70dca4 --- /dev/null +++ b/pkg/httpclient/model_verification_flow.go @@ -0,0 +1,490 @@ +/* +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" + "fmt" + "time" +) + +// checks if the VerificationFlow type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerificationFlow{} + +// VerificationFlow Used to verify an out-of-band communication channel such as an email address or a phone number. For more information head over to: https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation +type VerificationFlow struct { + // Active, if set, contains the registration method that is being used. It is initially not set. + Active *string `json:"active,omitempty"` + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address, a new request has to be initiated. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // ID represents the request's unique ID. When performing the verification flow, this represents the id in the verify ui's query parameter: http://?request= type: string format: uuid + Id string `json:"id"` + // IssuedAt is the time (UTC) when the request occurred. + IssuedAt *time.Time `json:"issued_at,omitempty"` + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. + RequestUrl *string `json:"request_url,omitempty"` + // ReturnTo contains the requested return_to URL. + 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"` + AdditionalProperties map[string]interface{} +} + +type _VerificationFlow VerificationFlow + +// NewVerificationFlow instantiates a new VerificationFlow 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 NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { + this := VerificationFlow{} + this.Id = id + this.State = state + this.Type = type_ + this.Ui = ui + return &this +} + +// NewVerificationFlowWithDefaults instantiates a new VerificationFlow 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 NewVerificationFlowWithDefaults() *VerificationFlow { + this := VerificationFlow{} + return &this +} + +// GetActive returns the Active field value if set, zero value otherwise. +func (o *VerificationFlow) GetActive() string { + if o == nil || IsNil(o.Active) { + var ret string + return ret + } + return *o.Active +} + +// GetActiveOk returns a tuple with the Active field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetActiveOk() (*string, bool) { + if o == nil || IsNil(o.Active) { + return nil, false + } + return o.Active, true +} + +// HasActive returns a boolean if a field has been set. +func (o *VerificationFlow) HasActive() bool { + if o != nil && !IsNil(o.Active) { + return true + } + + return false +} + +// SetActive gets a reference to the given string and assigns it to the Active field. +func (o *VerificationFlow) SetActive(v string) { + o.Active = &v +} + +// GetExpiresAt returns the ExpiresAt field value if set, zero value otherwise. +func (o *VerificationFlow) GetExpiresAt() time.Time { + if o == nil || IsNil(o.ExpiresAt) { + var ret time.Time + return ret + } + return *o.ExpiresAt +} + +// GetExpiresAtOk returns a tuple with the ExpiresAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetExpiresAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.ExpiresAt) { + return nil, false + } + return o.ExpiresAt, true +} + +// HasExpiresAt returns a boolean if a field has been set. +func (o *VerificationFlow) HasExpiresAt() bool { + if o != nil && !IsNil(o.ExpiresAt) { + return true + } + + return false +} + +// SetExpiresAt gets a reference to the given time.Time and assigns it to the ExpiresAt field. +func (o *VerificationFlow) SetExpiresAt(v time.Time) { + o.ExpiresAt = &v +} + +// GetId returns the Id field value +func (o *VerificationFlow) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *VerificationFlow) SetId(v string) { + o.Id = v +} + +// GetIssuedAt returns the IssuedAt field value if set, zero value otherwise. +func (o *VerificationFlow) GetIssuedAt() time.Time { + if o == nil || IsNil(o.IssuedAt) { + var ret time.Time + return ret + } + return *o.IssuedAt +} + +// GetIssuedAtOk returns a tuple with the IssuedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetIssuedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.IssuedAt) { + return nil, false + } + return o.IssuedAt, true +} + +// HasIssuedAt returns a boolean if a field has been set. +func (o *VerificationFlow) HasIssuedAt() bool { + if o != nil && !IsNil(o.IssuedAt) { + return true + } + + return false +} + +// SetIssuedAt gets a reference to the given time.Time and assigns it to the IssuedAt field. +func (o *VerificationFlow) SetIssuedAt(v time.Time) { + o.IssuedAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *VerificationFlow) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *VerificationFlow) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *VerificationFlow) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetReturnTo returns the ReturnTo field value if set, zero value otherwise. +func (o *VerificationFlow) GetReturnTo() string { + if o == nil || IsNil(o.ReturnTo) { + var ret string + return ret + } + return *o.ReturnTo +} + +// GetReturnToOk returns a tuple with the ReturnTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetReturnToOk() (*string, bool) { + if o == nil || IsNil(o.ReturnTo) { + return nil, false + } + return o.ReturnTo, true +} + +// HasReturnTo returns a boolean if a field has been set. +func (o *VerificationFlow) HasReturnTo() bool { + if o != nil && !IsNil(o.ReturnTo) { + return true + } + + return false +} + +// SetReturnTo gets a reference to the given string and assigns it to the ReturnTo field. +func (o *VerificationFlow) SetReturnTo(v string) { + o.ReturnTo = &v +} + +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// 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 *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || IsNil(o.State) { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +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 || IsNil(o.TransientPayload) { + 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 || IsNil(o.TransientPayload) { + return map[string]interface{}{}, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && !IsNil(o.TransientPayload) { + 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 { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *VerificationFlow) SetType(v string) { + o.Type = v +} + +// GetUi returns the Ui field value +func (o *VerificationFlow) GetUi() UiContainer { + if o == nil { + var ret UiContainer + return ret + } + + return o.Ui +} + +// GetUiOk returns a tuple with the Ui field value +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetUiOk() (*UiContainer, bool) { + if o == nil { + return nil, false + } + return &o.Ui, true +} + +// SetUi sets field value +func (o *VerificationFlow) SetUi(v UiContainer) { + o.Ui = v +} + +func (o VerificationFlow) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerificationFlow) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Active) { + toSerialize["active"] = o.Active + } + if !IsNil(o.ExpiresAt) { + toSerialize["expires_at"] = o.ExpiresAt + } + toSerialize["id"] = o.Id + if !IsNil(o.IssuedAt) { + toSerialize["issued_at"] = o.IssuedAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.ReturnTo) { + toSerialize["return_to"] = o.ReturnTo + } + if o.State != nil { + toSerialize["state"] = o.State + } + if !IsNil(o.TransientPayload) { + toSerialize["transient_payload"] = o.TransientPayload + } + toSerialize["type"] = o.Type + toSerialize["ui"] = o.Ui + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *VerificationFlow) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "state", + "type", + "ui", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varVerificationFlow := _VerificationFlow{} + + err = json.Unmarshal(data, &varVerificationFlow) + + if err != nil { + return err + } + + *o = VerificationFlow(varVerificationFlow) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "active") + delete(additionalProperties, "expires_at") + delete(additionalProperties, "id") + delete(additionalProperties, "issued_at") + delete(additionalProperties, "request_url") + delete(additionalProperties, "return_to") + delete(additionalProperties, "state") + delete(additionalProperties, "transient_payload") + delete(additionalProperties, "type") + delete(additionalProperties, "ui") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVerificationFlow struct { + value *VerificationFlow + isSet bool +} + +func (v NullableVerificationFlow) Get() *VerificationFlow { + return v.value +} + +func (v *NullableVerificationFlow) Set(val *VerificationFlow) { + v.value = val + v.isSet = true +} + +func (v NullableVerificationFlow) IsSet() bool { + return v.isSet +} + +func (v *NullableVerificationFlow) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerificationFlow(val *VerificationFlow) *NullableVerificationFlow { + return &NullableVerificationFlow{value: val, isSet: true} +} + +func (v NullableVerificationFlow) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerificationFlow) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_verification_flow_state.go b/pkg/httpclient/model_verification_flow_state.go new file mode 100644 index 0000000..82a55c6 --- /dev/null +++ b/pkg/httpclient/model_verification_flow_state.go @@ -0,0 +1,113 @@ +/* +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" + "fmt" +) + +// 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 +const ( + VERIFICATIONFLOWSTATE_CHOOSE_METHOD VerificationFlowState = "choose_method" + VERIFICATIONFLOWSTATE_SENT_EMAIL VerificationFlowState = "sent_email" + VERIFICATIONFLOWSTATE_PASSED_CHALLENGE VerificationFlowState = "passed_challenge" +) + +// All allowed values of VerificationFlowState enum +var AllowedVerificationFlowStateEnumValues = []VerificationFlowState{ + "choose_method", + "sent_email", + "passed_challenge", +} + +func (v *VerificationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := VerificationFlowState(value) + for _, existing := range AllowedVerificationFlowStateEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid VerificationFlowState", value) +} + +// NewVerificationFlowStateFromValue returns a pointer to a valid VerificationFlowState +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewVerificationFlowStateFromValue(v string) (*VerificationFlowState, error) { + ev := VerificationFlowState(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for VerificationFlowState: valid values are %v", v, AllowedVerificationFlowStateEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v VerificationFlowState) IsValid() bool { + for _, existing := range AllowedVerificationFlowStateEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to verificationFlowState value +func (v VerificationFlowState) Ptr() *VerificationFlowState { + return &v +} + +type NullableVerificationFlowState struct { + value *VerificationFlowState + isSet bool +} + +func (v NullableVerificationFlowState) Get() *VerificationFlowState { + return v.value +} + +func (v *NullableVerificationFlowState) Set(val *VerificationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableVerificationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableVerificationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerificationFlowState(val *VerificationFlowState) *NullableVerificationFlowState { + return &NullableVerificationFlowState{value: val, isSet: true} +} + +func (v NullableVerificationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerificationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/model_version.go b/pkg/httpclient/model_version.go new file mode 100644 index 0000000..26b7d51 --- /dev/null +++ b/pkg/httpclient/model_version.go @@ -0,0 +1,155 @@ +/* +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" +) + +// checks if the Version type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Version{} + +// Version struct for Version +type Version struct { + // Version is the service's version. + Version *string `json:"version,omitempty"` + AdditionalProperties map[string]interface{} +} + +type _Version Version + +// NewVersion instantiates a new Version 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 NewVersion() *Version { + this := Version{} + return &this +} + +// NewVersionWithDefaults instantiates a new Version 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 NewVersionWithDefaults() *Version { + this := Version{} + return &this +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *Version) GetVersion() string { + if o == nil || IsNil(o.Version) { + var ret string + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Version) GetVersionOk() (*string, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *Version) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given string and assigns it to the Version field. +func (o *Version) SetVersion(v string) { + o.Version = &v +} + +func (o Version) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Version) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *Version) UnmarshalJSON(data []byte) (err error) { + varVersion := _Version{} + + err = json.Unmarshal(data, &varVersion) + + if err != nil { + return err + } + + *o = Version(varVersion) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "version") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableVersion struct { + value *Version + isSet bool +} + +func (v NullableVersion) Get() *Version { + return v.value +} + +func (v *NullableVersion) Set(val *Version) { + v.value = val + v.isSet = true +} + +func (v NullableVersion) IsSet() bool { + return v.isSet +} + +func (v *NullableVersion) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVersion(val *Version) *NullableVersion { + return &NullableVersion{value: val, isSet: true} +} + +func (v NullableVersion) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVersion) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/httpclient/response.go b/pkg/httpclient/response.go new file mode 100644 index 0000000..50599b1 --- /dev/null +++ b/pkg/httpclient/response.go @@ -0,0 +1,48 @@ +/* +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 ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResponse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/pkg/httpclient/utils.go b/pkg/httpclient/utils.go new file mode 100644 index 0000000..d6fa017 --- /dev/null +++ b/pkg/httpclient/utils.go @@ -0,0 +1,362 @@ +/* +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 ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "time" +) + +// PtrBool is a helper routine that returns a pointer to given boolean value. +func PtrBool(v bool) *bool { return &v } + +// PtrInt is a helper routine that returns a pointer to given integer value. +func PtrInt(v int) *int { return &v } + +// PtrInt32 is a helper routine that returns a pointer to given integer value. +func PtrInt32(v int32) *int32 { return &v } + +// PtrInt64 is a helper routine that returns a pointer to given integer value. +func PtrInt64(v int64) *int64 { return &v } + +// PtrFloat32 is a helper routine that returns a pointer to given float value. +func PtrFloat32(v float32) *float32 { return &v } + +// PtrFloat64 is a helper routine that returns a pointer to given float value. +func PtrFloat64(v float64) *float64 { return &v } + +// PtrString is a helper routine that returns a pointer to given string value. +func PtrString(v string) *string { return &v } + +// PtrTime is helper routine that returns a pointer to given Time value. +func PtrTime(v time.Time) *time.Time { return &v } + +type NullableBool struct { + value *bool + isSet bool +} + +func (v NullableBool) Get() *bool { + return v.value +} + +func (v *NullableBool) Set(val *bool) { + v.value = val + v.isSet = true +} + +func (v NullableBool) IsSet() bool { + return v.isSet +} + +func (v *NullableBool) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBool(val *bool) *NullableBool { + return &NullableBool{value: val, isSet: true} +} + +func (v NullableBool) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBool) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt struct { + value *int + isSet bool +} + +func (v NullableInt) Get() *int { + return v.value +} + +func (v *NullableInt) Set(val *int) { + v.value = val + v.isSet = true +} + +func (v NullableInt) IsSet() bool { + return v.isSet +} + +func (v *NullableInt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt(val *int) *NullableInt { + return &NullableInt{value: val, isSet: true} +} + +func (v NullableInt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt32 struct { + value *int32 + isSet bool +} + +func (v NullableInt32) Get() *int32 { + return v.value +} + +func (v *NullableInt32) Set(val *int32) { + v.value = val + v.isSet = true +} + +func (v NullableInt32) IsSet() bool { + return v.isSet +} + +func (v *NullableInt32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt32(val *int32) *NullableInt32 { + return &NullableInt32{value: val, isSet: true} +} + +func (v NullableInt32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt64 struct { + value *int64 + isSet bool +} + +func (v NullableInt64) Get() *int64 { + return v.value +} + +func (v *NullableInt64) Set(val *int64) { + v.value = val + v.isSet = true +} + +func (v NullableInt64) IsSet() bool { + return v.isSet +} + +func (v *NullableInt64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt64(val *int64) *NullableInt64 { + return &NullableInt64{value: val, isSet: true} +} + +func (v NullableInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat32 struct { + value *float32 + isSet bool +} + +func (v NullableFloat32) Get() *float32 { + return v.value +} + +func (v *NullableFloat32) Set(val *float32) { + v.value = val + v.isSet = true +} + +func (v NullableFloat32) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat32(val *float32) *NullableFloat32 { + return &NullableFloat32{value: val, isSet: true} +} + +func (v NullableFloat32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat64 struct { + value *float64 + isSet bool +} + +func (v NullableFloat64) Get() *float64 { + return v.value +} + +func (v *NullableFloat64) Set(val *float64) { + v.value = val + v.isSet = true +} + +func (v NullableFloat64) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat64(val *float64) *NullableFloat64 { + return &NullableFloat64{value: val, isSet: true} +} + +func (v NullableFloat64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableString struct { + value *string + isSet bool +} + +func (v NullableString) Get() *string { + return v.value +} + +func (v *NullableString) Set(val *string) { + v.value = val + v.isSet = true +} + +func (v NullableString) IsSet() bool { + return v.isSet +} + +func (v *NullableString) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableString(val *string) *NullableString { + return &NullableString{value: val, isSet: true} +} + +func (v NullableString) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableString) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableTime struct { + value *time.Time + isSet bool +} + +func (v NullableTime) Get() *time.Time { + return v.value +} + +func (v *NullableTime) Set(val *time.Time) { + v.value = val + v.isSet = true +} + +func (v NullableTime) IsSet() bool { + return v.isSet +} + +func (v *NullableTime) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTime(val *time.Time) *NullableTime { + return &NullableTime{value: val, isSet: true} +} + +func (v NullableTime) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTime) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +// IsNil checks if an input is nil +func IsNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return reflect.ValueOf(i).IsNil() + case reflect.Array: + return reflect.ValueOf(i).IsZero() + } + return false +} + +type MappedNullable interface { + ToMap() (map[string]interface{}, error) +} + +// A wrapper for strict JSON decoding +func newStrictDecoder(data []byte) *json.Decoder { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.DisallowUnknownFields() + return dec +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} diff --git a/pkg/registrationhelpers/helpers.go b/pkg/registrationhelpers/helpers.go new file mode 100644 index 0000000..49ed014 --- /dev/null +++ b/pkg/registrationhelpers/helpers.go @@ -0,0 +1,535 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registrationhelpers + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strings" + "testing" + "time" + + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/clock" + "github.com/ory/x/configx" + "github.com/ory/x/httpx" + "github.com/ory/x/ioutilx" +) + +func setupServer(t *testing.T, reg *driver.RegistryDefault) *httptest.Server { + publicTS, _ := testhelpers.NewKratosServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + conf := reg.Config() + conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/default-return-to") //nolint:staticcheck + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") //nolint:staticcheck + return publicTS +} + +func ExpectValidationError(ctx context.Context, t *testing.T, ts *httptest.Server, conf *config.Config, flow string, values func(url.Values)) string { + isSPA := flow == "spa" + isAPI := flow == "api" + return testhelpers.SubmitRegistrationFormCtx(ctx, t, isAPI, nil, ts, values, + isSPA, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, ts.URL+registration.RouteSubmitFlow, conf.SelfServiceFlowRegistrationUI(ctx).String())) +} + +func CheckFormContent(t *testing.T, body []byte, requiredFields ...string) { + FieldNameSet(t, body, requiredFields) + OutdatedFieldsDoNotExist(t, body) + FormMethodIsPOST(t, body) +} + +// FieldNameSet checks if the fields have the right "name" set. +func FieldNameSet(t *testing.T, body []byte, fields []string) { + for _, f := range fields { + assert.Equal(t, f, gjson.GetBytes(body, fmt.Sprintf("ui.nodes.#(attributes.name==%s).attributes.name", f)).String(), "%s", body) + } +} + +// checks if some keys are not set, this should be used to catch regression issues +func OutdatedFieldsDoNotExist(t *testing.T, body []byte) { + for _, k := range []string{"request"} { + assert.Equal(t, false, gjson.GetBytes(body, fmt.Sprintf("ui.nodes.fields.#(name==%s)", k)).Exists()) + } +} + +func FormMethodIsPOST(t *testing.T, body []byte) { + assert.Equal(t, "POST", gjson.GetBytes(body, "ui.method").String()) +} + +//go:embed stub/basic.schema.json +var basicSchema []byte + +//go:embed stub/multifield.schema.json +var multifieldSchema []byte + +var skipIfNotEnabled = func(t *testing.T, flows []string, flow string) { + if !slices.Contains(flows, flow) { + t.Skipf("Skipping for %s flow because it was not included in the list of flows to be executed.", flow) + } +} + +func AssertSchemaDoesNotExist(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(v url.Values)) { + conf := reg.Config() + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + publicTS := setupServer(t, reg) + apiClient := testhelpers.NewDebugClient(t) + errTS := testhelpers.NewErrorTestServer(t, reg) + + reset := func() { + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) + } + reset() + + t.Run("case=should fail because schema does not exist", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.Equal(t, int64(http.StatusInternalServerError), gjson.Get(actual, "code").Int(), "%s", actual) + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "reason").String(), "no such file or directory", "%s", actual) + } + + values := url.Values{ + "traits.username": {testhelpers.RandomEmail()}, + "traits.foobar": {"bar"}, + "csrf_token": {nosurfx.FakeCSRFToken}, + } + payload(values) + + t.Run("type=api", func(t *testing.T) { + skipIfNotEnabled(t, flows, "api") + f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") + t.Cleanup(reset) + + body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, apiClient, values.Encode()) + assert.Contains(t, res.Request.URL.String(), publicTS.URL) + check(t, gjson.Get(body, "error").Raw) + }) + + t.Run("type=spa", func(t *testing.T) { + skipIfNotEnabled(t, flows, "spa") + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") + t.Cleanup(reset) + + body, res := testhelpers.RegistrationMakeRequest(t, false, true, f, apiClient, values.Encode()) + assert.Contains(t, res.Request.URL.String(), publicTS.URL) + check(t, gjson.Get(body, "error").Raw) + }) + + t.Run("type=browser", func(t *testing.T) { + skipIfNotEnabled(t, flows, "browser") + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") + t.Cleanup(reset) + + body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, apiClient, values.Encode()) + assert.Contains(t, res.Request.URL.String(), errTS.URL) + check(t, body) + }) + }) +} + +func AssertCSRFFailures(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(v url.Values)) { + conf := reg.Config() + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, multifieldSchema) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + publicTS := setupServer(t, reg) + apiClient := testhelpers.NewDebugClient(t) + _ = testhelpers.NewErrorTestServer(t, reg) + + values := url.Values{ + "csrf_token": {"invalid_token"}, + "traits.username": {testhelpers.RandomEmail()}, + "traits.foobar": {"bar"}, + } + + payload(values) + + t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) { + skipIfNotEnabled(t, flows, "browser") + + browserClient := testhelpers.NewClientWithCookies(t) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) + + actual, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), + json.RawMessage(actual), "%s", actual) + }) + + t.Run("case=should fail because of missing CSRF token/type=spa", func(t *testing.T) { + skipIfNotEnabled(t, flows, "spa") + + browserClient := testhelpers.NewClientWithCookies(t) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) + + actual, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, values.Encode()) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), + json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual) + }) + + t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) { + skipIfNotEnabled(t, flows, "api") + + f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) + + actual, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, gjson.Get(actual, "identity.id").Raw, "%s", actual) // registration successful + }) + + t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) { + skipIfNotEnabled(t, flows, "api") + + 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.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) + c := f.Ui + + req := testhelpers.NewPostRequest(t, true, c.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + actual := string(ioutilx.MustReadAll(res.Body)) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, actual, tc.exp) + }) + } + }) +} + +func AssertRegistrationRespectsValidation(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(url.Values)) { + conf := reg.Config() + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, multifieldSchema) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + publicTS := setupServer(t, reg) + + t.Run("case=should return an error because not passing validation", func(t *testing.T) { + email := testhelpers.RandomEmail() + 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+registration.RouteSubmitFlow, "%s", actual) + CheckFormContent(t, []byte(actual), "password", "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) + } + + values := func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + payload(v) + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + check(t, ExpectValidationError(t.Context(), t, publicTS, conf, f, values)) + }) + } + }) +} + +func AssertCommonErrorCases(t *testing.T, flows []string) { + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), + configx.WithValues(testhelpers.DefaultRawIdentitySchemaConfig(basicSchema)), + ) + uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + publicTS := setupServer(t, reg) + apiClient := testhelpers.NewDebugClient(t) + errTS := testhelpers.NewErrorTestServer(t, reg) + + 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.Context(), t, reg). + Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) + }) + + t.Run("type=api", func(t *testing.T) { + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t.Context(), t, 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) + defer func() { _ = res.Body.Close() }() + assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(ioutilx.MustReadAll(res.Body), "error").Raw)) + }) + }) + + 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.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) + body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.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=spa", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, apiClient, publicTS, true, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.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.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-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) + assert.Equal(t, "email", gjson.Get(body, "ui.nodes.#(attributes.name==\"traits.email\").attributes.type").String(), "%s", body) + }) + }) + 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.Context(), t, reg). + Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) + }) + + t.Run("type=api", func(t *testing.T) { + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t.Context(), t, 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) + defer func() { _ = res.Body.Close() }() + assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(ioutilx.MustReadAll(res.Body), "error").Raw)) + }) + }) + + 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.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) + body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.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=spa", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, apiClient, publicTS, true, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.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.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-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) + assert.Equal(t, "email", gjson.Get(body, "ui.nodes.#(attributes.name==\"traits.email\").attributes.type").String(), "%s", body) + }) + }) + + t.Run("case=should show the error ui because the method is missing in payload", func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) + body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "{}}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, true, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, "{}}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "foo=bar") + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) + }) + }) + + t.Run("case=should show the error ui because the request id is missing", 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.RegistrationFlow{Ui: kratos.UiContainer{ + Action: publicTS.URL + registration.RouteSubmitFlow + "?flow=" + x.NewUUID().String(), + }} + + t.Run("type=api", func(t *testing.T) { + actual, res := testhelpers.RegistrationMakeRequest(t, true, false, fakeFlow, apiClient, "{}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + check(t, gjson.Get(actual, "error").Raw) + }) + + t.Run("type=api", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + actual, res := testhelpers.RegistrationMakeRequest(t, false, true, fakeFlow, browserClient, "{}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + check(t, gjson.Get(actual, "error").Raw) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + actual, res := testhelpers.RegistrationMakeRequest(t, false, false, fakeFlow, browserClient, "") + assert.Contains(t, res.Request.URL.String(), errTS.URL) + check(t, actual) + }) + }) + + t.Run("case=should return an error because the request is expired", func(t *testing.T) { + lifespan := 500 * time.Millisecond + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationRequestLifespan, lifespan) + t.Cleanup(func() { + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationRequestLifespan, "10m") + }) + + // Freeze the clock so flow expiry is deterministic and the "expired N minutes ago" reason does not depend on + // the request round-trip. Each subtest creates a flow, then advances the clock past its lifespan instead of + // sleeping, so the handler and the expected error read the same instant. + mock := clock.NewMock(time.Now().UTC()) + reg.SetClock(mock) + t.Cleanup(func() { reg.SetClock(clock.New()) }) + + t.Run("type=api", func(t *testing.T) { + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.RegistrationMakeRequestCtx(t.Context(), t, true, false, f, apiClient, "{}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expiresAt), json.RawMessage(actual), []string{"use_flow_id", "expired_at", "since"}, "") + }) + + t.Run("type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, true, false, false) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, "{}") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expiresAt), json.RawMessage(actual), []string{"use_flow_id", "expired_at", "since"}, "expired", "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "") + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-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("case=should fail because the password was used in databreaches", func(t *testing.T) { + // This subtest needs the multifield schema, whereas the rest of the + // suite uses the basic schema. Swapping the shared config's default + // schema and restoring it in a cleanup would let that mutation race + // in-flight requests of sibling subtests reading the same config. It + // therefore owns its own registry, config, and server, preconfigured + // with the multifield schema. See .claude/rules/go.md: "subtests that + // need parallelization must own their resources". + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), + configx.WithValues(testhelpers.DefaultRawIdentitySchemaConfig(multifieldSchema)), + ) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + publicTS := setupServer(t, reg) + + email := testhelpers.RandomEmail() + 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+registration.RouteSubmitFlow, "%s", actual) + CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0").String(), "data breaches and must no longer be used.", "%s", actual) + + // but the method is still set + assert.Equal(t, "password", gjson.Get(actual, "ui.nodes.#(attributes.name==method).attributes.value").String(), "%s", actual) + } + + values := func(v url.Values) { + v.Set("traits.username", email) + v.Set("password", "password") + v.Set("traits.foobar", "bar") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + check(t, ExpectValidationError(t.Context(), t, publicTS, conf, f, values)) + }) + } + }) +} diff --git a/pkg/registrationhelpers/stub/basic.schema.json b/pkg/registrationhelpers/stub/basic.schema.json new file mode 100644 index 0000000..829c04c --- /dev/null +++ b/pkg/registrationhelpers/stub/basic.schema.json @@ -0,0 +1,28 @@ +{ + "$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": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/pkg/registrationhelpers/stub/multifield.schema.json b/pkg/registrationhelpers/stub/multifield.schema.json new file mode 100644 index 0000000..2f355fb --- /dev/null +++ b/pkg/registrationhelpers/stub/multifield.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 + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": [ + "foobar", + "username" + ] + } + }, + "additionalProperties": false +} diff --git a/pkg/settingshelpers/helpers.go b/pkg/settingshelpers/helpers.go new file mode 100644 index 0000000..e272a94 --- /dev/null +++ b/pkg/settingshelpers/helpers.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settingshelpers + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/settings" +) + +func ExpectValidationError(t *testing.T, ts *httptest.Server, hc *http.Client, conf *config.Config, flow string, values func(url.Values)) string { + isSPA := flow == "spa" + isAPI := flow == "api" + ctx := context.Background() + return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, ts, values, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, ts.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) +} diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go new file mode 100644 index 0000000..5030fb8 --- /dev/null +++ b/pkg/testhelpers/config.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/contextx" + "github.com/ory/x/randx" +) + +func DefaultIdentitySchemaConfig(url string) map[string]any { + return map[string]any{ + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: url}, + }, + } +} + +func DefaultRawIdentitySchemaConfig(schema []byte) map[string]any { + return DefaultIdentitySchemaConfig("base64://" + base64.URLEncoding.EncodeToString(schema)) +} + +func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { + return contextx.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") + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: url}, + }) + return func() { + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: schemaUrl.String()}, + }) + } +} + +// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID +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 contextx.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, + })) + t.Cleanup(func() { + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, schemas) + }) + return id +} + +// 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{ + {ID: "default", URL: "base64://" + base64.URLEncoding.EncodeToString(schema)}, + }) +} diff --git a/pkg/testhelpers/courier.go b/pkg/testhelpers/courier.go new file mode 100644 index 0000000..02c0a6c --- /dev/null +++ b/pkg/testhelpers/courier.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "regexp" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" +) + +func CourierExpectMessage(ctx context.Context, t *testing.T, reg interface { + courier.PersistenceProvider +}, recipient, subject string, +) *courier.Message { + t.Helper() + messages, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{ + Recipient: recipient, + }, []keysetpagination.Option{}) + require.NoError(t, err) + + sort.Slice(messages, func(i, j int) bool { + return messages[i].CreatedAt.After(messages[j].CreatedAt) + }) + + for _, m := range messages { + if strings.EqualFold(m.Recipient, recipient) && (strings.Contains(m.Subject, subject) || strings.Contains(m.Body, subject)) { + return &m + } + } + + require.Failf(t, "could not find courier messages", "could not find courier messages with recipient %s and subject %s", recipient, subject) + return nil +} + +func CourierExpectLinkInMessage(t *testing.T, message *courier.Message, offset int) string { + if offset == 0 { + offset = 1 + } + match := regexp.MustCompile(`(http[^\s]+)`).FindStringSubmatch(message.Body) + require.Len(t, match, offset*2) + + return match[offset] +} + +func CourierExpectCodeInMessage(t *testing.T, message *courier.Message, offset int) string { + if offset == 0 { + offset = 1 + } + match := regexp.MustCompile(CodeRegex).FindStringSubmatch(message.Body) + require.Len(t, match, offset*2) + + return match[offset] +} diff --git a/pkg/testhelpers/e2e_server.go b/pkg/testhelpers/e2e_server.go new file mode 100644 index 0000000..52217a8 --- /dev/null +++ b/pkg/testhelpers/e2e_server.go @@ -0,0 +1,225 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/driver" + "github.com/ory/x/jsonnetsecure" + + "golang.org/x/sync/errgroup" + + "github.com/ory/x/tlsx" + + "github.com/phayes/freeport" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cmd" + "github.com/ory/kratos/x" + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" +) + +type ConfigOptions = map[string]interface{} + +func StartE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptions ConfigOptions) (publicPort, adminPort int) { + return startE2EServerOnly(t, configFile, isTLS, configOptions, 0) +} + +func startE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptions ConfigOptions, tries int) (publicPort, adminPort int) { + adminPort, err := freeport.GetFreePort() + require.NoError(t, err) + + publicPort, err = freeport.GetFreePort() + require.NoError(t, err) + + publicUrl := fmt.Sprintf("http://127.0.0.1:%d", publicPort) + adminUrl := fmt.Sprintf("http://127.0.0.1:%d", adminPort) + + if isTLS { + publicUrl = fmt.Sprintf("https://127.0.0.1:%d", publicPort) + adminUrl = fmt.Sprintf("https://127.0.0.1:%d", adminPort) + } + + dsn := "sqlite://" + filepath.Join(t.TempDir(), "db.sqlite") + "?_fk=true&mode=rwc" + + ctx := t.Context() + defaultConfig := map[string]any{ + "dsn": dsn, + "dev": true, + "log.level": "error", + "log.leak_sensitive_values": true, + "serve.public.port": publicPort, + "serve.admin.port": adminPort, + "serve.public.base_url": publicUrl, + "serve.admin.base_url": adminUrl, + } + + jsonnetPool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) + t.Cleanup(jsonnetPool.Close) + + //nolint:staticcheck + //lint:ignore SA1029 we really want this + ctx, cancel := context.WithCancel(ctx) + executor := &cmdx.CommandExecuter{ + New: func() *cobra.Command { + return cmd.NewRootCmd(driver.WithJsonnetPool(jsonnetPool), driver.WithConfigOptions(configx.WithValues(defaultConfig), configx.WithValues(configOptions))) + }, + Ctx: ctx, + } + + t.Log("Starting migrations...") + _ = executor.ExecNoErr(t, "migrate", "sql", dsn, "--yes") + t.Logf("Migration done") + + t.Log("Starting server...") + stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} + eg := executor.ExecBackground(nil, io.MultiWriter(os.Stdout, stdOut), io.MultiWriter(os.Stdout, stdErr), "serve", "--config", configFile, "--watch-courier") + + err = waitTimeout(t, eg, time.Second) + if err != nil && tries < 5 { + if !errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "address already in use") || strings.Contains(stdErr.String(), "address already in use") { + t.Logf("Detected an instance with port reuse, retrying #%d...", tries) + time.Sleep(time.Millisecond * 500) + cancel() + return startE2EServerOnly(t, configFile, isTLS, configOptions, tries+1) + } + } + require.NoError(t, err) + + t.Cleanup(cancel) + return publicPort, adminPort +} + +// waitTimeout waits for the waitgroup for the specified max timeout. +// Returns true if waiting timed out. +func waitTimeout(t *testing.T, wg *errgroup.Group, timeout time.Duration) (err error) { + c := make(chan struct{}) + go func() { + defer close(c) + err = wg.Wait() + }() + select { + case <-c: + return + case <-time.After(timeout): + return + } +} + +func StartE2EServer(t *testing.T, configFile string, configOptions ConfigOptions) (publicUrl, adminUrl string) { + publicPort, adminPort := StartE2EServerOnly(t, configFile, false, configOptions) + return CheckE2EServerOnHTTP(t, publicPort, adminPort) +} + +func CheckE2EServerOnHTTP(t *testing.T, publicPort, adminPort int) (publicUrl, adminUrl string) { + publicUrl = fmt.Sprintf("http://127.0.0.1:%d", publicPort) + adminUrl = fmt.Sprintf("http://127.0.0.1:%d", adminPort) + waitToComeAlive(t, publicUrl, adminUrl) + return +} + +func waitToComeAlive(t *testing.T, publicUrl, adminUrl string) { + require.EventuallyWithT(t, func(t *assert.CollectT) { + //#nosec G402 -- TLS InsecureSkipVerify set true + tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client := &http.Client{Transport: tr} + + for _, url := range []string{ + publicUrl + "/health/ready", + adminUrl + "/health/ready", + publicUrl + "/health/alive", + adminUrl + "/health/alive", + } { + res, err := client.Get(url) + require.NoError(t, err) + + body := x.MustReadAll(res.Body) + _ = res.Body.Close() + + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + } + }, 10*time.Second, time.Second) +} + +func CheckE2EServerOnHTTPS(t *testing.T, publicPort, adminPort int) (publicUrl, adminUrl string) { + publicUrl = fmt.Sprintf("https://127.0.0.1:%d", publicPort) + adminUrl = fmt.Sprintf("https://127.0.0.1:%d", adminPort) + waitToComeAlive(t, publicUrl, adminUrl) + return +} + +// GenerateTLSCertificateFilesForTests writes a new, self-signed TLS +// certificate+key (in PEM format) to a temporary location on disk and returns +// the paths to both, as well as the respective contents in base64 encoding. The +// files are automatically cleaned up when the given *testing.T concludes its +// tests. +func GenerateTLSCertificateFilesForTests(t *testing.T) (certPath, keyPath, certBase64, keyBase64 string) { + tmpDir := t.TempDir() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + cert, err := tlsx.CreateSelfSignedCertificate(privateKey) + require.NoError(t, err) + + // write cert + certFile, err := os.CreateTemp(tmpDir, "test-*-cert.pem") + require.NoError(t, err, "Failed to create temp file for certificate: %v", err) + certPath = certFile.Name() + + var buf bytes.Buffer + enc := base64.NewEncoder(base64.StdEncoding, &buf) + 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() + t.Log("wrote", certPath) + + // write key + keyFile, err := os.CreateTemp(tmpDir, "test-*-key.pem") + require.NoError(t, err, "Failed to create temp file for key: %v", err) + keyPath = keyFile.Name() + buf.Reset() + enc = base64.NewEncoder(base64.StdEncoding, &buf) + keyOut := io.MultiWriter(enc, keyFile) + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err, "Failed to marshal private key: %v", err) + + 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() + t.Log("wrote", keyPath) + return +} diff --git a/pkg/testhelpers/errorx.go b/pkg/testhelpers/errorx.go new file mode 100644 index 0000000..17bd184 --- /dev/null +++ b/pkg/testhelpers/errorx.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +func NewErrorTestServer(t *testing.T, reg interface { + errorx.PersistenceProvider + config.Provider +}, +) *httptest.Server { + logger := logrusx.New("", "", logrusx.ForceLevel(logrus.TraceLevel)) + writer := herodot.NewJSONWriter(logger) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e, err := reg.SelfServiceErrorPersister().ReadErrorContainer(r.Context(), x.ParseUUID(r.URL.Query().Get("id"))) + require.NoError(t, err) + t.Logf("landed on error UI with: %s", e.Errors) + writer.Write(w, r, e.Errors) + })) + t.Cleanup(ts.Close) + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceErrorUI, ts.URL) + return ts +} + +func NewRedirTS(t *testing.T, body string, conf *config.Config) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if len(body) == 0 { + w.WriteHeader(http.StatusNoContent) + return + } + _, _ = w.Write([]byte(body)) + })) + t.Cleanup(ts.Close) + conf.MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL) + return ts +} + +func NewRedirSessionEchoTS(t *testing.T, reg interface { + httpx.WriterProvider + session.ManagementProvider + config.Provider +}, +) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // verify that the client has a session, and echo it back + sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + require.NoError(t, err, "Headers: %+v", r.Header) + reg.Writer().Write(w, r, sess) + })) + t.Cleanup(ts.Close) + reg.Config().MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/return-ts") + return ts +} + +func NewRedirNoSessionTS(t *testing.T, reg interface { + httpx.WriterProvider + session.ManagementProvider + config.Provider +}, +) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // verify that the client DOES NOT have a session + _, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + require.Error(t, err, "Headers: %+v", r.Header) + reg.Writer().Write(w, r, nil) + })) + t.Cleanup(ts.Close) + reg.Config().MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/return-ts") + return ts +} diff --git a/pkg/testhelpers/fake.go b/pkg/testhelpers/fake.go new file mode 100644 index 0000000..f95226b --- /dev/null +++ b/pkg/testhelpers/fake.go @@ -0,0 +1,18 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "strings" + + "github.com/ory/x/randx" +) + +func RandomEmail() string { + return strings.ToLower(randx.MustString(16, randx.Alpha) + "@ory.sh") +} + +func RandomPhone() string { + return "+49151" + randx.MustString(8, randx.Numeric) +} diff --git a/pkg/testhelpers/handler_mock.go b/pkg/testhelpers/handler_mock.go new file mode 100644 index 0000000..916c640 --- /dev/null +++ b/pkg/testhelpers/handler_mock.go @@ -0,0 +1,233 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/httprouterx" +) + +type mockDeps interface { + identity.PrivilegedPoolProvider + identity.ManagementProvider + session.ManagementProvider + session.PersistenceProvider + config.Provider +} + +func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + 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(t.Context(), i)) + + MockSetSessionWithIdentity(t, reg, conf, i)(w, r) + } +} + +func MockSetSessionWithIdentity(t *testing.T, reg mockDeps, _ *config.Config, i *identity.Identity) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + 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) + } + require.NoError(t, reg.SessionManager().UpsertAndIssueCookie(t.Context(), w, r, activeSession)) + + w.WriteHeader(http.StatusOK) + } +} + +func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request) ([]byte, *http.Response) { + return MockMakeAuthenticatedRequestWithClient(t, reg, conf, router, req, NewClientWithCookies(t)) +} + +func MockMakeAuthenticatedRequestWithClient(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request, client *http.Client) ([]byte, *http.Response) { + return MockMakeAuthenticatedRequestWithClientAndID(t, reg, conf, router, req, client, nil) +} + +func MockMakeAuthenticatedRequestWithClientAndID(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request, client *http.Client, id *identity.Identity) ([]byte, *http.Response) { + set := "/" + uuid.Must(uuid.NewV4()).String() + "/set" + if id == nil { + router.GET(set, MockSetSession(t, reg, conf)) + } else { + router.GET(set, MockSetSessionWithIdentity(t, reg, conf, id)) + } + + MockHydrateCookieClient(t, client, "http://"+req.URL.Host+set+"?"+req.URL.Query().Encode()) + + res, err := client.Do(req) + require.NoError(t, errors.WithStack(err)) + + body, err := io.ReadAll(res.Body) + require.NoError(t, errors.WithStack(err)) + + require.NoError(t, res.Body.Close()) + + return body, res +} + +// NewTestTransport returns a transport dedicated to a single test client. +// +// Test helpers spin up short-lived httptest.Server instances and close them +// when the helper returns. httptest.Server.Close unconditionally calls +// http.DefaultTransport.CloseIdleConnections as a courtesy for users of the +// default transport. When many parallel subtests share http.DefaultTransport +// (the zero value of http.Client.Transport), one subtest closing its server +// tears down idle connections that another subtest's in-flight request is +// using, surfacing as "http: CloseIdleConnections called". Giving every test +// client its own transport means that courtesy close only ever affects the +// unused global transport, never a live test client. +func NewTestTransport(t testing.TB) *http.Transport { + transport := http.DefaultTransport.(*http.Transport).Clone() + t.Cleanup(transport.CloseIdleConnections) + return transport +} + +// NewTestClient returns a jar-less client with a dedicated transport, see NewTestTransport. +func NewTestClient(t testing.TB) *http.Client { + return &http.Client{Transport: NewTestTransport(t)} +} + +func NewClientWithCookies(t *testing.T) *http.Client { + cj, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + return &http.Client{Jar: cj, Transport: NewTestTransport(t)} +} + +func NewNoRedirectClientWithCookies(t *testing.T) *http.Client { + cj, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + return &http.Client{ + Jar: cj, + Transport: NewTestTransport(t), + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) *http.Cookie { + var sessionCookie *http.Cookie + res, err := c.Get(u) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := x.MustReadAll(res.Body) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + var found bool + for _, rc := range res.Cookies() { + if rc.Name == config.DefaultSessionCookieName { + found = true + sessionCookie = rc + } + } + require.True(t, found, "got body: %s\ngot url: %s", body, res.Request.URL.String()) + return sessionCookie +} + +func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (http.HandlerFunc, *session.Session) { + var ct []identity.CredentialsType + + // if identity was not created with any credentials, + // then assume a 'password' credential type + if len(i.Credentials) == 0 { + return MockSessionCreateHandlerWithIdentityAndAMR(t, reg, i, []identity.CredentialsType{"password"}) + } + + // otherwise, mock session with appropriate credential types + for _, c := range i.Credentials { + ct = append(ct, c.Type) + } + + return MockSessionCreateHandlerWithIdentityAndAMR(t, reg, i, ct) +} + +// mockMethodAAL maps a credential type to the AAL a successful login +// through that method contributes. Mirrors the defaults the real +// strategies pass to CompletedLoginFor, so tests that only specify the +// method name get a realistic session AAL. +func mockMethodAAL(m identity.CredentialsType) identity.AuthenticatorAssuranceLevel { + switch m { + case identity.CredentialsTypeTOTP, + identity.CredentialsTypeWebAuthn, + identity.CredentialsTypeLookup: + return identity.AuthenticatorAssuranceLevel2 + default: + return identity.AuthenticatorAssuranceLevel1 + } +} + +func MockSessionCreateHandlerWithIdentityAndAMR(t *testing.T, reg mockDeps, i *identity.Identity, methods []identity.CredentialsType) (http.HandlerFunc, *session.Session) { + var sess session.Session + require.NoError(t, faker.FakeData(&sess)) + // require AuthenticatedAt to be time.Now() as we always compare it to the current time + sess.AuthenticatedAt = time.Now().UTC() + sess.IssuedAt = time.Now().UTC() + sess.ExpiresAt = time.Now().UTC().Add(time.Hour * 24) + sess.Active = true + + for _, m := range methods { + if m == identity.CredentialsTypeOIDC { + if c, ok := i.Credentials[m]; ok { + var target identity.CredentialsOIDC + if err := json.Unmarshal(c.Config, &target); err == nil { + for _, t := range target.Providers { + sess.CompletedLoginForWithProvider(c.Type, identity.AuthenticatorAssuranceLevel1, t.Provider, "") + } + continue + } + } + } + sess.CompletedLoginFor(m, mockMethodAAL(m)) + } + + sess.SetAuthenticatorAssuranceLevel() + + if _, err := reg.Config().DefaultIdentityTraitsSchemaURL(t.Context()); err != nil { + SetDefaultIdentitySchema(reg.Config(), "file://./stub/fake-session.schema.json") + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + + inserted, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), i.ID) + require.NoError(t, err) + sess.Identity = inserted + + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &sess)) + require.Len(t, inserted.Credentials, len(i.Credentials)) + + return func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().IssueCookie(t.Context(), w, r, &sess)) + }, &sess +} + +func MockSessionCreateHandler(t *testing.T, reg mockDeps) (http.HandlerFunc, *session.Session) { + return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{ + ID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`), + }) +} diff --git a/pkg/testhelpers/http.go b/pkg/testhelpers/http.go new file mode 100644 index 0000000..a07c5fb --- /dev/null +++ b/pkg/testhelpers/http.go @@ -0,0 +1,166 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func NewDebugClient(t *testing.T) *http.Client { + return &http.Client{Transport: NewTransportWithLogger(NewTestTransport(t), t)} +} + +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, + Transport: NewTestTransport(t), + 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 errors.New("stopped after 20 redirects") + } + return nil + } +} + +func NewPostRequest(t *testing.T, isAPI bool, url string, payload io.Reader) *http.Request { + req, err := http.NewRequest("POST", url, payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "text/html") + if isAPI { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + } + + return req +} + +func NewHTTPGetJSONRequest(t *testing.T, url string) *http.Request { + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + + // req.Header.Set("Content-Type", "application/json;charset=utf-8") + req.Header.Set("Accept", "application/json") + return req +} + +func NewHTTPGetAJAXRequest(t *testing.T, url string) *http.Request { + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + + req.Header.Set("Accept", "application/json") + return req +} + +func NewHTTPDeleteJSONRequest(t *testing.T, url string, in interface{}) *http.Request { + var body bytes.Buffer + require.NoError(t, json.NewEncoder(&body).Encode(in)) + req, err := http.NewRequest("DELETE", url, &body) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + return req +} + +func HTTPRequestJSON(t *testing.T, client *http.Client, method string, url string, in interface{}) ([]byte, *http.Response) { + var body bytes.Buffer + require.NoError(t, json.NewEncoder(&body).Encode(in)) + + req, err := http.NewRequest(method, url, &body) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + payload, err := io.ReadAll(res.Body) + require.NoError(t, err) + + return payload, res +} + +func HTTPPostForm(t *testing.T, client *http.Client, remote string, in *url.Values) ([]byte, *http.Response) { + if in == nil { + in = new(url.Values) + } + + res, err := client.PostForm(remote, *in) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + payload, err := io.ReadAll(res.Body) + require.NoError(t, err) + + return payload, res +} + +func NewTestHTTPRequest(t *testing.T, method, url string, body io.Reader) *http.Request { + req, err := http.NewRequest(method, url, body) + require.NoError(t, err) + return req +} + +func EasyGet(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { + res, err := c.Get(url) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body +} + +func EasyGetJSON(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body +} + +func EasyGetBody(t *testing.T, c *http.Client, url string) []byte { + _, body := EasyGet(t, c, url) // nolint: bodyclose + return body +} + +func EasyCookieJar(t *testing.T, o *cookiejar.Options) *cookiejar.Jar { + cj, err := cookiejar.New(o) + require.NoError(t, err) + return cj +} diff --git a/pkg/testhelpers/httptest.go b/pkg/testhelpers/httptest.go new file mode 100644 index 0000000..ce6a696 --- /dev/null +++ b/pkg/testhelpers/httptest.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func NewHTTPTestServer(t *testing.T, h http.Handler) *httptest.Server { + ts := httptest.NewServer(h) + t.Cleanup(ts.Close) + return ts +} diff --git a/pkg/testhelpers/identity.go b/pkg/testhelpers/identity.go new file mode 100644 index 0000000..6bbc7c5 --- /dev/null +++ b/pkg/testhelpers/identity.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "testing" + "time" + + "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/session" +) + +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 := 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/pkg/testhelpers/identity_schema.go b/pkg/testhelpers/identity_schema.go new file mode 100644 index 0000000..193fed8 --- /dev/null +++ b/pkg/testhelpers/identity_schema.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "github.com/ory/kratos/driver/config" +) + +func IdentitySchemasConfig(schemas map[string]string) map[string]any { + var s []config.Schema + for id, location := range schemas { + s = append(s, config.Schema{ID: id, URL: location}) + } + return map[string]any{config.ViperKeyIdentitySchemas: s} +} diff --git a/pkg/testhelpers/json.go b/pkg/testhelpers/json.go new file mode 100644 index 0000000..f9af044 --- /dev/null +++ b/pkg/testhelpers/json.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func JSONEq(t *testing.T, expected, actual interface{}, messageAndArgs ...interface{}) { + var eb, ab bytes.Buffer + require.NoError(t, json.NewEncoder(&eb).Encode(expected)) + require.NoError(t, json.NewEncoder(&ab).Encode(actual)) + assert.JSONEq(t, eb.String(), ab.String(), messageAndArgs...) +} + +func LogJSON(t *testing.T, v interface{}) { + out, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + t.Logf("\n%s\n---", out) +} diff --git a/pkg/testhelpers/mailhog.go b/pkg/testhelpers/mailhog.go new file mode 100644 index 0000000..90ea460 --- /dev/null +++ b/pkg/testhelpers/mailhog.go @@ -0,0 +1,106 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "errors" + "fmt" + "io" + stdlog "log" + "net" + "net/http/httptest" + "testing" + + "github.com/gorilla/pat" + "github.com/ian-kent/go-log/levels" + "github.com/ian-kent/go-log/log" + "github.com/mailhog/MailHog-Server/api" + mailhogconf "github.com/mailhog/MailHog-Server/config" + "github.com/mailhog/MailHog-Server/monkey" + "github.com/mailhog/MailHog-Server/smtp" + "github.com/mailhog/data" + "github.com/stretchr/testify/require" + + xmailhog "github.com/ory/x/mailhog" +) + +// StartMailhog starts a MailHog server for testing purposes. +// It returns the SMTP connection URL and the API URL. +// If withChaosMonkey is true, the SMTP server will randomly drop connections and simulate network issues. +func StartMailhog(t testing.TB, withChaosMonkey bool) (smtpAddr, apiAddr string) { + t.Helper() + + // hacky but should silence most MailHog logs during tests + log.Logger().SetLevel(levels.FATAL) + stdlog.Default().SetOutput(io.Discard) + + apiconf := &mailhogconf.Config{ + Storage: xmailhog.NewInMemory(), + MessageChan: make(chan *data.Message), + } + if withChaosMonkey { + jim := &monkey.Jim{ + DisconnectChance: 0.005, + AcceptChance: 0.99, + LinkSpeedAffect: 0.05, + LinkSpeedMin: 1250, + LinkSpeedMax: 12500, + RejectAuthChance: 0.05, + + // important: set to 0 to avoid flakes in tests, because those errors are not retryable + RejectSenderChance: 0, + RejectRecipientChance: 0, + } + jim.Configure(t.Logf) + apiconf.Monkey = jim + } + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { _ = ln.Close() }) + go serveSMTP(t.Context(), ln, apiconf) + + r := pat.New() + api.CreateAPI(apiconf, r) + s := httptest.NewServer(r) + t.Cleanup(s.Close) + + return fmt.Sprintf("smtp://%s?disable_starttls=true", ln.Addr().String()), s.URL +} + +func serveSMTP(ctx context.Context, ln net.Listener, cfg *mailhogconf.Config) { + for { + select { + case <-ctx.Done(): + return + default: + conn, err := ln.Accept() + if errors.Is(err, net.ErrClosed) { + return + } + if err != nil { + fmt.Printf("[SMTP] Error accepting connection: %s\n", err) + continue + } + + if cfg.Monkey != nil { + ok := cfg.Monkey.Accept(conn) + if !ok { + _ = conn.Close() + continue + } + } + + go smtp.Accept( + conn.(*net.TCPConn).RemoteAddr().String(), + io.ReadWriteCloser(conn), + cfg.Storage, + cfg.MessageChan, + cfg.Hostname, + cfg.Monkey, + ) + } + } +} diff --git a/pkg/testhelpers/network.go b/pkg/testhelpers/network.go new file mode 100644 index 0000000..10978db --- /dev/null +++ b/pkg/testhelpers/network.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/persistence" + "github.com/ory/x/networkx" +) + +func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.Persister) (uuid.UUID, persistence.Persister) { + if n, ok := ctx.Value("network").(*networkx.Network); ok && n != nil { + return n.ID, p.WithNetworkID(n.ID) + } + + n := networkx.NewNetwork() + require.NoError(t, p.GetConnection(ctx).Create(n)) + return n.ID, p.WithNetworkID(n.ID) +} + +func NewNetwork(t testing.TB, ctx context.Context, p persistence.Persister) (uuid.UUID, persistence.Persister) { + SkipIfNetworkContext(t, ctx) + + n := networkx.NewNetwork() + require.NoError(t, p.GetConnection(context.Background()).Create(n)) + return n.ID, p.WithNetworkID(n.ID) +} + +func ExistingNetwork(t *testing.T, p persistence.Persister, id uuid.UUID) persistence.Persister { + require.NoError(t, p.GetConnection(context.Background()).Save(&networkx.Network{ID: id})) + return p.WithNetworkID(id) +} + +func SkipIfNetworkContext(t testing.TB, ctx context.Context) { + a, b := ctx.Value("network").(*networkx.Network) + if a != nil && b { + t.Skip("Network was set in context") + } +} diff --git a/pkg/testhelpers/sdk.go b/pkg/testhelpers/sdk.go new file mode 100644 index 0000000..9e3de61 --- /dev/null +++ b/pkg/testhelpers/sdk.go @@ -0,0 +1,111 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/ui/node" + + kratos "github.com/ory/kratos/pkg/httpclient" +) + +func NewSDKClient(ts *httptest.Server) *kratos.APIClient { + return NewSDKClientFromURL(ts.URL) +} + +func NewSDKCustomClient(ts *httptest.Server, client *http.Client) *kratos.APIClient { + conf := kratos.NewConfiguration() + conf.Servers = kratos.ServerConfigurations{{URL: ts.URL}} + conf.HTTPClient = client + return kratos.NewAPIClient(conf) +} + +func NewSDKClientFromURL(u string) *kratos.APIClient { + conf := kratos.NewConfiguration() + conf.Servers = kratos.ServerConfigurations{{URL: u}} + return kratos.NewAPIClient(conf) +} + +func SDKFormFieldsToURLValues(ff []kratos.UiNode) url.Values { + values := url.Values{} + for _, f := range ff { + attr := f.Attributes.UiNodeInputAttributes + if attr == nil { + continue + } + + val := attr.Value + if val == nil { + continue + } + + switch v := val.(type) { + case bool: + values.Set(attr.Name, fmt.Sprintf("%v", v)) + case string: + values.Set(attr.Name, fmt.Sprintf("%v", v)) + case float32: + values.Set(attr.Name, fmt.Sprintf("%v", v)) + case float64: + values.Set(attr.Name, fmt.Sprintf("%v", v)) + } + } + return values +} + +func NewFakeCSRFNode() *kratos.UiNode { + return &kratos.UiNode{ + Group: node.DefaultGroup.String(), + Type: "input", + Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{ + Name: "csrf_token", + Required: new(true), + Type: "hidden", + Value: nosurfx.FakeCSRFToken, + }), + } +} + +func NewSDKEmailNode(group string) *kratos.UiNode { + return &kratos.UiNode{ + Type: "input", + Group: group, + Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{ + Name: "email", + Type: "email", + Required: new(true), + Value: "email", + }), + } +} + +func NewMethodSubmit(group, value string) *kratos.UiNode { + return &kratos.UiNode{ + Type: "input", + Group: group, + Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{ + Name: "method", + Type: "submit", + Value: value, + }), + } +} + +func NewPasswordNode() *kratos.UiNode { + return &kratos.UiNode{ + Type: "input", + Group: node.PasswordGroup.String(), + Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{ + Name: "password", + Type: "password", + Required: new(true), + }), + } +} diff --git a/pkg/testhelpers/selfservice.go b/pkg/testhelpers/selfservice.go new file mode 100644 index 0000000..3c83ff9 --- /dev/null +++ b/pkg/testhelpers/selfservice.go @@ -0,0 +1,256 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + + "github.com/go-faker/faker/v4" + "github.com/gobuffalo/httptest" + "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/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" +) + +func TestSelfServicePreHook( + configKey string, + makeRequestPre func(t *testing.T, ts *httptest.Server) (*http.Response, string), + newServer func(t *testing.T) *httptest.Server, + conf *config.Config, +) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=err if hooks err", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "err","ExecuteRegistrationPreHook": "err","ExecuteSettingsPreHook": "err","ExecuteVerificationPreHook": "err","ExecuteRecoveryPreHook": "err"}`)}}) + + res, body := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode, "%s", body) + assert.EqualValues(t, "err", body) + }) + + t.Run("case=abort if hooks aborts", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "abort","ExecuteRegistrationPreHook": "abort","ExecuteSettingsPreHook": "abort","ExecuteVerificationPreHook": "abort","ExecuteRecoveryPreHook": "abort"}`)}}) + + res, body := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Empty(t, body) + }) + + t.Run("case=redirect", func(t *testing.T) { + t.Skipf("Skipped because pre-redirect is no longer supported") + + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "redirect", Config: []byte(`{"to": "https://www.ory.com/"}`)}}) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.com/", res.Request.URL.String()) + }) + } +} + +func SelfServiceHookCreateFakeIdentity(t *testing.T, reg driver.Registry) *identity.Identity { + i := SelfServiceHookFakeIdentity(t) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + return i +} + +func SelfServiceHookFakeIdentity(t *testing.T) *identity.Identity { + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + i.Traits = identity.Traits(`{}`) + i.State = identity.StateActive + i.NID = uuid.Must(uuid.NewV4()) + return &i +} + +func SelfServiceHookConfigReset(t *testing.T, conf *config.Config) func() { + ctx := context.Background() + return func() { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", nil) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", nil) + } +} + +func SelfServiceHookSettingsSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookSettingsSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookLoginSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookLoginSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookRegistrationSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value) +} + +func SelfServiceHookLoginViperSetPost(t *testing.T, conf *config.Config, strategy string, c []config.SelfServiceHook) { + ctx := context.Background() + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, strategy), c) +} + +func SelfServiceHookRegistrationViperSetPost(t *testing.T, conf *config.Config, strategy string, c []config.SelfServiceHook) { + ctx := context.Background() + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, strategy), c) +} + +func SelfServiceHookLoginErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return SelfServiceHookErrorHandler(t, w, r, login.ErrHookAbortFlow, err) +} + +func SelfServiceHookRegistrationErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return SelfServiceHookErrorHandler(t, w, r, registration.ErrHookAbortFlow, err) +} + +func SelfServiceHookSettingsErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return SelfServiceHookErrorHandler(t, w, r, settings.ErrHookAbortFlow, err) +} + +func SelfServiceHookErrorHandler(t *testing.T, w http.ResponseWriter, _ *http.Request, abortErr error, actualErr error) bool { + if actualErr != nil { + t.Logf("received error: %+v", actualErr) + if errors.Is(actualErr, abortErr) { + return false + } + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(actualErr.Error())) + return false + } + return true +} + +func SelfServiceMakeLoginPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/login/pre", false, url.Values{}) +} + +func SelfServiceMakeLoginPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/login/post", asAPI, query) +} + +func SelfServiceMakeRegistrationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/registration/pre", false, url.Values{}) +} + +func SelfServiceMakeSettingsPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/settings/pre", false, url.Values{}) +} + +func SelfServiceMakeRecoveryPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/recovery/pre", false, url.Values{}) +} + +func SelfServiceMakeVerificationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/verification/pre", false, url.Values{}) +} + +func SelfServiceMakeRegistrationPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/registration/post", asAPI, query) +} + +func SelfServiceMakeSettingsPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/settings/post", asAPI, query) +} + +func SelfServiceMakeHookRequest(t *testing.T, ts *httptest.Server, suffix string, asAPI bool, query url.Values) (*http.Response, string) { + if len(query) > 0 { + suffix += "?" + query.Encode() + } + req, err := http.NewRequest("GET", ts.URL+suffix, nil) + require.NoError(t, err) + req.Header.Set("Accept", "text/html") + if asAPI { + req.Header.Set("Accept", "application/json") + } + res, err := ts.Client().Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, string(body) +} + +func GetSelfServiceRedirectLocation(t *testing.T, url string) string { + c := &http.Client{ + Transport: NewTestTransport(t), + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + return res.Header.Get("Location") +} + +func AssertMessage[B string | []byte](t *testing.T, body B, message string) { + t.Helper() + require.Len(t, gjson.Get(string(body), "ui.messages").Array(), 1) + assert.Equalf(t, message, gjson.Get(string(body), "ui.messages.0.text").String(), "%s", body) +} + +func AssertFieldMessage[B string | []byte](t *testing.T, body B, fieldName, message string) { + t.Helper() + messages := gjson.Get(string(body), "ui.nodes.#(attributes.name=="+fieldName+").messages") + require.Len(t, messages.Array(), 1, "expected field %s to have one message, got %s", fieldName, messages) + assert.Equalf(t, message, messages.Get("0.text").String(), "%s", body) +} diff --git a/pkg/testhelpers/selfservice_login.go b/pkg/testhelpers/selfservice_login.go new file mode 100644 index 0000000..a2002dd --- /dev/null +++ b/pkg/testhelpers/selfservice_login.go @@ -0,0 +1,362 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/ory/kratos/identity" + "github.com/ory/x/urlx" + + "github.com/tidwall/gjson" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" +) + +func NewLoginUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e, err := reg.LoginFlowPersister().GetLoginFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, e) + })) + ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost") + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, ts.URL+"/login-ts") + t.Cleanup(ts.Close) + return ts +} + +func NewLoginUIWith401Response(t *testing.T, c *config.Config) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost") + c.MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, ts.URL+"/login-ts") + t.Cleanup(ts.Close) + return ts +} + +type initFlowOptions struct { + aal identity.AuthenticatorAssuranceLevel + returnTo string + refresh bool + oauth2LoginChallenge string + via string + identitySchema string + ctx context.Context + expectActive *string +} + +func newInitFlowOptions(opts []InitFlowWithOption) *initFlowOptions { + return new(initFlowOptions).apply(opts) +} + +func (o *initFlowOptions) apply(opts []InitFlowWithOption) *initFlowOptions { + for _, opt := range opts { + opt(o) + } + return o +} + +func getURLFromInitOptions(ts *httptest.Server, path string, forced bool, opts ...InitFlowWithOption) string { + o := new(initFlowOptions).apply(opts) + q := url.Values{} + + if forced || o.refresh { + q.Set("refresh", "true") + } + + if o.aal != "" { + q.Set("aal", string(o.aal)) + } + + if o.returnTo != "" { + q.Set("return_to", string(o.returnTo)) + } + + if o.oauth2LoginChallenge != "" { + q.Set("login_challenge", o.oauth2LoginChallenge) + } + if o.via != "" { + q.Set("via", o.via) + } + if o.identitySchema != "" { + q.Set("identity_schema", o.identitySchema) + } + + u := urlx.ParseOrPanic(ts.URL + path) + u.RawQuery = q.Encode() + return u.String() +} + +type InitFlowWithOption func(*initFlowOptions) + +func InitFlowWithAAL(aal identity.AuthenticatorAssuranceLevel) InitFlowWithOption { + return func(o *initFlowOptions) { + o.aal = aal + } +} + +func InitFlowWithReturnTo(returnTo string) InitFlowWithOption { + return func(o *initFlowOptions) { + o.returnTo = returnTo + } +} + +func InitFlowWithRefresh() InitFlowWithOption { + return func(o *initFlowOptions) { + o.refresh = true + } +} + +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 + } +} + +func InitFlowWithIdentitySchema(schema string) InitFlowWithOption { + return func(o *initFlowOptions) { + o.identitySchema = schema + } +} + +func ExpectActive(active string) InitFlowWithOption { + return func(o *initFlowOptions) { + o.expectActive = &active + } +} + +// InitFlowWithVia sets the `via` query parameter which is used by the code MFA flows to determine the trait to use to send the code to the user +func InitFlowWithVia(via string) InitFlowWithOption { + return func(o *initFlowOptions) { + o.via = via + } +} + +// Deprecated: use InitializeLoginFlowViaBrowserCtx instead. +func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httptest.Server, forced, isSPA, expectInitError, expectGetError bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + return InitializeLoginFlowViaBrowserCtx(t.Context(), t, client, ts, forced, isSPA, expectInitError, expectGetError, opts...) +} + +func InitializeLoginFlowViaBrowserCtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced, isSPA, expectInitError, expectGetError bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + publicClient := NewSDKCustomClient(ts, client) + + 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.WithContext(ctx)) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + if isSPA { + require.Truef(t, gjson.ValidBytes(body), "body is not valid JSON: %s", body) + } + require.NoError(t, res.Body.Close()) + require.Equal(t, 200, res.StatusCode, "%s", body) + if expectInitError { + require.NotNil(t, res.Request.URL) + require.Contains(t, res.Request.URL.String(), "error-ts") + } + + flowID := res.Request.URL.Query().Get("flow") + if isSPA { + flowID = gjson.GetBytes(body, "id").String() + } + if !expectGetError { + require.NotEmpty(t, flowID) + } + + rs, r, err := publicClient.FrontendAPI.GetLoginFlow(ctx).Id(flowID).Execute() + if expectGetError { + require.Error(t, err) + require.Nil(t, rs) + } else { + require.NoError(t, err) + require.NoErrorf(t, err, "%s", ioutilx.MustReadAll(r.Body)) + assert.Equal(t, o.expectActive, rs.Active) + } + + return rs +} + +func InitializeLoginFlowViaAPICtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + return initializeLoginFlowViaAPI(ctx, t, client, ts, forced, false, opts...) +} + +func initializeLoginFlowViaAPI(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, expectError bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + publicClient := NewSDKCustomClient(ts, client) + + o := new(initFlowOptions).apply(opts) + req := publicClient.FrontendAPI.CreateNativeLoginFlow(ctx).Refresh(forced) + if o.aal != "" { + req = req.Aal(string(o.aal)) + } + if o.via != "" { + req = req.Via(o.via) + } + if o.identitySchema != "" { + req = req.IdentitySchema(o.identitySchema) + } + + rs, res, err := req.Execute() + if expectError { + require.Error(t, err) + require.Nil(t, rs) + } else { + require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body)) + assert.Equal(t, o.expectActive, rs.Active) + } + + return rs +} + +// Deprecated: use InitializeLoginFlowViaAPICtx instead. +func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + return InitializeLoginFlowViaAPICtx(t.Context(), t, client, ts, forced, opts...) +} + +func InitializeLoginFlowViaAPIExpectError(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + return initializeLoginFlowViaAPI(ctx, t, client, ts, forced, true, opts...) +} + +// Deprecated: use LoginMakeRequestCtx instead. +func LoginMakeRequest( + t *testing.T, + isAPI bool, + isSPA bool, + f *kratos.LoginFlow, + hc *http.Client, + values string, + opt ...RequestOption, +) (string, *http.Response) { + return LoginMakeRequestCtx(t.Context(), t, isAPI, isSPA, f, hc, values, opt...) +} + +type RequestOption func(*http.Request) + +func WithHeader(key, value string) RequestOption { + return func(r *http.Request) { + r.Header.Add(key, value) + } +} + +func LoginMakeRequestCtx( + ctx context.Context, + t *testing.T, + isAPI bool, + isSPA bool, + f *kratos.LoginFlow, + hc *http.Client, + values string, + opt ...RequestOption, +) (string, *http.Response) { + require.NotEmpty(t, f.Ui.Action) + + req := NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values)) + if isSPA && !isAPI { + req.Header.Set("Accept", "application/json") + req.Header.Set("Sec-Fetch-Mode", "cors") + } + for _, o := range opt { + o(req) + } + + res, err := hc.Do(req.WithContext(ctx)) + require.NoError(t, err, "action: %s", f.Ui.Action) + defer func() { _ = res.Body.Close() }() + + return string(ioutilx.MustReadAll(res.Body)), res +} + +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() + require.NoError(t, err) + return rs +} + +// Deprecated: use SubmitLoginFormCtx instead. +func SubmitLoginForm( + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + isSPA bool, + forced bool, + expectedStatusCode int, + expectedURL string, + opts ...InitFlowWithOption, +) string { + return SubmitLoginFormCtx(context.Background(), t, isAPI, hc, publicTS, withValues, isSPA, forced, expectedStatusCode, expectedURL, opts...) +} + +// SubmitLoginFormCtx initiates a login flow (for Browser and API!), fills out the form and modifies +// the form values with `withValues`, and submits the form. Returns the body and checks for expectedStatusCode and +// expectedURL on completion +func SubmitLoginFormCtx( + ctx context.Context, + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + isSPA bool, + forced bool, + expectedStatusCode int, + expectedURL string, + opts ...InitFlowWithOption, +) string { + if hc == nil { + hc = NewTestClient(t) + if !isAPI { + hc = NewClientWithCookies(t) + } + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + var f *kratos.LoginFlow + if isAPI { + f = InitializeLoginFlowViaAPICtx(ctx, t, hc, publicTS, forced, opts...) + } else { + f = InitializeLoginFlowViaBrowserCtx(ctx, t, hc, publicTS, forced, isSPA, false, false, opts...) + } + + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + payload := SDKFormFieldsToURLValues(f.Ui.Nodes) + withValues(payload) + b, res := LoginMakeRequestCtx(ctx, t, isAPI, isSPA, f, hc, EncodeFormAsJSON(t, isAPI, payload)) + assert.EqualValuesf(t, expectedStatusCode, res.StatusCode, "%s", b) + assert.Containsf(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) + + t.Logf("%+v", res.Header) + + return b +} diff --git a/pkg/testhelpers/selfservice_recovery.go b/pkg/testhelpers/selfservice_recovery.go new file mode 100644 index 0000000..b4e23d5 --- /dev/null +++ b/pkg/testhelpers/selfservice_recovery.go @@ -0,0 +1,135 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" +) + +func NewVerificationUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { + ctx := context.Background() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e, err := reg.VerificationFlowPersister().GetVerificationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, e) + })) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceVerificationUI, ts.URL+"/verification-ts") + t.Cleanup(ts.Close) + return ts +} + +func GetVerificationFlow(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.VerificationFlow { + publicClient := NewSDKCustomClient(ts, client) + + res, err := client.Get(ts.URL + verification.RouteInitBrowserFlow) + 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() + require.NoErrorf(t, err, "%s", res.Request.URL.String()) + assert.NotEmpty(t, rs.Active) + + return rs +} + +func InitializeVerificationFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server) *kratos.VerificationFlow { + publicClient := NewSDKCustomClient(ts, client) + req, err := http.NewRequest("GET", ts.URL+verification.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + if isSPA { + req.Header.Set("Accept", "application/json") + } + require.NoError(t, err) + + res, err := client.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + if isSPA { + var f kratos.VerificationFlow + require.NoError(t, json.NewDecoder(res.Body).Decode(&f)) + return &f + } + + rs, _, err := publicClient.FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + assert.NotEmpty(t, rs.Active) + + return rs +} + +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() + require.NoError(t, err) + assert.NotEmpty(t, rs.Active) + + return rs +} + +func VerificationMakeRequest( + t *testing.T, + isAPI bool, + f *kratos.VerificationFlow, + hc *http.Client, + values string, +) (string, *http.Response) { + require.NotEmpty(t, f.Ui.Action) + + res, err := hc.Do(NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values))) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + return string(ioutilx.MustReadAll(res.Body)), res +} + +// SubmitVerificationForm initiates a registration flow (for Browser and API!), fills out the form and modifies +// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON. +func SubmitVerificationForm( + t *testing.T, + isAPI bool, + isSPA bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + expectedStatusCode int, + expectedURL string, +) string { + var f *kratos.VerificationFlow + if isAPI { + f = InitializeVerificationFlowViaAPI(t, hc, publicTS) + } else { + f = InitializeVerificationFlowViaBrowser(t, hc, isSPA, publicTS) + } + + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + formPayload := SDKFormFieldsToURLValues(f.Ui.Nodes) + withValues(formPayload) + + b, res := VerificationMakeRequest(t, isAPI || isSPA, f, hc, EncodeFormAsJSON(t, isAPI || isSPA, formPayload)) + assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b) + assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) + + return b +} diff --git a/pkg/testhelpers/selfservice_registration.go b/pkg/testhelpers/selfservice_registration.go new file mode 100644 index 0000000..96d75ba --- /dev/null +++ b/pkg/testhelpers/selfservice_registration.go @@ -0,0 +1,202 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/selfservice/flow/registration" + + "github.com/tidwall/gjson" + + "github.com/ory/x/assertx" + + kratos "github.com/ory/kratos/pkg/httpclient" + + "github.com/ory/x/ioutilx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" +) + +func NewRegistrationUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { + ctx := context.Background() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e, err := reg.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, e) + })) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, ts.URL+"/registration-ts") + t.Cleanup(ts.Close) + return ts +} + +// Deprecated: Use context-based InitializeRegistrationFlowViaBrowserCtx instead +func InitializeRegistrationFlowViaBrowser(t *testing.T, client *http.Client, ts *httptest.Server, isSPA bool, expectInitError bool, expectGetError bool, opts ...InitFlowWithOption) *kratos.RegistrationFlow { + return InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, client, ts, isSPA, expectInitError, expectGetError, opts...) +} + +func InitializeRegistrationFlowViaBrowserCtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, isSPA bool, expectInitError bool, expectGetError bool, opts ...InitFlowWithOption) *kratos.RegistrationFlow { + req, err := http.NewRequest("GET", getURLFromInitOptions(ts, registration.RouteInitBrowserFlow, false, opts...), nil) + require.NoError(t, err) + req = req.WithContext(ctx) + + if isSPA { + req.Header.Set("Accept", "application/json") + } + + res, err := client.Do(req) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + if isSPA { + require.True(t, gjson.ValidBytes(body), "body is not valid JSON: %s", string(body)) + } + require.NoError(t, res.Body.Close()) + if expectInitError { + require.Equal(t, 200, res.StatusCode) + require.NotNil(t, res.Request.URL) + require.Contains(t, res.Request.URL.String(), "error-ts") + } + + flowID := res.Request.URL.Query().Get("flow") + if isSPA { + flowID = gjson.GetBytes(body, "id").String() + } + + rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.GetRegistrationFlow(ctx).Id(flowID).Execute() + if expectGetError { + require.Error(t, err) + require.Nil(t, rs) + } else { + require.NoError(t, err) + assert.Empty(t, rs.Active) + } + return rs +} + +func InitializeRegistrationFlowViaAPIExpectError(t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) { + o := new(initFlowOptions).apply(opts) + + _, _, err := NewSDKCustomClient(ts, client).FrontendAPI.CreateNativeRegistrationFlow(context.Background()).IdentitySchema(o.identitySchema).Execute() + require.Error(t, err) +} + +// Deprecated: Use context-based InitializeRegistrationFlowViaAPICtx instead +func InitializeRegistrationFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) *kratos.RegistrationFlow { + return InitializeRegistrationFlowViaAPICtx(context.Background(), t, client, ts, opts...) +} + +func InitializeRegistrationFlowViaAPICtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) *kratos.RegistrationFlow { + o := new(initFlowOptions).apply(opts) + + rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.CreateNativeRegistrationFlow(ctx).IdentitySchema(o.identitySchema).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() + require.NoError(t, err) + assert.Empty(t, rs.Active) + return rs +} + +func RegistrationMakeRequest( + t *testing.T, + isAPI bool, + isSPA bool, + f *kratos.RegistrationFlow, + hc *http.Client, + values string, +) (string, *http.Response) { + return RegistrationMakeRequestCtx(t.Context(), t, isAPI, isSPA, f, hc, values) +} + +func RegistrationMakeRequestCtx( + ctx context.Context, + t *testing.T, + isAPI bool, + isSPA bool, + f *kratos.RegistrationFlow, + hc *http.Client, + values string, +) (string, *http.Response) { + require.NotEmpty(t, f.Ui.Action) + + req := NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values)) + if isSPA { + req.Header.Set("Accept", "application/json") + req.Header.Set("Sec-Fetch-Mode", "cors") + } + req = req.WithContext(ctx) + + res, err := hc.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + return string(ioutilx.MustReadAll(res.Body)), res +} + +// Deprecated: Use context-based SubmitRegistrationFormCtx instead +func SubmitRegistrationForm( + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + isSPA bool, + expectedStatusCode int, + expectedURL string, + opts ...InitFlowWithOption, +) string { + return SubmitRegistrationFormCtx(context.Background(), t, isAPI, hc, publicTS, withValues, isSPA, expectedStatusCode, expectedURL, opts...) +} + +// SubmitRegistrationFormCtx (for Browser and API!), fills out the form and modifies +// the form values with `withValues`, and submits the form. Returns the body and checks for expectedStatusCode and +// expectedURL on completion +func SubmitRegistrationFormCtx( + ctx context.Context, + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + isSPA bool, + expectedStatusCode int, + expectedURL string, + opts ...InitFlowWithOption, +) string { + if hc == nil { + hc = NewTestClient(t) + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + var payload *kratos.RegistrationFlow + if isAPI { + payload = InitializeRegistrationFlowViaAPICtx(ctx, t, hc, publicTS, opts...) + } else { + payload = InitializeRegistrationFlowViaBrowserCtx(ctx, t, hc, publicTS, isSPA, false, false, opts...) + } + + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + values := SDKFormFieldsToURLValues(payload.Ui.Nodes) + withValues(values) + b, res := RegistrationMakeRequest(t, isAPI, isSPA, payload, hc, EncodeFormAsJSON(t, isAPI, values)) + assert.EqualValues(t, expectedStatusCode, res.StatusCode, assertx.PrettifyJSONPayload(t, b)) + assert.Containsf(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, b)) + return b +} diff --git a/pkg/testhelpers/selfservice_settings.go b/pkg/testhelpers/selfservice_settings.go new file mode 100644 index 0000000..900c319 --- /dev/null +++ b/pkg/testhelpers/selfservice_settings.go @@ -0,0 +1,276 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" + "github.com/ory/x/urlx" +) + +func NewSettingsUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { + ctx := context.Background() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e, err := reg.SettingsFlowPersister().GetSettingsFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, e) + })) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings-ts") + t.Cleanup(ts.Close) + return ts +} + +func InitializeSettingsFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server) *kratos.SettingsFlow { + t.Helper() + publicClient := NewSDKCustomClient(ts, client) + + req, err := http.NewRequest("GET", ts.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + if isSPA { + req.Header.Set("Accept", "application/json") + } + + res, err := client.Do(req) + require.NoError(t, err) + + var flowID string + if isSPA { + flowID = gjson.GetBytes(x.MustReadAll(res.Body), "id").String() + } else { + flowID = res.Request.URL.Query().Get("flow") + } + + require.NoError(t, res.Body.Close()) + + 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) + + return rs +} + +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() + require.NoError(t, err) + assert.Empty(t, rs.Active) + + return rs +} + +func EncodeFormAsJSON(t *testing.T, isApi bool, values url.Values) (payload string) { + if !isApi { + return values.Encode() + } + payload = "{}" + for k := range values { + var err error + payload, err = sjson.Set(payload, strings.ReplaceAll(k, ".", "\\."), values.Get(k)) + require.NoError(t, err) + } + return payload +} + +func ExpectStatusCode(isAPI bool, api, browser int) int { + if isAPI { + return api + } + return browser +} + +func ExpectURL(isAPI bool, api, browser string) string { + if isAPI { + return api + } + return browser +} + +func NewSettingsUITestServer(t *testing.T, conf *config.Config) *httptest.Server { + router := http.NewServeMux() + router.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL+"/login") + + return ts +} + +func NewSettingsUIEchoServer(t *testing.T, reg *driver.RegistryDefault) *httptest.Server { + router := http.NewServeMux() + router.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) { + res, err := reg.SettingsFlowPersister().GetSettingsFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, res) + }) + + router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + ctx := context.Background() + reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings") + reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL+"/login") + + return ts +} + +func NewSettingsLoginAcceptAPIServer(t *testing.T, publicClient *kratos.APIClient, conf *config.Config) *httptest.Server { + var called int + loginTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, 0, called) + called++ + + conf.MustSet(r.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + + res, _, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(r.URL.Query().Get("flow")).Execute() + + require.NoError(t, err) + require.NotEmpty(t, res.RequestUrl) + + redir := urlx.ParseOrPanic(res.RequestUrl).Query().Get("return_to") + t.Logf("Redirecting to: %s", redir) + http.Redirect(w, r, redir, http.StatusFound) + })) + t.Cleanup(func() { + loginTS.Close() + }) + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, loginTS.URL+"/login") + return loginTS +} + +// AddAndLoginIdentity adds the given identity to the store (like a registration flow) and returns an http.Client +// which contain their session. +func AddAndLoginIdentity(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity) (uuid.UUID, *http.Client) { + loggedIn := AddAndLoginIdentities(t, reg, map[string]*identity.Identity{"": i})[""] + return loggedIn.ID, loggedIn.Client +} + +// AddAndLoginIdentities adds the given identities to the store (like a registration flow) and returns http.Clients +// which contain their sessions. +func AddAndLoginIdentities(t *testing.T, reg *driver.RegistryDefault, ids map[string]*identity.Identity) map[string]struct { + Client *http.Client + ID uuid.UUID +} { + result := map[string]struct { + Client *http.Client + ID uuid.UUID + }{} + r := http.NewServeMux() + s := httptest.NewServer(r) + defer s.Close() + + for k, i := range ids { + i.ID = uuid.Must(uuid.NewV4()) + tid := x.NewUUID().String() + route, _ := MockSessionCreateHandlerWithIdentity(t, reg, i) + location := "/sessions/set/" + tid + + r.Handle("GET "+location, route) + result[k] = struct { + Client *http.Client + ID uuid.UUID + }{ + Client: NewSessionClient(t, s.URL+location), + ID: i.ID, + } + } + return result +} + +func SettingsMakeRequest( + t *testing.T, + isAPI bool, + isSPA bool, + f *kratos.SettingsFlow, + hc *http.Client, + values string, +) (string, *http.Response) { + require.NotEmpty(t, f.Ui.Action) + + req := NewPostRequest(t, isSPA || isAPI, f.Ui.Action, bytes.NewBufferString(values)) + if isSPA || isAPI { + req.Header.Set("Accept", "application/json") + } + if isSPA { + req.Header.Set("Sec-Fetch-Mode", "cors") + } + + res, err := hc.Do(req) + require.NoError(t, err, "action: %s", f.Ui.Action) + defer func() { _ = res.Body.Close() }() + + return string(ioutilx.MustReadAll(res.Body)), res +} + +// SubmitSettingsForm initiates a settings flow (for Browser and API!), fills out the form and modifies +// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON. +func SubmitSettingsForm( + t *testing.T, + isAPI bool, + isSPA bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + expectedStatusCode int, + expectedURL string, +) string { + if hc == nil { + hc = NewTestClient(t) + if !isAPI { + hc = NewClientWithCookies(t) + } + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + var payload *kratos.SettingsFlow + if isAPI { + payload = InitializeSettingsFlowViaAPI(t, hc, publicTS) + } else { + payload = InitializeSettingsFlowViaBrowser(t, hc, isSPA, publicTS) + } + + time.Sleep(time.Millisecond * 10) // add a bit of delay to allow `1ns` to time out. + + values := SDKFormFieldsToURLValues(payload.Ui.Nodes) + withValues(values) + + b, res := SettingsMakeRequest(t, isAPI, isSPA, payload, hc, EncodeFormAsJSON(t, isAPI || isSPA, values)) + assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b) + assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) + + return b +} diff --git a/pkg/testhelpers/selfservice_verification.go b/pkg/testhelpers/selfservice_verification.go new file mode 100644 index 0000000..fe2cb24 --- /dev/null +++ b/pkg/testhelpers/selfservice_verification.go @@ -0,0 +1,239 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/tidwall/gjson" + + kratos "github.com/ory/kratos/pkg/httpclient" + + "github.com/ory/x/ioutilx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "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) + })) + before := conf.GetProvider(ctx).Get(config.ViperKeySelfServiceVerificationAfter + ".hooks") + // 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", before) + }) +} + +func NewRecoveryAfterHookWebHookTarget(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 recovery hook is called with the correct data + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".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.ViperKeySelfServiceRecoveryAfter+".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) { + e, err := reg.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + require.NoError(t, err) + reg.Writer().Write(w, r, e) + })) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceRecoveryUI, ts.URL+"/recovery-ts") + t.Cleanup(ts.Close) + return ts +} + +func GetRecoveryFlowForType(t *testing.T, client *http.Client, ts *httptest.Server, ft flow.Type) *kratos.RecoveryFlow { + publicClient := NewSDKCustomClient(ts, client) + + var url string + switch ft { + case flow.TypeBrowser: + url = ts.URL + recovery.RouteInitBrowserFlow + case flow.TypeAPI: + url = ts.URL + recovery.RouteInitAPIFlow + default: + t.Errorf("unknown type: %s", ft) + t.FailNow() + } + + res, err := client.Get(url) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + var flowID string + switch ft { + case flow.TypeBrowser: + flowID = res.Request.URL.Query().Get("flow") + case flow.TypeAPI: + flowID = gjson.GetBytes(ioutilx.MustReadAll(res.Body), "id").String() + default: + t.Errorf("unknown type: %s", ft) + t.FailNow() + } + require.NotEmpty(t, flowID, "expected to receive a flow id, got none. %s", ioutilx.MustReadAll(res.Body)) + + rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()). + Id(flowID). + Execute() + require.NoError(t, err, "expected no error when fetching recovery flow: %s", err) + assert.NotEmpty(t, rs.Active) + + return rs +} + +func GetRecoveryFlow(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.RecoveryFlow { + return GetRecoveryFlowForType(t, client, ts, flow.TypeBrowser) +} + +func InitializeRecoveryFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server, values url.Values) *kratos.RecoveryFlow { + publicClient := NewSDKCustomClient(ts, client) + + u := ts.URL + recovery.RouteInitBrowserFlow + if values != nil { + u += "?" + values.Encode() + } + req, err := http.NewRequest("GET", u, nil) + require.NoError(t, err) + + if isSPA { + req.Header.Set("Accept", "application/json") + } + + res, err := client.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + if isSPA { + var f kratos.RecoveryFlow + require.NoError(t, json.NewDecoder(res.Body).Decode(&f)) + return &f + } + + require.NoError(t, res.Body.Close()) + rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + assert.NotEmpty(t, rs.Active) + + return rs +} + +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() + require.NoError(t, err) + assert.NotEmpty(t, rs.Active) + + return rs +} + +func RecoveryMakeRequest( + t *testing.T, + isAPI bool, + f *kratos.RecoveryFlow, + hc *http.Client, + values string, +) (string, *http.Response) { + require.NotEmpty(t, f.Ui.Action) + + res, err := hc.Do(NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values))) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + return string(ioutilx.MustReadAll(res.Body)), res +} + +// SubmitRecoveryForm initiates a registration flow (for Browser and API!), fills out the form and modifies +// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON. +func SubmitRecoveryForm( + t *testing.T, + isAPI bool, + isSPA bool, + hc *http.Client, + publicTS *httptest.Server, + withValues func(v url.Values), + expectedStatusCode int, + expectedURL string, +) string { + hc.Transport = NewTransportWithLogger(hc.Transport, t) + var f *kratos.RecoveryFlow + if isAPI { + f = InitializeRecoveryFlowViaAPI(t, hc, publicTS) + } else { + f = InitializeRecoveryFlowViaBrowser(t, hc, isSPA, publicTS, nil) + } + + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + formPayload := SDKFormFieldsToURLValues(f.Ui.Nodes) + withValues(formPayload) + + b, res := RecoveryMakeRequest(t, isAPI || isSPA, f, hc, EncodeFormAsJSON(t, isAPI || isSPA, formPayload)) + assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b) + assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) + + return b +} + +func PersistNewRecoveryFlow(t *testing.T, strategies recovery.Strategies, conf *config.Config, reg *driver.RegistryDefault) *recovery.Flow { + t.Helper() + req := NewTestHTTPRequest(t, "GET", conf.SelfPublicURL(context.Background()).String()+"/test", nil) + f, err := recovery.NewFlow(reg, conf.SelfServiceFlowRecoveryRequestLifespan(context.Background()), reg.GenerateCSRFToken(req), req, strategies, flow.TypeBrowser) + require.NoError(t, err, "Expected no error when creating a new recovery flow: %s", err) + + err = reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f) + require.NoError(t, err, "Expected no error when persisting a new recover flow: %s", err) + return f +} diff --git a/pkg/testhelpers/server.go b/pkg/testhelpers/server.go new file mode 100644 index 0000000..4ff930a --- /dev/null +++ b/pkg/testhelpers/server.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/urfave/negroni" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/httprouterx" +) + +func NewKratosServer(t *testing.T, reg driver.Registry) (public, admin *httptest.Server) { + return NewKratosServerWithRouters(t, reg, httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix()) +} + +func NewKratosServerWithCSRF(t *testing.T, reg driver.Registry) (public, admin *httptest.Server) { + public, admin, _, _ = NewKratosServerWithCSRFAndRouters(t, reg) + return +} + +func NewKratosServerWithCSRFAndRouters(t *testing.T, reg driver.Registry) (public, admin *httptest.Server, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) { + rp, ra = httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix() + csrfHandler := nosurfx.NewTestCSRFHandler(rp, reg) + reg.WithCSRFHandler(csrfHandler) + + ran := negroni.New() + ran.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni) + ran.UseHandler(ra) + + rpn := negroni.New() + rpn.UseFunc(x.HTTPLoaderContextMiddleware(reg)) + rpn.UseHandler(rp) + + public = httptest.NewServer(nosurfx.NewTestCSRFHandler(rpn, reg)) + admin = httptest.NewServer(ran) + + public.URL = strings.ReplaceAll(public.URL, "127.0.0.1", "localhost") + + initKratosServers(t, reg, public, admin, rp, ra) + + t.Cleanup(public.Close) + t.Cleanup(admin.Close) + return +} + +func NewKratosServerWithRouters(t *testing.T, reg driver.Registry, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) (public, admin *httptest.Server) { + np := negroni.New() + np.UseFunc(httprouterx.TrimTrailingSlashNegroni) + np.UseHandler(rp) + + na := negroni.New() + na.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni) + na.UseFunc(httprouterx.TrimTrailingSlashNegroni) + na.UseHandler(ra) + + public = httptest.NewServer(np) + admin = httptest.NewServer(na) + + initKratosServers(t, reg, public, admin, rp, ra) + + t.Cleanup(public.Close) + t.Cleanup(admin.Close) + return +} + +func initKratosServers(t *testing.T, reg driver.Registry, public, admin *httptest.Server, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) { + ctx := t.Context() + if len(reg.Config().GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) == 0 { + reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, "http://NewKratosServerWithRouters/you-forgot-to-set-me/login") + } + reg.Config().MustSet(ctx, config.ViperKeyPublicBaseURL, public.URL) + reg.Config().MustSet(ctx, config.ViperKeyAdminBaseURL, admin.URL) + + reg.RegisterPublicRoutes(ctx, rp) + reg.RegisterAdminRoutes(ctx, ra) +} diff --git a/pkg/testhelpers/session.go b/pkg/testhelpers/session.go new file mode 100644 index 0000000..d0a5ecb --- /dev/null +++ b/pkg/testhelpers/session.go @@ -0,0 +1,285 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/nosurf" + "github.com/ory/x/contextx" +) + +type SessionLifespanProvider struct { + e time.Duration +} + +func (p *SessionLifespanProvider) SessionLifespan(context.Context) time.Duration { + return p.e +} + +func NewSessionClient(t *testing.T, u string) *http.Client { + c := NewClientWithCookies(t) + MockHydrateCookieClient(t, c, u) + return c +} + +func maybePersistSession(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) { + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID) + if err != nil { + 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(ctx, sess)) +} + +func NewHTTPClientWithSessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(ctx, t, reg, sess) + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) + }) + + if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok { + handler = nosurf.New(handler) + } + + ts := httptest.NewServer(handler) + defer ts.Close() + + c := NewClientWithCookies(t) + MockHydrateCookieClient(t, c, ts.URL) + return c +} + +func NewHTTPClientWithSessionCookieLocalhost(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(ctx, t, reg, sess) + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) + }) + + if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok { + handler = nosurf.New(handler) + } + + ts := httptest.NewServer(handler) + defer ts.Close() + + c := NewClientWithCookies(t) + + ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost") + MockHydrateCookieClient(t, c, ts.URL) + return c +} + +func NewNoRedirectHTTPClientWithSessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(ctx, t, reg, sess) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) + })) + defer ts.Close() + + c := NewNoRedirectClientWithCookies(t) + + MockHydrateCookieClient(t, c, ts.URL) + return c +} + +func NewTransportWithLogger(parent http.RoundTripper, t *testing.T) *TransportWithLogger { + return &TransportWithLogger{ + RoundTripper: parent, + t: t, + } +} + +type TransportWithLogger struct { + http.RoundTripper + t *testing.T +} + +func (ct *TransportWithLogger) RoundTrip(req *http.Request) (*http.Response, error) { + ct.t.Logf("Made request to: %s", req.URL.String()) + if ct.RoundTripper == nil { + return http.DefaultTransport.RoundTrip(req) + } + return ct.RoundTripper.RoundTrip(req) +} + +func NewHTTPClientWithSessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(ctx, t, reg, sess) + + return &http.Client{ + Transport: NewTransportWithHeader(t, http.Header{ + "Authorization": {"Bearer " + sess.Token}, + }), + } +} + +func NewHTTPClientWithArbitrarySessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client { + return NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx, t, reg, nil) +} + +func NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, traits identity.Traits) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil).WithContext(contextx.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(ctx, t, reg, s) +} + +func NewHTTPClientWithArbitrarySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req = req.WithContext(contextx.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(ctx, t, reg, s) +} + +func NewNoRedirectHTTPClientWithArbitrarySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req = req.WithContext(contextx.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(ctx, t, reg, s) +} + +func NewHTTPClientWithIdentitySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour)) + s, err := NewActiveSession(req, reg, + id, + time.Now(), + identity.CredentialsTypePassword, + identity.AuthenticatorAssuranceLevel1, + ) + require.NoError(t, err, "Could not initialize session from identity.") + + return NewHTTPClientWithSessionCookie(ctx, t, reg, s) +} + +func NewHTTPClientWithIdentitySessionCookieLocalhost(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + s, err := NewActiveSession(req, reg, + id, + time.Now(), + identity.CredentialsTypePassword, + identity.AuthenticatorAssuranceLevel1, + ) + require.NoError(t, err, "Could not initialize session from identity.") + + return NewHTTPClientWithSessionCookieLocalhost(ctx, t, reg, s) +} + +func NewHTTPClientWithIdentitySessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour)) + s, err := NewActiveSession(req, reg, + id, + time.Now(), + identity.CredentialsTypePassword, + identity.AuthenticatorAssuranceLevel1, + ) + require.NoError(t, err, "Could not initialize session from identity.") + + return NewHTTPClientWithSessionToken(ctx, t, reg, s) +} + +func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, methods ...string) { + res, err := c.Get(ts.URL + session.RouteWhoami) + require.NoError(t, err) + sess := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.EqualValues(t, aal, gjson.GetBytes(sess, "authenticator_assurance_level").String()) + for _, method := range methods { + assert.EqualValues(t, method, gjson.GetBytes(sess, "authentication_methods.#(method=="+method+").method").String()) + } + assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods)) +} + +func NewAuthorizedTransport(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader { + maybePersistSession(ctx, t, reg, sess) + + return NewTransportWithHeader(t, http.Header{ + "Authorization": {"Bearer " + sess.Token}, + }) +} + +func NewTransportWithHeader(t *testing.T, h http.Header) *TransportWithHeader { + if t == nil { + panic("This function is for testing use only.") + } + return &TransportWithHeader{ + RoundTripper: NewTestTransport(t), + h: h, + } +} + +type TransportWithHeader struct { + http.RoundTripper + 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)) + } + return ct.RoundTripper.RoundTrip(req) +} + +func AssertNoCSRFCookieInResponse(t *testing.T, r *http.Response) { + found := false + for _, c := range r.Cookies() { + if strings.HasPrefix(c.Name, "csrf_token") { + found = true + } + } + require.False(t, found) +} diff --git a/pkg/testhelpers/session_active.go b/pkg/testhelpers/session_active.go new file mode 100644 index 0000000..a245abc --- /dev/null +++ b/pkg/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/pkg/testhelpers/snapshot.go b/pkg/testhelpers/snapshot.go new file mode 100644 index 0000000..152ee0d --- /dev/null +++ b/pkg/testhelpers/snapshot.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "encoding/json" + "testing" + + "github.com/bradleyjkemp/cupaloy/v2" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" +) + +func SnapshotTExcept(t *testing.T, actual interface{}, except []string) { + t.Helper() + compare, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err, "%+v", actual) + for _, e := range except { + compare, err = sjson.DeleteBytes(compare, e) + require.NoError(t, err, "%s", e) + } + + cupaloy.New( + cupaloy.CreateNewAutomatically(true), + cupaloy.FailOnUpdate(true), + cupaloy.SnapshotFileExtension(".json"), + ).SnapshotT(t, compare) +} diff --git a/pkg/testhelpers/strategies.go b/pkg/testhelpers/strategies.go new file mode 100644 index 0000000..b3b5a9b --- /dev/null +++ b/pkg/testhelpers/strategies.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "fmt" + "testing" + + "github.com/ory/kratos/driver/config" +) + +// Deprecated: use MethodEnableConfig instead. +func StrategyEnable(t *testing.T, c *config.Config, strategy string, enable bool) { + ctx := context.Background() + c.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, strategy), enable) +} + +func MethodEnableConfig[S ~string](method S, enable bool) map[string]any { + return map[string]any{ + fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, method): enable, + } +} diff --git a/pkg/testhelpers/strategy_code.go b/pkg/testhelpers/strategy_code.go new file mode 100644 index 0000000..904ae13 --- /dev/null +++ b/pkg/testhelpers/strategy_code.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "fmt" + + "github.com/ory/kratos/selfservice/strategy/code" +) + +var CodeRegex = fmt.Sprintf(`(\d{%d})`, code.CodeLength) diff --git a/pkg/testhelpers/stub/fake-session.schema.json b/pkg/testhelpers/stub/fake-session.schema.json new file mode 100644 index 0000000..558106d --- /dev/null +++ b/pkg/testhelpers/stub/fake-session.schema.json @@ -0,0 +1,7 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": {} +} diff --git a/proto/oidc/v1/state.proto b/proto/oidc/v1/state.proto new file mode 100644 index 0000000..2f63b46 --- /dev/null +++ b/proto/oidc/v1/state.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package oidc.v1; + +enum FlowKind { + FLOW_KIND_UNSPECIFIED = 0; + FLOW_KIND_LOGIN = 1; + FLOW_KIND_REGISTRATION = 2; + FLOW_KIND_SETTINGS = 3; +} + +message State { + bytes flow_id = 1; + bytes session_token_exchange_code_sha512 = 2; + string provider_id = 3; + string pkce_verifier = 4; + FlowKind flow_kind = 5; + // request_base_url is the scheme://host[:port] the browser used at flow + // init. It is captured so the OIDC/SAML callback can be redirected back + // onto the developer proxy / custom domain the user started on. Empty + // when there is nothing to rewrite. + string request_base_url = 6; +} diff --git a/quickstart-crdb.yml b/quickstart-crdb.yml new file mode 100644 index 0000000..07a202c --- /dev/null +++ b/quickstart-crdb.yml @@ -0,0 +1,18 @@ +version: "3.7" + +services: + kratos-migrate: + environment: + - DSN=cockroach://root@cockroachd:26257/defaultdb?sslmode=disable&max_conns=20&max_idle_conns=4 + + kratos: + environment: + - DSN=cockroach://root@cockroachd:26257/defaultdb?sslmode=disable&max_conns=20&max_idle_conns=4 + + cockroachd: + image: cockroachdb/cockroach:latest-v25.4 + ports: + - "26257:26257" + command: start-single-node --insecure + networks: + - intranet diff --git a/quickstart-debug.yml b/quickstart-debug.yml new file mode 100644 index 0000000..42cd0f3 --- /dev/null +++ b/quickstart-debug.yml @@ -0,0 +1,10 @@ +version: '3.7' + +services: + kratos-migrate: + environment: + - LOG_LEVEL=trace + + kratos: + environment: + - LOG_LEVEL=trace diff --git a/quickstart-latest.yml b/quickstart-latest.yml new file mode 100644 index 0000000..840ee0e --- /dev/null +++ b/quickstart-latest.yml @@ -0,0 +1,8 @@ +version: '3.7' + +services: + kratos-migrate: + image: oryd/kratos:latest + + kratos: + image: oryd/kratos:latest diff --git a/quickstart-mysql.yml b/quickstart-mysql.yml new file mode 100644 index 0000000..f0cda5c --- /dev/null +++ b/quickstart-mysql.yml @@ -0,0 +1,19 @@ +version: "3.7" + +services: + kratos-migrate: + environment: + - DSN=mysql://root:secret@tcp(mysqld:3306)/mysql?max_conns=20&max_idle_conns=4 + + kratos: + environment: + - DSN=mysql://root:secret@tcp(mysqld:3306)/mysql?max_conns=20&max_idle_conns=4 + + mysqld: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=secret + networks: + - intranet diff --git a/quickstart-oathkeeper.yml b/quickstart-oathkeeper.yml new file mode 100644 index 0000000..38dfd34 --- /dev/null +++ b/quickstart-oathkeeper.yml @@ -0,0 +1,30 @@ +version: '3.7' + +services: + kratos: + environment: + - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ + + kratos-selfservice-ui-node: + environment: + - PORT=4435 + - KRATOS_BROWSER_URL=http://127.0.0.1:4455/.ory/kratos/public + - JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json + - SECURITY_MODE=jwks + + oathkeeper: + image: oryd/oathkeeper:v0.40 + depends_on: + - kratos + ports: + - 4455:4455 + - 4456:4456 + command: + serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml" + environment: + - LOG_LEVEL=debug + restart: on-failure + networks: + - intranet + volumes: + - ./contrib/quickstart/oathkeeper:/etc/config/oathkeeper diff --git a/quickstart-postgres.yml b/quickstart-postgres.yml new file mode 100644 index 0000000..93cad3a --- /dev/null +++ b/quickstart-postgres.yml @@ -0,0 +1,21 @@ +version: "3.7" + +services: + kratos-migrate: + environment: + - DSN=postgres://kratos:secret@postgresd:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + + kratos: + environment: + - DSN=postgres://kratos:secret@postgresd:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + + postgresd: + image: postgres:14 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=kratos + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=kratos + networks: + - intranet diff --git a/quickstart-selinux.yml b/quickstart-selinux.yml new file mode 100644 index 0000000..c346995 --- /dev/null +++ b/quickstart-selinux.yml @@ -0,0 +1,26 @@ +version: '3.7' + +services: + kratos-migrate: + volumes: + - + type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - + type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos:z + + kratos: + volumes: + - + type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - + type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos:z diff --git a/quickstart-standalone.yml b/quickstart-standalone.yml new file mode 100644 index 0000000..8c79928 --- /dev/null +++ b/quickstart-standalone.yml @@ -0,0 +1,10 @@ +version: '3.7' + +services: + kratos-selfservice-ui-node: + ports: + - "4455:4455" + environment: + - PORT=4455 + - SECURITY_MODE= + - KRATOS_BROWSER_URL=http://127.0.0.1:4433/ diff --git a/quickstart-tracing.yml b/quickstart-tracing.yml new file mode 100644 index 0000000..aae1a46 --- /dev/null +++ b/quickstart-tracing.yml @@ -0,0 +1,29 @@ +########################################################################### +####### FOR DEMONSTRATION PURPOSES ONLY ####### +########################################################################### +# # +# If you have not yet read the tutorial, do so now: # +# https://www.ory.com/docs/hydra/5min-tutorial # +# # +# This set up is only for demonstration purposes. The login # +# endpoint can only be used if you follow the steps in the tutorial. # +# # +########################################################################### + +version: "3.7" + +services: + kratos: + depends_on: + - jaeger + environment: + - TRACING_PROVIDER=jaeger + ### Jaeger ### + - TRACING_PROVIDERS_JAEGER_SAMPLING_SERVER_URL=http://jaeger:5778/sampling + - TRACING_PROVIDERS_JAEGER_LOCAL_AGENT_ADDRESS=jaeger:6831 + jaeger: + image: jaegertracing/all-in-one:1.19.2 + ports: + - "16686:16686" # The UI port + networks: + - intranet diff --git a/quickstart-webauthn.yml b/quickstart-webauthn.yml new file mode 100644 index 0000000..d762ca1 --- /dev/null +++ b/quickstart-webauthn.yml @@ -0,0 +1,18 @@ +version: "3.7" + +services: + kratos-migrate: + volumes: + - type: bind + source: ./contrib/quickstart/kratos/webauthn + target: /etc/config/kratos + + kratos: + volumes: + - type: bind + source: ./contrib/quickstart/kratos/webauthn + target: /etc/config/kratos + + kratos-selfservice-ui-node: + environment: + - KRATOS_BROWSER_URL=http://localhost:4433/ diff --git a/quickstart.yml b/quickstart.yml new file mode 100644 index 0000000..6f73f07 --- /dev/null +++ b/quickstart.yml @@ -0,0 +1,62 @@ +version: '3.7' +services: + kratos-migrate: + image: oryd/kratos:v26.2.0 + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&_journal=WAL&mode=rwc + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + networks: + - intranet + kratos-selfservice-ui-node: + image: oryd/kratos-selfservice-ui-node:v26.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:v26.2.0 + ports: + - '4433:4433' # public + - '4434:4434' # admin + restart: unless-stopped + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&_journal=WAL + - LOG_LEVEL=trace + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos + networks: + - intranet + mailslurper: + image: oryd/mailslurper:latest-smtps + ports: + - '4436:4436' + - '4437:4437' + networks: + - intranet +networks: + intranet: +volumes: + kratos-sqlite: diff --git a/request/auth.go b/request/auth.go new file mode 100644 index 0000000..9817ebf --- /dev/null +++ b/request/auth.go @@ -0,0 +1,94 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package request + +import ( + "fmt" + "net/http" + + "github.com/hashicorp/go-retryablehttp" +) + +type ( + noopAuthStrategy struct{} + basicAuthStrategy struct { + user string + password string + } + apiKeyStrategy struct { + name string + value string + in string + } + AuthStrategy interface { + apply(req *retryablehttp.Request) + } +) + +func authStrategy(typ string, config map[string]any) (AuthStrategy, error) { + switch typ { + case "": + return NewNoopAuthStrategy(), nil + case "api_key": + name, ok := config["name"].(string) + if !ok { + return nil, fmt.Errorf("api_key auth strategy requires a string name") + } + value, ok := config["value"].(string) + if !ok { + return nil, fmt.Errorf("api_key auth strategy requires a string value") + } + in, _ := config["in"].(string) // in is optional + return NewAPIKeyStrategy(in, name, value), nil + case "basic_auth": + user, ok := config["user"].(string) + if !ok { + return nil, fmt.Errorf("basic_auth auth strategy requires a string user") + } + password, ok := config["password"].(string) + if !ok { + return nil, fmt.Errorf("basic_auth auth strategy requires a string password") + } + return NewBasicAuthStrategy(user, password), nil + } + + return nil, fmt.Errorf("unsupported auth type: %s", typ) +} + +func NewNoopAuthStrategy() AuthStrategy { + return &noopAuthStrategy{} +} + +func (c *noopAuthStrategy) apply(_ *retryablehttp.Request) {} + +func NewBasicAuthStrategy(user, password string) AuthStrategy { + return &basicAuthStrategy{ + user: user, + password: password, + } +} + +func (c *basicAuthStrategy) apply(req *retryablehttp.Request) { + req.SetBasicAuth(c.user, c.password) +} + +func NewAPIKeyStrategy(in, name, value string) AuthStrategy { + return &apiKeyStrategy{ + in: in, + name: name, + value: value, + } +} + +func (c *apiKeyStrategy) apply(req *retryablehttp.Request) { + switch c.in { + case "cookie": + req.AddCookie(&http.Cookie{Name: c.name, Value: c.value}) + default: + // TODO add deprecation warning + fallthrough + case "header", "": + req.Header.Set(c.name, c.value) + } +} diff --git a/request/auth_test.go b/request/auth_test.go new file mode 100644 index 0000000..e5113f6 --- /dev/null +++ b/request/auth_test.go @@ -0,0 +1,120 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package request + +import ( + "net/http" + "testing" + + "github.com/hashicorp/go-retryablehttp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNoopAuthStrategy(t *testing.T) { + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := noopAuthStrategy{} + + auth.apply(&req) + + assert.Empty(t, req.Header, "Empty auth strategy shall not modify any request headers") +} + +func TestBasicAuthStrategy(t *testing.T) { + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := basicAuthStrategy{ + user: "test-user", + password: "test-pass", + } + + auth.apply(&req) + + assert.Len(t, req.Header, 1) + + user, pass, _ := req.BasicAuth() + assert.Equal(t, "test-user", user) + assert.Equal(t, "test-pass", pass) +} + +func TestApiKeyInHeaderStrategy(t *testing.T) { + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := apiKeyStrategy{ + in: "header", + name: "my-api-key-name", + value: "my-api-key-value", + } + + auth.apply(&req) + + require.Len(t, req.Header, 1) + + actualValue := req.Header.Get("my-api-key-name") + assert.Equal(t, "my-api-key-value", actualValue) +} + +func TestApiKeyInCookieStrategy(t *testing.T) { + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := apiKeyStrategy{ + in: "cookie", + name: "my-api-key-name", + value: "my-api-key-value", + } + + auth.apply(&req) + + cookies := req.Cookies() + assert.Len(t, cookies, 1) + + assert.Equal(t, "my-api-key-name", cookies[0].Name) + assert.Equal(t, "my-api-key-value", cookies[0].Value) +} + +func TestAuthStrategy(t *testing.T) { + t.Parallel() + + for _, tc := range map[string]struct { + name string + config map[string]any + expected AuthStrategy + }{ + "noop": { + name: "", + config: map[string]any{}, + expected: &noopAuthStrategy{}, + }, + "basic_auth": { + name: "basic_auth", + config: map[string]any{ + "user": "test-api-user", + "password": "secret", + }, + expected: &basicAuthStrategy{}, + }, + "api-key/header": { + name: "api_key", + config: map[string]any{ + "in": "header", + "name": "my-api-key", + "value": "secret", + }, + expected: &apiKeyStrategy{}, + }, + "api-key/cookie": { + name: "api_key", + config: map[string]any{ + "in": "cookie", + "name": "my-api-key", + "value": "secret", + }, + expected: &apiKeyStrategy{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + strategy, err := authStrategy(tc.name, tc.config) + require.NoError(t, err) + + assert.IsTypef(t, tc.expected, strategy, "auth strategy should be of the expected type") + }) + } +} diff --git a/request/builder.go b/request/builder.go new file mode 100644 index 0000000..7fd09b2 --- /dev/null +++ b/request/builder.go @@ -0,0 +1,301 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package request + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/google/go-jsonnet" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/fetcher" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +var ErrCancel = errors.New("request cancel by JsonNet") + +const ( + ContentTypeForm = "application/x-www-form-urlencoded" + ContentTypeJSON = "application/json" +) + +type ( + Dependencies interface { + logrusx.Provider + otelx.Provider + httpx.ClientProvider + jsonnetsecure.VMProvider + } + Builder struct { + r *retryablehttp.Request + Config *Config + deps Dependencies + cache *ristretto.Cache[[]byte, []byte] + bodySizeHint uint + } + options struct { + cache *ristretto.Cache[[]byte, []byte] + bodySizeHint uint + } + BuilderOption = func(*options) +) + +func WithCache(cache *ristretto.Cache[[]byte, []byte]) BuilderOption { + return func(o *options) { + o.cache = cache + } +} + +func WithBodySizeHint(hint uint) BuilderOption { + return func(o *options) { + o.bodySizeHint = hint + } +} + +func NewBuilder(c *Config, deps Dependencies, o ...BuilderOption) (*Builder, error) { + var opts options + for _, f := range o { + f(&opts) + } + + r, err := retryablehttp.NewRequest(c.Method, c.URL, nil) + if err != nil { + return nil, err + } + + c.header = make(http.Header, len(c.Headers)) + for k, v := range c.Headers { + c.header.Add(k, v) + } + if c.header.Get("Content-Type") == "" { + c.header.Set("Content-Type", ContentTypeJSON) + } + + c.auth, err = authStrategy(c.Auth.Type, c.Auth.Config) + if err != nil { + return nil, err + } + + return &Builder{ + r: r, + Config: c, + deps: deps, + cache: opts.cache, + bodySizeHint: opts.bodySizeHint, + }, nil +} + +func (b *Builder) addBody(ctx context.Context, body interface{}) (err error) { + ctx, span := b.deps.Tracer(ctx).Tracer().Start(ctx, "request.Builder.addBody") + defer otelx.End(span, &err) + + if isNilInterface(body) { + return nil + } + + if b.Config.TemplateURI == "" { + return errors.New("got empty template path for request with body") + } + + tpl, err := b.readTemplate(ctx) + if err != nil { + return err + } + + switch b.r.Header.Get("Content-Type") { + case ContentTypeForm: + if err := b.addURLEncodedBody(ctx, tpl, body); err != nil { + return err + } + case "": + b.r.Header.Set("Content-Type", ContentTypeJSON) + fallthrough + case ContentTypeJSON: + if err := b.addJSONBody(ctx, tpl, body); err != nil { + return err + } + default: + return errors.New("invalid config - incorrect Content-Type for request with body") + } + + return nil +} + +func (b *Builder) addJSONBody(ctx context.Context, jsonnetSnippet []byte, body interface{}) error { + buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint)) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", "") + + if err := enc.Encode(body); err != nil { + return errors.WithStack(err) + } + + vm, err := b.deps.JsonnetVM(ctx) + if err != nil { + return errors.WithStack(err) + } + vm.TLACode("ctx", buf.String()) + + res, err := vm.EvaluateAnonymousSnippet( + b.Config.TemplateURI, + string(jsonnetSnippet), + ) + if err != nil { + // Unfortunately we can not use errors.As / errors.Is, see: + // https://github.com/google/go-jsonnet/issues/592 + if strings.Contains(err.Error(), (&jsonnet.RuntimeError{Msg: "cancel"}).Error()) { + return errors.WithStack(ErrCancel) + } + + return errors.WithStack(err) + } + + rb := strings.NewReader(res) + if err := b.r.SetBody(io.NopCloser(rb)); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (b *Builder) addURLEncodedBody(ctx context.Context, jsonnetSnippet []byte, body interface{}) error { + buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint)) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", "") + + if err := enc.Encode(body); err != nil { + return errors.WithStack(err) + } + + vm, err := b.deps.JsonnetVM(ctx) + if err != nil { + return errors.WithStack(err) + } + vm.TLACode("ctx", buf.String()) + + res, err := vm.EvaluateAnonymousSnippet(b.Config.TemplateURI, string(jsonnetSnippet)) + if err != nil { + return errors.WithStack(err) + } + + values := map[string]string{} + if err := json.Unmarshal([]byte(res), &values); err != nil { + return errors.WithStack(err) + } + + u := url.Values{} + + for key, value := range values { + u.Add(key, value) + } + + rb := strings.NewReader(u.Encode()) + if err := b.r.SetBody(io.NopCloser(rb)); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (b *Builder) BuildRequest(ctx context.Context, body interface{}) (*retryablehttp.Request, error) { + b.r.Header = b.Config.header + b.Config.auth.apply(b.r) + + // According to the HTTP spec any request method, but TRACE is allowed to + // have a body. Even this is a bad practice for some of them, like for GET + if b.Config.Method != http.MethodTrace { + if err := b.addBody(ctx, body); err != nil { + return nil, err + } + } + + return b.r.WithContext(ctx), nil +} + +func (b *Builder) addRawBody(body any) (err error) { + if isNilInterface(body) { + return nil + } + buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint)) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + + if err := enc.Encode(body); err != nil { + return errors.WithStack(err) + } + switch contentType := b.r.Header.Get("Content-Type"); contentType { + case "": + b.r.Header.Set("Content-Type", ContentTypeJSON) + fallthrough + case ContentTypeJSON: + if err := b.r.SetBody(buf); err != nil { + return errors.WithStack(err) + } + default: + return herodot.ErrMisconfiguration().WithDetail("invalid_content_type", contentType) + } + + return nil +} + +func (b *Builder) BuildRawRequest(ctx context.Context, body any) (*retryablehttp.Request, error) { + b.r.Header = b.Config.header + b.Config.auth.apply(b.r) + + // According to the HTTP spec any request method, but TRACE is allowed to + // have a body. Even this is a bad practice for some of them, like for GET + if b.Config.Method != http.MethodTrace { + if err := b.addRawBody(body); err != nil { + return nil, err + } + } + + return b.r.WithContext(ctx), nil +} + +func (b *Builder) readTemplate(ctx context.Context) ([]byte, error) { + templateURI := b.Config.TemplateURI + + if templateURI == "" { + return nil, nil + } + + f := fetcher.NewFetcher(fetcher.WithClient(b.deps.HTTPClient(ctx)), fetcher.WithCache(b.cache, 60*time.Minute)) + tpl, err := f.FetchContext(ctx, templateURI) + if errors.Is(err, fetcher.ErrUnknownScheme) { + // legacy filepath + templateURI = "file://" + templateURI + b.deps.Logger().WithError(err).Warnf( + "support for filepaths without a 'file://' scheme will be dropped in the next release, please use %s instead in your config", + templateURI) + + tpl, err = f.FetchContext(ctx, templateURI) + } + // this handles the first error if it is a known scheme error, or the second fetch error + if err != nil { + return nil, err + } + + return tpl.Bytes(), nil +} + +func isNilInterface(i interface{}) bool { + return i == nil || (reflect.ValueOf(i).Kind() == reflect.Pointer && reflect.ValueOf(i).IsNil()) +} diff --git a/request/builder_test.go b/request/builder_test.go new file mode 100644 index 0000000..1139331 --- /dev/null +++ b/request/builder_test.go @@ -0,0 +1,343 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package request + +import ( + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type testRequestBody struct { + To string `json:"to"` + From string `json:"from"` + Body string `json:"body"` +} + +//go:embed stub/test_body.jsonnet +var testJSONNetTemplate []byte + +func TestBuildRequest(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + method string + url string + authStrategy AuthStrategy + expectedHeader http.Header + bodyTemplateURI string + body *testRequestBody + expectedJSONBody string + expectedRawBody string + config Config + }{ + { + name: "POST request without auth", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint1", + authStrategy: NewNoopAuthStrategy(), + bodyTemplateURI: "file://./stub/test_body.jsonnet", + body: &testRequestBody{ + To: "+15056445993", + From: "+12288534869", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+12288534869", + "to": "+15056445993" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint1", + Method: "POST", + TemplateURI: "file://./stub/test_body.jsonnet", + }, + }, + { + name: "POST request with legacy template path", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint1", + authStrategy: NewNoopAuthStrategy(), + bodyTemplateURI: "./stub/test_body.jsonnet", + body: &testRequestBody{ + To: "+15056445993", + From: "+12288534869", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+12288534869", + "to": "+15056445993" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint1", + Method: "POST", + TemplateURI: "./stub/test_body.jsonnet", + }, + }, + { + name: "POST request with base64 encoded template path", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint1", + authStrategy: NewNoopAuthStrategy(), + bodyTemplateURI: "base64://" + base64.StdEncoding.EncodeToString(testJSONNetTemplate), + body: &testRequestBody{ + To: "+15056445993", + From: "+12288534869", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+12288534869", + "to": "+15056445993" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint1", + Method: "POST", + TemplateURI: "base64://" + base64.StdEncoding.EncodeToString(testJSONNetTemplate), + }, + }, + { + name: "POST request with custom header", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint2", + authStrategy: NewNoopAuthStrategy(), + expectedHeader: map[string][]string{"Custom-Header": {"test"}}, + bodyTemplateURI: "file://./stub/test_body.jsonnet", + body: &testRequestBody{ + To: "+12127110378", + From: "+15822228108", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+15822228108", + "to": "+12127110378" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint2", + Method: "POST", + Headers: map[string]string{ + "Custom-Header": "test", + }, + TemplateURI: "file://./stub/test_body.jsonnet", + }, + }, + { + name: "GET request with body", + method: "GET", + url: "https://test.kratos.ory.sh/my_endpoint3", + authStrategy: NewBasicAuthStrategy("test-api-user", "secret"), + bodyTemplateURI: "file://./stub/test_body.jsonnet", + body: &testRequestBody{ + To: "+14134242223", + From: "+13104661805", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+13104661805", + "to": "+14134242223" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint3", + Method: "GET", + Auth: AuthConfig{ + Type: "basic_auth", + Config: map[string]any{ + "user": "test-api-user", + "password": "secret", + }, + }, + TemplateURI: "file://./stub/test_body.jsonnet", + }, + }, + { + name: "GET request without body", + method: "GET", + url: "https://test.kratos.ory.sh/my_endpoint4", + authStrategy: NewBasicAuthStrategy("test-api-user", "secret"), + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint4", + Method: "GET", + Auth: AuthConfig{ + Type: "basic_auth", + Config: map[string]any{ + "user": "test-api-user", + "password": "secret", + }, + }, + }, + }, + { + name: "DELETE request with body", + method: "DELETE", + url: "https://test.kratos.ory.sh/my_endpoint5", + authStrategy: NewAPIKeyStrategy("header", "my-api-key", "secret"), + bodyTemplateURI: "file://./stub/test_body.jsonnet", + body: &testRequestBody{ + To: "+12235499085", + From: "+14253787846", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+14253787846", + "to": "+12235499085" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint5", + Method: "DELETE", + TemplateURI: "file://./stub/test_body.jsonnet", + Auth: AuthConfig{ + Type: "api_key", + Config: map[string]any{ + "in": "header", + "name": "my-api-key", + "value": "secret", + }, + }, + }, + }, + { + name: "POST request with urlencoded body", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint6", + bodyTemplateURI: "file://./stub/test_body.jsonnet", + authStrategy: NewAPIKeyStrategy("cookie", "my-api-key", "secret"), + expectedHeader: map[string][]string{"Content-Type": {ContentTypeForm}}, + body: &testRequestBody{ + To: "+14134242223", + From: "+13104661805", + Body: "test-sms-body", + }, + expectedRawBody: "body=test-sms-body&from=%2B13104661805&to=%2B14134242223", + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint6", + Method: "POST", + TemplateURI: "file://./stub/test_body.jsonnet", + Headers: map[string]string{ + "Content-Type": ContentTypeForm, + }, + Auth: AuthConfig{ + Type: "api_key", + Config: map[string]any{ + "in": "cookie", + "name": "my-api-key", + "value": "secret", + }, + }, + }, + }, + { + name: "POST request with default body type", + method: "POST", + url: "https://test.kratos.ory.sh/my_endpoint7", + bodyTemplateURI: "file://./stub/test_body.jsonnet", + authStrategy: NewBasicAuthStrategy("test-api-user", "secret"), + expectedHeader: map[string][]string{"Content-Type": {ContentTypeJSON}}, + body: &testRequestBody{ + To: "+14134242223", + From: "+13104661805", + Body: "test-sms-body", + }, + expectedJSONBody: `{ + "body": "test-sms-body", + "from": "+13104661805", + "to": "+14134242223" + }`, + config: Config{ + URL: "https://test.kratos.ory.sh/my_endpoint7", + Method: "POST", + TemplateURI: "file://./stub/test_body.jsonnet", + Auth: AuthConfig{ + Type: "basic_auth", + Config: map[string]any{ + "user": "test-api-user", + "password": "secret", + }, + }, + }, + }, + } { + t.Run("request-type="+tc.name, func(t *testing.T) { + t.Parallel() + + rb, err := NewBuilder(&tc.config, newTestDependencyProvider(t)) + require.NoError(t, err) + + assert.Equal(t, tc.bodyTemplateURI, rb.Config.TemplateURI) + assert.Equal(t, tc.authStrategy, rb.Config.auth) + + req, err := rb.BuildRequest(context.Background(), tc.body) + require.NoError(t, err) + + assert.Equal(t, tc.url, req.URL.String()) + assert.Equal(t, tc.method, req.Method) + + if tc.expectedJSONBody != "" { + requestBody, err := req.BodyBytes() + require.NoError(t, err) + + assert.JSONEq(t, tc.expectedJSONBody, string(requestBody)) + } else if tc.expectedRawBody != "" { + requestBody, err := req.BodyBytes() + require.NoError(t, err) + + assert.Equal(t, tc.expectedRawBody, string(requestBody)) + } + + if tc.expectedHeader != nil { + mustContainHeader(t, tc.expectedHeader, req.Header) + } + }) + } + + t.Run("cancel request", func(t *testing.T) { + rb, err := NewBuilder(&Config{ + URL: "https://test.kratos.ory.sh/my_endpoint6", + Method: "POST", + TemplateURI: "file://./stub/cancel_body.jsonnet", + }, newTestDependencyProvider(t)) + require.NoError(t, err) + + _, err = rb.BuildRequest(context.Background(), json.RawMessage(`{}`)) + require.ErrorIs(t, err, ErrCancel) + }) +} + +type testDependencyProvider struct { + x.BasicRegistry + *jsonnetsecure.TestProvider +} + +func newTestDependencyProvider(t *testing.T) *testDependencyProvider { + return &testDependencyProvider{ + BasicRegistry: x.BasicRegistry{ + L: logrusx.New("kratos", "test"), + T: otelx.NewNoop(), + }, + TestProvider: jsonnetsecure.NewTestProvider(t), + } +} + +func mustContainHeader(t *testing.T, expected http.Header, actual http.Header) { + t.Helper() + for k := range expected { + require.Contains(t, actual, k) + assert.Equal(t, expected[k], actual[k]) + } +} diff --git a/request/config.go b/request/config.go new file mode 100644 index 0000000..6aff7d9 --- /dev/null +++ b/request/config.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package request + +import ( + "net/http" +) + +type ( + AuthConfig = struct { + Type string `json:"type" koanf:"type"` + Config map[string]any `json:"config" koanf:"config"` + } + ResponseConfig = struct { + Parse bool `json:"parse" koanf:"parse"` + Ignore bool `json:"ignore" koanf:"ignore"` + } + Config struct { + ID string `json:"id" koanf:"id"` + Method string `json:"method" koanf:"method"` + URL string `json:"url" koanf:"url"` + TemplateURI string `json:"body" koanf:"body"` + Headers map[string]string `json:"headers" koanf:"headers"` + Auth AuthConfig `json:"auth" koanf:"auth"` + EmitAnalyticsEvent *bool `json:"emit_analytics_event" koanf:"emit_analytics_event"` + CanInterrupt bool `json:"can_interrupt" koanf:"can_interrupt"` + Response ResponseConfig `json:"response" koanf:"response"` + + auth AuthStrategy + header http.Header + } +) diff --git a/request/stub/cancel_body.jsonnet b/request/stub/cancel_body.jsonnet new file mode 100644 index 0000000..68b89aa --- /dev/null +++ b/request/stub/cancel_body.jsonnet @@ -0,0 +1 @@ +error "cancel" diff --git a/request/stub/test_body.jsonnet b/request/stub/test_body.jsonnet new file mode 100644 index 0000000..63f5ec8 --- /dev/null +++ b/request/stub/test_body.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + from: ctx.from, + to: ctx.to, + body: ctx.body, +} diff --git a/schema/context.go b/schema/context.go new file mode 100644 index 0000000..98a6f4f --- /dev/null +++ b/schema/context.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "strings" + + "github.com/ory/jsonschema/v3" +) + +func ContextSetRoot(err *jsonschema.ValidationError, head string) *jsonschema.ValidationError { + err.InstancePtr = prefixPointer(err.InstancePtr, head) + + if err.Context != nil { + switch ctx := err.Context.(type) { + case *jsonschema.ValidationErrorContextRequired: + for k, pointer := range ctx.Missing { + ctx.Missing[k] = prefixPointer(pointer, head) + } + } + } + + for k := range err.Causes { + err.Causes[k] = ContextSetRoot(err.Causes[k], head) + } + + return err +} + +func prefixPointer(pointer, head string) string { + if pointer == "#" || pointer == "#/" { + pointer = "#/" + head + } else { + pointer = "#/" + head + "/" + strings.TrimPrefix(pointer, "#/") + } + return pointer +} diff --git a/schema/context_test.go b/schema/context_test.go new file mode 100644 index 0000000..9a53932 --- /dev/null +++ b/schema/context_test.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/jsonschema/v3" +) + +func TestContextSetRoot(t *testing.T) { + for k, tc := range []struct { + in jsonschema.ValidationError + head string + expect jsonschema.ValidationError + }{ + { + head: "traits", + in: jsonschema.ValidationError{ + InstancePtr: "#/foo/bar", + Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"#/foo/bar/baz"}}, + Causes: []*jsonschema.ValidationError{{ + InstancePtr: "#/foo/bar", + Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"#/foo/bar/baz"}}, + }}, + }, + expect: jsonschema.ValidationError{ + InstancePtr: "#/traits/foo/bar", + Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"#/traits/foo/bar/baz"}}, + Causes: []*jsonschema.ValidationError{{ + InstancePtr: "#/traits/foo/bar", + Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"#/traits/foo/bar/baz"}}, + }}, + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + assert.EqualValues(t, tc.expect, *ContextSetRoot(&tc.in, tc.head)) + }) + } +} diff --git a/schema/errors.go b/schema/errors.go new file mode 100644 index 0000000..aad6bab --- /dev/null +++ b/schema/errors.go @@ -0,0 +1,424 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/kratos/text" +) + +type ValidationError struct { + *jsonschema.ValidationError + Messages text.Messages +} + +func NewRequiredError(missingPtr, missingFieldName string) error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: fmt.Sprintf("missing properties: %s", missingFieldName), + InstancePtr: missingPtr, + Context: &jsonschema.ValidationErrorContextRequired{ + Missing: []string{missingFieldName}, + }, + }, + Messages: new(text.Messages).Add(text.NewValidationErrorRequired(missingFieldName)), + }) +} + +func NewTOTPVerifierWrongError(instancePtr string) error { + t := text.NewErrorValidationTOTPVerifierWrong() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: instancePtr, + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewWebAuthnVerifierWrongError(instancePtr string) error { + t := text.NewErrorValidationWebAuthnVerifierWrong() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: instancePtr, + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewDeviceAuthnVerifierWrongError(instancePtr string) error { + t := text.NewErrorValidationDeviceAuthnVerifierWrong() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: instancePtr, + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewDeviceAuthnRelaxedAttestationNoLongerValidError(instancePtr string) error { + t := text.NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: instancePtr, + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewDeviceAuthnKeyReenrollmentRequiredError(instancePtr string) error { + t := text.NewErrorValidationDeviceAuthnKeyReenrollmentRequired() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: instancePtr, + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewLookupAlreadyUsed() error { + t := text.NewErrorValidationLookupAlreadyUsed() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(t), + }) +} + +func NewErrorValidationLookupInvalid() error { + t := text.NewErrorValidationLookupInvalid() + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: t.Text, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(t), + }) +} + +type ValidationErrorContextPasswordPolicyViolation struct { + Reason string +} + +func (r *ValidationErrorContextPasswordPolicyViolation) AddContext(_, _ string) {} + +func (r *ValidationErrorContextPasswordPolicyViolation) FinishInstanceContext() {} + +func NewPasswordPolicyViolationError(instancePtr string, message *text.Message) error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: fmt.Sprintf("the password does not fulfill the password policy because: %s", message.Text), + InstancePtr: instancePtr, + Context: &ValidationErrorContextPasswordPolicyViolation{ + Reason: message.Text, + }, + }, + Messages: new(text.Messages).Add(message), + }) +} + +func NewMissingIdentifierError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: "could not find any identifiers", + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationIdentifierMissing()), + }) +} + +type ValidationErrorContextInvalidCredentialsError struct{} + +func (r *ValidationErrorContextInvalidCredentialsError) AddContext(_, _ string) {} + +func (r *ValidationErrorContextInvalidCredentialsError) FinishInstanceContext() {} + +func NewInvalidCredentialsError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, + InstancePtr: "#/", + }, + 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"` + IdentifierHint string `json:"credential_identifier_hint"` +} + +func (r *ValidationErrorContextDuplicateCredentialsError) AddContext(_, _ string) {} + +func (r *ValidationErrorContextDuplicateCredentialsError) FinishInstanceContext() {} + +type DuplicateCredentialsHinter interface { + AvailableCredentials() []string + AvailableOIDCProviders() []string + IdentifierHint() string + HasHints() bool +} + +func NewDuplicateCredentialsError(err error) error { + if hinter := DuplicateCredentialsHinter(nil); errors.As(err, &hinter) && hinter.HasHints() { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `an account with the same identifier (email, phone, username, ...) exists already`, + InstancePtr: "#/", + Context: &ValidationErrorContextDuplicateCredentialsError{ + AvailableCredentials: hinter.AvailableCredentials(), + AvailableOIDCProviders: hinter.AvailableOIDCProviders(), + IdentifierHint: hinter.IdentifierHint(), + }, + }, + Messages: new(text.Messages).Add(text.NewErrorValidationDuplicateCredentialsWithHints(hinter.AvailableCredentials(), hinter.AvailableOIDCProviders(), hinter.IdentifierHint())), + }) + } + + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `an account with the same identifier (email, phone, username, ...) exists already`, + InstancePtr: "#/", + Context: &ValidationErrorContextDuplicateCredentialsError{}, + }, + Messages: new(text.Messages).Add(text.NewErrorValidationDuplicateCredentials()), + }) +} + +func NewNoLoginStrategyResponsible() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `could not find a strategy to login with`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginNoStrategyFound()), + }) +} + +func NewNoRegistrationStrategyResponsible() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `could not find a strategy to sign up with`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationRegistrationNoStrategyFound()), + }) +} + +func NewNoSettingsStrategyResponsible() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `could not find a strategy to update settings with`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationSettingsNoStrategyFound()), + }) +} + +func NewNoRecoveryStrategyResponsible() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `could not find a strategy to recover your account with`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationRecoveryNoStrategyFound()), + }) +} + +func NewNoVerificationStrategyResponsible() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `could not find a strategy to verify your account with`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationVerificationNoStrategyFound()), + }) +} + +func NewAddressNotVerifiedError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `account address not yet verified`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationAddressNotVerified()), + }) +} + +func NewNoTOTPDeviceRegistered() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `you have no TOTP device set up`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoTOTPDevice()), + }) +} + +func NewNoLookupDefined() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `you have no backup recovery codes set up`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoLookup()), + }) +} + +func NewNoWebAuthnRegistered() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `you have no WebAuthn device set up`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoWebAuthnDevice()), + }) +} + +func NewNoDeviceAuthnRegistered() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `you have no DeviceAuthn device set up`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoDeviceAuthnDevice()), + }) +} + +func NewHookValidationError(instancePtr, message string, messages text.Messages) *ValidationError { + return &ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: message, + InstancePtr: instancePtr, + }, + Messages: messages, + } +} + +type ValidationListError struct { + Validations []*ValidationError +} + +func (e ValidationListError) Error() string { + var detailError string + for pos, validationErr := range e.Validations { + detailError = detailError + fmt.Sprintf("\n(%d) %s", pos, validationErr.Error()) + } + return fmt.Sprintf("%d validation errors occurred:%s", len(e.Validations), detailError) +} + +func (e *ValidationListError) Add(v *ValidationError) { + e.Validations = append(e.Validations, v) +} + +func (e ValidationListError) HasErrors() bool { + return len(e.Validations) > 0 +} + +func (e *ValidationListError) WithError(instancePtr, message string, details text.Messages) { + e.Validations = append(e.Validations, &ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: message, + InstancePtr: instancePtr, + }, + Messages: details, + }) +} + +func NewValidationListError(errs []*ValidationError) error { + return errors.WithStack(&ValidationListError{Validations: errs}) +} + +func NewNoWebAuthnCredentials() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `account does not exist or has no security key set up`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationSuchNoWebAuthnUser()), + }) +} + +func NewNoCodeAuthnCredentials() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `account does not exist or has not setup up sign in with code`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoCodeUser()), + }) +} + +func NewTraitsMismatch() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the submitted form data has changed from the previous submission`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationTraitsMismatch()), + }) +} + +func NewRegistrationCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed()), + }) +} + +func NewLoginCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()), + }) +} + +func NewLinkedCredentialsDoNotMatch() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `linked credentials do not match; please start a new flow`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginLinkedCredentialsDoNotMatch()), + }) +} + +func NewUnknownAddressError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the supplied address does not match any known addresses.`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationAddressUnknown()), + }, + ) +} diff --git a/schema/errors_test.go b/schema/errors_test.go new file mode 100644 index 0000000..4e389f6 --- /dev/null +++ b/schema/errors_test.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/kratos/text" +) + +func TestListValidationErrors(t *testing.T) { + testErr := ValidationListError{} + + assert.False(t, testErr.HasErrors()) + + testErr.WithError("#/traits/password", "error message", new(text.Messages).Add(text.NewErrorValidationDuplicateCredentials())) + assert.True(t, testErr.HasErrors()) + assert.Len(t, testErr.Validations, 1) + + validationError := &ValidationError{ + 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()), + } + testErr.Add(validationError) + assert.Len(t, testErr.Validations, 2) + assert.Equal(t, "2 validation errors occurred:"+ + "\n(0) I[#/traits/password] S[] error message"+ + "\n(1) I[#/] S[] the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number", + testErr.Error()) +} diff --git a/schema/extension.go b/schema/extension.go new file mode 100644 index 0000000..38c81e5 --- /dev/null +++ b/schema/extension.go @@ -0,0 +1,184 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/embedx" + "github.com/ory/x/jsonschemax" +) + +const ( + ExtensionName string = "ory.sh/kratos" +) + +type ( + ExtensionConfig struct { + Credentials struct { + Password struct { + Identifier bool `json:"identifier"` + } `json:"password"` + 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"` + Code struct { + Identifier bool `json:"identifier"` + Via string `json:"via"` + } `json:"code"` + } `json:"credentials"` + Verification struct { + Via string `json:"via"` + } `json:"verification"` + Recovery struct { + Via string `json:"via"` + } `json:"recovery"` + Organization struct { + Matcher string `json:"matcher"` + } `json:"organizations"` + RawSchema map[string]interface{} `json:"-"` + } + + ValidateExtension interface { + Run(ctx jsonschema.ValidationContext, config ExtensionConfig, value interface{}) error + Finish() error + } + CompileExtension interface { + Run(ctx jsonschema.CompilerContext, config ExtensionConfig, rawSchema map[string]interface{}) error + } + + ExtensionRunner struct { + meta *jsonschema.Schema + compile func(ctx jsonschema.CompilerContext, rawSchema map[string]interface{}) (interface{}, error) + validate func(ctx jsonschema.ValidationContext, s interface{}, v interface{}) error + + validateRunners []ValidateExtension + compileRunners []CompileExtension + } + + 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...) + } +} + +func WithCompileRunners(runners ...CompileExtension) ExtensionRunnerOption { + return func(r *ExtensionRunner) { + r.compileRunners = append(r.compileRunners, runners...) + } +} + +func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*ExtensionRunner, error) { + var err error + r := new(ExtensionRunner) + // The extension runner compiles the bundled identity-extension meta + // schema, which contains no external refs. The disallow flag has no + // effect here, but we pass false to keep behavior strictly equivalent + // to the legacy jsonschema.NewCompiler(). + c := NewCompiler(false) + + if err = embedx.AddSchemaResources(c, embedx.IdentityExtension); err != nil { + return nil, errors.WithStack(err) + } + + r.meta, err = c.Compile(ctx, embedx.IdentityExtension.GetSchemaID()) + if err != nil { + return nil, errors.WithStack(err) + } + + r.compile = func(ctx jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) { + if raw, ok := m[ExtensionName]; ok { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(raw); err != nil { + return nil, errors.WithStack(err) + } + + var e ExtensionConfig + if err := json.NewDecoder(&b).Decode(&e); err != nil { + return nil, errors.WithStack(err) + } + + for _, runner := range r.compileRunners { + if err := runner.Run(ctx, e, m); err != nil { + return nil, err + } + } + e.RawSchema = m + + return &e, nil + } + return nil, nil + } + + r.validate = func(ctx jsonschema.ValidationContext, s interface{}, v interface{}) error { + c, ok := s.(*ExtensionConfig) + if !ok { + return nil + } + + for _, runner := range r.validateRunners { + if err := runner.Run(ctx, *c, v); err != nil { + return err + } + } + return nil + } + + for _, opt := range opts { + opt(r) + } + + return r, nil +} + +func (r *ExtensionRunner) Register(compiler *jsonschema.Compiler) *ExtensionRunner { + compiler.Extensions[ExtensionName] = r.Extension() + return r +} + +func (r *ExtensionRunner) Extension() jsonschema.Extension { + return jsonschema.Extension{ + Meta: r.meta, + Compile: r.compile, + Validate: r.validate, + } +} + +func (r *ExtensionRunner) AddRunner(run ValidateExtension) *ExtensionRunner { + r.validateRunners = append(r.validateRunners, run) + return r +} + +func (r *ExtensionRunner) Finish() error { + for _, runner := range r.validateRunners { + if err := runner.Finish(); err != nil { + return err + } + } + return nil +} diff --git a/schema/extension_test.go b/schema/extension_test.go new file mode 100644 index 0000000..d430612 --- /dev/null +++ b/schema/extension_test.go @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/jsonschema/v3" + _ "github.com/ory/jsonschema/v3/fileloader" +) + +type extensionStub struct { + identifiers []string + accountNames []string +} + +func (r *extensionStub) Run(ctx jsonschema.ValidationContext, config ExtensionConfig, value interface{}) error { + if config.Credentials.Password.Identifier { + r.identifiers = append(r.identifiers, fmt.Sprintf("%s", value)) + } + if config.Credentials.TOTP.AccountName { + r.accountNames = append(r.accountNames, fmt.Sprintf("%s", value)) + } + return nil +} + +func (r *extensionStub) Finish() error { + return nil +} + +var ctx = context.Background() + +func TestExtensionRunner(t *testing.T) { + t.Run("method=get identifier", func(t *testing.T) { + for k, tc := range []struct { + expectErr error + schema string + doc string + expect []string + }{ + { + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/schema.json", + expect: []string{"foo@ory.sh"}, + }, + { + doc: `{"emails":["foo@ory.sh","bar@ory.sh"]}`, + schema: "file://./stub/extension/schema.nested.json", + expect: []string{"foo@ory.sh", "bar@ory.sh"}, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c := jsonschema.NewCompiler() + runner, err := NewExtensionRunner(ctx) + require.NoError(t, err) + + r := new(extensionStub) + runner.AddRunner(r).Register(c) + + err = c.MustCompile(ctx, tc.schema).Validate(bytes.NewBufferString(tc.doc)) + if tc.expectErr != nil { + require.EqualError(t, err, tc.expectErr.Error()) + } + + assert.EqualValues(t, tc.expect, r.identifiers) + assert.EqualValues(t, tc.expect, r.accountNames) + }) + } + }) + + t.Run("method=applies meta schema", func(t *testing.T) { + c := jsonschema.NewCompiler() + runner, err := NewExtensionRunner(ctx) + require.NoError(t, err) + + runner.Register(c) + + _, err = c.Compile(ctx, "file://./stub/extension/invalid.schema.json") + assert.Error(t, err) + assert.Contains(t, err.Error(), "expected boolean, but got number") + }) +} diff --git a/schema/handler.go b/schema/handler.go new file mode 100644 index 0000000..c730ca0 --- /dev/null +++ b/schema/handler.go @@ -0,0 +1,265 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/fs" + "net/http" + "net/url" + "os" + "strings" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/errorsx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/pagination/migrationpagination" +) + +type ( + handlerDependencies interface { + httpx.WriterProvider + logrusx.Provider + IdentitySchemaProvider + nosurfx.CSRFProvider + config.Provider + otelx.Provider + httpx.ClientProvider + } + Handler struct { + r handlerDependencies + } + HandlerProvider interface { + SchemaHandler() *Handler + } +) + +func NewHandler(r handlerDependencies) *Handler { + return &Handler{r: r} +} + +const ( + SchemasPath string = "schemas" + maxSchemaSize = 1024 * 1024 // 1 MB +) + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.r.CSRFHandler().IgnoreGlobs( + "/"+SchemasPath+"/*", + httprouterx.AdminPrefix+"/"+SchemasPath+"/*", + ) + public.GET(fmt.Sprintf("/%s/{id}", SchemasPath), h.getIdentitySchema) + public.GET(fmt.Sprintf("/%s", SchemasPath), h.getAll) + public.GET(fmt.Sprintf("%s/%s/{id}", httprouterx.AdminPrefix, SchemasPath), h.getIdentitySchema) + public.GET(fmt.Sprintf("%s/%s", httprouterx.AdminPrefix, SchemasPath), h.getAll) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(fmt.Sprintf("/%s/{id}", SchemasPath), redir.RedirectToPublicRoute(h.r)) + admin.GET(fmt.Sprintf("/%s", SchemasPath), redir.RedirectToPublicRoute(h.r)) +} + +// Raw JSON Schema +// +// swagger:model identitySchema +type _ json.RawMessage + +// Get Identity JSON Schema Response +// +// swagger:parameters getIdentitySchema +type _ struct { + // ID must be set to the ID of schema you want to get + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route GET /schemas/{id} identity getIdentitySchema +// +// # Get Identity JSON Schema +// +// Return a specific identity schema. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: identitySchema +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) getIdentitySchema(w http.ResponseWriter, r *http.Request) { + ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "schema.Handler.getIdentitySchema") + defer span.End() + + ss, err := h.r.IdentityTraitsSchemas(ctx) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + id := r.PathValue("id") + s, err := ss.GetByID(id) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound().WithReasonf("Identity schema `%s` could not be found.", id))) + return + } + + raw, err := h.ReadSchema(ctx, s.URL) + if err != nil { + code, ok := errorsx.GetCodeFromHerodotError(err) + + if errors.Is(err, fs.ErrNotExist) || (ok && code == http.StatusNotFound) { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrMisconfiguration().WithReason("The file for this JSON Schema ID could not be found/fetched. This is a configuration issue.").WithDebugf("%+v", err))) + } else if ok && code == http.StatusBadGateway { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrUpstreamError().WithReason("The file for this JSON Schema ID could not be fetched. This is an upstream issue.").WithDebugf("%+v", err))) + } else { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError().WithReason("The file for this JSON Schema ID could not be read. This is an I/O issue.").WithDebugf("%+v", err))) + } + return + } + + w.Header().Add("Content-Type", "application/json") + h.r.Writer().Write(w, r, json.RawMessage(raw)) +} + +// List of Identity JSON Schemas +// +// swagger:model identitySchemas +type IdentitySchemas []identitySchemaContainer + +// An Identity JSON Schema Container +// +// swagger:model identitySchemaContainer +type identitySchemaContainer struct { + // The ID of the Identity JSON Schema + // required: true + ID string `json:"id"` + // The actual Identity JSON Schema + // required: true + Schema json.RawMessage `json:"schema"` +} + +// List Identity JSON Schemas Response +// +// swagger:parameters listIdentitySchemas +type _ struct { + migrationpagination.RequestParameters +} + +// List Identity JSON Schemas Response +// +// swagger:response identitySchemas +type _ struct { + migrationpagination.ResponseHeaderAnnotation + + // in: body + Body IdentitySchemas +} + +// swagger:route GET /schemas identity listIdentitySchemas +// +// # Get all Identity Schemas +// +// Returns a list of all identity schemas currently in use. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: identitySchemas +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) getAll(w http.ResponseWriter, r *http.Request) { + ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "schema.Handler.getAll") + defer span.End() + + page, itemsPerPage := x.ParsePagination(r) + + allSchemas, err := h.r.IdentityTraitsSchemas(r.Context()) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + total := allSchemas.Total() + schemas := allSchemas.List(page, itemsPerPage) + + ss := make(IdentitySchemas, len(schemas)) + for i, schema := range schemas { + raw, err := h.ReadSchema(ctx, schema.URL) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("The file for a JSON Schema ID could not be found or opened. This is a configuration issue.").WithWrap(err))) + return + } + ss[i] = identitySchemaContainer{ + ID: schema.ID, + Schema: raw, + } + } + + x.PaginationHeader(w, *r.URL, int64(total), page, itemsPerPage) + h.r.Writer().Write(w, r, ss) +} + +func (h *Handler) ReadSchema(ctx context.Context, uri *url.URL) (data []byte, err error) { + ctx, span := h.r.Tracer(ctx).Tracer().Start(ctx, "schema.Handler.ReadSchema") + defer otelx.End(span, &err) + + switch uri.Scheme { + case "file": + data, err = os.ReadFile(uri.Host + uri.Path) //nolint:gosec + if err != nil { + return nil, errors.WithStack(fmt.Errorf("could not read schema file: %w", err)) + } + case "base64": + data, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(uri.String(), "base64://")) + if err != nil { + return nil, errors.WithStack(fmt.Errorf("could not decode schema file: %w", err)) + } + default: + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, errors.WithStack(fmt.Errorf("could not create request: %w", err)) + } + resp, err := h.r.HTTPClient(ctx).Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("could not fetch schema").WithError(err.Error()).WithDetail("uri", uri)) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, herodot.ErrNotFound().WithDetail("url", uri) + } + return nil, errors.WithStack(herodot.ErrUpstreamError().WithError("upstream error").WithDetail("status_code", resp.StatusCode).WithDetail("uri", uri)) + } + data, err = io.ReadAll(io.LimitReader(resp.Body, maxSchemaSize)) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("could not read schema response").WithError(err.Error()).WithDetail("uri", uri)) + } + } + return data, nil +} diff --git a/schema/handler_test.go b/schema/handler_test.go new file mode 100644 index 0000000..1649260 --- /dev/null +++ b/schema/handler_test.go @@ -0,0 +1,232 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "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/pkg" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/urlx" +) + +func TestHandler(t *testing.T) { + router := httprouterx.NewRouterPublic() + ts := contextx.NewConfigurableTestServer(router) + t.Cleanup(ts.Close) + + mux := http.NewServeMux() + mux.HandleFunc("GET /identity.schema.json", func(w http.ResponseWriter, r *http.Request) { + file, err := os.Open("./stub/identity.schema.json") + require.NoError(t, err) + _, err = io.Copy(w, file) + require.NoError(t, err) + }) + mux.HandleFunc("GET /500", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + }) + fileServer := httptest.NewServer(mux) + t.Cleanup(fileServer.Close) + + schemas := map[string]struct { + uri string + getRaw func() ([]byte, error) + expectedHttpResponseCode int + }{ + "default": { + uri: "file://./stub/identity.schema.json", + getRaw: func() ([]byte, error) { return os.ReadFile("./stub/identity.schema.json") }, + expectedHttpResponseCode: http.StatusOK, + }, + "identity2": { + uri: "file://./stub/identity-2.schema.json", + getRaw: func() ([]byte, error) { return os.ReadFile("./stub/identity-2.schema.json") }, + expectedHttpResponseCode: http.StatusOK, + }, + "base64": { + uri: "base64://ewogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAicHJvcGVydGllcyI6IHsKICAgICJiYXIiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJiYXIiCiAgXQp9", + getRaw: func() ([]byte, error) { + return base64.StdEncoding.DecodeString("ewogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAicHJvcGVydGllcyI6IHsKICAgICJiYXIiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJiYXIiCiAgXQp9") + }, + expectedHttpResponseCode: http.StatusOK, + }, + "unreachable": { + uri: "http://127.0.0.1:12345/unreachable-schema", + getRaw: func() ([]byte, error) { + return nil, fmt.Errorf("connection refused") + }, + expectedHttpResponseCode: http.StatusBadGateway, + }, + "no-file": { + uri: "file://./stub/does-not-exist.schema.json", + getRaw: func() ([]byte, error) { return nil, fmt.Errorf("no such file or directory") }, + expectedHttpResponseCode: http.StatusInternalServerError, + }, + "directory": { + uri: "file://./stub", + getRaw: func() ([]byte, error) { return nil, fmt.Errorf("is a directory") }, + // On an existing directory, `open(2)` succeeds but `read(2)` fails so it looks like an I/O error. + expectedHttpResponseCode: http.StatusInternalServerError, + }, + "file-network": { + uri: fileServer.URL + "/identity.schema.json", + getRaw: func() ([]byte, error) { return os.ReadFile("./stub/identity.schema.json") }, + expectedHttpResponseCode: http.StatusOK, + }, + "file-network-not-found": { + uri: fileServer.URL + "/not-found", + getRaw: func() ([]byte, error) { return nil, fmt.Errorf("could not be found") }, + expectedHttpResponseCode: http.StatusInternalServerError, + }, + "file-network-500": { + uri: fileServer.URL + "/500", + getRaw: func() ([]byte, error) { return nil, fmt.Errorf("giving up") }, + expectedHttpResponseCode: http.StatusBadGateway, + }, + "preset://email": { + uri: "file://./stub/identity-2.schema.json", + getRaw: func() ([]byte, error) { return os.ReadFile("./stub/identity-2.schema.json") }, + expectedHttpResponseCode: http.StatusOK, + }, + } + configSchemas := make(config.Schemas, 0, len(schemas)) + for id, s := range schemas { + configSchemas = append(configSchemas, config.Schema{ + ID: id, + URL: s.uri, + }) + } + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyPublicBaseURL: ts.URL, + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: configSchemas, + })) + reg.SchemaHandler().RegisterPublicRoutes(router) + + getReq := func(ctx context.Context, t *testing.T, path string, expectCode int) []byte { + res, err := ts.Client(ctx).Get(ts.URL + path) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + require.EqualValuesf(t, expectCode, res.StatusCode, "%s", body) + return body + } + + for id, s := range schemas { + t.Run(fmt.Sprintf("case=get %s schema", id), func(t *testing.T) { + + _, err := s.getRaw() + actual := getReq(t.Context(), t, fmt.Sprintf("/schemas/%s", url.PathEscape(id)), s.expectedHttpResponseCode) + require.True(t, json.Valid(actual), string(actual)) + + switch s.expectedHttpResponseCode { + case http.StatusOK: + require.NoError(t, err) + case http.StatusInternalServerError, http.StatusBadGateway: + + default: + panic("unreachable") + } + }) + } + + t.Run("case=get schema with base64 encoded ID", func(t *testing.T) { + t.Parallel() + + expected, err := schemas["preset://email"].getRaw() + require.NoError(t, err) + + actual := getReq(t.Context(), t, "/schemas/"+base64.RawURLEncoding.EncodeToString([]byte("preset://email")), http.StatusOK) + require.JSONEq(t, string(expected), string(actual)) + }) + + t.Run("case=get all schemas", func(t *testing.T) { + t.Parallel() + + defaultSchema, err := configSchemas.FindSchemaByID("default") + require.NoError(t, err) + identity2Schema, err := configSchemas.FindSchemaByID("identity2") + require.NoError(t, err) + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{*defaultSchema, *identity2Schema}) + + getSchemasPaginated := func(t *testing.T, page, perPage, expectCode int) []byte { + return getReq(ctx, t, fmt.Sprintf("/schemas?page=%d&per_page=%d", page, perPage), expectCode) + } + + body := getSchemasPaginated(t, 0, 10, http.StatusOK) + + var result []client.IdentitySchemaContainer + require.NoErrorf(t, json.Unmarshal(body, &result), "%s", body) + + var actualIDs []string + for _, s := range result { + actualIDs = append(actualIDs, s.Id) + } + assert.Equal(t, []string{defaultSchema.ID, identity2Schema.ID}, actualIDs) + + assertCorrectSchema := func(t *testing.T, r client.IdentitySchemaContainer) { + expected, err := schemas[r.Id].getRaw() + require.NoError(t, err) + actual, err := json.Marshal(r.Schema) + require.NoError(t, err) + assert.JSONEq(t, string(expected), string(actual)) + } + + for _, r := range result { + assertCorrectSchema(t, r) + } + + for page := range 2 { + t.Run(fmt.Sprintf("page=%d", page), func(t *testing.T) { + body := getSchemasPaginated(t, page, 1, http.StatusOK) + + var result []client.IdentitySchemaContainer + require.NoError(t, json.Unmarshal(body, &result)) + + require.Len(t, result, 1) + assert.Equal(t, actualIDs[page], result[0].Id) + assertCorrectSchema(t, result[0]) + }) + } + }) + + t.Run("case=read schema", func(t *testing.T) { + t.Parallel() + + for _, s := range schemas { + expected, expectedErr := s.getRaw() + + actual, err := reg.SchemaHandler().ReadSchema(t.Context(), urlx.ParseOrPanic(s.uri)) + if expectedErr == nil { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, expectedErr.Error()) // not using error.is because some of the errors are not accessible + } + + if expectedErr == nil { + require.JSONEq(t, string(expected), string(actual)) + } + } + }) +} diff --git a/schema/loader.go b/schema/loader.go new file mode 100644 index 0000000..f6df4ef --- /dev/null +++ b/schema/loader.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/url" + + _ "github.com/ory/jsonschema/v3/base64loader" + _ "github.com/ory/jsonschema/v3/fileloader" + "github.com/ory/jsonschema/v3/httploader" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/httpx" +) + +// loadRefURL resolves the URL of a `$ref` inside a schema. It enforces the +// scheme allowlist and then delegates to the jsonschema package's global +// loader table. The global `file` loader remains registered so that +// operator-configured top-level schema URLs (resolved outside the compiler) +// keep working. +func loadRefURL(ctx context.Context, raw string) (io.ReadCloser, error) { + u, err := url.Parse(raw) + if err != nil { + return nil, fmt.Errorf("invalid $ref URL %q: %w", raw, err) + } + if u.Scheme != "base64" { + return nil, fmt.Errorf("$ref scheme %q is not permitted in identity schemas", u.Scheme) + } + return jsonschema.LoadURL(ctx, raw) +} + +// NewCompiler returns a jsonschema.Compiler. When disallowRefs is true, the +// compiler rejects `file://` URLs (and any other non-allowlisted scheme) in +// `$ref` values, preventing an attacker-supplied schema from reading local +// files on the Kratos host. When disallowRefs is false, the compiler uses +// the jsonschema library's default loader table, which preserves legacy +// behavior for operators who intentionally reference local files. +// +// The flag is controlled by `security.disallow_ref_in_identity_schemas`. +// Ory Network forces it on. +func NewCompiler(disallowRefs bool) *jsonschema.Compiler { + c := jsonschema.NewCompiler() + if disallowRefs { + c.LoadURL = loadRefURL + } + return c +} + +// NewCompilerWithURL returns a NewCompiler with the top-level schema at +// schemaURL already loaded and registered via the trusted global loader +// (which always supports `file://` for operator-configured top-level URLs, +// regardless of disallowRefs). See NewCompiler for the semantics of +// disallowRefs. +func NewCompilerWithURL(ctx context.Context, schemaURL string, disallowRefs bool) (*jsonschema.Compiler, error) { + ctx = ensureGuardedHTTPClient(ctx) + + resource, err := jsonschema.LoadURL(ctx, schemaURL) + if err != nil { + return nil, errors.WithStack(err) + } + defer func() { _ = resource.Close() }() + + // Read the body with a hard size cap so a malicious schema URL cannot + // OOM kratos by returning a multi-GB body. + data, err := io.ReadAll(io.LimitReader(resource, MaxSchemaBodyBytes+1)) + if err != nil { + return nil, errors.WithStack(err) + } + if len(data) > MaxSchemaBodyBytes { + return nil, errors.Errorf("identity schema rejected: body exceeds %d bytes", MaxSchemaBodyBytes) + } + + // Decode once for structural prevalidation. The upstream compiler will + // decode again from the same bytes — accepting a small CPU duplication + // in exchange for the security gate. + var doc any + if err := json.Unmarshal(data, &doc); err != nil { + return nil, errors.WithStack(err) + } + if err := preValidateSchema(doc); err != nil { + return nil, errors.WithStack(err) + } + + c := NewCompiler(disallowRefs) + if err := c.AddResource(schemaURL, bytes.NewReader(data)); err != nil { + return nil, errors.WithStack(err) + } + return c, nil +} + +var defaultHTTPClient = httpx.NewResilientClient(httpx.ResilientClientDisallowInternalIPs()) + +// ensureGuardedHTTPClient guarantees that the jsonschema httploader will +// pick up a client whose dialer rejects internal IP ranges. +func ensureGuardedHTTPClient(ctx context.Context) context.Context { + if ctx.Value(httploader.ContextKey) != nil { + return ctx + } + return context.WithValue(ctx, httploader.ContextKey, defaultHTTPClient) +} diff --git a/schema/prevalidate.go b/schema/prevalidate.go new file mode 100644 index 0000000..4ad8ba1 --- /dev/null +++ b/schema/prevalidate.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" +) + +// MaxSchemaBodyBytes caps the raw byte size of an identity schema fetched +// from any source. Documents above this size cannot be cached or compiled. +// The cap protects against memory exhaustion from a customer-controlled URL +// returning an arbitrarily large body. It is independent of the schema's +// internal structure: realistic identity schemas are well under this limit. +const MaxSchemaBodyBytes = 1 << 20 // 1 MiB. + +// preValidateSchema walks a parsed identity schema document and rejects +// patterns that would let a customer-supplied schema crash kratos at +// compile or validate time. +// +// Specifically, this function rejects: +// +// - Any cycle in the document's `$ref` chain graph: a sequence of `$ref` +// nodes P₀ → P₁ → … → Pₙ → P₀ in which every step is a `$ref`. The +// upstream compiler memoizes ref resolution to terminate compilation, +// but the resulting `*Schema` graph contains the cycle, and at +// validate time `s.Ref.validate(v)` recurses indefinitely and crashes +// the process via fatal stack overflow (jsonschema/v3 +// schema.go:155-169 has no validate-time cycle guard, and stack +// overflow is unrecoverable). +// - A `pattern` or `patternProperties` key whose value is not a valid +// regular expression — the upstream compiler uses regexp.MustCompile, +// which panics, and the compile path has no panic recovery. +// +// Cycles whose intermediate nodes have non-`$ref` validation (e.g. a +// `$ref` to an ancestor that contains a `properties` map) are NOT +// rejected: each cycle iteration consumes at least one level of input, so +// recursion is bounded by JSON parser depth. Detecting the dangerous +// pure-ref subgraph is the goal. +// +// This function does not bound the schema's structural size (nesting +// depth, key count, array arity). Realistic identity schemas vary widely, +// and bounding them at the loader layer rejected legitimate documents. +// The body size cap (MaxSchemaBodyBytes) plus Go stdlib's JSON nesting +// limit (10000) provide the structural backstop. +func preValidateSchema(doc any) error { + v := &preValidator{refs: map[string]string{}} + if err := v.walk(doc, ""); err != nil { + return err + } + return v.detectRefCycles() +} + +type preValidator struct { + // refs maps each `$ref` location's JSON-pointer path to its target + // JSON-pointer path. Targets that cannot be resolved as in-document + // fragments (external URLs, malformed refs) are excluded — those go + // through the loadRefURL scheme allowlist. + refs map[string]string +} + +func (p *preValidator) walk(v any, path string) error { + switch v := v.(type) { + case map[string]any: + // Record `$ref` for cycle detection in detectRefCycles. Root + // pointers (`#`, `#/`, empty) map to the empty path. Anything + // without a `#/` prefix is external — out of scope here; + // loadRefURL handles scheme enforcement. + if ref, ok := v["$ref"].(string); ok { + switch { + case ref == "" || ref == "#" || ref == "#/": + p.refs[path] = "" + case strings.HasPrefix(ref, "#/"): + p.refs[path] = strings.TrimPrefix(ref, "#") + } + } + + // Pre-compile `pattern` regexes so an invalid one returns a + // kratos-side error instead of panicking deep in + // regexp.MustCompile during the upstream compile. + if pat, ok := v["pattern"].(string); ok { + if _, err := regexp.Compile(pat); err != nil { + return fmt.Errorf("identity schema rejected: invalid regex in pattern: %w", err) + } + } + + // patternProperties keys are themselves regexes. + if patternProps, ok := v["patternProperties"].(map[string]any); ok { + for raw := range patternProps { + if _, err := regexp.Compile(raw); err != nil { + return fmt.Errorf("identity schema rejected: invalid regex in patternProperties key %q: %w", raw, err) + } + } + } + + for k, sub := range v { + if err := p.walk(sub, path+"/"+escapeJSONPointer(k)); err != nil { + return err + } + } + + case []any: + for i, sub := range v { + if err := p.walk(sub, path+"/"+strconv.Itoa(i)); err != nil { + return err + } + } + } + + return nil +} + +// detectRefCycles reports an error if the document's `$ref` chain graph +// contains a cycle. Walking from each `$ref` location, follow the target +// path. If the target is itself a `$ref` location, continue. If the chain +// revisits a location, the resulting `*Schema` graph has a cycle that +// crashes Validate via stack overflow. +// +// The chain ends as soon as it reaches a node that is not itself a `$ref` +// — that node has its own validation logic (`properties`, `type`, etc.) +// which consumes input on each cycle iteration, so the recursion is +// bounded. Only pure `$ref` chains form unbounded loops. +func (p *preValidator) detectRefCycles() error { + for start := range p.refs { + visited := map[string]struct{}{} + cur := start + var chain []string + for { + if _, ok := visited[cur]; ok { + idx := slices.Index(chain, cur) + cycle := append(chain[idx:], cur) + return fmt.Errorf("identity schema rejected: self-referential $ref cycle: %s", + formatRefCycle(cycle)) + } + visited[cur] = struct{}{} + chain = append(chain, cur) + next, ok := p.refs[cur] + if !ok { + break + } + cur = next + } + } + return nil +} + +// escapeJSONPointer encodes a property name as a JSON-pointer reference +// token (RFC 6901): `~` → `~0`, `/` → `~1`. The order matters — `~` must be +// escaped first so a literal `/` does not collide with the escape produced +// for `~`. +func escapeJSONPointer(s string) string { + s = strings.ReplaceAll(s, "~", "~0") + s = strings.ReplaceAll(s, "/", "~1") + return s +} + +// formatRefCycle renders a path slice as a human-readable arrow chain, +// rendering the empty (root) path as "#". +func formatRefCycle(paths []string) string { + parts := make([]string, len(paths)) + for i, p := range paths { + if p == "" { + parts[i] = "#" + } else { + parts[i] = "#" + p + } + } + return strings.Join(parts, " → ") +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 0000000..2739c28 --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,178 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "cmp" + "context" + "encoding/base64" + "io" + "net/url" + "strings" + "sync" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/pagination" + "github.com/ory/x/urlx" +) + +var _ IdentitySchemaList = (*Schemas)(nil) + +type Schemas []Schema + +type IdentitySchemaProvider interface { + IdentityTraitsSchemas(ctx context.Context) (IdentitySchemaList, error) +} + +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) + + if ss, ok := s.findSchemaByID(id); ok { + return ss, nil + } + + if decodedID, ok := TryDecodeID(id); ok { + if ss, ok := s.findSchemaByID(decodedID); ok { + return ss, nil + } + } + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to find JSON Schema ID: %s", id)) +} + +func (s Schemas) Total() int { + return len(s) +} + +func (s Schemas) List(page, perPage int) Schemas { + if page < 0 { + page = 0 + } + if perPage < 1 { + perPage = 1 + } + start, end := pagination.Index((page+1)*perPage, page*perPage, len(s)) + return s[start:end] +} + +func (s Schemas) findSchemaByID(id string) (*Schema, bool) { + for _, ss := range s { + if ss.ID == id { + return &ss, true + } + } + return nil, false +} + +var ( + orderedKeyCacheMutex sync.RWMutex + orderedKeyCache map[string][]string +) + +func init() { + orderedKeyCache = make(map[string][]string) +} + +func computeKeyPositions(schema []byte, dest *[]string, parents []string) { + switch gjson.GetBytes(schema, "type").String() { + case "object": + gjson.GetBytes(schema, "properties").ForEach(func(key, value gjson.Result) bool { + computeKeyPositions([]byte(value.Raw), dest, append(parents, strings.ReplaceAll(key.String(), ".", "\\."))) + return true + }) + default: + *dest = append(*dest, strings.Join(parents, ".")) + } +} + +func GetKeysInOrder(ctx context.Context, schemaRef string) ([]string, error) { + orderedKeyCacheMutex.RLock() + keysInOrder, ok := orderedKeyCache[schemaRef] + orderedKeyCacheMutex.RUnlock() + if !ok { + sio, err := jsonschema.LoadURL(ctx, schemaRef) + if err != nil { + return nil, errors.WithStack(err) + } + schema, err := io.ReadAll(io.LimitReader(sio, 1024*1024)) + if err != nil { + return nil, errors.WithStack(err) + } + + computeKeyPositions(schema, &keysInOrder, []string{}) + orderedKeyCacheMutex.Lock() + orderedKeyCache[schemaRef] = keysInOrder + orderedKeyCacheMutex.Unlock() + } + + return keysInOrder, nil +} + +type Schema struct { + 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 IDToURL(host, s.ID) +} + +func IDToURL(host *url.URL, id string) *url.URL { + return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(id))) +} + +func TryDecodeID(encoded string) (string, bool) { + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return "", false + } + return string(decoded), true +} diff --git a/schema/schema_test.go b/schema/schema_test.go new file mode 100644 index 0000000..3cebaae --- /dev/null +++ b/schema/schema_test.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/schema" + schematest "github.com/ory/kratos/schema/test" +) + +func TestDefaultIdentityTraitsProvider(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + schematest.TestIdentitySchemaProvider(t, schema.NewDefaultIdentityTraitsProvider(reg)) +} + +func TestGetKeysInOrder(t *testing.T) { + for i, tc := range []struct { + schemaRef string + keys []string + path string + }{ + {schemaRef: "file://./stub/identity.schema.json", keys: []string{"bar", "email"}}, + {schemaRef: "file://./stub/complex.schema.json", keys: []string{ + "meal.name", "meal.chef", "traits.email", + "traits.stringy", "traits.numby", "traits.booly", "traits.should_big_number", "traits.should_long_string", + "fruits", "vegetables", + }}, + } { + t.Run(fmt.Sprintf("case=%d schemaRef=%s", i, tc.schemaRef), func(t *testing.T) { + actual, err := schema.GetKeysInOrder(context.Background(), tc.schemaRef) + require.NoError(t, err) + assert.Equal(t, tc.keys, actual) + }) + } +} diff --git a/schema/stub/complex.schema.json b/schema/stub/complex.schema.json new file mode 100644 index 0000000..7afe6e9 --- /dev/null +++ b/schema/stub/complex.schema.json @@ -0,0 +1,87 @@ +{ + "$id": "https://example.com/complex.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["meal"], + "properties": { + "meal": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "chef": { + "type": "string" + } + } + }, + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + }, + "fruits": { + "type": "array", + "items": { + "type": "string" + } + }, + "vegetables": { + "type": "array", + "items": { + "$ref": "#/definitions/veggie" + } + } + }, + "definitions": { + "veggie": { + "type": "object", + "required": [ + "veggieName", + "veggieLike" + ], + "properties": { + "veggieName": { + "type": "string" + }, + "veggieLike": { + "type": "boolean" + }, + "veggieAmount": { + "type": "number" + } + } + } + } +} diff --git a/schema/stub/extension/invalid.schema.json b/schema/stub/extension/invalid.schema.json new file mode 100644 index 0000000..fefe888 --- /dev/null +++ b/schema/stub/extension/invalid.schema.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": 0 + } + } + } + } + } +} diff --git a/schema/stub/extension/schema.json b/schema/stub/extension/schema.json new file mode 100644 index 0000000..7d0d7ac --- /dev/null +++ b/schema/stub/extension/schema.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "totp": { + "account_name": true + } + } + } + } + } +} diff --git a/schema/stub/extension/schema.nested.json b/schema/stub/extension/schema.nested.json new file mode 100644 index 0000000..63d4c90 --- /dev/null +++ b/schema/stub/extension/schema.nested.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "totp": { + "account_name": true + } + } + } + } + } + } +} diff --git a/schema/stub/identity-2.schema.json b/schema/stub/identity-2.schema.json new file mode 100644 index 0000000..ec435cd --- /dev/null +++ b/schema/stub/identity-2.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } +} diff --git a/schema/stub/identity.schema.json b/schema/stub/identity.schema.json new file mode 100644 index 0000000..fe94126 --- /dev/null +++ b/schema/stub/identity.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } +} diff --git a/schema/stub/validator/firstName.schema.json b/schema/stub/validator/firstName.schema.json new file mode 100644 index 0000000..582fedb --- /dev/null +++ b/schema/stub/validator/firstName.schema.json @@ -0,0 +1,22 @@ +{ + "$id": "https://example.com/firstName.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false +} diff --git a/schema/stub/validator/whatever.schema.json b/schema/stub/validator/whatever.schema.json new file mode 100644 index 0000000..dd6e8a7 --- /dev/null +++ b/schema/stub/validator/whatever.schema.json @@ -0,0 +1,22 @@ +{ + "$id": "https://example.com/whatever.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "whatever": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false +} diff --git a/schema/test/schema_provider.go b/schema/test/schema_provider.go new file mode 100644 index 0000000..71044ad --- /dev/null +++ b/schema/test/schema_provider.go @@ -0,0 +1,103 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/schema" + "github.com/ory/x/contextx" + "github.com/ory/x/urlx" +) + +func TestIdentitySchemaProvider(t *testing.T, provider schema.IdentitySchemaProvider) { + urlFromID := func(id string) string { + return fmt.Sprintf("http://%s.com", id) + } + + schemas := schema.Schemas{ + {ID: "foo"}, + {ID: "preset://email"}, + {ID: config.DefaultIdentityTraitsSchemaID}, + } + + for i := range schemas { + raw := urlFromID(schemas[i].ID) + schemas[i].RawURL = raw + schemas[i].URL = urlx.ParseOrPanic(raw) + } + + ctx := contextx.WithConfigValues(t.Context(), map[string]any{ + config.ViperKeyIdentitySchemas: func() (cs config.Schemas) { + for _, s := range schemas { + cs = append(cs, config.Schema{ + ID: s.ID, + URL: s.RawURL, + }) + } + return cs + }(), + }) + + list, err := provider.IdentityTraitsSchemas(ctx) + require.NoError(t, err) + + t.Run("GetByID", func(t *testing.T) { + t.Run("case=get with raw schemaID", func(t *testing.T) { + for _, schema := range schemas { + actual, err := list.GetByID(schema.ID) + require.NoError(t, err) + require.Equal(t, schema, *actual) + } + }) + + t.Run("case=get with encoded schemaID", func(t *testing.T) { + for _, schema := range schemas { + encodedID := base64.RawURLEncoding.EncodeToString([]byte(schema.ID)) + + actual, err := list.GetByID(encodedID) + require.NoError(t, err) + require.Equal(t, schema, *actual) + } + }) + + t.Run("case=get default schema", func(t *testing.T) { + s1, err := list.GetByID("") + require.NoError(t, err) + s2, err := list.GetByID(config.DefaultIdentityTraitsSchemaID) + require.NoError(t, err) + assert.Equal(t, &schemas[2], s1) + assert.Equal(t, &schemas[2], s2) + }) + + t.Run("case=should return error on not existing id", func(t *testing.T) { + s, err := list.GetByID("not existing id") + require.Error(t, err) + assert.Equal(t, (*schema.Schema)(nil), s) + }) + }) + + t.Run("List", func(t *testing.T) { + t.Run("case=get all schemas", func(t *testing.T) { + p0 := list.List(0, 4) + assert.Equal(t, schemas, p0) + }) + + t.Run("case=smaller pages", func(t *testing.T) { + p0, p1 := list.List(0, 2), list.List(1, 2) + assert.Equal(t, schemas, append(p0, p1...)) + }) + + t.Run("case=indexes out of range", func(t *testing.T) { + p0 := list.List(-1, 10) + assert.Equal(t, schemas, p0) + }) + }) +} diff --git a/schema/validator.go b/schema/validator.go new file mode 100644 index 0000000..0e052ab --- /dev/null +++ b/schema/validator.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "context" + "encoding/json" + "sync" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/jsonschema/v3" +) + +type Validator struct { + sync.RWMutex +} + +type ValidationProvider interface { + SchemaValidator() *Validator +} + +func NewValidator() *Validator { + return &Validator{} +} + +type validatorOptions struct { + e *ExtensionRunner + disallowRefs bool +} + +func WithExtensionRunner(e *ExtensionRunner) func(*validatorOptions) { + return func(o *validatorOptions) { + o.e = e + } +} + +// WithDisallowRefs toggles the restriction on `$ref` URL schemes. When true, +// the compiler rejects `$ref` values whose scheme is not `http`, `https`, or +// `base64`, preventing local-file reads via `file://`. Callers should forward +// the `security.disallow_ref_in_identity_schemas` config value here. +func WithDisallowRefs(disallow bool) func(*validatorOptions) { + return func(o *validatorOptions) { + o.disallowRefs = disallow + } +} + +func (v *Validator) Validate( + ctx context.Context, + href string, + document json.RawMessage, + opts ...func(*validatorOptions), +) error { + var o validatorOptions + for _, opt := range opts { + opt(&o) + } + + compiler, err := NewCompilerWithURL(ctx, href, o.disallowRefs) + if err != nil { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to load or parse the JSON schema.").WithWrap(err).WithDebugf("%s", err)) + } + + if o.e != nil { + o.e.Register(compiler) + } + + schema, err := compiler.Compile(ctx, href) + if err != nil { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to parse validate JSON object against JSON schema.").WithWrap(err).WithDebugf("%s", err)) + } + + // we decode explicitly here, so we can handle the error, and it is not lost in the schema validation + dec, err := jsonschema.DecodeJSON(bytes.NewBuffer(document)) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to parse validate JSON object against JSON schema.").WithWrap(err).WithDebugf("%s", err)) + } + if err := schema.ValidateInterface(dec); err != nil { + return errors.WithStack(err) + } + + if o.e != nil { + return o.e.Finish() + } + + return nil +} diff --git a/schema/validator_test.go b/schema/validator_test.go new file mode 100644 index 0000000..7670210 --- /dev/null +++ b/schema/validator_test.go @@ -0,0 +1,416 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "cmp" + "context" + "encoding/base64" + "encoding/json" + stderrors "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + + "github.com/ory/herodot" + "github.com/ory/jsonschema/v3/httploader" + "github.com/ory/x/httpx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSchemaValidator(t *testing.T) { + router := http.NewServeMux() + fs := http.StripPrefix("/schema", http.FileServer(http.Dir("stub/validator"))) + router.HandleFunc("/schema/{name}", func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + }) + ts := httptest.NewServer(router) + defer ts.Close() + + ctx := context.WithValue(ctx, httploader.ContextKey, httpx.NewResilientClient()) + for k, tc := range []struct { + i json.RawMessage + err string + u string + }{ + { + i: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + }, + { + i: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": -1 }`), + err: "I[#/age] S[#/properties/age/minimum] must be >= 1 but found -1", + }, + { + i: json.RawMessage(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + err: `I[#] S[#/additionalProperties] additionalProperties "whatever" not allowed`, + }, + { + u: ts.URL + "/schema/whatever.schema.json", + i: json.RawMessage(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + }, + { + u: ts.URL + "/schema/whatever.schema.json", + i: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + err: `I[#] S[#/additionalProperties] additionalProperties "firstName" not allowed`, + }, + { + u: ts.URL, + i: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + err: "Invalid configuration", + }, + { + u: "not-a-url", + i: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + err: "Invalid configuration", + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + err := NewValidator().Validate(ctx, cmp.Or(tc.u, ts.URL+"/schema/firstName.schema.json"), tc.i) + if tc.err == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.err) + } + }) + } +} + +// TestSchemaValidator_FileRefExfiltration asserts that identity schemas +// cannot exfiltrate server-side files via `$ref: "file://..."`. The jsonschema +// `file` loader is registered process-wide so operator-configured top-level +// `file://` schemas still work, but per-compiler $ref resolution must reject +// the `file` scheme. +func TestSchemaValidator_FileRefExfiltration(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + secretContents := `"LEAKED_COOKIE_SECRET_VALUE"` + + schemaWithRef := func(refURL string) string { + return fmt.Sprintf(`{ + "$id": "https://test.example.com/probe.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": {"identifier": true} + } + } + }, + "field": {"$ref": %q} + }, + "required": ["email"] + } + } + }`, refURL) + } + + serveSchema := func(t *testing.T, body string) string { + mux := http.NewServeMux() + mux.HandleFunc("/schema", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(body)) + }) + ts := httptest.NewServer(mux) + t.Cleanup(ts.Close) + return ts.URL + "/schema" + } + + ctx := context.WithValue(context.Background(), httploader.ContextKey, httpx.NewResilientClient()) + + t.Run("case=file ref is rejected before any file access", func(t *testing.T) { + t.Parallel() + // Fragment that would pass validation if the file were read, and fail + // if not. Its contents are a valid JSON-schema fragment. + schemaFragment := fmt.Sprintf(`{"type": "string", "const": %s}`, secretContents) + fragmentPath := filepath.Join(dir, "fragment.json") + require.NoError(t, os.WriteFile(fragmentPath, []byte(schemaFragment), 0o600)) + + u := serveSchema(t, schemaWithRef("file://"+fragmentPath)) + + t.Run("with disallowRefs=true", func(t *testing.T) { + err := NewValidator().Validate(ctx, u, + json.RawMessage(`{"traits": {"email": "a@b.c", "field": "wrong"}}`), + WithDisallowRefs(true)) + require.Error(t, err) + + // The file scheme must be rejected, and the file contents must not + // leak into the error. + var he *herodot.DefaultError + require.True(t, stderrors.As(err, &he)) + assert.Contains(t, he.Debug(), `"file"`, "rejected scheme should appear in cause") + assert.NotContains(t, he.Error(), "LEAKED_COOKIE_SECRET_VALUE") + assert.NotContains(t, he.Debug(), "LEAKED_COOKIE_SECRET_VALUE") + }) + + t.Run("with disallowRefs=false preserves legacy exfiltration", func(t *testing.T) { + // When the feature flag is off (default for existing deployments), + // the legacy behavior must remain: the $ref is dereferenced and + // the file contents flow into validation errors. This documents + // the intentional opt-in nature of the mitigation. + err := NewValidator().Validate(ctx, u, + json.RawMessage(`{"traits": {"email": "a@b.c", "field": "wrong"}}`)) + require.Error(t, err) + assert.Contains(t, err.Error(), "LEAKED_COOKIE_SECRET_VALUE", + "with the flag off, the file contents should still be reachable — this test documents the exploitable default") + }) + }) +} + +// TestSchemaValidator_RejectsSelfReferentialRef asserts that an identity +// schema whose `$ref` resolves back to the same schema (e.g. `{"$ref":"#"}`) +// is rejected at compile time. +// +// Background: github.com/ory/jsonschema/v3 has compile-time cycle detection +// (compiler.go:139-202 caches the *Schema in r.schemas[ref] before recursing, +// so compilation terminates), but the resulting *Schema has its `Ref` field +// pointing at itself. At validate time, schema.go:155-169 unconditionally +// invokes s.Ref.validate(v) — for the self-pointer case this recurses +// indefinitely until the goroutine stack hits Go's 1GB limit and the runtime +// raises `fatal error: stack overflow`. That fatal is **not recoverable** +// via recover(); it terminates the entire process. A customer who can +// upload an identity schema with a self-pointer can therefore crash any +// kratos pod that compiles and validates against it. +// +// The compile path is the safe place to test this. Calling +// NewValidator().Validate would crash the test binary — see the comment in +// the body. The only legitimate fix is a kratos-side pre-parse gate that +// walks the parsed schema tree, resolves each `$ref`, and rejects refs whose +// target is the same schema (or any schema ancestor that produces an +// equivalent self-cycle). +func TestSchemaValidator_RejectsSelfReferentialRef(t *testing.T) { + t.Parallel() + + const schemaJSON = `{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#"}` + schemaURL := "base64://" + base64.StdEncoding.EncodeToString([]byte(schemaJSON)) + + ctx := context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + + // The kratos-side prevalidate gate (schema/prevalidate.go) rejects the + // schema at NewCompilerWithURL time, before the upstream compiler + // produces a *Schema with Ref pointing at itself. That is the safest + // place to fail: every caller of NewCompilerWithURL is protected, and + // no path can construct the self-pointer that crashes Validate. + _, err := NewCompilerWithURL(ctx, schemaURL, true) + require.Error(t, err, + "self-referential $ref ({\"$ref\":\"#\"}) must be rejected at the loader, "+ + "before producing a *Schema whose Ref field points at itself") + assert.Contains(t, err.Error(), "self-referential $ref") + + // End-to-end sanity: Validate must also reject and must not crash the + // process via fatal stack overflow. With the gate in place, Validate's + // internal call to NewCompilerWithURL fails before reaching the validate + // path that would otherwise infinite-recurse on s.Ref == s. + err = NewValidator().Validate(ctx, schemaURL, + json.RawMessage(`{}`), + WithDisallowRefs(true)) + require.Error(t, err) +} + +// TestSchemaValidator_PreValidate exercises the kratos-side pre-parse gate +// for identity schemas. Each subtest constructs a schema that hits one of +// the documented checks in prevalidate.go and asserts that NewValidator() +// rejects it before the upstream compiler runs. +// +// The gate enforces $ref-cycle detection and pattern-regex pre-compilation +// only. Structural counters (nesting depth, key count, array arity, total +// nodes) are intentionally not enforced here — realistic identity schemas +// vary widely and any limit would either be too tight for legitimate +// documents or too loose to add value above Go stdlib's JSON depth cap. +func TestSchemaValidator_PreValidate(t *testing.T) { + t.Parallel() + + loadCtx := func(t *testing.T) context.Context { + t.Helper() + return context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + } + + cases := []struct { + name string + schemaJSON string + errFragment string + }{ + { + name: "self-referential $ref to root", + schemaJSON: `{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#"}`, + errFragment: "self-referential $ref", + }, + { + name: "self-referential $ref to root, second attempt", + schemaJSON: `{ + "properties": { + "traits": { + "properties": { + "name": { + "$ref": "#/properties/traits/properties/name" + } + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +}`, + errFragment: "self-referential $ref", + }, + { + name: "indirect $ref cycle a → b → a", + schemaJSON: `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "a": {"$ref": "#/$defs/b"}, + "b": {"$ref": "#/$defs/a"} + }, + "$ref": "#/$defs/a" +}`, + errFragment: "self-referential $ref cycle", + }, + { + name: "invalid regex in pattern", + schemaJSON: `{"$schema":"http://json-schema.org/draft-07/schema#","type":"string","pattern":"(*invalid"}`, + errFragment: "invalid regex in pattern", + }, + { + name: "invalid regex in patternProperties key", + schemaJSON: `{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","patternProperties":{"(*invalid":{"type":"string"}}}`, + errFragment: "invalid regex in patternProperties", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + schemaURL := "base64://" + base64.StdEncoding.EncodeToString([]byte(c.schemaJSON)) + err := NewValidator().Validate(loadCtx(t), schemaURL, + json.RawMessage(`{"traits":{"name":"value"}}`), + WithDisallowRefs(true)) + require.Error(t, err) + assert.Contains(t, fmt.Sprintf("%+v", err), c.errFragment, + "expected error to mention %q, got %v", c.errFragment, err) + }) + } +} + +// TestSchemaValidator_AcceptsRealisticDeepSchema asserts that a schema with +// realistic structural complexity is accepted. Earlier hardening rejected +// documents above 32 nesting levels / 1024 keys / 128 array elements; that +// proved too tight for legitimate operator schemas. This test pins the +// looser policy: the gate must allow these dimensions, and only fail on +// the targeted unsafe shapes (cycles, invalid regex). +func TestSchemaValidator_AcceptsRealisticDeepSchema(t *testing.T) { + t.Parallel() + + const depth = 64 + var sb strings.Builder + sb.WriteString(`{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"traits":`) + for range depth { + sb.WriteString(`{"type":"object","properties":{"x":`) + } + sb.WriteString(`{"type":"string"}`) + for range depth { + sb.WriteString(`}}`) + } + sb.WriteString(`}}`) + + schemaURL := "base64://" + base64.StdEncoding.EncodeToString([]byte(sb.String())) + ctx := context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + + _, err := NewCompilerWithURL(ctx, schemaURL, true) + require.NoError(t, err, + "schema with %d nesting levels must be accepted; the prevalidate gate "+ + "intentionally does not enforce a depth cap", depth) +} + +// TestSchemaValidator_BodyTooLarge asserts that a schema URL whose response +// body exceeds MaxSchemaBodyBytes is rejected before any parse or compile +// runs. This guards against memory exhaustion when a customer-controlled +// URL serves a multi-megabyte (or larger) body. +func TestSchemaValidator_BodyTooLarge(t *testing.T) { + t.Parallel() + + body := bytes.Repeat([]byte("X"), MaxSchemaBodyBytes+1024) + mux := http.NewServeMux() + mux.HandleFunc("/big.schema.json", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(body) + }) + ts := httptest.NewServer(mux) + t.Cleanup(ts.Close) + + // httptest binds 127.0.0.1 — that is a private IP for the SSRF guard. + // Use a non-guarded resilient client so we exercise the size cap, not + // the SSRF guard. + ctx := context.WithValue(t.Context(), httploader.ContextKey, httpx.NewResilientClient()) + + err := NewValidator().Validate(ctx, ts.URL+"/big.schema.json", + json.RawMessage(`{}`), + WithDisallowRefs(true)) + require.Error(t, err) + assert.Contains(t, fmt.Sprintf("%+v", err), "body exceeds") +} + +// TestLoadRefURL pins the actual scheme allowlist enforced by loadRefURL. +// The implementation only accepts base64. Any future relaxation must +// update this test. +func TestLoadRefURL(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + raw string + wantSub string + }{ + {"file scheme rejected", "file:///etc/passwd", `scheme "file"`}, + {"http scheme rejected", "http://example.com/schema.json", `scheme "http"`}, + {"https scheme rejected", "https://example.com/schema.json", `scheme "https"`}, + {"data scheme rejected", "data:application/json,{}", `scheme "data"`}, + {"empty scheme rejected", "/relative/path", `scheme ""`}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + _, err := loadRefURL(t.Context(), c.raw) + require.Error(t, err) + assert.Contains(t, err.Error(), c.wantSub) + }) + } +} + +// TestEnsureGuardedHTTPClient_PrivateIPRejected verifies that a context +// without an attached HTTP client gets a default SSRF-guarded one when +// passed to NewCompilerWithURL. A schema URL pointing at httptest's +// loopback listener (127.0.0.1) must be rejected by the dialer before any +// HTTP request hits the test server. +func TestEnsureGuardedHTTPClient_PrivateIPRejected(t *testing.T) { + t.Parallel() + + var hit atomic.Bool + mux := http.NewServeMux() + mux.HandleFunc("/schema.json", func(w http.ResponseWriter, r *http.Request) { + hit.Store(true) + _, _ = w.Write([]byte(`{"type":"object"}`)) + }) + ts := httptest.NewServer(mux) + t.Cleanup(ts.Close) + + // Bare context — no httploader.ContextKey attached. NewCompilerWithURL + // must attach a guarded default client and refuse the loopback dial. + _, err := NewCompilerWithURL(t.Context(), ts.URL+"/schema.json", true) + require.Error(t, err, "fetch via SSRF-guarded default client must reject loopback addresses") + assert.False(t, hit.Load(), + "server handler was invoked — SSRF guard did not block the request") +} diff --git a/script/add-down-migrations.sh b/script/add-down-migrations.sh new file mode 100755 index 0000000..d2339af --- /dev/null +++ b/script/add-down-migrations.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# This script adds empty down migrations for any migration that misses them. +# Adding them is necessary because if the down migration is missing, the +# migration will only be applied once, even if the database is completely +# rolled back. +# In newer versions of ory/x/popx, the migration box enforces that all up +# migrations have a down migration. Use this script to add them. + +set -Eeuo pipefail + +for f in $(find . -name "*.up.sql"); do + base=$(basename $f) + dir=$(dirname $f) + migra_name=$(echo $base | sed -e "s/\..*\.up\.sql//" | sed -e "s/\.up\.sql//") + if ! compgen -G "$dir/$migra_name*.down.sql" > /dev/null; then + echo "Adding empty down migration for $f" + touch $dir/$migra_name.down.sql + fi +done diff --git a/script/debug-entrypoint.sh b/script/debug-entrypoint.sh new file mode 100755 index 0000000..7fd87d1 --- /dev/null +++ b/script/debug-entrypoint.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +FILE_CHANGE_LOG_FILE=/tmp/changes.log +SERVICE_ARGS="$@" + +log() { + echo "***** $1 *****" +} + +init() { + log "Initializing" + truncate -s 0 ${FILE_CHANGE_LOG_FILE} + tail -f ${FILE_CHANGE_LOG_FILE} & +} + +build() { + log "Building ${SERVICE_NAME} binary" + go env -w GOPROXY="proxy.golang.org,direct" + go mod download + go build -buildvcs=false -gcflags "all=-N -l" -o /${SERVICE_NAME} +} + +start() { + log "Starting Delve" + dlv --listen=:${DELVE_PORT} --headless=true --api-version=2 --accept-multiclient exec /${SERVICE_NAME} -- ${SERVICE_ARGS} & +} + +restart() { + build + + log "Killing old processes" + killall dlv + killall ${SERVICE_NAME} + + start +} + +watch() { + log "Watching for changes" + inotifywait -e "MODIFY,DELETE,MOVED_TO,MOVED_FROM" -m -r ${PWD} | ( + while true; do + read path action file + ext=${file: -3} + if [[ "$ext" == ".go" ]]; then + echo "$file" + fi + done + ) | ( + WAITING="" + while true; do + file="" + read -t 1 file + if test -z "$file"; then + if test ! -z "$WAITING"; then + echo "CHANGED" + WAITING="" + fi + else + log "File ${file} changed" >> ${FILE_CHANGE_LOG_FILE} + WAITING=1 + fi + done + ) | ( + while true; do + read TMP + restart + done + ) +} + +# main part +init +build +start +watch diff --git a/script/render-schemas.sh b/script/render-schemas.sh new file mode 100755 index 0000000..5f4570c --- /dev/null +++ b/script/render-schemas.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +schema_version="${1:-$(git rev-parse --short HEAD)}" + +sed "s!ory://tracing-config!https://raw.githubusercontent.com/ory/kratos/$schema_version/oryx/otelx/config.schema.json!g;" embedx/config.schema.json > .schemastore/config.schema.json + +git config user.email "60093411+ory-bot@users.noreply.github.com" +git config user.name "ory-bot" + +git add .schemastore/config.schema.json +git commit -m "autogen: render config schema" || true diff --git a/script/test-envs.sh b/script/test-envs.sh new file mode 100755 index 0000000..b784ecb --- /dev/null +++ b/script/test-envs.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +export TEST_DATABASE_MYSQL="mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true" +export TEST_DATABASE_POSTGRESQL="postgres://postgres:secret@127.0.0.1:3445/postgres?sslmode=disable" +export TEST_DATABASE_COCKROACHDB="cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable" diff --git a/script/testenv.sh b/script/testenv.sh new file mode 100755 index 0000000..94704f8 --- /dev/null +++ b/script/testenv.sh @@ -0,0 +1,10 @@ +#!/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 --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:latest-v25.4 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 +docker pull oryd/hydra:v2.2.0 + +source script/test-envs.sh diff --git a/script/unset-e2e-uis.sh b/script/unset-e2e-uis.sh new file mode 100644 index 0000000..68c124e --- /dev/null +++ b/script/unset-e2e-uis.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +unset NODE_UI_PATH +unset RN_UI_PATH +unset REACT_UI_PATH diff --git a/selfservice/errorx/error.go b/selfservice/errorx/error.go new file mode 100644 index 0000000..14e5911 --- /dev/null +++ b/selfservice/errorx/error.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "github.com/gofrs/uuid" +) + +// swagger:model flowError +type ErrorContainer struct { + // ID of the error container. + // + // required: true + ID uuid.UUID `db:"id" json:"id"` + NID uuid.UUID `json:"-" db:"nid"` + + // The error + Errors json.RawMessage `json:"error" db:"errors"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + + SeenAt sql.NullTime `json:"-" db:"seen_at"` + WasSeen bool `json:"-" db:"was_seen"` + CSRFToken string `db:"csrf_token" json:"-"` +} + +func (e ErrorContainer) TableName(ctx context.Context) string { + return "selfservice_errors" +} diff --git a/selfservice/errorx/handler.go b/selfservice/errorx/handler.go new file mode 100644 index 0000000..5100018 --- /dev/null +++ b/selfservice/errorx/handler.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx + +import ( + "cmp" + "encoding/json" + "net/http" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" +) + +const RouteGet = "/self-service/errors" + +var stub500, _ = json.Marshal(herodot.ErrInternalServerError().WithReasonf("This is a stub error.")) + +type ( + handlerDependencies interface { + httpx.WriterProvider + PersistenceProvider + config.Provider + } + HandlerProvider interface { + SelfServiceErrorHandler() *Handler + } + Handler struct { + r handlerDependencies + csrf nosurfx.CSRFToken + } +) + +func NewHandler( + r handlerDependencies, +) *Handler { + return &Handler{r: r, csrf: nosurf.Token} +} + +func (h *Handler) WithTokenGenerator(f func(r *http.Request) string) { + h.csrf = f +} + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + public.GET(RouteGet, h.publicFetchError) +} + +func (h *Handler) RegisterAdminRoutes(public *httprouterx.RouterAdmin) { + public.GET(RouteGet, redir.RedirectToPublicRoute(h.r)) +} + +// swagger:parameters getFlowError +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getFlowError struct { + // Error is the error's ID + // + // in: query + // required: true + ID string `json:"id"` +} + +// swagger:route GET /self-service/errors frontend getFlowError +// +// # Get User-Flow Errors +// +// This endpoint returns the error associated with a user-facing self service errors. +// +// This endpoint supports stub values to help you implement the error UI: +// +// - `?id=stub:500` - returns a stub 500 (Internal Server Error) error. +// +// More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-facing-errors). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: flowError +// 403: errorGeneric +// 404: errorGeneric +// 500: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) publicFetchError(w http.ResponseWriter, r *http.Request) { + if err := h.fetchError(w, r); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } +} + +func (h *Handler) fetchError(w http.ResponseWriter, r *http.Request) error { + id := cmp.Or(r.URL.Query().Get("error"), r.URL.Query().Get("id")) + switch id { + case "stub:500": + h.r.Writer().Write(w, r, &ErrorContainer{ID: x.NewUUID(), Errors: stub500}) + return nil + } + + es, err := h.r.SelfServiceErrorPersister().ReadErrorContainer(r.Context(), x.ParseUUID(id)) + if err != nil { + return err + } + + h.r.Writer().Write(w, r, es) + return nil +} diff --git a/selfservice/errorx/handler_test.go b/selfservice/errorx/handler_test.go new file mode 100644 index 0000000..5bf8aaf --- /dev/null +++ b/selfservice/errorx/handler_test.go @@ -0,0 +1,133 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/nosurf" + "github.com/ory/x/assertx" + "github.com/ory/x/errorsx" + "github.com/ory/x/httprouterx" +) + +func TestHandler(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + h := errorx.NewHandler(reg) + + t.Run("case=public authorization", func(t *testing.T) { + router := httprouterx.NewRouterPublic() + ns := nosurfx.NewTestCSRFHandler(router, reg) + + h.RegisterPublicRoutes(router) + router.GET("/regen", func(w http.ResponseWriter, r *http.Request) { + ns.RegenerateToken(w, r) + w.WriteHeader(http.StatusNoContent) + }) + router.GET("/set-error", func(w http.ResponseWriter, r *http.Request) { + id, err := reg.SelfServiceErrorPersister().CreateErrorContainer(context.Background(), nosurf.Token(r), herodot.ErrNotFound().WithReason("foobar")) + require.NoError(t, err) + _, _ = w.Write([]byte(id.String())) + }) + + ts := httptest.NewServer(ns) + defer ts.Close() + + getBody := func(t *testing.T, hc *http.Client, path string, expectedCode int) []byte { + res, err := hc.Get(ts.URL + path) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.EqualValues(t, expectedCode, res.StatusCode) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return body + } + expectedError := x.MustEncodeJSON(t, herodot.ErrNotFound().WithReason("foobar")) + + t.Run("call with valid csrf cookie", func(t *testing.T) { + hc := testhelpers.NewTestClient(t) + id := getBody(t, hc, "/set-error", http.StatusOK) + actual := getBody(t, hc, errorx.RouteGet+"?id="+string(id), http.StatusOK) + assert.JSONEq(t, expectedError, gjson.GetBytes(actual, "error").Raw, "%s", actual) + + // We expect a forbid error if the error is not found, regardless of CSRF + _ = getBody(t, hc, errorx.RouteGet+"?id=does-not-exist", http.StatusNotFound) + }) + }) + + t.Run("case=stubs", func(t *testing.T) { + router := httprouterx.NewRouterPublic() + h.RegisterPublicRoutes(router) + ts := httptest.NewServer(router) + defer ts.Close() + + res, err := ts.Client().Get(ts.URL + errorx.RouteGet + "?id=stub:500") + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.EqualValues(t, "This is a stub error.", gjson.GetBytes(actual, "error.reason").String()) + }) + + t.Run("case=errors types", func(t *testing.T) { + router := httprouterx.NewRouterPublic() + h.RegisterPublicRoutes(router) + ts := httptest.NewServer(router) + defer ts.Close() + + for k, tc := range []struct { + gave error + }{ + {gave: herodot.ErrNotFound().WithReason("foobar")}, + {gave: herodot.ErrNotFound().WithReason("foobar")}, + {gave: errors.WithStack(herodot.ErrNotFound().WithReason("foobar"))}, + {gave: errors.WithStack(herodot.ErrNotFound().WithReason("foobar").WithTrace(errors.New("asdf")))}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + csrf := x.NewUUID() + id, err := reg.SelfServiceErrorPersister().CreateErrorContainer(context.Background(), csrf.String(), tc.gave) + require.NoError(t, err) + + res, err := ts.Client().Get(ts.URL + errorx.RouteGet + "?id=" + id.String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + gg := errorsx.Cause(tc.gave) + expected, err := json.Marshal(errorx.ErrorContainer{ + ID: id, + Errors: x.RequireJSONMarshal(t, gg), + }) + require.NoError(t, err) + + assertx.EqualAsJSONExcept(t, json.RawMessage(expected), json.RawMessage(actual), []string{"created_at", "updated_at"}) + assert.Empty(t, gjson.GetBytes(actual, "csrf_token").String()) + assert.JSONEq(t, string(x.RequireJSONMarshal(t, gg)), gjson.GetBytes(actual, "error").Raw) + t.Logf("%s", actual) + }) + } + }) +} diff --git a/selfservice/errorx/manager.go b/selfservice/errorx/manager.go new file mode 100644 index 0000000..46b8cb2 --- /dev/null +++ b/selfservice/errorx/manager.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx + +import ( + "context" + "net/http" + "net/url" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/driver/config" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/x" +) + +type ( + managerDependencies interface { + PersistenceProvider + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + config.Provider + } + + Manager struct { + d managerDependencies + } + + ManagementProvider interface { + // SelfServiceErrorManager returns the errorx.Manager. + SelfServiceErrorManager() *Manager + } +) + +func NewManager(d managerDependencies) *Manager { + return &Manager{d: d} +} + +// Create is a simple helper that saves all errors in the store and returns the +// error url, appending the error ID. +func (m *Manager) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) (string, error) { + m.d.Logger().WithError(err).WithRequest(r).Errorf("An error occurred and is being forwarded to the error user interface.") + + id, addErr := m.d.SelfServiceErrorPersister().CreateErrorContainer(ctx, m.d.GenerateCSRFToken(r), err) + if addErr != nil { + return "", addErr + } + q := url.Values{} + q.Set("id", id.String()) + + return urlx.CopyWithQuery(m.d.Config().SelfServiceFlowErrorURL(ctx), q).String(), nil +} + +// Forward is a simple helper that saves all errors in the store and forwards the HTTP Request +// to the error url, appending the error ID. +func (m *Manager) Forward(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) { + if x.IsJSONRequest(r) { + m.d.Writer().WriteError(w, r, err) + return + } + + to, errCreate := m.Create(ctx, w, r, err) + if errCreate != nil { + // Everything failed. Resort to standard error output. + m.d.Logger().WithError(errCreate).WithRequest(r).Error("Failed to create error container.") + m.d.Writer().WriteError(w, r, err) + return + } + + if x.AcceptsJSON(r) { + m.d.Writer().WriteError(w, r, err) + return + } + + http.Redirect(w, r, to, http.StatusSeeOther) +} diff --git a/selfservice/errorx/persistence.go b/selfservice/errorx/persistence.go new file mode 100644 index 0000000..886c60e --- /dev/null +++ b/selfservice/errorx/persistence.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type ( + Persister interface { + // CreateErrorContainer adds an error to the manager and returns a + // unique identifier or an error if insertion fails. + CreateErrorContainer(ctx context.Context, csrfToken string, err error) (uuid.UUID, error) + + // ReadErrorContainer returns an error by its unique identifier and + // marks the error as read. If an error occurs during retrieval the + // second return parameter is an error. + ReadErrorContainer(ctx context.Context, id uuid.UUID) (*ErrorContainer, error) + + // ClearErrorContainers clears read containers that are older than a + // certain amount of time. If force is set to true, unread errors will + // be cleared as well. + ClearErrorContainers(ctx context.Context, olderThan time.Duration, force bool) error + } + + PersistenceProvider interface { + SelfServiceErrorPersister() Persister + } +) diff --git a/selfservice/errorx/test/persistence.go b/selfservice/errorx/test/persistence.go new file mode 100644 index 0000000..775dad7 --- /dev/null +++ b/selfservice/errorx/test/persistence.go @@ -0,0 +1,88 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorx + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/persistence" + "github.com/ory/x/sqlcon" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + + "github.com/ory/kratos/x" +) + +func TestPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { + toJSON := func(t *testing.T, in interface{}) string { + out, err := json.Marshal(in) + require.NoError(t, err) + return string(out) + } + + return func(t *testing.T) { + _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=not found", func(t *testing.T) { + _, err := p.ReadErrorContainer(ctx, x.NewUUID()) + require.Error(t, err) + }) + + t.Run("case=en- and decode properly", func(t *testing.T) { + actualID, err := p.CreateErrorContainer(ctx, "nosurf", herodot.ErrNotFound().WithReason("foobar")) + require.NoError(t, err) + + actual, err := p.ReadErrorContainer(ctx, actualID) + require.NoError(t, err) + + assert.JSONEq(t, `{"code":404,"status":"Not Found","reason":"foobar","message":"The requested resource could not be found"}`, gjson.Get(toJSON(t, actual), "error").String(), toJSON(t, actual)) + }) + + t.Run("case=clear", func(t *testing.T) { + actualID, err := p.CreateErrorContainer(ctx, "nosurf", herodot.ErrNotFound().WithReason("foobar")) + require.NoError(t, err) + + _, err = p.ReadErrorContainer(ctx, actualID) + require.NoError(t, err) + + // The row must age past the one-second cutoff before ClearErrorContainers + // removes it, and ReadErrorContainer refreshes seen_at on every read, so + // polling would keep resetting the age. Wait once instead. The extra 0.5s + // covers MySQL's one-second timestamp resolution. + time.Sleep(time.Second + time.Millisecond*500) + require.NoError(t, p.ClearErrorContainers(ctx, time.Second, false)) + got, err := p.ReadErrorContainer(ctx, actualID) + require.Error(t, err, "%+v", got) + }) + + t.Run("case=network", func(t *testing.T) { + t.Run("can not read error from another network", func(t *testing.T) { + created, err := p.CreateErrorContainer(ctx, "nosurf", herodot.ErrNotFound().WithReason("foobar")) + require.NoError(t, err) + + _, other := testhelpers.NewNetwork(t, ctx, p) + _, err = other.ReadErrorContainer(ctx, created) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + t.Run("can not clear another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + require.NoError(t, other.ClearErrorContainers(ctx, time.Second, true)) + + c, err := p.ReadErrorContainer(ctx, created) + require.NoError(t, err) + assert.Contains(t, string(c.Errors), "foobar") + }) + }) + }) + } +} diff --git a/selfservice/flow/.schema/method.schema.json b/selfservice/flow/.schema/method.schema.json new file mode 100644 index 0000000..61a76b6 --- /dev/null +++ b/selfservice/flow/.schema/method.schema.json @@ -0,0 +1,10 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/flow/method.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + } + } +} diff --git a/selfservice/flow/config.go b/selfservice/flow/config.go new file mode 100644 index 0000000..8082ec3 --- /dev/null +++ b/selfservice/flow/config.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "github.com/ory/kratos/ui/container" +) + +// swagger:ignore +type MethodConfigurator interface { + container.NodeGetter + + container.ErrorParser + + // form.NodeSetter + // form.NodeUnsetter + container.ValueSetter + + container.Resetter + container.MessageResetter + container.CSRFSetter + container.FieldSorter +} diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go new file mode 100644 index 0000000..9481852 --- /dev/null +++ b/selfservice/flow/continue_with.go @@ -0,0 +1,350 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "encoding/base64" + "net/url" + + "github.com/ory/herodot" + + "github.com/gofrs/uuid" + + "github.com/ory/x/urlx" +) + +// swagger:model continueWith +type ContinueWith interface { + //swagger:ignore + GetAction() string +} + +// swagger:enum ContinueWithActionSetOrySessionToken +type ContinueWithActionSetOrySessionToken string + +const ( + ContinueWithActionSetOrySessionTokenString ContinueWithActionSetOrySessionToken = "set_ory_session_token" // #nosec G101 -- only a key constant +) + +var _ ContinueWith = new(ContinueWithSetOrySessionToken) + +// Indicates that a session was issued, and the application should use this token for authenticated requests +// +// swagger:model continueWithSetOrySessionToken +type ContinueWithSetOrySessionToken struct { + // Action will always be `set_ory_session_token` + // + // required: true + Action ContinueWithActionSetOrySessionToken `json:"action"` + + // Token is the token of the session + // + // required: true + OrySessionToken string `json:"ory_session_token"` +} + +func (ContinueWithSetOrySessionToken) AppendTo(url.Values) url.Values { + return nil +} + +func NewContinueWithSetToken(t string) *ContinueWithSetOrySessionToken { + return &ContinueWithSetOrySessionToken{ + Action: ContinueWithActionSetOrySessionTokenString, + OrySessionToken: t, + } +} + +func (c ContinueWithSetOrySessionToken) GetAction() string { + return string(c.Action) +} + +// swagger:enum ContinueWithActionShowVerificationUI +type ContinueWithActionShowVerificationUI string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionShowVerificationUIString ContinueWithActionShowVerificationUI = "show_verification_ui" +) + +var _ ContinueWith = new(ContinueWithVerificationUI) + +// Indicates, that the UI flow could be continued by showing a verification ui +// +// swagger:model continueWithVerificationUi +type ContinueWithVerificationUI struct { + // Action will always be `show_verification_ui` + // + // required: true + Action ContinueWithActionShowVerificationUI `json:"action"` + // Flow contains the ID of the verification flow + // + // required: true + Flow ContinueWithVerificationUIFlow `json:"flow"` +} + +func (c ContinueWithVerificationUI) GetAction() string { + return string(c.Action) +} + +// swagger:model continueWithVerificationUiFlow +type ContinueWithVerificationUIFlow struct { + // The ID of the verification flow + // + // required: true + ID uuid.UUID `json:"id"` + + // The address that should be verified in this flow + // + // required: true + VerifiableAddress string `json:"verifiable_address"` + + // 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"` +} + +func NewContinueWithVerificationUI(id uuid.UUID, address, url string) *ContinueWithVerificationUI { + return &ContinueWithVerificationUI{ + Action: ContinueWithActionShowVerificationUIString, + Flow: ContinueWithVerificationUIFlow{ + ID: id, + VerifiableAddress: address, + URL: url, + }, + } +} + +func (c ContinueWithVerificationUI) AppendTo(src *url.URL) *url.URL { + values := src.Query() + values.Set("flow", c.Flow.ID.String()) + return urlx.CopyWithQuery(src, values) +} + +type FlowWithContinueWith interface { + Flow + AddContinueWith(ContinueWith) + ContinueWith() []ContinueWith +} + +// swagger:enum ContinueWithActionShowSettingsUI +type ContinueWithActionShowSettingsUI string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionShowSettingsUIString ContinueWithActionShowSettingsUI = "show_settings_ui" +) + +var _ ContinueWith = new(ContinueWithSettingsUI) + +// Indicates, that the UI flow could be continued by showing a settings ui +// +// swagger:model continueWithSettingsUi +type ContinueWithSettingsUI struct { + // Action will always be `show_settings_ui` + // + // required: true + Action ContinueWithActionShowSettingsUI `json:"action"` + + // Flow contains the ID of the settings flow + // + // required: true + Flow ContinueWithSettingsUIFlow `json:"flow"` +} + +func (c ContinueWithSettingsUI) GetAction() string { + return string(c.Action) +} + +// swagger:model continueWithSettingsUiFlow +type ContinueWithSettingsUIFlow struct { + // The ID of the settings flow + // + // 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, redirectTo string) *ContinueWithSettingsUI { + return &ContinueWithSettingsUI{ + Action: ContinueWithActionShowSettingsUIString, + Flow: ContinueWithSettingsUIFlow{ + ID: f.GetID(), + URL: redirectTo, + }, + } +} + +// swagger:enum ContinueWithActionShowRecoveryUI +type ContinueWithActionShowRecoveryUI string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionShowRecoveryUIString ContinueWithActionShowRecoveryUI = "show_recovery_ui" +) + +// Indicates, that the UI flow could be continued by showing a recovery ui +// +// swagger:model continueWithRecoveryUi +type ContinueWithRecoveryUI struct { + // Action will always be `show_recovery_ui` + // + // required: true + Action ContinueWithActionShowRecoveryUI `json:"action"` + // Flow contains the ID of the recovery flow + // + // required: true + Flow ContinueWithRecoveryUIFlow `json:"flow"` +} + +// swagger:model continueWithRecoveryUiFlow +type ContinueWithRecoveryUIFlow struct { + // The ID of the recovery flow + // + // required: true + ID uuid.UUID `json:"id"` + + // 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"` +} + +func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { + return &ContinueWithRecoveryUI{ + Action: ContinueWithActionShowRecoveryUIString, + Flow: ContinueWithRecoveryUIFlow{ + ID: f.GetID(), + }, + } +} + +func (c ContinueWithRecoveryUI) GetAction() string { + return string(c.Action) +} + +// 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 (c ContinueWithRedirectBrowserTo) GetAction() string { + return string(c.Action) +} + +// swagger:enum ContinueWithActionShowPINEntryUI +type ContinueWithActionShowPINEntryUI string + +// #nosec G101 -- only an action constant +const ( + ContinueWithActionShowPINEntryUIString ContinueWithActionShowPINEntryUI = "show_pin_entry_ui" +) + +var _ ContinueWith = new(ContinueWithDeviceAuthnPINEntryUI) + +// Instructs the client to show its PIN entry UI +// +// Returned after a PIN-protected DeviceAuthn enrollment or secret rotation. +// It carries the one-time HPKE-sealed pin_secret, which the device opens with +// the transport private key it generated for this enrollment and binds to the +// user's PIN. The plaintext secret never leaves the device; the server only +// ever stores its at-rest ciphertext. +// +// The enrolled key's client_key_id is not included: it is the SHA-256 fingerprint +// of the device's own public key, which the device derives locally. Non-PIN keys +// need no client action at all and therefore produce no continue_with. +// +// swagger:model continueWithDeviceAuthnPinEntryUi +type ContinueWithDeviceAuthnPINEntryUI struct { + // Action will always be `show_pin_entry_ui` + // + // required: true + Action ContinueWithActionShowPINEntryUI `json:"action"` + + // Data carries the sealed pin_secret material. + // + // required: true + Data ContinueWithDeviceAuthnPINEntryUIData `json:"data"` +} + +// The one-time HPKE-sealed pin_secret material +// +// Contains the encapsulated key and the sealed ciphertext that the device +// opens with the X25519 transport private key it generated for this +// enrollment or rotation. Open it with HPKE (RFC 9180) using the suite +// DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, the ASCII info +// string "ory/deviceauthn/pin-secret/v1", and the key's client_key_id (its +// ASCII hex form) as the AAD. +// +// swagger:model continueWithDeviceAuthnPinEntryUiData +type ContinueWithDeviceAuthnPINEntryUIData struct { + // The base64-encoded HPKE encapsulated key (the `enc` output of the seal + // operation). + // + // required: true + Enc string `json:"enc"` + + // The base64-encoded HPKE ciphertext of the sealed pin_secret. + // + // required: true + Ciphertext string `json:"ciphertext"` +} + +func NewContinueWithDeviceAuthnPINEntryUI(enc, ciphertext []byte) *ContinueWithDeviceAuthnPINEntryUI { + return &ContinueWithDeviceAuthnPINEntryUI{ + Action: ContinueWithActionShowPINEntryUIString, + Data: ContinueWithDeviceAuthnPINEntryUIData{ + Enc: base64.StdEncoding.EncodeToString(enc), + Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), + }, + } +} + +func (c ContinueWithDeviceAuthnPINEntryUI) GetAction() string { + return string(c.Action) +} + +func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError { + if err.DetailsField == nil { + err.DetailsField = map[string]interface{}{} + } + err.DetailsField["continue_with"] = continueWith + + return err +} diff --git a/selfservice/flow/duplicate_credentials.go b/selfservice/flow/duplicate_credentials.go new file mode 100644 index 0000000..5852520 --- /dev/null +++ b/selfservice/flow/duplicate_credentials.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "encoding/json" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/identity" + "github.com/ory/x/sqlxx" +) + +const internalContextDuplicateCredentialsPath = "registration_duplicate_credentials" + +type DuplicateCredentialsData struct { + CredentialsType identity.CredentialsType + CredentialsConfig sqlxx.JSONRawMessage + DuplicateIdentifier string +} + +// SetDuplicateCredentials sets the duplicate credentials data in the flow's internal context. +func SetDuplicateCredentials(flow InternalContexter, creds DuplicateCredentialsData) error { + if flow.GetInternalContext() == nil { + flow.EnsureInternalContext() + } + bytes, err := sjson.SetBytes( + flow.GetInternalContext(), + internalContextDuplicateCredentialsPath, + creds, + ) + if err != nil { + return err + } + flow.SetInternalContext(bytes) + + return nil +} + +// DuplicateCredentials returns the duplicate credentials data from the flow's internal context. +func DuplicateCredentials(flow InternalContexter) (*DuplicateCredentialsData, error) { + if flow.GetInternalContext() == nil { + flow.EnsureInternalContext() + } + raw := gjson.GetBytes(flow.GetInternalContext(), internalContextDuplicateCredentialsPath) + if !raw.IsObject() { + return nil, nil + } + var creds DuplicateCredentialsData + err := json.Unmarshal([]byte(raw.Raw), &creds) + + return &creds, err +} diff --git a/selfservice/flow/error.go b/selfservice/flow/error.go new file mode 100644 index 0000000..17ea317 --- /dev/null +++ b/selfservice/flow/error.go @@ -0,0 +1,223 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/swagger" + "github.com/ory/x/clock" + "github.com/ory/x/logrusx" + "github.com/ory/x/urlx" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/text" +) + +var ( + ErrStrategyNotResponsible = errors.New("strategy is not responsible for this request") + ErrCompletedByStrategy = errors.New("flow response completed by strategy") + ErrStrategyAsksToReturnToUI = errors.New("flow strategy is redirecting to the ui") +) + +// Is sent when a flow is replaced by a different flow of the same class +// +// swagger:model errorFlowReplaced +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type errorFlowReplaced struct { + Error swagger.GenericError `json:"error"` + // The flow ID that should be used for the new flow as it contains the correct messages. + FlowID uuid.UUID `json:"use_flow_id"` +} + +// ReplacedError is sent when a flow is replaced by a different flow of the same class +type ReplacedError struct { + *herodot.DefaultError `json:"error"` + + // The flow ID that should be used for the new flow as it contains the correct messages. + FlowID uuid.UUID `json:"use_flow_id"` + + flow Flow + + // TODO: This error could be enhanced by providing a "flow class" (e.g. "Recovery", "Settings", "Verification", "Login", etc.) +} + +func (e *ReplacedError) WithFlow(flow Flow) *ReplacedError { + e.FlowID = flow.GetID() + e.flow = flow + return e +} + +func (e *ReplacedError) GetFlow() Flow { + return e.flow +} + +func (e *ReplacedError) EnhanceJSONError() interface{} { + return e +} + +func NewFlowReplacedError(message *text.Message) *ReplacedError { + return &ReplacedError{ + DefaultError: nosurfx.ErrGone().WithID(text.ErrIDSelfServiceFlowReplaced). + WithError("self-service flow replaced"). + WithReason(message.Text), + } +} + +// Is sent when a flow is expired +// +// swagger:model selfServiceFlowExpiredError +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type selfServiceFlowExpiredError struct { + Error swagger.GenericError `json:"error"` + + // When the flow has expired + ExpiredAt time.Time `json:"expired_at"` + + // Please use the "expired_at" field instead to have a more accurate result. + // + // Deprecated: true + Since time.Duration `json:"since"` + + // The flow ID that should be used for the new flow as it contains the correct messages. + FlowID uuid.UUID `json:"use_flow_id"` +} + +// ExpiredError is sent when a flow is expired +type ExpiredError struct { + *herodot.DefaultError `json:"error"` + + // When the flow has expired + ExpiredAt time.Time `json:"expired_at"` + + // DEPRECATED: Please use the "expired_at" field instead to have a more accurate result. + Since time.Duration `json:"since"` + + // The flow ID that should be used for the new flow as it contains the correct messages. + FlowID uuid.UUID `json:"use_flow_id"` + + flow Flow +} + +func (e *ExpiredError) Unwrap() error { + return e.DefaultError +} + +func (e *ExpiredError) WithContinueWith(continueWith ...ContinueWith) *ExpiredError { + e.DefaultError = ErrorWithContinueWith(e.DefaultError, continueWith...) + return e +} + +func (e *ExpiredError) WithFlow(flow Flow) *ExpiredError { + e.FlowID = flow.GetID() + e.flow = flow + return e +} + +func (e *ExpiredError) GetFlow() Flow { + return e.flow +} + +func (e *ExpiredError) EnhanceJSONError() interface{} { + return e +} + +func NewFlowExpiredError(c clock.Clock, at time.Time) *ExpiredError { + ago := c.Now().Sub(at) + return &ExpiredError{ + ExpiredAt: at.UTC(), + Since: ago, + DefaultError: nosurfx.ErrGone().WithID(text.ErrIDSelfServiceFlowExpired). + WithError("self-service flow expired"). + WithReasonf("The self-service flow expired %.2f minutes ago, initialize a new one.", ago.Minutes()), + } +} + +// Is sent when a flow requires a browser to change its location. +// +// swagger:model errorBrowserLocationChangeRequired +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type errorBrowserLocationChangeRequired struct { + Error swagger.ErrorGeneric `json:"error"` + + // Points to where to redirect the user to next. + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +// BrowserLocationChangeRequiredError is sent when a flow requires a browser to change its location. +type BrowserLocationChangeRequiredError struct { + *herodot.DefaultError `json:"error"` + + // Points to where to redirect the user to next. + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +func (e *BrowserLocationChangeRequiredError) EnhanceJSONError() interface{} { + return e +} + +func NewBrowserLocationChangeRequiredError(redirectTo string) *BrowserLocationChangeRequiredError { + return &BrowserLocationChangeRequiredError{ + RedirectBrowserTo: redirectTo, + DefaultError: &herodot.DefaultError{ + IDField: text.ErrIDSelfServiceBrowserLocationChangeRequiredError, + CodeField: http.StatusUnprocessableEntity, + StatusField: http.StatusText(http.StatusUnprocessableEntity), + ReasonField: fmt.Sprintf("In order to complete this flow please redirect the browser to: %s", redirectTo), + DebugField: "", + ErrorField: "browser location change required", + }, + } +} + +func HandleHookError(_ http.ResponseWriter, r *http.Request, f Flow, traits identity.Traits, group node.UiNodeGroup, flowError error, logger logrusx.Provider, csrf nosurfx.CSRFTokenGeneratorProvider) error { + if f != nil { + if traits != nil { + cont, err := container.NewFromStruct("", group, traits, "traits") + if err != nil { + logger.Logger().WithError(err).Error("could not update flow UI") + return err + } + + for _, n := range cont.Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.GetUI().Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + if f.GetType() == TypeBrowser { + f.GetUI().SetCSRF(csrf.GenerateCSRFToken(r)) + } + } + + return flowError +} + +func GetFlowExpiredRedirectURL(ctx context.Context, config *config.Config, route, returnTo string) *url.URL { + redirectURL := urlx.AppendPaths(config.SelfPublicURL(ctx), route) + if returnTo != "" { + redirectURL = urlx.CopyWithQuery(redirectURL, url.Values{"return_to": {returnTo}}) + } + + return redirectURL +} diff --git a/selfservice/flow/error_test.go b/selfservice/flow/error_test.go new file mode 100644 index 0000000..c938e07 --- /dev/null +++ b/selfservice/flow/error_test.go @@ -0,0 +1,165 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "encoding/json" + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "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/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type testCSRFTokenGenerator struct{} + +func (t *testCSRFTokenGenerator) GenerateCSRFToken(_ *http.Request) string { + return "csrf_token_value" +} + +// testFlow is a minimalistic flow implementation to satisfy interface and is used only in tests. +type testFlow struct { + // ID represents the flow's unique ID. + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id" rw:"r"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type Type `json:"type" db:"type" faker:"flow_type"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // Flow State + // + // 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. + // + // required: true + State State `json:"state" db:"state"` +} + +func (t *testFlow) GetID() uuid.UUID { + return t.ID +} + +func (t *testFlow) GetType() Type { + return t.Type +} + +func (t *testFlow) GetRequestURL() string { + return t.RequestURL +} + +func (t *testFlow) AppendTo(url *url.URL) *url.URL { + return AppendFlowTo(url, t.ID) +} + +func (t *testFlow) GetUI() *container.Container { + return t.UI +} + +func (t *testFlow) GetState() State { + return t.State +} + +func (t *testFlow) GetFlowName() FlowName { + return FlowName("test") +} + +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() + ui := &container.Container{ + Method: "POST", + Action: "/test", + } + + ui.Nodes.Append(node.NewInputField("traits.username", nil, node.PasswordGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute)) + ui.Nodes.Append(node.NewInputField("traits.password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute)) + + return &testFlow{ + ID: id, + UI: ui, + RequestURL: requestURL, + Type: flowType, + } +} + +func prepareTraits(username, password string) identity.Traits { + payload := struct { + Username string `json:"username"` + Password string `json:"password"` + }{username, password} + + data, _ := json.Marshal(payload) + return data +} + +func TestHandleHookError(t *testing.T) { + r := &http.Request{URL: &url.URL{RawQuery: ""}} + logger := logrusx.New("kratos", "test", logrusx.ForceLevel(logrus.FatalLevel)) + l := &x.BasicRegistry{L: logger, C: httpx.NewResilientClient(), T: otelx.NewNoop()} + csrf := testCSRFTokenGenerator{} + f := newTestFlow(r, TypeBrowser) + tr := prepareTraits("foo", "bar") + + t.Run("case=fill_in_traits", func(t *testing.T) { + ve := schema.NewValidationListError([]*schema.ValidationError{schema.NewHookValidationError("traits.username", "invalid username", text.Messages{})}) + + err := HandleHookError(nil, r, f, tr, node.PasswordGroup, ve, l, &csrf) + assert.ErrorIs(t, err, ve) + if assert.NotEmpty(t, f.GetUI()) { + ui := f.GetUI() + assert.Len(t, ui.Nodes, 3) + assert.ElementsMatch(t, ui.Nodes, + node.Nodes{ + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "traits.username", Type: node.InputAttributeTypeText, FieldValue: "foo", Required: true}, Meta: &node.Meta{}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "traits.password", Type: node.InputAttributeTypePassword, FieldValue: "bar", Required: true}, Meta: &node.Meta{}}, + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "csrf_token", Type: node.InputAttributeTypeHidden, FieldValue: "csrf_token_value", Required: true}}, + }) + } + }) + + t.Run("case=unmarshal_fail", func(t *testing.T) { + ve := schema.NewValidationListError([]*schema.ValidationError{schema.NewHookValidationError("traits.username", "invalid username", text.Messages{})}) + + err := HandleHookError(nil, r, f, []byte("garbage"), node.PasswordGroup, ve, l, &csrf) + var jsonErr *json.SyntaxError + assert.ErrorAs(t, err, &jsonErr) + }) +} diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go new file mode 100644 index 0000000..033ab15 --- /dev/null +++ b/selfservice/flow/flow.go @@ -0,0 +1,70 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/x/redir" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func AppendFlowTo(src *url.URL, id uuid.UUID) *url.URL { + return urlx.CopyWithQuery(src, url.Values{"flow": {id.String()}}) +} + +func GetFlowID(r *http.Request) (uuid.UUID, error) { + rid := x.ParseUUID(r.URL.Query().Get("flow")) + if rid == uuid.Nil { + return rid, errors.WithStack(herodot.ErrBadRequest().WithReasonf("The flow query parameter is missing or malformed.")) + } + return rid, nil +} + +type Flow interface { + GetID() uuid.UUID + GetType() Type + GetRequestURL() string + AppendTo(*url.URL) *url.URL + GetUI() *container.Container + GetState() State + SetState(State) + GetFlowName() FlowName + GetTransientPayload() json.RawMessage +} + +type FlowWithRedirect interface { + SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (opts []redir.SecureRedirectOption) +} + +type InternalContexter interface { + EnsureInternalContext() + GetInternalContext() sqlxx.JSONRawMessage + SetInternalContext(sqlxx.JSONRawMessage) +} + +type OAuth2ChallengeProvider interface { + GetOAuth2LoginChallenge() sqlxx.NullString +} + +// HydraLoginRequestProvider is implemented by flows that can carry the OAuth2 +// login request that initiated them. +type HydraLoginRequestProvider interface { + GetHydraLoginRequest() *hydraclientgo.OAuth2LoginRequest +} diff --git a/selfservice/flow/flow_identity_schema.go b/selfservice/flow/flow_identity_schema.go new file mode 100644 index 0000000..40b54aa --- /dev/null +++ b/selfservice/flow/flow_identity_schema.go @@ -0,0 +1,60 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + "database/sql" + "database/sql/driver" + "net/url" + + "github.com/ory/kratos/driver/config" +) + +// swagger:type string +type IdentitySchema string + +// Scan implements the Scanner interface. +func (is *IdentitySchema) Scan(value any) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *is = IdentitySchema(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (is *IdentitySchema) Value() (driver.Value, error) { + if is == nil || len(*is) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(*is)}.Value() +} + +// URL returns the URL of the identity schema, or the default identity traits +// schema URL if the schema is empty. +func (is *IdentitySchema) URL(ctx context.Context, config *config.Config) (*url.URL, error) { + if is == nil || len(*is) == 0 { + return config.DefaultIdentityTraitsSchemaURL(ctx) + } + schemas, err := config.IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + schema, err := schemas.FindSchemaByID(string(*is)) + if err != nil { + return nil, err + } + + return config.ParseURI(schema.URL) +} + +// ID returns the ID of the identity schema, or the default identity schema ID. +func (is *IdentitySchema) ID(ctx context.Context, config *config.Config) string { + if is == nil || len(*is) == 0 { + return config.DefaultIdentityTraitsSchemaID(ctx) + } + return string(*is) +} diff --git a/selfservice/flow/flow_test.go b/selfservice/flow/flow_test.go new file mode 100644 index 0000000..242a635 --- /dev/null +++ b/selfservice/flow/flow_test.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "net/http" + "net/url" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/kratos/x" +) + +func TestGetFlowID(t *testing.T) { + _, err := GetFlowID(&http.Request{URL: &url.URL{RawQuery: ""}}) + require.Error(t, err) + assert.Contains(t, + errors.Cause(err).(*herodot.DefaultError).ReasonField, + "flow query parameter is missing or malformed") + + _, err = GetFlowID(&http.Request{URL: &url.URL{RawQuery: "flow=not-uuid"}}) + require.Error(t, err) + assert.Contains(t, + errors.Cause(err).(*herodot.DefaultError).ReasonField, + "flow query parameter is missing or malformed") + + expected := x.NewUUID() + actual, err := GetFlowID(&http.Request{URL: &url.URL{RawQuery: "flow=" + expected.String()}}) + require.NoError(t, err) + assert.Equal(t, expected.String(), actual.String()) +} diff --git a/selfservice/flow/internal_context.go b/selfservice/flow/internal_context.go new file mode 100644 index 0000000..21b0079 --- /dev/null +++ b/selfservice/flow/internal_context.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/identity" + "github.com/ory/x/sqlxx" +) + +// InternalContextKeyCourierBaseURL is the key under which recovery and +// verification flows stash the base URL captured at flow init. Email +// senders read it back at courier-send time to build the link domain. The +// key is top-level (not strategy-scoped) because the value is flow-wide. +const InternalContextKeyCourierBaseURL = "courier_base_url" + +// InternalContextKeyRequestBaseURL is the key under which login, +// registration, and settings flows stash the customer-facing base URL +// captured at flow init. The OIDC/SAML strategy reads it back at +// provider-submit time and embeds it in the encrypted state, so the +// social-sign-in callback can be redirected onto the host the user +// started on (Ory CLI/Tunnel, custom domain) instead of the bare +// projects.oryapis host the IdP redirects to. The key is top-level (not +// strategy-scoped) because the value is flow-wide. +const InternalContextKeyRequestBaseURL = "request_base_url" + +func PrefixInternalContextKey(t identity.CredentialsType, suffix string) string { + return string(t) + "_" + suffix +} + +// SetRequestBaseURL stashes the base URL captured at flow init (by a +// proxy-aware middleware, via x.WithBaseURL on the request context) into +// the flow's InternalContext under InternalContextKeyRequestBaseURL. Empty +// input is a no-op (preserves the "no rewrite" path). Inputs longer than +// 8192 bytes are rejected so a pathological header cannot bloat the row — +// the same implicit ceiling the courier base URL uses. +func SetRequestBaseURL(f InternalContexter, baseURL string) error { + if baseURL == "" || len(baseURL) > 8192 { + return nil + } + f.EnsureInternalContext() + out, err := sjson.SetBytes(f.GetInternalContext(), InternalContextKeyRequestBaseURL, baseURL) + if err != nil { + return errors.WithStack(err) + } + f.SetInternalContext(sqlxx.JSONRawMessage(out)) + return nil +} + +// GetRequestBaseURL returns the base URL captured at flow init, or the +// empty string when nothing was captured (the OIDC/SAML callback then does +// not rewrite — matching plain projects.oryapis traffic). +func GetRequestBaseURL(f InternalContexter) string { + return gjson.GetBytes(f.GetInternalContext(), InternalContextKeyRequestBaseURL).String() +} diff --git a/selfservice/flow/internal_context_test.go b/selfservice/flow/internal_context_test.go new file mode 100644 index 0000000..d718843 --- /dev/null +++ b/selfservice/flow/internal_context_test.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/sqlxx" +) + +type fakeInternalContexter struct { + ic sqlxx.JSONRawMessage +} + +func (f *fakeInternalContexter) EnsureInternalContext() { + if len(f.ic) == 0 { + f.ic = sqlxx.JSONRawMessage("{}") + } +} +func (f *fakeInternalContexter) GetInternalContext() sqlxx.JSONRawMessage { return f.ic } +func (f *fakeInternalContexter) SetInternalContext(b sqlxx.JSONRawMessage) { f.ic = b } + +func TestRequestBaseURLInternalContext(t *testing.T) { + t.Run("round-trips through InternalContext", func(t *testing.T) { + f := &fakeInternalContexter{} + require.NoError(t, SetRequestBaseURL(f, "http://localhost:4000")) + assert.Equal(t, "http://localhost:4000", GetRequestBaseURL(f)) + assert.Contains(t, string(f.GetInternalContext()), InternalContextKeyRequestBaseURL) + }) + + t.Run("empty input is a no-op and leaves no key", func(t *testing.T) { + f := &fakeInternalContexter{ic: sqlxx.JSONRawMessage("{}")} + require.NoError(t, SetRequestBaseURL(f, "")) + assert.Empty(t, GetRequestBaseURL(f)) + assert.Equal(t, "{}", string(f.GetInternalContext())) + }) + + t.Run("oversized input is rejected (row-bloat guard)", func(t *testing.T) { + f := &fakeInternalContexter{} + require.NoError(t, SetRequestBaseURL(f, "https://"+strings.Repeat("a", 8193))) + assert.Empty(t, GetRequestBaseURL(f)) + }) + + t.Run("missing key returns empty", func(t *testing.T) { + assert.Empty(t, GetRequestBaseURL(&fakeInternalContexter{ic: sqlxx.JSONRawMessage(`{"other":"x"}`)})) + }) +} diff --git a/selfservice/flow/login/aal.go b/selfservice/flow/login/aal.go new file mode 100644 index 0000000..fd674d6 --- /dev/null +++ b/selfservice/flow/login/aal.go @@ -0,0 +1,18 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" +) + +func CheckAAL(f *Flow, expected identity.AuthenticatorAssuranceLevel) error { + if f.RequestedAAL != expected { + return errors.WithStack(flow.ErrStrategyNotResponsible) + } + return nil +} diff --git a/selfservice/flow/login/aal_test.go b/selfservice/flow/login/aal_test.go new file mode 100644 index 0000000..6b79bcb --- /dev/null +++ b/selfservice/flow/login/aal_test.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" +) + +func TestCheckAAL(t *testing.T) { + f := &login.Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + assert.NoError(t, login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1)) + assert.ErrorIs(t, login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2), flow.ErrStrategyNotResponsible) +} diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go new file mode 100644 index 0000000..b17e89c --- /dev/null +++ b/selfservice/flow/login/error.go @@ -0,0 +1,208 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + "net/url" + + "github.com/gofrs/uuid" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/x" +) + +var ErrHookAbortFlow = errors.New("aborted login hook execution") + +func ErrAlreadyLoggedIn() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDAlreadyLoggedIn).WithError("you are already logged in").WithReason("A valid session was detected and thus login is not possible. Did you forget to set `?refresh=true`?") +} + +func ErrAddressNotVerified() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDAddressNotVerified).WithError("your email or phone address is not yet verified").WithReason("Your account's email or phone address are not verified yet. Please check your email or phone inbox or re-request verification.") +} + +// ErrSessionHasAALAlready is returned when one attempts to upgrade the AAL of an active session which already has that AAL. +func ErrSessionHasAALAlready() *herodot.DefaultError { + return herodot.ErrUnauthorized().WithID(text.ErrIDSessionHasAALAlready).WithError("session has the requested authenticator assurance level already").WithReason("The session has the requested AAL already.") +} + +// ErrSessionRequiredForHigherAAL is returned when someone requests AAL2 or AAL3 even though no active session exists yet. +func ErrSessionRequiredForHigherAAL() *herodot.DefaultError { + return herodot.ErrUnauthorized().WithID(text.ErrIDSessionRequiredForHigherAAL).WithError("aal2 and aal3 can only be requested if a session exists already").WithReason("You can not requested a higher AAL (AAL2/AAL3) without an active session.") +} + +type ( + errorHandlerDependencies interface { + clock.Provider + errorx.ManagementProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + config.Provider + sessiontokenexchange.PersistenceProvider + hydra.Provider + + FlowPersistenceProvider + HandlerProvider + } + + ErrorHandlerProvider interface{ LoginFlowErrorHandler() *ErrorHandler } + + ErrorHandler struct { + d errorHandlerDependencies + } +) + +func NewFlowErrorHandler(d errorHandlerDependencies) *ErrorHandler { + return &ErrorHandler{d: d} +} + +func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r *http.Request, f *Flow, err error) (*flow.ExpiredError, error) { + errExpired := new(flow.ExpiredError) + if !errors.As(err, &errExpired) { + return nil, nil + } + // create new flow because the old one is not valid + newFlow, err := s.d.LoginHandler().FromOldFlow(w, r, *f) + if err != nil { + return nil, err + } + + newFlow.UI.Messages.Add(text.NewErrorValidationLoginFlowExpired(s.d.Clock(), errExpired.ExpiredAt)) + if err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), newFlow); err != nil { + return nil, err + } + + return errExpired.WithFlow(newFlow), nil +} + +func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f *Flow, ct identity.CredentialsType, group node.UiNodeGroup, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.ErrorHandler.WriteFlowError", + trace.WithAttributes(attribute.String("error", err.Error()))) + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + logger := s.d.Logger(). + WithError(err). + WithRequest(r). + WithField("login_flow", f.ToLoggerField()) + + logger.Info("Encountered self-service login error.") + + if f == nil { + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginFailed(r.Context(), uuid.Nil, "", "", "", false, err)) + s.forward(w, r, nil, err) + return + } + + span.SetAttributes(attribute.String("flow_id", f.ID.String())) + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginFailed(r.Context(), f.ID, string(f.Type), ct.String(), string(f.RequestedAAL), f.Refresh, err)) + + if expired, inner := s.PrepareReplacementForExpiredFlow(w, r, f, err); inner != nil { + s.WriteFlowError(w, r, f, ct, group, inner) + return + } else if expired != nil { + if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, expired) + } else { + http.Redirect(w, r, expired.GetFlow().AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther) + } + return + } + + f.UI.ResetMessages() + if err := f.UI.ParseError(group, err); err != nil { + s.forward(w, r, f, err) + return + } + + if err := sortNodes(r.Context(), f.UI.Nodes); err != nil { + s.forward(w, r, f, err) + return + } + + if err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + s.forward(w, r, f, err) + return + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther) + return + } + + codes, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID) + if f.Type == flow.TypeAPI && hasCode && group == node.OpenIDConnectGroup { + returnTo, err := url.Parse(f.ReturnTo) + if err != nil { + s.forward(w, r, f, errors.WithStack(err)) + return + } + q := returnTo.Query() + q.Set("code", codes.ReturnToCode) + returnTo.RawQuery = q.Encode() + http.Redirect(w, r, returnTo.String(), http.StatusSeeOther) + return + } + + updatedFlow, innerErr := s.d.LoginFlowPersister().GetLoginFlow(r.Context(), f.ID) + if innerErr != nil { + s.forward(w, r, updatedFlow, innerErr) + return + } + + // If the flow has an OAuth2LoginChallenge, we try to fetch the login request from Hydra because we return the flow object as JSON. + if updatedFlow.OAuth2LoginChallenge != "" { + hlr, err := s.d.Hydra().GetLoginRequest(ctx, string(updatedFlow.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. + s.forward(w, r, updatedFlow, err) + return + } + updatedFlow.HydraLoginRequest = hlr + } + + s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), updatedFlow) +} + +func (s *ErrorHandler) forward(w http.ResponseWriter, r *http.Request, rr *Flow, err error) { + if rr == nil { + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if rr.Type == flow.TypeAPI { + s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) + } else { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } +} diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go new file mode 100644 index 0000000..33a8b3d --- /dev/null +++ b/selfservice/flow/login/error_test.go @@ -0,0 +1,320 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/ui/node" + + "github.com/gobuffalo/httptest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/assertx" + "github.com/ory/x/clock" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" +) + +type opts struct { + loginChallenge string +} + +func withLoginChallenge(challenge string) func(*opts) { + return func(o *opts) { + o.loginChallenge = challenge + } +} + +func TestHandleError(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + public, _ := testhelpers.NewKratosServer(t, reg) + reg.SetHydra(hydra.NewFake()) + + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/password.schema.json") + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + testhelpers.NewLoginUIFlowEchoServer(t, reg) + testhelpers.NewErrorTestServer(t, reg) + + h := reg.LoginFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var loginFlow *login.Flow + var flowError error + var group node.UiNodeGroup + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(w, r, loginFlow, "", group, flowError) + }) + + reset := func() { + loginFlow = nil + flowError = nil + group = "" + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type, options ...func(*opts)) *login.Flow { + req := &http.Request{URL: urlx.ParseOrPanic("/")} + f, err := login.NewFlow(reg, req, ft) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(ttl) + + optsD := &opts{} + for _, o := range options { + o(optsD) + } + if optsD.loginChallenge != "" { + f.OAuth2LoginChallenge = sqlxx.NullString(optsD.loginChallenge) + } + + for _, s := range reg.LoginStrategies(context.Background()) { + switch s := s.(type) { + case login.UnifiedFormHydrator: + require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + case login.AAL1FormHydrator: + require.NoError(t, s.PopulateLoginMethodFirstFactor(req, f)) + } + } + + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f)) + return f + } + + expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + anHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=relative error", func(t *testing.T) { + t.Cleanup(reset) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceErrorUI, "/login-ts") + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + assert.Regexp( + t, + "^/login-ts.*$", + testhelpers.GetSelfServiceRedirectLocation(t, ts.URL+"/error"), + ) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, tc.t) + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusGone, res.StatusCode, "%+v\n\t%s", res.Request, body) + + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.GetBytes(body, "use_flow_id").String()) + // "since" and "error.reason" both derive from time.Since(at), recomputed when the expected error is + // built here — after the response was generated. Across a rounding boundary the two timings disagree, + // so exclude both from the exact comparison and verify the reason template separately. + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(clock.New(), anHourAgo), json.RawMessage(body), []string{"since", "error.reason", "redirect_browser_to", "use_flow_id"}) + assert.Regexp(t, `^The self-service flow expired \d+\.\d{2} minutes ago, initialize a new one\.$`, gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, loginFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + + t.Run("case=refuses to parse oauth2 login challenge when Hydra is not configured", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, flow.TypeBrowser, withLoginChallenge(hydra.FakeInvalidLoginChallenge)) + group = node.PasswordGroup + flowError = herodot.ErrBadRequest().WithReason("missing field 'password' fake error") + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%+v\n\t%s", res.Request, body) + require.Equal(t, "Unable to get OAuth 2.0 Login Challenge.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=fetches hydra login request", func(t *testing.T) { + t.Cleanup(reset) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, nil) + }) + + loginFlow = newFlow(t, time.Minute, flow.TypeBrowser, withLoginChallenge(hydra.FakeValidLoginChallenge)) + group = node.PasswordGroup + flowError = herodot.ErrBadRequest().WithReason("missing field 'password' fake error") + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%+v\n\t%s", res.Request, body) + require.NotEmpty(t, gjson.GetBytes(body, "oauth2_login_request").Value(), "%s", body) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectLoginUI := func(t *testing.T) (*login.Flow, *http.Response) { + res, err := testhelpers.NewTestClient(t).Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowLoginUI(ctx).String()+"?flow=") + + lf, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return lf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = &login.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + group = node.PasswordGroup + + lf, _ := expectLoginUI(t) + require.Len(t, lf.UI.Messages, 1) + assert.Equal(t, int(text.ErrorValidationLoginFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + group = node.PasswordGroup + + lf, _ := expectLoginUI(t) + require.NotEmpty(t, lf.UI.Nodes, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + loginFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + }) +} diff --git a/selfservice/flow/login/export_test.go b/selfservice/flow/login/export_test.go new file mode 100644 index 0000000..eafe4a7 --- /dev/null +++ b/selfservice/flow/login/export_test.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + + "github.com/ory/kratos/session" +) + +func CheckAALForTest(ctx context.Context, e *HookExecutor, s *session.Session, flow *Flow) error { + return e.checkAAL(ctx, s, flow) +} diff --git a/selfservice/flow/login/extension_identifier_label.go b/selfservice/flow/login/extension_identifier_label.go new file mode 100644 index 0000000..6945f94 --- /dev/null +++ b/selfservice/flow/login/extension_identifier_label.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + + "github.com/ory/kratos/text" + "github.com/ory/x/jsonschemax" + + "github.com/ory/kratos/schema" +) + +func GetIdentifierLabelFromSchema(ctx context.Context, schemaURL string, disallowRefs bool) (*text.Message, error) { + runner, err := schema.NewExtensionRunner(ctx) + if err != nil { + return nil, err + } + c, err := schema.NewCompilerWithURL(ctx, schemaURL, disallowRefs) + if err != nil { + return nil, err + } + c.ExtractAnnotations = true + runner.Register(c) + + paths, err := jsonschemax.ListPaths(ctx, schemaURL, c) + if err != nil { + return nil, err + } + + labels := []jsonschemax.Path{} + for _, path := range paths { + if ext := path.CustomProperties[schema.ExtensionName]; ext != nil { + config, ok := ext.(*schema.ExtensionConfig) + if !ok { + continue + } + if config.Credentials.Password.Identifier || + config.Credentials.WebAuthn.Identifier || + config.Credentials.Passkey.DisplayName || + config.Credentials.TOTP.AccountName || + config.Credentials.Code.Identifier { + labels = append(labels, path) + } + } + } + + metaLabel := text.NewInfoNodeLabelID() + if len(labels) == 1 && labels[0].Title != "" { + metaLabel = text.NewInfoNodeLabelGenerated(labels[0].Title, labels[0].Name) + } + return metaLabel, nil +} diff --git a/selfservice/flow/login/extension_identifier_label_test.go b/selfservice/flow/login/extension_identifier_label_test.go new file mode 100644 index 0000000..6fdcb5f --- /dev/null +++ b/selfservice/flow/login/extension_identifier_label_test.go @@ -0,0 +1,158 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "testing" + + "github.com/ory/kratos/text" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/schema" +) + +func constructSchema(t *testing.T, ecModifier, ucModifier func(*schema.ExtensionConfig)) string { + var emailConfig, usernameConfig schema.ExtensionConfig + + if ecModifier != nil { + ecModifier(&emailConfig) + } + if ucModifier != nil { + ucModifier(&usernameConfig) + } + + ec, err := json.Marshal(&emailConfig) + require.NoError(t, err) + uc, err := json.Marshal(&usernameConfig) + require.NoError(t, err) + + ec, err = sjson.DeleteBytes(ec, "verification") + require.NoError(t, err) + ec, err = sjson.DeleteBytes(ec, "recovery") + require.NoError(t, err) + ec, err = sjson.DeleteBytes(ec, "credentials.code.via") + require.NoError(t, err) + ec, err = sjson.DeleteBytes(ec, "organizations.matcher") + require.NoError(t, err) + + uc, err = sjson.DeleteBytes(uc, "verification") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "recovery") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "credentials.code.via") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "organizations.matcher") + require.NoError(t, err) + + return "base64://" + base64.StdEncoding.EncodeToString(fmt.Appendf(nil, ` +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "title": "Email", + "type": "string", + "ory.sh/kratos": %s + }, + "username": { + "title": "Username", + "type": "string", + "ory.sh/kratos": %s + } + } + } + } +}`, ec, uc)) +} + +func TestGetIdentifierLabelFromSchema(t *testing.T) { + for _, tc := range []struct { + name string + emailConfig, usernameConfig func(*schema.ExtensionConfig) + expected *text.Message + }{ + { + name: "email for password", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "email for webauthn", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.WebAuthn.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "email for totp", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.TOTP.AccountName = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "email for code", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Code.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "email for passkey", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Passkey.DisplayName = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "email for all", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + c.Credentials.WebAuthn.Identifier = true + c.Credentials.TOTP.AccountName = true + c.Credentials.Code.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"), + }, + { + name: "username works as well", + usernameConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Username", "traits.username"), + }, + { + name: "multiple identifiers", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + usernameConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelID(), + }, + { + name: "no identifiers", + expected: text.NewInfoNodeLabelID(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + label, err := GetIdentifierLabelFromSchema(t.Context(), constructSchema(t, tc.emailConfig, tc.usernameConfig), true) + require.NoError(t, err) + assert.Equal(t, tc.expected, label) + }) + } +} diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go new file mode 100644 index 0000000..ea9cdf9 --- /dev/null +++ b/selfservice/flow/login/flow.go @@ -0,0 +1,349 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "cmp" + "context" + "database/sql" + "encoding/json" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +// Login Flow +// +// 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. +// +// swagger:model loginFlow +type Flow struct { + // ID represents the flow's unique ID. When performing the login flow, this + // represents the id in the login UI's query parameter: http:///?flow= + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id" rw:"r"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"` + + // Ory OAuth 2.0 Login Challenge. + // + // This value is set using the `login_challenge` query parameter of the registration and login endpoints. + // If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + OAuth2LoginChallenge sqlxx.NullString `json:"oauth2_login_challenge,omitempty" faker:"-" db:"oauth2_login_challenge_data"` + + // HydraLoginRequest is an optional field whose presence indicates that Kratos + // is being used as an identity provider in a Hydra OAuth2 flow. Kratos + // populates this field by retrieving its value from Hydra and it is used by + // the login and consent UIs. + HydraLoginRequest *hydraclientgo.OAuth2LoginRequest `json:"oauth2_login_request,omitempty" faker:"-" db:"-"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type flow.Type `json:"type" db:"type" faker:"flow_type"` + + // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, + // a new flow has to be initiated. + // + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the flow started. + // + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // InternalContext stores internal context used by internals - for example MFA keys. + InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // ReturnTo contains the requested return_to URL. + ReturnTo string `json:"return_to,omitempty" db:"-"` + + // The active login method + // + // If set contains the login method used. If the flow is new, it is unset. + Active identity.CredentialsType `json:"active,omitempty" db:"active_method"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"created_at" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + + // CSRFToken contains the anti-csrf token associated with this flow. Only set for browser flows. + CSRFToken string `json:"-" db:"csrf_token"` + + // Refresh stores whether this login flow should enforce re-authentication. + Refresh bool `json:"refresh" db:"forced"` + + // RequestedAAL stores if the flow was requested to update the authenticator assurance level. + // + // This value can be one of "aal1", "aal2", "aal3". + RequestedAAL identity.AuthenticatorAssuranceLevel `json:"requested_aal" faker:"len=4" db:"requested_aal"` + + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. + // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", + // and only on creating the login flow. + SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // 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. + // + // required: true + State State `json:"state" faker:"-" db:"state"` + + // Only used internally + IDToken string `json:"-" db:"-"` + + // 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 `db:"-"` + + // IdentitySchema optionally holds the ID of the identity schema that is used + // for this flow. This value can be set by the user when creating the flow and + // should be retained when the flow is saved or converted to another flow. + IdentitySchema flow.IdentitySchema `json:"identity_schema,omitempty" faker:"-" db:"identity_schema_id"` + + // TestFlow marks this flow as an admin-created dry-run OIDC test. Test + // flows short-circuit the OIDC callback: no identity is persisted and no + // session is issued. The captured debug data is returned via the derived + // TestContext field below. + // + // Nullable so that existing rows created before the column was added read + // as a SQL NULL (effectively false); see IsTest. + TestFlow sql.NullBool `json:"-" faker:"-" db:"test_flow"` + + // TestContext is the derived API view of test-mode data. It is populated + // from InternalContext["test"] during marshal and is nil for regular + // login flows. See test_context.go. + // + // required: false + // readOnly: true + TestContext *TestContext `json:"test_context,omitempty" faker:"-" db:"-"` +} + +var ( + _ flow.Flow = (*Flow)(nil) + _ flow.FlowWithContinueWith = (*Flow)(nil) +) + +// flowDependencies are the dependencies NewFlow needs to construct a login +// flow: the configuration (for the lifespan and return-to validation), the +// clock, and the CSRF token generator. +type flowDependencies interface { + config.Provider + clock.Provider + nosurfx.CSRFTokenGeneratorProvider +} + +func NewFlow(reg flowDependencies, r *http.Request, flowType flow.Type) (*Flow, error) { + conf := reg.Config() + now := reg.Clock().Now().UTC() + exp := conf.SelfServiceFlowLoginRequestLifespan(r.Context()) + id := x.NewUUID() + requestURL := x.RequestURL(r).String() + + // Pre-validate the return to URL which is contained in the HTTP request. + _, err := redir.SecureRedirectTo(r, + conf.SelfServiceBrowserDefaultReturnTo(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + return nil, err + } + + hydraLoginChallenge, err := hydra.GetLoginChallengeID(conf, r) + if err != nil { + return nil, err + } + + refresh, _ := strconv.ParseBool(r.URL.Query().Get("refresh")) + + f := &Flow{ + ID: id, + OAuth2LoginChallenge: hydraLoginChallenge, + ExpiresAt: now.Add(exp), + IssuedAt: now, + UI: &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), + }, + RequestURL: requestURL, + CSRFToken: reg.GenerateCSRFToken(r), + Type: flowType, + Refresh: refresh, + RequestedAAL: identity.AuthenticatorAssuranceLevel(strings.ToLower(cmp.Or( + r.URL.Query().Get("aal"), + string(identity.AuthenticatorAssuranceLevel1)))), + InternalContext: []byte("{}"), + State: flow.StateChooseMethod, + } + if err := flow.SetRequestBaseURL(f, x.BaseURLStringFromContext(r.Context())); err != nil { + return nil, err + } + return f, nil +} + +func (f *Flow) GetType() flow.Type { return f.Type } +func (f *Flow) GetRequestURL() string { return f.RequestURL } +func (f *Flow) GetID() uuid.UUID { return f.ID } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext } +func (f *Flow) SetInternalContext(bytes sqlxx.JSONRawMessage) { f.InternalContext = bytes } +func (f *Flow) GetUI() *container.Container { return f.UI } +func (f *Flow) GetState() flow.State { return f.State } +func (Flow) GetFlowName() flow.FlowName { return flow.LoginFlow } +func (Flow) TableName() string { return "selfservice_login_flows" } +func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems } +func (f *Flow) SetReturnToVerification(to string) { f.ReturnToVerification = to } +func (f *Flow) GetOAuth2LoginChallenge() sqlxx.NullString { return f.OAuth2LoginChallenge } +func (f *Flow) GetHydraLoginRequest() *hydraclientgo.OAuth2LoginRequest { return f.HydraLoginRequest } +func (f *Flow) AppendTo(src *url.URL) *url.URL { return flow.AppendFlowTo(src, f.ID) } +func (f *Flow) SetState(state flow.State) { f.State = state } +func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload } + +// 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 } + +func (f *Flow) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} + +func (f *Flow) EnsureInternalContext() { + if !gjson.ParseBytes(f.InternalContext).IsObject() { + f.InternalContext = []byte("{}") + } +} + +func (f Flow) MarshalJSON() ([]byte, error) { + type local Flow + f.SetReturnTo() + // Populate the derived TestContext field for test-mode flows so the + // admin UI sees provider_id and (once captured) debug_payload. + if err := (&f).LoadTestContext(); err != nil { + return nil, err + } + return json.Marshal(local(f)) +} + +// UnmarshalJSON drops any incoming test_context. The field is derived from +// InternalContext and must only be set via LoadTestContext, otherwise a caller +// submitting a Flow blob could forge a debug payload. +func (f *Flow) UnmarshalJSON(data []byte) error { + type local Flow + if err := json.Unmarshal(data, (*local)(f)); err != nil { + return err + } + f.TestContext = nil + return nil +} + +func (f *Flow) SetReturnTo() { + // Return to is already set, do not overwrite it. + if len(f.ReturnTo) > 0 { + return + } + if u, err := url.Parse(f.RequestURL); err == nil { + f.ReturnTo = u.Query().Get("return_to") + } +} + +func (f *Flow) AfterFind(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AfterSave(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (opts []redir.SecureRedirectOption) { + return []redir.SecureRedirectOption{ + redir.SecureRedirectReturnTo(f.ReturnTo), + redir.SecureRedirectUseSourceURL(f.RequestURL), + redir.SecureRedirectAllowURLs(cfg.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(cfg.Config().SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), + } +} + +func (f *Flow) AddContinueWith(c flow.ContinueWith) { + f.ContinueWithItems = append(f.ContinueWithItems, c) +} + +func (f *Flow) ToLoggerField() map[string]any { + if f == nil { + return map[string]any{} + } + return map[string]any{ + "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 new file mode 100644 index 0000000..129e96c --- /dev/null +++ b/selfservice/flow/login/flow_test.go @@ -0,0 +1,283 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/pkg" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/urlx" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/x" +) + +func TestFakeFlow(t *testing.T) { + t.Parallel() + var r login.Flow + require.NoError(t, faker.FakeData(&r)) + + assert.Equal(t, uuid.Nil, r.ID) + assert.NotEmpty(t, r.IssuedAt) + assert.NotEmpty(t, r.ExpiresAt) + assert.NotEmpty(t, r.RequestURL) + assert.NotEmpty(t, r.Active) + assert.NotNil(t, r.UI) +} + +func TestNewFlow(t *testing.T) { + t.Parallel() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + t.Run("captures request base URL from context at init", func(t *testing.T) { + // A proxy-aware middleware (cloud CaptureOriginalBaseURLMiddleware) + // stashes the validated customer-facing base URL on the request + // context at flow init. NewFlow must persist it on the flow so the + // OIDC/SAML provider submit can read it back. + req := (&http.Request{URL: &url.URL{Path: "/"}, Host: "slug.projects.oryapis.com"}). + WithContext(x.WithBaseURL(ctx, urlx.ParseOrPanic("http://localhost:4000"))) + r, err := login.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", flow.GetRequestBaseURL(r)) + + // No captured base URL → nothing persisted (plain oryapis traffic). + r, err = login.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/"}, Host: "slug.projects.oryapis.com"}, flow.TypeBrowser) + require.NoError(t, err) + assert.Empty(t, flow.GetRequestBaseURL(r)) + }) + + t.Run("type=aal", func(t *testing.T) { + r, err := login.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "aal=aal2&refresh=true"}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.True(t, r.Refresh) + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, r.RequestedAAL) + + r, err = login.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "refresh=true"}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.True(t, r.Refresh) + 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(reg, &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(reg, &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(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, flow.TypeBrowser) + require.Error(t, err) + + _, err = login.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + }) + + t.Run("type=browser", func(t *testing.T) { + t.Run("case=regular flow creation without a session", func(t *testing.T) { + r, err := login.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/"), + Host: "ory.sh", TLS: &tls.ConnectionState{}, + }, flow.TypeBrowser) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeBrowser, r.Type) + assert.False(t, r.Refresh) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) + + t.Run("case=regular flow creation", func(t *testing.T) { + r, err := login.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("https://ory.sh/"), + Host: "ory.sh", + }, flow.TypeBrowser) + + require.NoError(t, err) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) + }) + + t.Run("type=api", func(t *testing.T) { + t.Run("case=flow with refresh", func(t *testing.T) { + r, err := login.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/?refresh=true"), + Host: "ory.sh", + }, flow.TypeAPI) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeAPI, r.Type) + assert.True(t, r.Refresh) + assert.Equal(t, "http://ory.sh/?refresh=true", r.RequestURL) + }) + + t.Run("case=flow without refresh", func(t *testing.T) { + r, err := login.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/"), + Host: "ory.sh", + }, flow.TypeAPI) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeAPI, r.Type) + assert.False(t, r.Refresh) + assert.Equal(t, "http://ory.sh/", r.RequestURL) + }) + }) + + t.Run("should parse login_challenge when Hydra is configured", func(t *testing.T) { + _, err := login.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("https://ory.sh/?login_challenge=badee1"), Host: "ory.sh"}, flow.TypeBrowser) + require.Error(t, err) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://hydra") + + r, err := login.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("https://ory.sh/?login_challenge=8aadcb8fc1334186a84c4da9813356d9"), Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "8aadcb8fc1334186a84c4da9813356d9", string(r.OAuth2LoginChallenge)) + }) +} + +func TestFlow(t *testing.T) { + t.Parallel() + r := &login.Flow{ID: x.NewUUID()} + assert.Equal(t, r.ID, r.GetID()) + + t.Run("case=expired", func(t *testing.T) { + for _, tc := range []struct { + r *login.Flow + valid bool + }{ + { + r: &login.Flow{ExpiresAt: time.Now().Add(time.Hour), IssuedAt: time.Now().Add(-time.Minute)}, + valid: true, + }, + {r: &login.Flow{ExpiresAt: time.Now().Add(-time.Hour), IssuedAt: time.Now().Add(-time.Minute)}}, + } { + if tc.valid { + require.NoError(t, tc.r.Valid(clock.New())) + } else { + require.Error(t, tc.r.Valid(clock.New())) + } + } + }) +} + +func TestGetType(t *testing.T) { + t.Parallel() + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=%s", ft), func(t *testing.T) { + r := &login.Flow{Type: ft} + assert.Equal(t, ft, r.GetType()) + }) + } +} + +func TestGetRequestURL(t *testing.T) { + t.Parallel() + expectedURL := "http://foo/bar/baz" + f := &login.Flow{RequestURL: expectedURL} + assert.Equal(t, expectedURL, f.GetRequestURL()) +} + +func TestFlowEncodeJSON(t *testing.T) { + t.Parallel() + assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, &login.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, &login.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, login.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) +} + +func TestFlowTestContextNotUnmarshalable(t *testing.T) { + t.Parallel() + + // test_context is a derived field populated from internal_context during + // MarshalJSON. It must not be settable via incoming JSON, otherwise an + // attacker who can submit a Flow blob could forge a debug payload. + raw := []byte(`{ + "id": "00000000-0000-0000-0000-000000000000", + "test_context": { + "provider_id": "forged", + "debug_payload": {"id_token_claims": {"sub": "forged"}} + } + }`) + + var f login.Flow + require.NoError(t, json.Unmarshal(raw, &f)) + assert.Nil(t, f.TestContext, "test_context must not be populated from JSON input") +} + +func TestFlowDontOverrideReturnTo(t *testing.T) { + t.Parallel() + f := &login.Flow{ReturnTo: "/foo"} + f.SetReturnTo() + assert.Equal(t, "/foo", f.ReturnTo) + + f = &login.Flow{RequestURL: "https://foo.bar?return_to=/bar"} + f.SetReturnTo() + assert.Equal(t, "/bar", f.ReturnTo) +} + +func TestDuplicateCredentials(t *testing.T) { + t.Parallel() + t.Run("case=returns previous data", func(t *testing.T) { + t.Parallel() + f := new(login.Flow) + dc := flow.DuplicateCredentialsData{ + CredentialsType: "foo", + CredentialsConfig: sqlxx.JSONRawMessage(`{"bar":"baz"}`), + DuplicateIdentifier: "bar", + } + + require.NoError(t, flow.SetDuplicateCredentials(f, dc)) + actual, err := flow.DuplicateCredentials(f) + require.NoError(t, err) + assert.Equal(t, dc, *actual) + }) + + t.Run("case=returns nil data", func(t *testing.T) { + t.Parallel() + f := new(login.Flow) + actual, err := flow.DuplicateCredentials(f) + require.NoError(t, err) + assert.Nil(t, actual) + }) +} diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go new file mode 100644 index 0000000..800ecaa --- /dev/null +++ b/selfservice/flow/login/handler.go @@ -0,0 +1,999 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + "strconv" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "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/events" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" +) + +const ( + RouteInitBrowserFlow = "/self-service/login/browser" + RouteInitAPIFlow = "/self-service/login/api" + + RouteGetFlow = "/self-service/login/flows" + + RouteSubmitFlow = "/self-service/login" +) + +type ( + dependencies interface { + clock.Provider + HookExecutorProvider + FlowPersistenceProvider + errorx.ManagementProvider + hydra.Provider + StrategyProvider + session.HandlerProvider + session.ManagementProvider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + otelx.Provider + config.Provider + ErrorHandlerProvider + sessiontokenexchange.PersistenceProvider + logrusx.Provider + TestStrategyProvider + } + HandlerProvider interface { + LoginHandler() *Handler + } + Handler struct{ d dependencies } +) + +func NewHandler(d dependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) + h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) + // The test-login-flow delete endpoint is authorized by the HMAC cookie + // set on the OIDC callback, not the global nosurf middleware. + h.d.CSRFHandler().IgnorePath(RouteDeleteTestFlow) + + public.GET(RouteInitBrowserFlow, h.createBrowserLoginFlow) + public.GET(RouteInitAPIFlow, h.createNativeLoginFlow) + public.GET(RouteGetFlow, h.getLoginFlow) + + public.POST(RouteSubmitFlow, h.updateLoginFlow) + public.GET(RouteSubmitFlow, h.updateLoginFlow) + + public.DELETE(RouteDeleteTestFlow, h.deleteTestLoginFlow) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) + + admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + + admin.POST(RouteAdminCreateTestFlow, h.adminCreateTestLoginFlow) +} + +type FlowOption func(f *Flow) + +func WithFlowReturnTo(returnTo string) FlowOption { + return func(f *Flow) { + f.ReturnTo = returnTo + } +} + +func WithOrganizationID(organizationID uuid.NullUUID) FlowOption { + return func(f *Flow) { + f.OrganizationID = organizationID + } +} + +func WithInternalContext(internalContext []byte) FlowOption { + return func(f *Flow) { + f.InternalContext = internalContext + } +} + +func WithFormErrorMessage(messages []text.Message) FlowOption { + return func(f *Flow) { + for i := range messages { + f.UI.Messages.Add(&messages[i]) + } + } +} + +func WithIsAccountLinking() FlowOption { + return func(f *Flow) { + f.isAccountLinkingFlow = true + } +} + +func WithLoginChallenge(loginChallenge string) FlowOption { + return func(f *Flow) { + f.OAuth2LoginChallenge = sqlxx.NullString(loginChallenge) + } +} + +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(h.d, r, ft) + if err != nil { + return nil, nil, err + } + for _, o := range opts { + o(f) + } + + if f.RequestedAAL == "" { + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + } + + switch cs := stringsx.SwitchExact(string(f.RequestedAAL)); { + case cs.AddCase(string(identity.AuthenticatorAssuranceLevel1)): + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + identitySchema := "" + if requestedSchema := r.URL.Query().Get("identity_schema"); requestedSchema != "" { + identitySchema, err = conf.SelfServiceFlowIdentitySchema(r.Context(), requestedSchema) + if err != nil { + return nil, nil, err + } + } + f.IdentitySchema = flow.IdentitySchema(identitySchema) + case cs.AddCase(string(identity.AuthenticatorAssuranceLevel2)): + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + default: + return nil, nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to parse AuthenticationMethod Assurance Level (AAL): %s", cs.ToUnknownCaseErr())) + } + + // We assume an error means the user has no session. + // The full expansion is required: the strategies' form hydrators and the organization + // filter read the session's identity. + sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) { + // No session exists yet + 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(err) + } + f.SessionTokenExchangeCode = e.InitCode + } + + // We can not request an AAL > 1 because we must first verify the first factor. + if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { + return nil, nil, errors.WithStack(ErrSessionRequiredForHigherAAL()) + } + + // We are setting refresh to false if no session exists. + f.Refresh = false + + goto preLoginHook + } else if err != nil { + // Some other error happened - return that one. + return nil, nil, err + } else { + // A session exists already + if f.Refresh { + // We are refreshing so let's continue + goto preLoginHook + } + + // We are not refreshing - so are we requesting MFA? + + // If level is 1 we are not requesting AAL -> we are logged in already. + if f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { + return nil, sess, errors.WithStack(ErrAlreadyLoggedIn()) + } + + // We are requesting an assurance level which the session already has. So we are not upgrading the session + // in which case we want to return an error. + if f.RequestedAAL <= sess.AuthenticatorAssuranceLevel { + return nil, sess, errors.WithStack(ErrAlreadyLoggedIn()) + } + + // Looks like we are requesting an AAL which is higher than what the session has. + goto preLoginHook + } + +preLoginHook: + loginStrategies := h.d.LoginStrategies(r.Context(), PrepareOrganizations(r, f, sess)...) + for _, s := range loginStrategies { + var populateErr error + + switch f.RequestedAAL { + case identity.AuthenticatorAssuranceLevel1: + switch s := s.(type) { + case AAL1FormHydrator: + switch { + case f.IsRefresh() && sess != nil: + // Refreshing takes precedence over identifier_first auth which can not be a refresh flow. + // Therefor this comes first. + populateErr = s.PopulateLoginMethodFirstFactorRefresh(r, f, sess) + case h.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()) && !f.isAccountLinkingFlow: + populateErr = s.PopulateLoginMethodIdentifierFirstIdentification(r, f) + default: + populateErr = s.PopulateLoginMethodFirstFactor(r, f) + } + case AAL2FormHydrator: + continue + default: + populateErr = errors.WithStack( + x.PseudoPanic.WithReason("A login strategy was expected to implement interface AAL1FormHydrator but did not."), + ) + } + case identity.AuthenticatorAssuranceLevel2: + switch s := s.(type) { + case AAL2FormHydrator: + switch { + case f.IsRefresh(): + // Refresh takes precedence. + populateErr = s.PopulateLoginMethodSecondFactorRefresh(r, f) + default: + populateErr = s.PopulateLoginMethodSecondFactor(r, f) + } + case AAL1FormHydrator: + continue + default: + populateErr = errors.WithStack( + x.PseudoPanic.WithReason("A login strategy was expected to implement interface AAL2FormHydrator but did not."), + ) + } + } + + if populateErr != nil { + return nil, nil, populateErr + } + } + + if f.Refresh { + f.UI.Messages.Set(text.NewInfoLoginReAuth()) + } + + if sess != nil && f.RequestedAAL > sess.AuthenticatorAssuranceLevel && f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { + f.UI.Messages.Add(text.NewInfoLoginMFA()) + } + + if err := sortNodes(r.Context(), f.UI.Nodes); err != nil { + return nil, nil, err + } + + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(h.d.GenerateCSRFToken(r)) + } + + if err := h.d.LoginHookExecutor().PreLoginHook(w, r, f); err != nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, err) + return f, sess, nil + } + + if err := h.d.LoginFlowPersister().CreateLoginFlow(r.Context(), f); err != nil { + return nil, nil, err + } + + // If the strategy supports fast login, we can attempt a fast path. + // We execute the fast login here because it requires the flow to be created first. + for _, s := range loginStrategies { + if strategy, ok := s.(FastLoginStrategy); ok { + if err := strategy.FastLogin2FA(w, r, f, sess); errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + span := trace.SpanFromContext(r.Context()) + span.AddEvent(events.NewLoginInitiated(r.Context(), f.ID, ft.String(), f.Refresh, f.OrganizationID, string(f.RequestedAAL))) + return nil, nil, err + } else if err != nil { + return nil, nil, err + } + break + } + } + + span := trace.SpanFromContext(r.Context()) + span.AddEvent(events.NewLoginInitiated(r.Context(), f.ID, ft.String(), f.Refresh, f.OrganizationID, string(f.RequestedAAL))) + return f, nil, nil +} + +func (h *Handler) FromOldFlow(w http.ResponseWriter, r *http.Request, of Flow) (*Flow, error) { + nf, _, err := h.NewLoginFlow(w, r, of.Type) + if err != nil { + return nil, err + } + + nf.RequestURL = of.RequestURL + nf.IdentitySchema = of.IdentitySchema + return nf, nil +} + +// Create Native Login Flow Parameters +// +// swagger:parameters createNativeLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createNativeLoginFlow struct { + // Refresh a login session + // + // If set to true, this will refresh an existing login session by + // asking the user to sign in again. This will reset the + // authenticated_at time of the session. + // + // in: query + Refresh bool `json:"refresh"` + + // Request a Specific AuthenticationMethod Assurance Level + // + // Use this parameter to upgrade an existing session's authenticator assurance level (AAL). This + // allows you to ask for multi-factor authentication. When an identity sign in using e.g. username+password, + // the AAL is 1. If you wish to "upgrade" the session's security by asking the user to perform TOTP / WebAuth/ ... + // you would set this to "aal2". + // + // in: query + RequestAAL identity.AuthenticatorAssuranceLevel `json:"aal"` + + // The Session Token of the Identity performing the settings flow. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // EnableSessionTokenExchangeCode requests the login flow to include a code that can be used to retrieve the session token + // after the login flow has been completed. + // + // in: query + EnableSessionTokenExchangeCode bool `json:"return_session_token_exchange_code"` + + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // An optional organization ID that should be used for logging this user in. + // This parameter is only effective in the Ory Network. + // + // 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"` + + // An optional identity schema to use for the login flow. + // + // required: false + // in: query + IdentitySchema string `json:"identity_schema"` +} + +// swagger:route GET /self-service/login/api frontend createNativeLoginFlow +// +// # Create Login Flow for Native Apps +// +// This endpoint initiates a login flow for native apps that do not use a browser, such as mobile devices, smart TVs, and so on. +// +// If a valid provided session cookie or session token is provided, a 400 Bad Request error +// will be returned unless the URL query parameter `?refresh=true` is set. +// +// To fetch an existing login flow call `/self-service/login/flows?flow=`. +// +// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server +// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make +// you vulnerable to a variety of CSRF attacks, including CSRF login attacks. +// +// In the case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// +// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: loginFlow +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createNativeLoginFlow(w http.ResponseWriter, r *http.Request) { + 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 { + if !errors.Is(err, flow.ErrCompletedByStrategy) { + h.d.Writer().WriteError(w, r, err) + } + return + } + + h.d.Writer().Write(w, r, f) +} + +// Initialize Browser Login Flow Parameters +// +// swagger:parameters createBrowserLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserLoginFlow struct { + // Refresh a login session + // + // If set to true, this will refresh an existing login session by + // asking the user to sign in again. This will reset the + // authenticated_at time of the session. + // + // in: query + Refresh bool `json:"refresh"` + + // Request a Specific AuthenticationMethod Assurance Level + // + // Use this parameter to upgrade an existing session's authenticator assurance level (AAL). This + // allows you to ask for multi-factor authentication. When an identity sign in using e.g. username+password, + // the AAL is 1. If you wish to "upgrade" the session's security by asking the user to perform TOTP / WebAuth/ ... + // you would set this to "aal2". + // + // in: query + RequestAAL identity.AuthenticatorAssuranceLevel `json:"aal"` + + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` + + // An optional Hydra login challenge. If present, Kratos will cooperate with + // Ory Hydra to act as an OAuth2 identity provider. + // + // The value for this parameter comes from `login_challenge` URL Query parameter sent to your + // application (e.g. `/login?login_challenge=abcde`). + // + // required: false + // in: query + HydraLoginChallenge string `json:"login_challenge"` + + // An optional organization ID that should be used for logging this user in. + // This parameter is only effective in the Ory Network. + // + // 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"` + + // An optional identity schema to use for the login flow. + // + // required: false + // in: query + IdentitySchema string `json:"identity_schema"` +} + +// swagger:route GET /self-service/login/browser frontend createBrowserLoginFlow +// +// # Create Login Flow for Browsers +// +// This endpoint initializes a browser-based user login flow. This endpoint will set the appropriate +// cookies and anti-CSRF measures required for browser-based flows. +// +// If this endpoint is opened as a link in the browser, it will be redirected to +// `selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session +// exists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter +// `?refresh=true` was set. +// +// If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// +// The optional query parameter login_challenge is set when using Kratos with +// Hydra in an OAuth2 flow. See the oauth2_provider.url configuration +// option. +// +// This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: loginFlow +// 303: emptyResponse +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request) { + 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 + ) + if r.URL.Query().Has("login_challenge") { + var err error + hydraLoginChallenge, err = hydra.GetLoginChallengeID(h.d.Config(), r) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + hydraLoginRequest, err = h.d.Hydra().GetLoginRequest(ctx, string(hydraLoginChallenge)) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + if !hydraLoginRequest.GetSkip() { + q := r.URL.Query() + q.Set("refresh", "true") + r.URL.RawQuery = q.Encode() + } + + // on OAuth2 flows, we need to use the RequestURL + // as the ReturnTo URL. + // This is because a user might want to switch between + // 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(ctx) { + // replace the return_to query parameter + q := r.URL.Query() + q.Set("return_to", hydraLoginRequest.RequestUrl) + r.URL.RawQuery = q.Encode() + } + } + + a, sess, err := h.NewLoginFlow(w, r, flow.TypeBrowser) + if errors.Is(err, ErrAlreadyLoggedIn()) { + if hydraLoginRequest != nil { + if !hydraLoginRequest.GetSkip() { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, errors.WithStack(herodot.ErrForbidden().WithReason("ErrAlreadyLoggedIn indicated we can skip login, but Hydra asked us to refresh"))) + return + } + + rt, hydraErr := h.d.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(hydraLoginChallenge), + IdentityID: sess.IdentityID.String(), + SessionID: sess.ID.String(), + AuthenticationMethods: sess.AMR, + }) + if hydraErr != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, hydraErr) + return + } + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), err, rt) + return + } + + returnTo, redirErr := redir.SecureRedirectTo(r, h.d.Config().SelfServiceBrowserDefaultReturnTo(ctx), + redir.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(ctx)), + redir.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), + ) + if redirErr != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, redirErr) + return + } + + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), err, returnTo.String()) + return + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + a.HydraLoginRequest = hydraLoginRequest + + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), a, a.AppendTo(h.d.Config().SelfServiceFlowLoginUI(ctx)).String()) +} + +// Get Login Flow Parameters +// +// swagger:parameters getLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getLoginFlow struct { + // The Login Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/login?flow=abcde`). + // + // required: true + // in: query + ID string `json:"id"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route GET /self-service/login/flows frontend getLoginFlow +// +// # Get Login Flow +// +// This endpoint returns a login flow's context with, for example, error details and other information. +// +// Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. +// For AJAX requests you must ensure that cookies are included in the request or requests will fail. +// +// If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain +// and you need to forward the incoming HTTP Cookie header to this endpoint: +// +// ```js +// // pseudo-code example +// router.get('/login', async function (req, res) { +// const flow = await client.getLoginFlow(req.header('cookie'), req.query['flow']) +// +// res.render('login', flow) +// }) +// ``` +// +// This request may fail due to several reasons. The `error.id` can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `self_service_flow_expired`: The flow is expired and you should request a new one. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: loginFlow +// 403: errorGeneric +// 404: errorGeneric +// 410: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) getLoginFlow(w http.ResponseWriter, r *http.Request) { + 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 + } + + // Browser flows must include the CSRF token + // + // Resolves: https://github.com/ory/kratos/issues/1282 + // + // Test flows have their own scheme: + // - choose_method (uncaptured): public read. The flow carries no PII + // and was admin-created with a server-minted token that the browser + // never received, so a CSRF check would always fail. + // - captured: require the CSRF cookie to match f.CSRFToken. The OIDC + // callback regenerated the cookie+token pair, so only the browser + // that completed the round trip can read the captured DebugPayload. + switch { + case ar.IsTest(): + // TestContext is a derived field (loaded from internal_context); the + // SQL fetch above doesn't populate it. + if err := ar.LoadTestContext(); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + if ar.TestContext != nil && ar.TestContext.DebugPayload != nil && + !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), ar.CSRFToken) { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("missing or invalid CSRF token"))) + return + } + case ar.Type == flow.TypeBrowser && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), ar.CSRFToken): + h.d.Writer().WriteError(w, r, nosurfx.CSRFErrorReason(r, h.d)) + return + } + + if ar.ExpiresAt.Before(h.d.Clock().Now()) { + if ar.Type == flow.TypeBrowser { + redirectURL := flow.GetFlowExpiredRedirectURL(ctx, h.d.Config(), RouteInitBrowserFlow, ar.ReturnTo) + + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.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."). + WithDetail("redirect_to", redirectURL.String()). + WithDetail("return_to", ar.ReturnTo))) + return + } + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.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(ctx), RouteInitAPIFlow).String()))) + return + } + + if 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(ctx, w, r, err) + return + } + ar.HydraLoginRequest = hlr + } + + h.d.Writer().Write(w, r, ar) +} + +// Update Login Flow Parameters +// +// swagger:parameters updateLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateLoginFlow struct { + // The Login Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/login?flow=abcde`). + // + // required: true + // in: query + Flow string `json:"flow"` + + // in: body + // required: true + Body updateLoginFlowBody + + // The Session Token of the Identity performing the settings flow. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:model updateLoginFlowBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateLoginFlowBody struct{} + +// swagger:route POST /self-service/login frontend updateLoginFlow +// +// # Submit a Login Flow +// +// Use this endpoint to complete a login flow. This endpoint +// behaves differently for API and browser flows. +// +// API flows expect `application/json` to be sent in the body and responds with +// - HTTP 200 and a application/json body with the session token on success; +// - HTTP 410 if the original flow expired with the appropriate error messages set and optionally a `use_flow_id` parameter in the body; +// - HTTP 400 on form validation errors. +// +// Browser flows expect a Content-Type of `application/x-www-form-urlencoded` or `application/json` to be sent in the body and respond with +// - a HTTP 303 redirect to the post/after login URL or the `return_to` value if it was set and if the login succeeded; +// - a HTTP 303 redirect to the login UI URL with the flow ID containing the validation errors otherwise. +// +// Browser flows with an accept header of `application/json` will not redirect but instead respond with +// - HTTP 200 and a application/json body with the signed in identity and a `Set-Cookie` header on success; +// - HTTP 303 redirect to a fresh login flow if the original flow expired with the appropriate error messages set; +// - HTTP 400 on form validation errors. +// +// If this endpoint is called with `Accept: application/json` in the header, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// - `browser_location_change_required`: Usually sent when an AJAX request indicates that the browser needs to open a specific URL. +// Most likely used in Social Sign In flows. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Schemes: http, https +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Header: +// - Set-Cookie +// +// Responses: +// 200: successfulNativeLogin +// 303: emptyResponse +// 400: loginFlow +// 410: errorGeneric +// 422: errorBrowserLocationChangeRequired +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) updateLoginFlow(w http.ResponseWriter, r *http.Request) { + var err error + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.updateLoginFlow") + ctx = semconv.ContextWithAttributes(ctx, + attribute.String(events.AttributeKeySelfServiceStrategyUsed.String(), "login"), + attribute.String(events.AttributeKeySelfServiceFlowName.String(), "login"), + ) + 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(ctx, rid) + if err != nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, err) + return + } + + // The full expansion is required: the session is handed to every login strategy below. + sess, err := h.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandEverything, identity.ExpandEverything) + if err == nil { + if f.Refresh { + // If we want to refresh, continue the login + goto continueLogin + } + + if f.RequestedAAL > sess.AuthenticatorAssuranceLevel { + // If we want to upgrade AAL, continue the login + goto continueLogin + } + + if x.IsJSONRequest(r) || f.Type == flow.TypeAPI { + // We are not upgrading AAL, nor are we refreshing. Error! + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, errors.WithStack(ErrAlreadyLoggedIn())) + return + } + + 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 + // having a session. + if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, errors.WithStack(ErrSessionRequiredForHigherAAL())) + return + } + + sess = session.NewInactiveSession() + } else { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, err) + return + } + +continueLogin: + if err := f.Valid(h.d.Clock()); err != nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, err) + return + } + + var ct identity.CredentialsType + var i *identity.Identity + var group node.UiNodeGroup + for _, ss := range h.d.AllLoginStrategies() { + interim, err := ss.Login(w, r, f, sess) + group = ss.NodeGroup() + if errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ss.ID(), group, err) + return + } + + // What can happen is that we re-authenticate as another user. In this case, we need to use a completely fresh + // session! + if sess.IdentityID != uuid.Nil && sess.IdentityID != interim.ID { + sess = session.NewInactiveSession() + } + + method := ss.CompletedAuthenticationMethod(ctx) + sess.CompletedLoginForMethod(method) + i = interim + ct = ss.ID() + break + } + + if i == nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ct, node.DefaultGroup, errors.WithStack(schema.NewNoLoginStrategyResponsible())) + return + } + + if err := h.d.LoginHookExecutor().PostLoginHook(w, r, group, f, i, sess, ""); err != nil { + if errors.Is(err, ErrAddressNotVerified()) { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ct, node.DefaultGroup, errors.WithStack(schema.NewAddressNotVerifiedError())) + return + } + + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ct, node.DefaultGroup, err) + return + } +} diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go new file mode 100644 index 0000000..20c448b --- /dev/null +++ b/selfservice/flow/login/handler_test.go @@ -0,0 +1,994 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/gobuffalo/httptest" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + stdtotp "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/totp" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func init() { + corpx.RegisterFakes() +} + +func TestFlowLifecycle(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + fakeHydra := hydra.NewFake() + reg.SetHydra(fakeHydra) + + routerPublic := httprouterx.NewRouterPublic() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerPublic, httprouterx.NewRouterAdminWithPrefix()) + loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + returnToTS := testhelpers.NewRedirTS(t, "return_to", conf) + errorTS := testhelpers.NewErrorTestServer(t, reg) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL) + + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/password.schema.json"}, + {ID: "email", URL: "file://./stub/email.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://./stub/phone.schema.json", SelfserviceSelectable: true}, + {ID: "not-allowed", URL: "file://./stub/password.schema.json"}, + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + + assertion := func(body []byte, isForced, isApi bool) { + r := gjson.GetBytes(body, "refresh") + assert.True(t, r.Exists(), "%s", body) + assert.Equal(t, isForced, r.Bool(), "%s", body) + if isApi { + assert.Equal(t, "api", gjson.GetBytes(body, "type").String()) + } else { + assert.Equal(t, "browser", gjson.GetBytes(body, "type").String()) + } + } + + initAuthenticatedFlow := func(t *testing.T, extQuery url.Values, isAPI bool) (*http.Response, []byte) { + route := login.RouteInitBrowserFlow + if isAPI { + route = login.RouteInitAPIFlow + } + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) + req.URL.RawQuery = extQuery.Encode() + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req) + if isAPI { + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + } + return res, body + } + + initUnauthenticatedFlow := func(t *testing.T, extQuery url.Values, isAPI bool) (*http.Response, []byte) { + route := login.RouteInitBrowserFlow + if isAPI { + route = login.RouteInitAPIFlow + } + client := ts.Client() + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) + + req.URL.RawQuery = extQuery.Encode() + res, err := client.Do(req) + require.NoError(t, errors.WithStack(err)) + + body, err := io.ReadAll(res.Body) + require.NoError(t, errors.WithStack(err)) + require.NoError(t, res.Body.Close()) + return res, body + } + + initFlowWithAccept := func(t *testing.T, query url.Values, isAPI bool, accept string) (*http.Response, []byte) { + route := login.RouteInitBrowserFlow + if isAPI { + route = login.RouteInitAPIFlow + } + c := ts.Client() + req, err := http.NewRequest("GET", ts.URL+route+"?"+query.Encode(), nil) + require.NoError(t, err) + if accept != "" { + req.Header.Set("Accept", accept) + } + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + initFlow := func(t *testing.T, query url.Values, isAPI bool) (*http.Response, []byte) { + return initFlowWithAccept(t, query, isAPI, "") + } + + initSPAFlow := func(t *testing.T, query url.Values) (*http.Response, []byte) { + return initFlowWithAccept(t, query, false, "application/json") + } + + id1mail, id2mail := x.NewUUID().String(), x.NewUUID().String() + identity1 := &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{id1mail}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar + }, + }, + State: identity.StateActive, + Traits: identity.Traits(`{"username":"` + id1mail + `"}`), + } + identity2 := &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{id2mail}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar + }, + }, + State: identity.StateActive, + Traits: identity.Traits(`{"username":"` + id2mail + `"}`), + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), identity1)) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), identity2)) + + t.Run("lifecycle=submit", func(t *testing.T) { + t.Run("interaction=unauthenticated", func(t *testing.T) { + run := func(t *testing.T, tt flow.Type, aal string, values url.Values) (string, *http.Response) { + f := login.Flow{ + Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), + UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + res, err := testhelpers.NewTestClient(t).PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), values) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + return string(body), res + } + + t.Run("case=ensure aal can only be upgraded with a session on submit", func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + body, res := run(t, flow.TypeAPI, "aal2", url.Values{"method": {"password"}}) + assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.Get(body, "error.reason").String()) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := run(t, flow.TypeBrowser, "aal2", url.Values{"method": {"password"}}) + assert.Contains(t, res.Request.URL.String(), errorTS.URL) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.Get(body, "reason").String()) + }) + }) + + t.Run("case=end up with method missing when aal is ok", func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + body, res := run(t, flow.TypeAPI, "aal1", url.Values{"method": {"not-exist"}}) + assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow) + assertx.EqualAsJSON(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), body) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := run(t, flow.TypeBrowser, "aal1", url.Values{"method": {"not-exist"}}) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + assertx.EqualAsJSON(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), body) + }) + }) + + t.Run("case=reset the session when refresh is true but identity is different", func(t *testing.T) { + testhelpers.NewRedirSessionEchoTS(t, reg) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL) + }) + + run := func(t *testing.T, tt flow.Type) (string, string) { + f := login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: false, RequestedAAL: "aal1"} + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + hc := testhelpers.NewClientWithCookies(t) + res, err := hc.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + firstSession := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + f = login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: true, RequestedAAL: "aal1"} + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + vv := testhelpers.EncodeFormAsJSON(t, tt == flow.TypeAPI, url.Values{"method": {"password"}, "password_identifier": {id2mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}}) + + req, err := http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(vv)) + require.NoError(t, err) + + if tt == flow.TypeAPI { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+gjson.GetBytes(firstSession, "session_token").String()) + } else { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + res, err = hc.Do(req) + require.NoError(t, err) + secondSession := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + return string(firstSession), string(secondSession) + } + + t.Run("type=browser", func(t *testing.T) { + a, b := run(t, flow.TypeBrowser) + + assert.Equal(t, id1mail, gjson.Get(a, "identity.traits.username").String()) + assert.Equal(t, id2mail, gjson.Get(b, "identity.traits.username").String()) + + assert.NotEmpty(t, gjson.Get(b, "id").String()) + assert.NotEqual(t, gjson.Get(b, "id").String(), gjson.Get(a, "id").String()) + }) + + t.Run("type=api", func(t *testing.T) { + a, b := run(t, flow.TypeAPI) + + assert.Equal(t, id1mail, gjson.Get(a, "session.identity.traits.username").String()) + assert.Equal(t, id2mail, gjson.Get(b, "session.identity.traits.username").String()) + + assert.NotEmpty(t, gjson.Get(a, "session_token").String()) + assert.NotEqual(t, gjson.Get(a, "session_token").String(), gjson.Get(b, "session_token").String()) + + assert.NotEmpty(t, gjson.Get(b, "session.id").String()) + assert.NotEqual(t, gjson.Get(b, "session.id").String(), gjson.Get(a, "id").String()) + }) + }) + + t.Run("case=changed kratos session identifiers when refresh is true", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL) + }) + + t.Run("type=browser", func(t *testing.T) { + // Setup flow + f := login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: false, RequestedAAL: "aal1"} + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + // Submit Login + hc := testhelpers.NewClientWithCookies(t) + res, err := hc.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + + // Check response and session cookie presence + assert.Equal(t, http.StatusOK, res.StatusCode) + require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1) + require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session") + cookies1 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow)) + + req, err := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil) + require.NoError(t, err) + + res, err = hc.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + firstSession := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + // Refresh + f = login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: true, RequestedAAL: "aal1"} + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + vv := testhelpers.EncodeFormAsJSON(t, false, url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}}) + + req, err = http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(vv)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // Submit Login + res, err = hc.Do(req) + require.NoError(t, err) + + // Check response and session cookie presence + assert.Equal(t, http.StatusOK, res.StatusCode) + require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1) + require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session") + cookies2 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow)) + + req, err = http.NewRequest("GET", ts.URL+"/sessions/whoami", nil) + require.NoError(t, err) + + res, err = hc.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + secondSession := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + // Sessions should still be resolvable despite different kratos session identifier due to nonce + assert.NotEqual(t, cookies1[0].String(), cookies2[0].String()) + assert.Equal(t, id1mail, gjson.Get(string(firstSession), "identity.traits.username").String()) + assert.Equal(t, id1mail, gjson.Get(string(secondSession), "identity.traits.username").String()) + assert.Equal(t, gjson.Get(string(secondSession), "id").String(), gjson.Get(string(firstSession), "id").String()) + }) + }) + }) + + t.Run("case=ensure aal is checked for upgradeability on session", func(t *testing.T) { + run := func(t *testing.T, tt flow.Type, values url.Values) (string, *http.Response) { + f := login.Flow{ + Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), + UI: container.New(""), Refresh: false, RequestedAAL: "aal1", + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + req, err := http.NewRequest("GET", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req) + return string(body), res + } + + t.Run("type=api", func(t *testing.T) { + body, res := run(t, flow.TypeAPI, url.Values{"method": {"password"}}) + assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow) + assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn().Reason(), gjson.Get(body, "ui.messages.0.text").String(), body) + }) + + t.Run("type=browser", func(t *testing.T) { + _, res := run(t, flow.TypeBrowser, url.Values{"method": {"password"}}) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + }) + + t.Run("case=should return an error because the request is expired", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, "50ms") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, "10m") + }) + + // Freeze the clock so the handler and the expected error read the same instant. The "expired N minutes + // ago" reason is then deterministic instead of depending on the request round-trip time. + now := time.Now().UTC() + reg.SetClock(clock.NewMock(now)) + t.Cleanup(func() { reg.SetClock(clock.New()) }) + + expired := now.Add(-time.Minute) + run := func(t *testing.T, tt flow.Type, aal string, values string, isSPA bool) (string, *http.Response) { + f := login.Flow{ + Type: tt, ExpiresAt: expired, IssuedAt: now, + UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) + + req, err := http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values)) + require.NoError(t, err) + + if isSPA { + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + } else { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + res, err := testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + return string(body), res + } + + t.Run("type=api", func(t *testing.T) { + actual, res := run(t, flow.TypeAPI, "aal1", `{"method":"password"}`, false) + assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expired), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := run(t, flow.TypeBrowser, "aal1", url.Values{"method": {"password"}}.Encode(), false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "expired", "%s", body) + }) + + t.Run("type=SPA", func(t *testing.T) { + actual, res := run(t, flow.TypeBrowser, "aal1", `{"method":"password"}`, true) + assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expired), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) + }) + }) + + t.Run("case=should return to settings flow after successful mfa login after recovery", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeTOTP.String(), true) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToTS.URL}) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, string(identity.AuthenticatorAssuranceLevel1)) + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, string(identity.AuthenticatorAssuranceLevel1)) + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeTOTP.String(), false) + }) + + key, err := totp.NewKey(context.Background(), "foo", reg) + require.NoError(t, err) + email := testhelpers.RandomEmail() + id := &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + }, + }, + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + + require.NoError(t, reg.IdentityManager().CreateIdentities(context.Background(), []*identity.Identity{id}, identity.ManagerAllowWriteProtectedTraits)) + + id.SetCredentials(identity.CredentialsTypeTOTP, identity.Credentials{ + Type: identity.CredentialsTypeTOTP, + Identifiers: []string{id.ID.String()}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`), + }) + require.NoError(t, reg.IdentityManager().Update(context.Background(), id, identity.ManagerAllowWriteProtectedTraits)) + + h := func(w http.ResponseWriter, r *http.Request) { + 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)) + require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess)) + require.Equal(t, identity.AuthenticatorAssuranceLevel1, sess.AuthenticatorAssuranceLevel) + } + + routerPublic.GET("/mock-session", h) + + client := testhelpers.NewClientWithCookies(t) + + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/mock-session") + + settingsURL := ts.URL + settings.RouteInitBrowserFlow + "?return_to=" + url.QueryEscape(returnToTS.URL) + req, err := http.NewRequest("GET", settingsURL, nil) + require.NoError(t, err) + + // we initialize the settings flow with a session that has AAL1 set + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + // we expect the request to redirect to the login flow because the AAL1 session is not sufficient + requestURL, err := url.Parse(resp.Request.Referer()) + require.NoError(t, err) + require.Equal(t, login.RouteInitBrowserFlow, requestURL.Path) + require.Equal(t, "aal2", requestURL.Query().Get("aal")) + require.Equal(t, settingsURL, requestURL.Query().Get("return_to")) + + // we expect to be on the login page now + respURL := resp.Request.URL + require.NoError(t, err) + require.Equal(t, "/login-ts", respURL.Path) + flowID := respURL.Query().Get("flow") + require.NotEmpty(t, flowID) + + code, err := stdtotp.GenerateCode(key.Secret(), time.Now()) + require.NoError(t, err) + + req, err = http.NewRequest("GET", ts.URL+login.RouteGetFlow+"?id="+flowID, nil) + require.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body := string(x.MustReadAll(resp.Body)) + defer func() { _ = resp.Body.Close() }() + + totpNode := gjson.Get(body, "ui.nodes.#(attributes.name==totp_code)").String() + require.NotEmpty(t, totpNode) + require.NotEmpty(t, gjson.Get(body, "ui.action").String()) + + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + + req, err = http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+flowID, strings.NewReader(url.Values{ + "method": {"totp"}, + "totp_code": {code}, + "csrf_token": {csrfToken}, + }.Encode())) + + require.NoError(t, err) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + resp, err = client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, resp.StatusCode) + + location, err := resp.Location() + require.NoError(t, err) + require.Equal(t, settings.RouteInitBrowserFlow, location.Path) + }) + }) + + t.Run("lifecycle=init", func(t *testing.T) { + t.Run("suite=identity schema in query", func(t *testing.T) { + for _, tc := range []struct { + name string + query url.Values + wantErr bool + wantIdentifier string + }{{ + name: "not-allowed", + query: url.Values{"identity_schema": {"not-allowed"}}, + wantErr: true, + }, { + name: "not-found", + query: url.Values{"identity_schema": {"not-found"}}, + wantErr: true, + }, { + name: "phone", + query: url.Values{"identity_schema": {"phone"}}, + wantIdentifier: "Phone Number", + }, { + name: "email", + query: url.Values{"identity_schema": {"email"}}, + wantIdentifier: "E-Mail Address", + }, { + name: "default", + query: url.Values{"identity_schema": {"default"}}, + wantIdentifier: "Username", + }} { + t.Run("case="+tc.name, func(t *testing.T) { + t.Run("flow=api", func(t *testing.T) { + res, body := initFlow(t, tc.query, true) + if tc.wantErr { + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + return + } + assert.Equalf(t, tc.wantIdentifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).meta.label.text").String(), "%s", body) + }) + + t.Run("flow=browser", func(t *testing.T) { + res, body := initFlow(t, tc.query, false) + if tc.wantErr { + require.Contains(t, res.Request.URL.String(), errorTS.URL, "%s", body) + assert.EqualValues(t, "Bad Request", gjson.GetBytes(body, "status").String(), "%s", body) + return + } + assert.Equalf(t, tc.wantIdentifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).meta.label.context.title").String(), "%s", body) + }) + }) + } + }) + + t.Run("flow=api", func(t *testing.T) { + t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertion(body, false, true) + assert.Empty(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{returnToTS.URL, "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) { + res, body := initFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=can request refresh and aal at the same time on authenticated request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertx.EqualAsJSON(t, "Please confirm this action by verifying that it is you.", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body) + assertx.EqualAsJSON(t, "Please complete the second authentication challenge.", gjson.GetBytes(body, "ui.messages.1.text").String(), "%s", body) + }) + + t.Run("case=can not request aal2 on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{"aal": {"aal2"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=ignores aal1 if session has aal1 already", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal1"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertx.EqualAsJSON(t, "A valid session was detected and thus login is not possible. Did you forget to set `?refresh=true`?", gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=aal0 is not a valid value", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal0"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertx.EqualAsJSON(t, "Unable to parse AuthenticationMethod Assurance Level (AAL): expected one of [aal1, aal2] but got aal0", gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=indicates two factor auth", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginMFA().Text) + }) + + t.Run("case=does not set forced flag on unauthenticated request with refresh=true", func(t *testing.T) { + res, body := initFlow(t, url.Values{"refresh": {"true"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertion(body, false, true) + }) + + t.Run("case=does not set forced flag on authenticated request without refresh=true", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{}, true) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("case=does not set forced flag on authenticated request with refresh=false", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"false"}}, true) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("case=does set forced flag on authenticated request with refresh=true", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertion(body, true, true) + }) + + t.Run("case=check info message on authenticated request with refresh=true", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assertion(body, true, true) + assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginReAuth().Text) + }) + }) + + t.Run("flow=browser", func(t *testing.T) { + t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{}, false) + assertion(body, false, false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + }) + + t.Run("case=never returns a session token exchange code", func(t *testing.T) { + _, body := initFlow(t, urlx.ParseOrPanic("/?return_session_token_exchange_code=true").Query(), false) + assertion(body, false, false) + assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + }) + + t.Run("case=can not request refresh and aal at the same time on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, false) + assert.Contains(t, res.Request.URL.String(), errorTS.URL) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("case=can request refresh and aal at the same time on authenticated request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + assertx.EqualAsJSON(t, "Please confirm this action by verifying that it is you.", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body) + assertx.EqualAsJSON(t, "Please complete the second authentication challenge.", gjson.GetBytes(body, "ui.messages.1.text").String(), "%s", body) + }) + + t.Run("case=redirects if aal2 is requested and set up already without refresh", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}, "set_aal": {"aal2"}}, false) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + + t.Run("case=can not request aal2 on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{"aal": {"aal2"}}, false) + assert.Contains(t, res.Request.URL.String(), errorTS.URL) + assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "reason").String()) + }) + + t.Run("case=ignores aal1 if session has aal1 already", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, url.Values{"aal": {"aal1"}}, false) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + + t.Run("case=aal0 is not a valid value", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal0"}}, false) + assert.Contains(t, res.Request.URL.String(), errorTS.URL) + assertx.EqualAsJSON(t, "Unable to parse AuthenticationMethod Assurance Level (AAL): expected one of [aal1, aal2] but got aal0", gjson.GetBytes(body, "reason").String()) + }) + + t.Run("case=indicates two factor auth", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}}, false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginMFA().Text) + }) + + t.Run("case=makes request with JSON", func(t *testing.T) { + res, body := initSPAFlow(t, url.Values{}) + assertion(body, false, false) + assert.NotContains(t, res.Request.URL.String(), loginTS.URL) + }) + + t.Run("case=does not set forced flag on unauthenticated request with refresh=true", func(t *testing.T) { + res, body := initFlow(t, url.Values{"refresh": {"true"}}, false) + assertion(body, false, false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + }) + + t.Run("case=does not set forced flag on authenticated request without refresh=true", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, url.Values{}, false) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + + t.Run("case=does not set forced flag on authenticated request with refresh=false", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, url.Values{"refresh": {"false"}}, false) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + + t.Run("case=does set forced flag on authenticated request with refresh=true", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, false) + assertion(body, true, false) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + }) + + t.Run("case=redirects with 303", func(t *testing.T) { + c := &http.Client{} + // don't get the reference, instead copy the values, so we don't alter the client directly. + *c = *ts.Client() + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", ts.URL+login.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + }) + + t.Run("case=refuses to parse oauth2 login challenge when Hydra is not configured", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}}, false) + require.Contains(t, res.Request.URL.String(), errorTS.URL) + require.Contains(t, string(body), "refusing to parse") + }) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + + t.Run("case=oauth2 flow init should override return_to to the oauth2 request_url", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.com", "https://example.com"}) + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderOverrideReturnTo, true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderOverrideReturnTo, false) + }) + + res, _ := initUnauthenticatedFlow(t, url.Values{ + "return_to": {"https://example.com"}, + "login_challenge": {hydra.FakeValidLoginChallenge}, + }, false) + require.Equal(t, http.StatusOK, res.StatusCode) + require.Contains(t, res.Request.URL.String(), loginTS.URL) + + c := ts.Client() + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteGetFlow, nil) + req.URL.RawQuery = url.Values{"id": {res.Request.URL.Query().Get("flow")}}.Encode() + + res, err := c.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, errors.WithStack(err)) + + require.NoError(t, res.Body.Close()) + + assert.Equal(t, "https://www.ory.com", gjson.GetBytes(body, "return_to").Value()) + }) + + t.Run("case=invalid oauth2 login challenge returns 400 Bad Request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeInvalidLoginChallenge}}, false) + assert.Contains(t, res.Request.URL.String(), errorTS.URL) + assert.Equal(t, int64(http.StatusBadRequest), gjson.GetBytes(body, "code").Int()) + assert.Contains(t, gjson.GetBytes(body, "reason").String(), "Unable to get OAuth 2.0 Login Challenge") + }) + + t.Run("case=oauth2 flow init succeeds", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}}, false) + require.Contains(t, res.Request.URL.String(), loginTS.URL) + }) + + t.Run("case=oauth2 flow init adds oauth2_login_request field", func(t *testing.T) { + res, body := initSPAFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}}) + assert.NotContains(t, res.Request.URL.String(), loginTS.URL) + + assert.NotEmpty(t, gjson.GetBytes(body, "oauth2_login_request").Value(), "%s", body) + }) + + t.Run("case=oauth2 flow with existing session and JSON request should not return null", func(t *testing.T) { + // This test reproduces issue #10255 where the /self-service/login/browser endpoint + // returns null when called with an existing session, a Hydra login challenge, and + // an Accept: application/json header. + + // Has a side effect on this test suite but since its running serial and there is no significantly easier way it's acceptable. + fakeHydra.Skip = true + t.Cleanup(func() { + fakeHydra.Skip = false + }) + + // Make a login request with an authenticated session, Hydra login challenge, and JSON accept + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil) + req.Header.Set("Accept", "application/json") + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req) + + // Before the fix, this would return 200 OK with "null" as body because of variable shadowing + // After the fix, it should return a proper error response with ErrAlreadyLoggedIn + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Response should be 400 Bad Request, got body: %s", body) + assert.NotEqual(t, "null", string(body), "Response should not be null") + assert.NotEmpty(t, gjson.GetBytes(body, "error.id").String(), "Should have error.id field, got body: %s", body) + assert.Equal(t, "session_already_available", gjson.GetBytes(body, "error.id").String(), "Should return session_already_available error, got body: %s", body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), "A valid session was detected", "Should have proper error reason, got body: %s", body) + }) + }) + + t.Run("case=relative redirect when self-service login ui is a relative URL", func(t *testing.T) { + reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, "/login-ts") + assert.Regexp( + t, + "^/login-ts.*$", + testhelpers.GetSelfServiceRedirectLocation(t, ts.URL+login.RouteInitBrowserFlow), + ) + }) + }) +} + +func TestGetFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + returnToTS := testhelpers.NewRedirTS(t, "", conf) + + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/password.schema.json") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/password.schema.json"}, + {ID: "email", URL: "file://./stub/email.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://./stub/phone.schema.json", SelfserviceSelectable: true}, + {ID: "not-allowed", URL: "file://./stub/password.schema.json"}, + }) + + setupLoginUI := func(t *testing.T, c *http.Client) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // It is important that we use a HTTP request to fetch the flow because that will show us if CSRF works or not + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+login.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL) + t.Cleanup(ts.Close) + return ts + } + + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{ + "enabled": true, + }) + + t.Run("case=fetching successful", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupLoginUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + + assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), gjson.GetBytes(body, "id").String(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), public.URL, "%s", body) + }) + + t.Run("case=csrf cookie missing", func(t *testing.T) { + client := testhelpers.NewTestClient(t) + setupLoginUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + + assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=expired", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupLoginUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + + // Expire the flow + f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), f)) + + // Try the flow but it is expired + res, body := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+f.ID.String()) + assert.EqualValues(t, http.StatusGone, res.StatusCode) + assert.Equal(t, public.URL+login.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) + }) + + t.Run("case=expired with return_to and schema_id", func(t *testing.T) { + returnTo := returnToTS.URL + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + + client := testhelpers.NewClientWithCookies(t) + setupLoginUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo+"&identity_schema=email") + + // Expire the flow + f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), f)) + + // Retrieve the flow and verify that return_to is in the response + getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, login.RouteGetFlow, f.ID, returnTo) + getBody := testhelpers.EasyGetBody(t, client, getURL) + assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) + + // submit the flow but it is expired + u := public.URL + login.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := client.PostForm(u, url.Values{"password_identifier": {"email@ory.sh"}, "csrf_token": {f.CSRFToken}, "password": {"password"}, "method": {"password"}}) + require.NoError(t, err) + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + f, err = reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String())) + require.NoError(t, err) + assert.Equal(t, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo+"&identity_schema=email", f.RequestURL) + }) + + t.Run("case=not found", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupLoginUI(t, client) + + res, _ := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+x.NewUUID().String()) + assert.EqualValues(t, http.StatusNotFound, res.StatusCode) + }) +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go new file mode 100644 index 0000000..0adaba1 --- /dev/null +++ b/selfservice/flow/login/hook.go @@ -0,0 +1,464 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type ( + PreHookExecutor interface { + ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + + PostHookExecutor interface { + ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *Flow, s *session.Session) error + } + + HooksProvider interface { + PreLoginHooks(ctx context.Context) ([]PreHookExecutor, error) + PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookExecutor, error) + } +) + +type ( + executorDependencies interface { + config.Provider + hydra.Provider + identity.PrivilegedPoolProvider + identity.ManagementProvider + session.ManagementProvider + session.PersistenceProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + sessiontokenexchange.PersistenceProvider + HandlerProvider + + FlowPersistenceProvider + HooksProvider + StrategyProvider + } + HookExecutor struct { + d executorDependencies + } + HookExecutorProvider interface { + LoginHookExecutor() *HookExecutor + } +) + +func PostHookExecutorNames(e []PostHookExecutor) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func NewHookExecutor(d executorDependencies) *HookExecutor { + return &HookExecutor{d: d} +} + +func (e *HookExecutor) checkAAL(ctx context.Context, s *session.Session, a *Flow) error { + err := e.d.SessionManager().DoesSessionSatisfy(ctx, s, e.d.Config().SessionWhoAmIAAL(ctx)) + if err == nil { + return nil + } + + if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { + if a != nil && aalErr.PassReturnToAndLoginChallengeParameters(a.RequestURL) != nil { + _ = aalErr.WithDetail("pass_request_params_error", "failed to pass request parameters to aalErr.RedirectTo") + } + return aalErr + } + + return err +} + +func (e *HookExecutor) handleLoginError(_ http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *Flow, i *identity.Identity, flowError error) error { + if f != nil { + if i != nil { + cont, err := container.NewFromStruct("", g, i.Traits, "traits") + if err != nil { + e.d.Logger().WithError(err).Warn("could not update flow UI") + return err + } + + for _, n := range cont.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(e.d.GenerateCSRFToken(r)) + } + } + + return flowError +} + +func (e *HookExecutor) PostLoginHook( + w http.ResponseWriter, + r *http.Request, + g node.UiNodeGroup, + f *Flow, + i *identity.Identity, + s *session.Session, + provider string, +) (err error) { + ctx := r.Context() + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostLoginHook") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + // We need to set the identity here because we check the available AAL in maybeLinkCredentials. + s.IdentityID = i.ID + s.Identity = i + + if err := e.maybeLinkCredentials(ctx, s, i, f); err != nil { + return err + } + + if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil { + return err + } + + c := e.d.Config() + // Verify the redirect URL before we do any other processing. + returnTo, err := redir.SecureRedirectTo(r, + c.SelfServiceBrowserDefaultReturnTo(ctx), + redir.SecureRedirectReturnTo(f.ReturnTo), + redir.SecureRedirectUseSourceURL(f.RequestURL), + redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), + ) + if err != nil { + return err + } + span.SetAttributes(otelx.StringAttrs(map[string]string{ + "return_to": returnTo.String(), + "flow_type": string(flow.TypeBrowser), + "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", f.Active). + Debug("Running ExecuteLoginPostHook.") + hooks, err := e.d.PostLoginHooks(ctx, f.Active) + if err != nil { + return err + } + for k, executor := range hooks { + 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(hooks)). + WithField("identity_id", i.ID). + 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, f, i, err) + } + + e.d.Logger(). + WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", PostHookExecutorNames(hooks)). + WithField("identity_id", i.ID). + WithField("flow_method", f.Active). + Debug("ExecuteLoginPostHook completed successfully.") + } + + if f.Type == flow.TypeAPI { + span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI))) + if err := e.d.SessionPersister().UpsertSession(ctx, s); err != nil { + return errors.WithStack(err) + } + e.d.Logger(). + WithRequest(r). + WithField("session_id", s.ID). + WithField("identity_id", i.ID). + Info("Identity authenticated successfully and was issued an Ory Kratos Session Token.") + + span.AddEvent(events.NewLoginSucceeded(ctx, &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: i.ID, + FlowID: f.ID, + FlowType: string(f.Type), + RequestedAAL: string(f.RequestedAAL), + IsRefresh: f.Refresh, + Method: f.Active.String(), + SSOProvider: provider, + })) + 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, f, s.ID, g); err != nil { + return errors.WithStack(err) + } else if handled { + return nil + } + + response := &APIFlowResponse{ + Session: s, + Token: s.Token, + ContinueWith: f.ContinueWith(), + } + if e.checkAAL(ctx, classified, f) != nil { + // 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 + } + + e.d.Writer().Write(w, r, response) + return nil + } + + if err := e.d.SessionManager().UpsertAndIssueCookie(ctx, w, r, s); err != nil { + return errors.WithStack(err) + } + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + WithField("session_id", s.ID). + Info("Identity authenticated successfully and was issued an Ory Kratos Session Cookie.") + + span.AddEvent(events.NewLoginSucceeded(ctx, &events.LoginSucceededOpts{ + SessionID: s.ID, + FlowID: f.ID, + IdentityID: i.ID, FlowType: string(f.Type), RequestedAAL: string(f.RequestedAAL), IsRefresh: f.Refresh, Method: f.Active.String(), + SSOProvider: provider, + })) + + if x.IsJSONRequest(r) { + span.SetAttributes(attribute.String("flow_type", "spa")) + + // Browser flows rely on cookies. Adding tokens in the mix will confuse consumers. + s.Token = "" + + // If we detect that whoami would require a higher AAL, we redirect! + if err := e.checkAAL(ctx, classified, f); err != nil { + if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { + if data, _ := flow.DuplicateCredentials(f); data == nil { + span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2")) + e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo)) + return nil + } + + // Special case: If we are in a flow that wants to link credentials, we create a + // new login flow here that asks for the require AAL, but also copies over the + // internal context and the organization ID. + r.URL, err = url.Parse(aalErr.RedirectTo) + if err != nil { + return errors.WithStack(err) + } + newFlow, _, err := e.d.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser, + WithInternalContext(f.InternalContext), + WithOrganizationID(f.OrganizationID), + ) + if err != nil { + return errors.WithStack(err) + } + + x.SendFlowCompletedAsRedirectOrJSON(w, r, e.d.Writer(), newFlow, newFlow.AppendTo(e.d.Config().SelfServiceFlowLoginUI(ctx)).String()) + return nil + } + return err + } + + // 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 f.OAuth2LoginChallenge != "" { + postChallengeURL, err := e.d.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(f.OAuth2LoginChallenge), + IdentityID: i.ID.String(), + SessionID: s.ID.String(), + AuthenticationMethods: s.AMR, + }) + if err != nil { + return err + } + span.SetAttributes(attribute.String("return_to", postChallengeURL), attribute.String("redirect_reason", "oauth2 login challenge")) + e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(postChallengeURL)) + return nil + } + + 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.checkAAL(ctx, classified, f); err != nil { + if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { + if data, _ := flow.DuplicateCredentials(f); data == nil { + http.Redirect(w, r, aalErr.RedirectTo, http.StatusSeeOther) + return nil + } + + // Special case: If we are in a flow that wants to link credentials, we create a + // new login flow here that asks for the require AAL, but also copies over the + // internal context and the organization ID. + r.URL, err = url.Parse(aalErr.RedirectTo) + if err != nil { + return errors.WithStack(err) + } + newFlow, _, err := e.d.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser, + WithInternalContext(f.InternalContext), + WithOrganizationID(f.OrganizationID), + ) + if err != nil { + return errors.WithStack(err) + } + + x.SendFlowCompletedAsRedirectOrJSON(w, r, e.d.Writer(), newFlow, newFlow.AppendTo(e.d.Config().SelfServiceFlowLoginUI(ctx)).String()) + return nil + } + return errors.WithStack(err) + } + + finalReturnTo := returnTo.String() + if f.OAuth2LoginChallenge != "" { + rt, err := e.d.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(f.OAuth2LoginChallenge), + IdentityID: i.ID.String(), + SessionID: s.ID.String(), + AuthenticationMethods: s.AMR, + }) + if err != nil { + return err + } + 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")) + } + + redir.ContentNegotiationRedirection(w, r, s, e.d.Writer(), finalReturnTo) + return nil +} + +func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + hooks, err := e.d.PreLoginHooks(r.Context()) + if err != nil { + return err + } + for _, h := range hooks { + if err := h.ExecuteLoginPreHook(w, r, a); err != nil { + return err + } + } + + return nil +} + +// maybeLinkCredentials links the identity with the credentials of the inner context of the login flow. +func (e *HookExecutor) maybeLinkCredentials(ctx context.Context, sess *session.Session, ident *identity.Identity, loginFlow *Flow) (err error) { + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostLoginHook.maybeLinkCredentials") + defer otelx.End(span, &err) + + if e.checkAAL(ctx, sess, loginFlow) != nil { + // we don't yet want to link credentials because the required AAL is not satisfied + return nil + } + + lc, err := flow.DuplicateCredentials(loginFlow) + if err != nil { + return err + } else if lc == nil { + return nil + } + + if err = e.checkDuplicateCredentialsIdentifierMatch(ctx, ident, lc.DuplicateIdentifier); err != nil { + return err + } + strategy, err := e.d.AllLoginStrategies().Strategy(lc.CredentialsType) + if err != nil { + return err + } + + linkableStrategy, ok := strategy.(LinkableStrategy) + if !ok { + // This should never happen because we check for this in the registration flow. + return errors.Errorf("strategy is not linkable: %T", linkableStrategy) + } + + if err := linkableStrategy.Link(ctx, ident, lc.CredentialsConfig); err != nil { + return err + } + + if err = linkableStrategy.CompletedLogin(sess, lc); err != nil { + return err + } + + return nil +} + +func (e *HookExecutor) checkDuplicateCredentialsIdentifierMatch(ctx context.Context, i *identity.Identity, match string) error { + if len(i.Credentials) == 0 { + if err := e.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { + return err + } + } + + for _, credentials := range i.Credentials { + for _, identifier := range credentials.Identifiers { + if identifier == match { + return nil + } + } + } + return schema.NewLinkedCredentialsDoNotMatch() +} diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go new file mode 100644 index 0000000..c8c5649 --- /dev/null +++ b/selfservice/flow/login/hook_test.go @@ -0,0 +1,536 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/x/configx" + + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "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/contextx" +) + +func TestLoginExecutor(t *testing.T) { + t.Parallel() + ctx := context.Background() + + for _, strategy := range identity.AllCredentialTypes { + t.Run("strategy="+strategy.String(), func(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.SetHydra(hydra.NewFake()) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + + newServer := func(t *testing.T, ft flow.Type, useIdentity *identity.Identity, flowCallback ...func(*login.Flow)) *httptest.Server { + router := http.NewServeMux() + + router.HandleFunc("GET /login/pre", func(w http.ResponseWriter, r *http.Request) { + loginFlow, err := login.NewFlow(reg, r, ft) + require.NoError(t, err) + if testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, reg.LoginHookExecutor().PreLoginHook(w, r, loginFlow)) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.HandleFunc("GET /login/post", func(w http.ResponseWriter, r *http.Request) { + loginFlow, err := login.NewFlow(reg, r, ft) + require.NoError(t, err) + loginFlow.Active = strategy + loginFlow.RequestURL = x.RequestURL(r).String() + for _, cb := range flowCallback { + cb(loginFlow) + } + + sess := session.NewInactiveSession() + sess.CompletedLoginFor(identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + if useIdentity == nil { + useIdentity = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) + } + + testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, + reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, "")) + }) + + router.HandleFunc("GET /login/post2fa", func(w http.ResponseWriter, r *http.Request) { + loginFlow, err := login.NewFlow(reg, r, ft) + require.NoError(t, err) + loginFlow.Active = strategy + loginFlow.RequestURL = x.RequestURL(r).String() + for _, cb := range flowCallback { + cb(loginFlow) + } + + sess := session.NewInactiveSession() + sess.CompletedLoginFor(identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess.CompletedLoginFor(identity.CredentialsTypeTOTP, identity.AuthenticatorAssuranceLevel2) + if useIdentity == nil { + useIdentity = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) + } + + testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, + reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, "")) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + return ts + } + + makeRequestPost := testhelpers.SelfServiceMakeLoginPostHookRequest + viperSetPost := testhelpers.SelfServiceHookLoginViperSetPost + + t.Run("method=PostLoginHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, 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{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.JSONEq(t, fmt.Sprintf(`[{"action":"redirect_browser_to","redirect_browser_to":"%s"}]`, returnToServer.URL), gjson.Get(body, "continue_with").Raw) + }) + + 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(`{}`)}}) + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equal(t, returnToServer.URL, res.Request.URL.String()) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + viperSetPost(t, conf, strategy.String(), []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPostHook": "abort"}`)}}) + + ts := newServer(t, flow.TypeBrowser, nil) + res, body := makeRequestPost(t, ts, false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.Empty(t, body) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL}) + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{"return_to": {returnToServer.URL + "/kratos"}}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equal(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+"."+config.DefaultBrowserReturnURL, returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + testhelpers.SelfServiceHookLoginSetDefaultRedirectTo(t, conf, returnToServer.URL+"/not-kratos") + testhelpers.SelfServiceHookLoginSetDefaultRedirectToStrategy(t, conf, strategy.String(), returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + 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(`{}`)}}) + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, res.Request.URL.String()) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, flow.TypeAPI, nil), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.identity.id").String()) + }) + + t.Run("suite=handle login challenge with browser and application/json", func(t *testing.T) { + t.Run("case=includes the return_to address for a valid challenge", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + withOAuthChallenge := func(f *login.Flow) { + f.OAuth2LoginChallenge = hydra.FakeValidLoginChallenge + } + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil, withOAuthChallenge), true, url.Values{}) + require.EqualValuesf(t, http.StatusUnprocessableEntity, res.StatusCode, "%s", body) + assert.Equal(t, hydra.FakePostLoginURL, gjson.Get(body, "redirect_browser_to").String(), "%s", body) + }) + + t.Run("case=returns an error for an invalid challenge", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + withOAuthChallenge := func(f *login.Flow) { + f.OAuth2LoginChallenge = hydra.FakeInvalidLoginChallenge + } + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil, withOAuthChallenge), true, url.Values{}) + require.EqualValuesf(t, http.StatusInternalServerError, res.StatusCode, "%s", body) + assert.Equal(t, hydra.ErrFakeAcceptLoginRequestFailed.Error(), body, "%s", body) + }) + }) + + t.Run("case=pass without hooks for browser flow with application/json", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, nil), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.identity.id").String()) + assert.Empty(t, gjson.Get(body, "session.token").String()) + assert.Empty(t, gjson.Get(body, "session_token").String()) + }) + + t.Run("case=work normally if AAL is satisfied", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + useIdentity := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword}, + identity.CredentialsTypeTOTP: {Type: identity.CredentialsTypeTOTP}, + }} + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity)) + + t.Run("browser client", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, res.Request.URL.String()) + }) + + t.Run("api client returns the session with identity and the token", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeAPI, useIdentity), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.identity").String()) + assert.NotEmpty(t, gjson.Get(body, "session_token").String()) + }) + + t.Run("browser JSON client returns the session with identity but not the token", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.id").String()) + assert.NotEmpty(t, gjson.Get(body, "session.identity").String()) + assert.Empty(t, gjson.Get(body, "session_token").String()) + }) + }) + + t.Run("case=redirect to login if AAL is too low", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "highest_available") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + }) + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://hydra") + + useIdentity := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), Identifiers: []string{testhelpers.RandomEmail()}}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), Identifiers: []string{testhelpers.RandomEmail()}}, + }} + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity)) + + t.Run("browser client", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), false, url.Values{}) + require.EqualValuesf(t, http.StatusNotFound, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), "/self-service/login/browser?aal=aal2") + }) + + t.Run("browser client with login challenge", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), false, url.Values{ + "login_challenge": []string{hydra.FakeValidLoginChallenge}, + }) + require.EqualValuesf(t, http.StatusNotFound, res.StatusCode, "%s", body) + + assert.Equal(t, res.Request.URL.Path, "/self-service/login/browser") + assert.Equal(t, res.Request.URL.Query().Get("aal"), "aal2") + assert.Equal(t, res.Request.URL.Query().Get("login_challenge"), hydra.FakeValidLoginChallenge) + }) + + t.Run("api client returns the token and the session without the identity", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeAPI, useIdentity), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Empty(t, gjson.Get(body, "session.identity").String()) + assert.NotEmpty(t, gjson.Get(body, "session_token").String()) + }) + + t.Run("browser JSON client", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), true, url.Values{}) + require.EqualValuesf(t, http.StatusUnprocessableEntity, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) + assert.Contains(t, gjson.Get(body, "redirect_browser_to").String(), "/self-service/login/browser?aal=aal2", "%s", body) + }) + + t.Run("browser JSON client with login challenge", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), true, url.Values{ + "login_challenge": []string{hydra.FakeValidLoginChallenge}, + }) + require.EqualValuesf(t, http.StatusUnprocessableEntity, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) + + redirectBrowserTo, err := url.Parse(gjson.Get(body, "redirect_browser_to").String()) + require.NoError(t, err) + + assert.Equal(t, redirectBrowserTo.Path, "/self-service/login/browser") + assert.Equal(t, redirectBrowserTo.Query().Get("aal"), "aal2") + assert.Equal(t, redirectBrowserTo.Query().Get("login_challenge"), hydra.FakeValidLoginChallenge) + }) + }) + }) + + t.Run("case=maybe links credential", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, "selfservice.methods.totp.enabled", true) + + email1, email2 := testhelpers.RandomEmail(), testhelpers.RandomEmail() + passwordOnlyIdentity := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + Identifiers: []string{email1}, + }, + }} + twoFAIdentitiy := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + Identifiers: []string{email2}, + }, + identity.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + Config: []byte(`{"totp_url":"otpauth://totp/test"}`), + Identifiers: []string{email2}, + }, + }} + require.NoError(t, reg.Persister().CreateIdentity(ctx, passwordOnlyIdentity)) + require.NoError(t, reg.Persister().CreateIdentity(ctx, twoFAIdentitiy)) + + credsOIDCPWOnly, err := identity.NewCredentialsOIDC( + &identity.CredentialsOIDCEncryptedTokens{IDToken: "id-token", AccessToken: "access-token", RefreshToken: "refresh-token"}, + "my-provider", + email1, + "", + ) + require.NoError(t, err) + credsOIDC2FA, err := identity.NewCredentialsOIDC( + &identity.CredentialsOIDCEncryptedTokens{IDToken: "id-token", AccessToken: "access-token", RefreshToken: "refresh-token"}, + "my-provider", + email2, + "", + ) + require.NoError(t, err) + + t.Run("sub-case=does not link after first factor when second factor is available", func(t *testing.T) { + duplicateCredentialsData := flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDC2FA.Config, + DuplicateIdentifier: email2, + } + ts := newServer(t, flow.TypeBrowser, twoFAIdentitiy, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, duplicateCredentialsData)) + }) + res, _ := makeRequestPost(t, ts, false, url.Values{}) + + assert.Equal(t, reg.Config().SelfServiceFlowLoginUI(ctx).Host, res.Request.URL.Host) + assert.Equal(t, reg.Config().SelfServiceFlowLoginUI(ctx).Path, res.Request.URL.Path) + newFlowID := res.Request.URL.Query().Get("flow") + assert.NotEmpty(t, newFlowID) + + newFlow, err := reg.LoginFlowPersister().GetLoginFlow(ctx, uuid.Must(uuid.FromString(newFlowID))) + require.NoError(t, err) + newFlowDuplicateCredentialsData, err := flow.DuplicateCredentials(newFlow) + require.NoError(t, err) + + // Duplicate credentials data should have been copied over + assert.Equal(t, duplicateCredentialsData.CredentialsType, newFlowDuplicateCredentialsData.CredentialsType) + assert.Equal(t, duplicateCredentialsData.DuplicateIdentifier, newFlowDuplicateCredentialsData.DuplicateIdentifier) + assert.JSONEq(t, string(duplicateCredentialsData.CredentialsConfig), string(newFlowDuplicateCredentialsData.CredentialsConfig)) + + // AAL should be AAL2 + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, newFlow.RequestedAAL) + + // TOTP nodes should be present + found := false + for _, n := range newFlow.UI.Nodes { + if n.Group == node.TOTPGroup { + found = true + break + } + } + assert.True(t, found, "could not find TOTP nodes in %+v", newFlow.UI.Nodes) + + ident, err := reg.Persister().GetIdentity(ctx, twoFAIdentitiy.ID, identity.ExpandCredentials) + require.NoError(t, err) + assert.Len(t, ident.Credentials, 2) + }) + + t.Run("sub-case=links after second factor when second factor is available", func(t *testing.T) { + ts := newServer(t, flow.TypeBrowser, twoFAIdentitiy, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDC2FA.Config, + DuplicateIdentifier: email2, + })) + }) + res, body := testhelpers.SelfServiceMakeHookRequest(t, ts, "/login/post2fa", false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equalf(t, returnToServer.URL, res.Request.URL.String(), "%s", body) + + ident, err := reg.Persister().GetIdentity(ctx, twoFAIdentitiy.ID, identity.ExpandCredentials) + require.NoError(t, err) + assert.Len(t, ident.Credentials, 3) + }) + + t.Run("sub-case=links matching identity", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, passwordOnlyIdentity, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDCPWOnly.Config, + DuplicateIdentifier: email1, + })) + }), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equalf(t, returnToServer.URL, res.Request.URL.String(), "%s", body) + + ident, err := reg.Persister().GetIdentity(ctx, passwordOnlyIdentity.ID, identity.ExpandCredentials) + require.NoError(t, err) + assert.Len(t, ident.Credentials, 2) + }) + + t.Run("sub-case=errors on non-matching identity", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, passwordOnlyIdentity, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDCPWOnly.Config, + DuplicateIdentifier: "wrong@example.com", + })) + }), false, url.Values{}) + require.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, schema.NewLinkedCredentialsDoNotMatch().Error(), body, "%s", body) + }) + }) + + t.Run("type=api", func(t *testing.T) { + t.Run("method=PreLoginHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceLoginBeforeHooks, + testhelpers.SelfServiceMakeLoginPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, flow.TypeAPI, nil) + }, + conf, + )) + }) + + t.Run("type=browser", func(t *testing.T) { + t.Run("method=PreLoginHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceLoginBeforeHooks, + testhelpers.SelfServiceMakeLoginPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, flow.TypeBrowser, nil) + }, + conf, + )) + }) + }) + } + + t.Run("method=checkAAL", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyPublicBaseURL: returnToServer.URL, + config.ViperKeySelfServiceBrowserDefaultReturnTo: returnToServer.URL, + }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json"))) + + t.Run("returns no error when sufficient", func(t *testing.T) { + ctx := contextx.WithConfigValue(ctx, config.ViperKeySessionWhoAmIAAL, identity.AuthenticatorAssuranceLevel1) + assert.NoError(t, + login.CheckAALForTest(ctx, reg.LoginHookExecutor(), &session.Session{ + AMR: session.AuthenticationMethods{{ + Method: identity.CredentialsTypePassword, + AAL: identity.AuthenticatorAssuranceLevel1, + }}, + AuthenticatorAssuranceLevel: identity.AuthenticatorAssuranceLevel1, + }, nil), + ) + + ctx = contextx.WithConfigValue(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + assert.NoError(t, + login.CheckAALForTest(ctx, reg.LoginHookExecutor(), &session.Session{ + AMR: session.AuthenticationMethods{{ + Method: identity.CredentialsTypePassword, + AAL: identity.AuthenticatorAssuranceLevel1, + }, { + Method: identity.CredentialsTypeLookup, + AAL: identity.AuthenticatorAssuranceLevel2, + }}, + AuthenticatorAssuranceLevel: identity.AuthenticatorAssuranceLevel2, + }, nil), + ) + }) + + t.Run("copies parameters to redirect URL when AAL is not sufficient", func(t *testing.T) { + ctx := contextx.WithConfigValue(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + aalErr := new(session.ErrAALNotSatisfied) + require.ErrorAs(t, + login.CheckAALForTest(ctx, reg.LoginHookExecutor(), &session.Session{ + AMR: session.AuthenticationMethods{{ + Method: identity.CredentialsTypePassword, + AAL: identity.AuthenticatorAssuranceLevel1, + }}, + AuthenticatorAssuranceLevel: identity.AuthenticatorAssuranceLevel1, + Identity: &identity.Identity{ + InternalAvailableAAL: identity.NullableAuthenticatorAssuranceLevel{ + NullString: sql.NullString{String: string(identity.AuthenticatorAssuranceLevel2), Valid: true}, + }, + }, + }, &login.Flow{ + RequestURL: "https://www.ory.com/?return_to=https://www.ory.com/kratos&login_challenge=challenge", + }), + &aalErr, + ) + assert.Equal(t, returnToServer.URL+"/self-service/login/browser?aal=aal2&login_challenge=challenge&return_to=https%3A%2F%2Fwww.ory.com%2Fkratos", aalErr.RedirectTo) + }) + }) +} diff --git a/selfservice/flow/login/organizations.go b/selfservice/flow/login/organizations.go new file mode 100644 index 0000000..e47aac9 --- /dev/null +++ b/selfservice/flow/login/organizations.go @@ -0,0 +1,42 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" +) + +var organizationFilter = []StrategyFilter{func(s Strategy) bool { + a, b := s.(flow.OrganizationImplementor) + return b && a.SupportsOrganizations() +}} + +func PrepareOrganizations(r *http.Request, f *Flow, sess *session.Session) []StrategyFilter { + if f.RequestedAAL != identity.AuthenticatorAssuranceLevel1 { + return []StrategyFilter{} + } + + if f.OrganizationID.Valid { + return organizationFilter + } + + orgID := flow.ParseOrganizationFromURLQuery(r.Context(), r.URL.Query()) + if sess != nil && sess.Identity != nil && sess.Identity.OrganizationID.Valid { + orgID = sess.Identity.OrganizationID + } + + if !orgID.Valid { + return []StrategyFilter{} + } + + f.OrganizationID = orgID + // We only apply the filter on AAL1, because the OIDC strategy can only satsify + // AAL1. + return organizationFilter +} diff --git a/selfservice/flow/login/organizations_test.go b/selfservice/flow/login/organizations_test.go new file mode 100644 index 0000000..106b994 --- /dev/null +++ b/selfservice/flow/login/organizations_test.go @@ -0,0 +1,137 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" +) + +func TestPrepareOrganizations(t *testing.T) { + t.Run("should return empty filter if AAL is not AAL1", func(t *testing.T) { + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel2} + r := &http.Request{URL: new(url.URL)} + sess := &session.Session{} + + filters := PrepareOrganizations(r, f, sess) + assert.Empty(t, filters) + }) + + t.Run("should return empty filter if OrganizationID is not set", func(t *testing.T) { + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + r := &http.Request{URL: new(url.URL)} + sess := &session.Session{} + + filters := PrepareOrganizations(r, f, sess) + assert.Empty(t, filters) + }) + + t.Run("should return organization filter if OrganizationID is valid", func(t *testing.T) { + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + r := &http.Request{URL: new(url.URL)} + sess := &session.Session{ + Identity: &identity.Identity{OrganizationID: uuid.NullUUID{Valid: true}}, + } + + filters := PrepareOrganizations(r, f, sess) + require.NotEmpty(t, filters) + assert.Equal(t, organizationFilter, filters) + }) + + t.Run("should parse OrganizationID from URL query if not in session", func(t *testing.T) { + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + r := &http.Request{ + URL: &url.URL{ + RawQuery: "organization=123e4567-e89b-12d3-a456-426614174000", + }, + } + sess := &session.Session{} + filters := PrepareOrganizations(r, f, sess) + require.NotEmpty(t, filters) + assert.Equal(t, organizationFilter, filters) + }) + + t.Run("should use organization ID already in flow", func(t *testing.T) { + orgID := uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true} + f := &Flow{ + RequestedAAL: identity.AuthenticatorAssuranceLevel1, + OrganizationID: orgID, + } + r := &http.Request{URL: new(url.URL)} + sess := &session.Session{} + + filters := PrepareOrganizations(r, f, sess) + require.NotEmpty(t, filters) + assert.Equal(t, organizationFilter, filters) + assert.Equal(t, orgID, f.OrganizationID) + }) + + t.Run("should prioritize session org ID over query param when both present", func(t *testing.T) { + sessionOrgID := uuid.Must(uuid.NewV4()) + queryOrgID := uuid.Must(uuid.NewV4()) + + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + r := &http.Request{ + URL: &url.URL{ + RawQuery: "organization=" + queryOrgID.String(), + }, + } + sess := &session.Session{ + Identity: &identity.Identity{ + OrganizationID: uuid.NullUUID{UUID: sessionOrgID, Valid: true}, + }, + } + + filters := PrepareOrganizations(r, f, sess) + require.NotEmpty(t, filters) + assert.Equal(t, organizationFilter, filters) + assert.Equal(t, sessionOrgID, f.OrganizationID.UUID) + }) + + t.Run("should not return filter when organization is set but AAL2 requested", func(t *testing.T) { + orgID := uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true} + f := &Flow{ + RequestedAAL: identity.AuthenticatorAssuranceLevel2, + OrganizationID: orgID, + } + r := &http.Request{URL: new(url.URL)} + sess := &session.Session{} + + filters := PrepareOrganizations(r, f, sess) + assert.Empty(t, filters) + // Organization ID should remain set in the flow + assert.Equal(t, orgID, f.OrganizationID) + }) + + t.Run("should use organization ID from query when session has no org ID", func(t *testing.T) { + queryOrgID := uuid.Must(uuid.NewV4()) + + f := &Flow{RequestedAAL: identity.AuthenticatorAssuranceLevel1} + r := &http.Request{ + URL: &url.URL{ + RawQuery: "organization=" + queryOrgID.String(), + }, + } + sess := &session.Session{ + Identity: &identity.Identity{ + // No organization ID set + OrganizationID: uuid.NullUUID{Valid: false}, + }, + } + + filters := PrepareOrganizations(r, f, sess) + require.NotEmpty(t, filters) + assert.Equal(t, organizationFilter, filters) + assert.Equal(t, queryOrgID, f.OrganizationID.UUID) + }) +} diff --git a/selfservice/flow/login/persistence.go b/selfservice/flow/login/persistence.go new file mode 100644 index 0000000..bab99ba --- /dev/null +++ b/selfservice/flow/login/persistence.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type ( + FlowPersister interface { + UpdateLoginFlow(context.Context, *Flow) error + CreateLoginFlow(context.Context, *Flow) error + GetLoginFlow(context.Context, uuid.UUID) (*Flow, error) + ForceLoginFlow(ctx context.Context, id uuid.UUID) error + DeleteExpiredLoginFlows(context.Context, time.Time, int) error + DeleteTestLoginFlow(context.Context, uuid.UUID) error + } + FlowPersistenceProvider interface { + LoginFlowPersister() FlowPersister + } +) diff --git a/selfservice/flow/login/session.go b/selfservice/flow/login/session.go new file mode 100644 index 0000000..35e2aff --- /dev/null +++ b/selfservice/flow/login/session.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" +) + +// The Response for Login Flows via API +// +// swagger:model successfulNativeLogin +type APIFlowResponse struct { + // 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! + Token string `json:"session_token,omitempty"` + + // The Session + // + // The session contains information about the user, the session device, and so on. + // This is only available for API flows, not for Browser flows! + // + // 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 new file mode 100644 index 0000000..c9d9145 --- /dev/null +++ b/selfservice/flow/login/sort.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + + "github.com/ory/kratos/ui/node" +) + +func sortNodes(ctx context.Context, n node.Nodes) error { + return n.SortBySchema(ctx, + node.SortByGroups([]node.UiNodeGroup{ + node.OpenIDConnectGroup, + node.DefaultGroup, + node.WebAuthnGroup, + node.PasskeyGroup, + node.CodeGroup, + node.PasswordGroup, + node.TOTPGroup, + node.LookupGroup, + }), + node.SortUseOrder([]string{ + "csrf_token", + "identifier", + "password", + }), + ) +} diff --git a/selfservice/flow/login/state.go b/selfservice/flow/login/state.go new file mode 100644 index 0000000..576fad6 --- /dev/null +++ b/selfservice/flow/login/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import "github.com/ory/kratos/selfservice/flow" + +// Login Flow State +// +// 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. +// +// swagger:model loginFlowState +type State = flow.State diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go new file mode 100644 index 0000000..fc39054 --- /dev/null +++ b/selfservice/flow/login/strategy.go @@ -0,0 +1,64 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/sqlxx" +) + +type Strategy interface { + ID() identity.CredentialsType + NodeGroup() node.UiNodeGroup + Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error) + CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod +} + +type Strategies []Strategy + +type LinkableStrategy interface { + Link(ctx context.Context, i *identity.Identity, credentials sqlxx.JSONRawMessage) error + CompletedLogin(sess *session.Session, data *flow.DuplicateCredentialsData) error + SetDuplicateCredentials(f flow.InternalContexter, duplicateIdentifier string, credentials identity.Credentials, provider string) error +} + +type FastLoginStrategy interface { + FastLogin1FA(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (err error) + FastLogin2FA(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (err error) +} + +func (s Strategies) Strategy(id identity.CredentialsType) (Strategy, error) { + ids := make([]identity.CredentialsType, len(s)) + for k, ss := range s { + ids[k] = ss.ID() + if ss.ID() == id { + return ss, nil + } + } + + return nil, errors.Errorf(`unable to find strategy for %s have %v`, id, ids) +} + +func (s Strategies) MustStrategy(id identity.CredentialsType) Strategy { + strategy, err := s.Strategy(id) + if err != nil { + panic(err) + } + return strategy +} + +type StrategyFilter func(strategy Strategy) bool + +type StrategyProvider interface { + AllLoginStrategies() Strategies + LoginStrategies(ctx context.Context, filters ...StrategyFilter) Strategies +} diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go new file mode 100644 index 0000000..c731024 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator.go @@ -0,0 +1,73 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + stderr "errors" + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" +) + +type UnifiedFormHydrator interface { + ID() identity.CredentialsType + PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error +} + +type AAL1FormHydrator interface { + ID() identity.CredentialsType + PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *Flow, sess *session.Session) error + PopulateLoginMethodFirstFactor(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 +} + +type AAL2FormHydrator interface { + ID() identity.CredentialsType + PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *Flow) error +} + +var ErrBreakLoginPopulate = stderr.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 0000000..863a103 --- /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/login/stub/email.schema.json b/selfservice/flow/login/stub/email.schema.json new file mode 100644 index 0000000..56367d3 --- /dev/null +++ b/selfservice/flow/login/stub/email.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/email.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "title": "E-Mail Address", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/selfservice/flow/login/stub/fake-session.schema.json b/selfservice/flow/login/stub/fake-session.schema.json new file mode 100644 index 0000000..bbcc9f4 --- /dev/null +++ b/selfservice/flow/login/stub/fake-session.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/flow/login/stub/login.schema.json b/selfservice/flow/login/stub/login.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/login/stub/login.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/login/stub/password.schema.json b/selfservice/flow/login/stub/password.schema.json new file mode 100644 index 0000000..39161c0 --- /dev/null +++ b/selfservice/flow/login/stub/password.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Username", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/selfservice/flow/login/stub/phone.schema.json b/selfservice/flow/login/stub/phone.schema.json new file mode 100644 index 0000000..786a39c --- /dev/null +++ b/selfservice/flow/login/stub/phone.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/phone.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "title": "Phone Number", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/selfservice/flow/login/stub/registration.schema.json b/selfservice/flow/login/stub/registration.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/login/stub/registration.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/login/stub/updated.schema.json b/selfservice/flow/login/stub/updated.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/login/stub/updated.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/login/test/persistence.go b/selfservice/flow/login/test/persistence.go new file mode 100644 index 0000000..d33f9a0 --- /dev/null +++ b/selfservice/flow/login/test/persistence.go @@ -0,0 +1,272 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlcon" +) + +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { + var clearids = func(r *login.Flow) { + r.ID = uuid.UUID{} + } + + return func(t *testing.T) { + _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=should error when the login flow does not exist", func(t *testing.T) { + _, err := p.GetLoginFlow(ctx, x.NewUUID()) + require.Error(t, err) + }) + + var newFlow = func(t *testing.T) *login.Flow { + var r login.Flow + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + return &r + } + + t.Run("case=should create with set ids", func(t *testing.T) { + var r login.Flow + require.NoError(t, faker.FakeData(&r)) + require.NoError(t, p.CreateLoginFlow(ctx, &r)) + }) + + t.Run("case=should create a new login flow and properly set IDs", func(t *testing.T) { + r := newFlow(t) + err := p.CreateLoginFlow(ctx, r) + require.NoError(t, err, "%#v", err) + assert.NotEqual(t, uuid.Nil, r.ID) + }) + + t.Run("case=should create and fetch a login flow", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateLoginFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetLoginFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Active, actual.Active) + assertx.EqualAsJSON(t, expected.UI, actual.UI, "expected:\t%s\nactual:\t%s", expected.UI, actual.UI) + }) + + t.Run("case=should properly set the flow type", func(t *testing.T) { + expected := newFlow(t) + expected.Refresh = true + expected.Type = flow.TypeAPI + expected.UI = container.New("ory-sh") + + err := p.CreateLoginFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetLoginFlow(ctx, expected.ID) + require.NoError(t, err) + assert.Equal(t, flow.TypeAPI, actual.Type) + + actual.UI = container.New("not-ory-sh") + actual.Type = flow.TypeBrowser + actual.Refresh = true + + require.NoError(t, p.UpdateLoginFlow(ctx, actual)) + + actual, err = p.GetLoginFlow(ctx, actual.ID) + require.NoError(t, err) + assert.Equal(t, flow.TypeBrowser, actual.Type) + assert.True(t, actual.Refresh) + assert.Equal(t, "not-ory-sh", actual.UI.Action) + }) + + t.Run("case=should not cause data loss when updating a request without changes", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateLoginFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetLoginFlow(ctx, expected.ID) + require.NoError(t, err) + + require.NoError(t, p.UpdateLoginFlow(ctx, actual)) + + actual, err = p.GetLoginFlow(ctx, expected.ID) + require.NoError(t, err) + assertx.EqualAsJSON(t, expected.UI, actual.UI) + }) + + t.Run("case=should create and update and properly deal with internal context", func(t *testing.T) { + for k, tc := range []struct { + in []byte + expect string + }{ + {in: []byte("[]"), expect: "{}"}, + {expect: "{}"}, + {in: []byte("null"), expect: "{}"}, + {in: []byte(`{"foo":"bar"}`), expect: `{"foo":"bar"}`}, + } { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + r := newFlow(t) + r.InternalContext = tc.in + require.NoError(t, p.CreateLoginFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + + r.InternalContext = tc.in + require.NoError(t, p.UpdateLoginFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + }) + } + }) + + t.Run("case=network", func(t *testing.T) { + id := x.NewUUID() + nid, p := testhelpers.NewNetwork(t, ctx, p) + + t.Run("sets id on creation", func(t *testing.T) { + expected := &login.Flow{ID: id, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} + require.NoError(t, p.CreateLoginFlow(ctx, expected)) + assert.EqualValues(t, id, expected.ID) + assert.EqualValues(t, nid, expected.NID) + + actual, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not update id", func(t *testing.T) { + expected, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + require.NoError(t, p.UpdateLoginFlow(ctx, expected)) + + actual, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not force on another network", func(t *testing.T) { + expected, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + + _, other := testhelpers.NewNetwork(t, ctx, p) + + expected.RequestURL = "updated" + require.Error(t, other.ForceLoginFlow(ctx, expected.ID)) + + _, err = other.GetLoginFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + actual, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + require.False(t, actual.Refresh) + }) + + t.Run("can not update on another network", func(t *testing.T) { + expected, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + + _, other := testhelpers.NewNetwork(t, ctx, p) + + expected.RequestURL = "updated" + require.Error(t, other.UpdateLoginFlow(ctx, expected)) + + _, err = other.GetLoginFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + actual, err := p.GetLoginFlow(ctx, id) + require.NoError(t, err) + require.NotEqual(t, "updated", actual.RequestURL) + }) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetLoginFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=test-flow persister helpers", func(t *testing.T) { + // seedFlow creates a login flow under the given persister. When + // isTest is true the flow is marked as a test flow. + seedFlow := func(t *testing.T, p persistence.Persister, isTest bool, expiresAt time.Time) *login.Flow { + t.Helper() + f := &login.Flow{ + ID: x.NewUUID(), + Type: flow.TypeBrowser, + State: flow.StateChooseMethod, + RequestURL: "https://example.com/self-service/login/browser", + IssuedAt: time.Now().UTC(), + ExpiresAt: expiresAt, + CSRFToken: "csrf", + UI: container.New(""), + InternalContext: []byte(`{}`), + } + if isTest { + require.NoError(t, f.SetTestContext(&login.TestContext{ProviderID: "google"})) + } + require.NoError(t, p.CreateLoginFlow(ctx, f)) + return f + } + + t.Run("DeleteTestLoginFlow rejects non-test-flow rows", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + f := seedFlow(t, p, false, time.Now().UTC().Add(time.Hour)) + + err := p.DeleteTestLoginFlow(ctx, f.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + // Row must still be there. + _, err = p.GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + }) + + t.Run("DeleteTestLoginFlow rejects wrong NID", func(t *testing.T) { + _, pA := testhelpers.NewNetwork(t, ctx, p) + _, pB := testhelpers.NewNetwork(t, ctx, p) + fA := seedFlow(t, pA, true, time.Now().UTC().Add(time.Hour)) + + err := pB.DeleteTestLoginFlow(ctx, fA.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + // Row survives from the owning NID's perspective. + _, err = pA.GetLoginFlow(ctx, fA.ID) + require.NoError(t, err) + }) + + t.Run("DeleteTestLoginFlow removes only the targeted row", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + a := seedFlow(t, p, true, time.Now().UTC().Add(time.Hour)) + b := seedFlow(t, p, true, time.Now().UTC().Add(time.Hour)) + + require.NoError(t, p.DeleteTestLoginFlow(ctx, a.ID)) + + _, err := p.GetLoginFlow(ctx, a.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + _, err = p.GetLoginFlow(ctx, b.ID) + require.NoError(t, err) + }) + }) + } +} diff --git a/selfservice/flow/login/test_context.go b/selfservice/flow/login/test_context.go new file mode 100644 index 0000000..d9897e0 --- /dev/null +++ b/selfservice/flow/login/test_context.go @@ -0,0 +1,155 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "database/sql" + "encoding/json" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" +) + +// InternalContextTestKey is the JSON path inside a login flow's +// internal_context where test-mode data (provider id, debug payload) is +// stored. Using a dedicated sub-namespace keeps test fields out of the +// flow's first-class columns while still letting a single row carry them. +const InternalContextTestKey = "test" + +// Admin-test extension of a login flow. Populated only for flows created by +// the admin test endpoint; included in the flow's API response so the admin +// UI can render the pre-scoped provider and (once captured) the debug +// round-trip result. +// +// swagger:model loginFlowTestContext +type TestContext struct { + // The ID of the OIDC provider this test flow targets. + // + // required: true + ProviderID string `json:"provider_id"` + + // Structured debug data captured during the provider round-trip. + // Nil while the flow is still awaiting the callback. + DebugPayload *DebugPayload `json:"debug_payload,omitempty"` +} + +// Diagnostic payload captured during a test-mode OIDC round-trip. +// Contains the parsed claims, the Jsonnet mapper input and output, and any +// schema validation errors. Bearer tokens (id_token, access_token, +// refresh_token) are intentionally excluded to limit the blast radius of +// the debug payload leaking through audit logs or admin browsers. +// +// swagger:model loginFlowTestDebugPayload +type DebugPayload struct { + // Claims extracted from the ID token. + IDTokenClaims map[string]any `json:"id_token_claims,omitempty"` + + // Claims returned from the provider's userinfo endpoint, if any. + Userinfo map[string]any `json:"userinfo,omitempty"` + + // URL of the Jsonnet mapper that was executed on the claims. + JsonnetMapperURL string `json:"jsonnet_mapper_url,omitempty"` + + // Input JSON that was fed into the Jsonnet mapper. + JsonnetInput map[string]any `json:"jsonnet_input,omitempty"` + + // Output JSON returned by the Jsonnet mapper. + JsonnetOutput map[string]any `json:"jsonnet_output,omitempty"` + + // Anything the Jsonnet mapper wrote to standard error. + JsonnetStderr string `json:"jsonnet_stderr,omitempty"` + + // Identity-schema validation errors produced from the mapped traits. + SchemaValidationErrors []SchemaValidationError `json:"schema_validation_errors,omitempty"` + + // A classified error if any step of the round-trip failed. + Error *StepError `json:"error,omitempty"` +} + +// Classified failure of a step in an OIDC test round-trip. +// Populated when any step (token exchange, claims decode, Jsonnet +// evaluation, schema validation) cannot complete. +// +// swagger:model loginFlowTestStepError +type StepError struct { + // Machine-readable identifier of the failed step (for example, + // "token_exchange" or "schema_validate"). + Step string `json:"step"` + + // Short classification of the failure cause (for example, + // "access_denied" or "traits_invalid"). + Reason string `json:"reason"` + + // Human-readable message describing the failure. + Message string `json:"message"` +} + +// One identity-schema validation failure recorded while evaluating the +// traits produced by the Jsonnet mapper. +// +// swagger:model loginFlowTestSchemaValidationError +type SchemaValidationError struct { + // JSON pointer to the field that failed validation. + Path string `json:"path"` + + // Human-readable description of the validation failure. + Message string `json:"message"` +} + +// IsTest reports whether this login flow is an admin-created test flow. The +// dedicated column lets persistence filter without parsing internal_context. +// A NULL or false test_flow column both read as false. +func (f *Flow) IsTest() bool { return f.TestFlow.Valid && f.TestFlow.Bool } + +// LoadTestContext parses internal_context into the derived TestContext field +// so the flow's API response exposes it. A non-test flow or a flow with no +// test payload results in TestContext being nil. +func (f *Flow) LoadTestContext() error { + if !f.IsTest() || len(f.InternalContext) == 0 { + f.TestContext = nil + return nil + } + var shell struct { + Test *TestContext `json:"test"` + } + if err := json.Unmarshal(f.InternalContext, &shell); err != nil { + return errors.WithStack(err) + } + f.TestContext = shell.Test + return nil +} + +// SetTestContext writes the derived TestContext back into internal_context +// and flips the TestFlow column. Call this after mutating f.TestContext (for +// example, when capturing a debug payload on the callback). +func (f *Flow) SetTestContext(tc *TestContext) error { + f.TestContext = tc + if tc == nil { + f.TestFlow = sql.NullBool{Valid: true, Bool: false} + if len(f.InternalContext) == 0 { + return nil + } + updated, err := sjson.DeleteBytes(f.InternalContext, InternalContextTestKey) + if err != nil { + return errors.WithStack(err) + } + f.InternalContext = updated + return nil + } + + f.TestFlow = sql.NullBool{Valid: true, Bool: true} + if len(f.InternalContext) == 0 { + f.InternalContext = []byte("{}") + } + encoded, err := json.Marshal(tc) + if err != nil { + return errors.WithStack(err) + } + updated, err := sjson.SetRawBytes(f.InternalContext, InternalContextTestKey, encoded) + if err != nil { + return errors.WithStack(err) + } + f.InternalContext = updated + return nil +} diff --git a/selfservice/flow/login/test_context_test.go b/selfservice/flow/login/test_context_test.go new file mode 100644 index 0000000..64599db --- /dev/null +++ b/selfservice/flow/login/test_context_test.go @@ -0,0 +1,164 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "database/sql" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" +) + +func TestFlow_IsTest(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + tf sql.NullBool + want bool + }{ + {name: "null", tf: sql.NullBool{Valid: false}, want: false}, + {name: "false", tf: sql.NullBool{Valid: true, Bool: false}, want: false}, + {name: "true", tf: sql.NullBool{Valid: true, Bool: true}, want: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + f := &login.Flow{TestFlow: tc.tf} + assert.Equal(t, tc.want, f.IsTest()) + }) + } +} + +func TestFlow_LoadTestContext_NonTestFlow(t *testing.T) { + t.Parallel() + f := &login.Flow{ + TestFlow: sql.NullBool{Valid: false}, + InternalContext: sqlxx.JSONRawMessage(`{"test":{"provider_id":"google"},"foo":"bar"}`), + } + require.NoError(t, f.LoadTestContext()) + assert.Nil(t, f.TestContext) +} + +func TestFlow_LoadTestContext_EmptyInternalContext(t *testing.T) { + t.Parallel() + f := &login.Flow{ + TestFlow: sql.NullBool{Valid: true, Bool: true}, + InternalContext: nil, + } + require.NoError(t, f.LoadTestContext()) + assert.Nil(t, f.TestContext) +} + +func TestFlow_LoadTestContext_RoundTrip(t *testing.T) { + t.Parallel() + tc := &login.TestContext{ + ProviderID: "google", + DebugPayload: &login.DebugPayload{ + IDTokenClaims: map[string]any{"sub": "x"}, + }, + } + f := &login.Flow{InternalContext: sqlxx.JSONRawMessage(`{}`)} + require.NoError(t, f.SetTestContext(tc)) + assert.True(t, f.IsTest()) + + // Simulate a round-trip through persistence by clearing the derived + // field and reloading from InternalContext. + f.TestContext = nil + require.NoError(t, f.LoadTestContext()) + require.NotNil(t, f.TestContext) + assert.Equal(t, "google", f.TestContext.ProviderID) + require.NotNil(t, f.TestContext.DebugPayload) + assert.Equal(t, "x", f.TestContext.DebugPayload.IDTokenClaims["sub"]) +} + +func TestFlow_SetTestContext_Nil_RemovesKey(t *testing.T) { + t.Parallel() + f := &login.Flow{ + InternalContext: sqlxx.JSONRawMessage(`{"test":{"provider_id":"google"},"foo":"bar"}`), + TestFlow: sql.NullBool{Valid: true, Bool: true}, + } + require.NoError(t, f.SetTestContext(nil)) + + assert.False(t, f.IsTest()) + require.NoError(t, f.LoadTestContext()) + assert.Nil(t, f.TestContext) + assert.Equal(t, "bar", gjson.GetBytes(f.InternalContext, "foo").String()) + assert.False(t, gjson.GetBytes(f.InternalContext, "test").Exists()) +} + +func TestFlow_SetTestContext_Nil_EmptyInternalContext(t *testing.T) { + t.Parallel() + f := &login.Flow{InternalContext: nil} + require.NoError(t, f.SetTestContext(nil)) + assert.Equal(t, sql.NullBool{Valid: true, Bool: false}, f.TestFlow) + assert.True(t, len(f.InternalContext) == 0 || string(f.InternalContext) == "{}") +} + +func TestFlow_SetTestContext_PreservesSiblingKeys(t *testing.T) { + t.Parallel() + f := &login.Flow{ + InternalContext: sqlxx.JSONRawMessage(`{"foo":"bar","baz":1}`), + } + require.NoError(t, f.SetTestContext(&login.TestContext{ProviderID: "x"})) + + assert.Equal(t, "bar", gjson.GetBytes(f.InternalContext, "foo").String()) + assert.Equal(t, int64(1), gjson.GetBytes(f.InternalContext, "baz").Int()) + assert.Equal(t, "x", gjson.GetBytes(f.InternalContext, "test.provider_id").String()) +} + +// newFlowForMarshal returns a minimal well-formed login flow suitable for +// MarshalJSON testing. +func newFlowForMarshal(t *testing.T) *login.Flow { + t.Helper() + return &login.Flow{ + ID: x.NewUUID(), + UI: container.New(""), + InternalContext: sqlxx.JSONRawMessage(`{}`), + } +} + +func TestFlow_MarshalJSON_TestFlowIncludesContext(t *testing.T) { + t.Parallel() + f := newFlowForMarshal(t) + require.NoError(t, f.SetTestContext(&login.TestContext{ + ProviderID: "google", + DebugPayload: &login.DebugPayload{ + IDTokenClaims: map[string]any{"sub": "alice"}, + }, + })) + + b, err := json.Marshal(*f) + require.NoError(t, err) + assert.Equal(t, "google", gjson.GetBytes(b, "test_context.provider_id").String()) + assert.Equal(t, "alice", gjson.GetBytes(b, "test_context.debug_payload.id_token_claims.sub").String()) +} + +func TestFlow_MarshalJSON_NonTestFlowOmitsContext(t *testing.T) { + t.Parallel() + f := newFlowForMarshal(t) + + b, err := json.Marshal(*f) + require.NoError(t, err) + assert.False(t, gjson.GetBytes(b, "test_context").Exists()) +} + +func TestFlow_MarshalJSON_UncapturedTestFlow(t *testing.T) { + t.Parallel() + f := newFlowForMarshal(t) + require.NoError(t, f.SetTestContext(&login.TestContext{ProviderID: "google"})) + + b, err := json.Marshal(*f) + require.NoError(t, err) + assert.Equal(t, "google", gjson.GetBytes(b, "test_context.provider_id").String()) + assert.False(t, gjson.GetBytes(b, "test_context.debug_payload").Exists()) +} diff --git a/selfservice/flow/login/test_handler.go b/selfservice/flow/login/test_handler.go new file mode 100644 index 0000000..55a9f46 --- /dev/null +++ b/selfservice/flow/login/test_handler.go @@ -0,0 +1,233 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/x" + "github.com/ory/nosurf" +) + +const ( + // RouteAdminCreateTestFlow is the admin route for creating a dry-run OIDC + // test login flow. The admin router prepends /admin automatically, so the + // full path is POST /admin/test-login-flows. + RouteAdminCreateTestFlow = "/test-login-flows" + + // RouteDeleteTestFlow is the public route for deleting a test login flow. + // It is authorized by the flow's CSRF bearer token, not by a session. + RouteDeleteTestFlow = "/self-service/login/test" + + // maxProviderIDLength bounds the provider_id payload field. Provider IDs + // are short identifiers; anything longer indicates user error or abuse. + maxProviderIDLength = 255 +) + +// TestStrategy populates a login flow's UI with a single submit button +// for the given OIDC provider. The OIDC strategy implements this interface. +// Defined in the login package to avoid an import cycle. +type TestStrategy interface { + PopulateTestLoginFlow(r *http.Request, f *Flow, providerID string) error +} + +// TestStrategyProvider exposes the enabled TestStrategy for the +// current request context. +type TestStrategyProvider interface { + TestStrategy(ctx context.Context) TestStrategy +} + +// Parameters for creating a test OIDC login flow. +// +// swagger:parameters createTestLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createTestLoginFlow struct { + // in: body + // required: true + Body createTestLoginFlowBody +} + +// Request body for creating a test OIDC login flow. +// +// swagger:model createTestLoginFlowBody +type createTestLoginFlowBody struct { + // ID of the OIDC provider to test. Must match a provider configured on + // the project that serves this request. + // + // required: true + // maxLength: 255 + ProviderID string `json:"provider_id"` +} + +// swagger:route POST /admin/test-login-flows identity createTestLoginFlow +// +// # Create a test OIDC login flow +// +// Creates a dry-run OIDC test login flow pre-scoped to one provider. The +// returned flow carries a single-submit UI and a CSRF bearer token. No +// identity is persisted and no session is issued when the flow completes; +// the captured debug data is returned in the flow's test_context. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 201: loginFlow +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) adminCreateTestLoginFlow(w http.ResponseWriter, r *http.Request) { + // The strategy annotates this span with the provider lookup outcome + // (test_login_flow.* attributes); without it, a failed lookup is + // invisible in traces because the whole path is in-memory. + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "login.Handler.adminCreateTestLoginFlow") + defer span.End() + r = r.WithContext(ctx) + + var body createTestLoginFlowBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("could not parse request body"))) + return + } + if body.ProviderID == "" { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("provider_id is required"))) + return + } + if len(body.ProviderID) > maxProviderIDLength { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("provider_id must be at most %d characters", maxProviderIDLength))) + return + } + + f, err := NewFlow(h.d, r, flow.TypeBrowser) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + // Admin-created flows have no browser cookie to bind against; mint a + // random UUID as the opaque CSRF bearer and validate it on submit. + f.CSRFToken = x.NewUUID().String() + + if err := f.SetTestContext(&TestContext{ProviderID: body.ProviderID}); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + // Mark as "refresh" so the OIDC strategy's alreadyAuthenticated guard does + // not short-circuit the submit if the admin's browser happens to carry an + // unrelated Ory session cookie. Test flows always run the OIDC round-trip + // regardless of session state. + f.Refresh = true + + strategy := h.d.TestStrategy(r.Context()) + if strategy == nil { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError().WithReason("OIDC strategy is not registered; test login flows cannot be created"))) + return + } + if err := strategy.PopulateTestLoginFlow(r, f, body.ProviderID); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if err := h.d.LoginFlowPersister().CreateLoginFlow(r.Context(), f); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().WriteCode(w, r, http.StatusCreated, f) +} + +// Delete Test Login Flow Parameters +// +// swagger:parameters deleteTestLoginFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type deleteTestLoginFlow struct { + // ID of the test login flow to delete. + // + // required: true + // in: query + ID string `json:"id"` + + // HTTP Cookies. A captured test flow requires the ory_kratos_test_flow + // cookie set by the OIDC callback; a flow still in the initial + // choose-method state does not. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route DELETE /self-service/login/test frontend deleteTestLoginFlow +// +// # Delete a test OIDC login flow +// +// Deletes a dry-run OIDC test login flow. A flow whose debug payload has +// been captured requires the HMAC cookie set by the OIDC callback; a flow +// still in the initial choose-method state is deletable with just the flow +// ID (it carries no PII, and the admin may want to abandon it). +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 403: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) deleteTestLoginFlow(w http.ResponseWriter, r *http.Request) { + id, err := uuid.FromString(r.URL.Query().Get("id")) + if err != nil { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid flow id"))) + return + } + + // Load the flow and discriminate by whether a debug payload has been + // captured. A return of sqlcon.ErrNoRows maps to 404 — we don't leak + // whether the ID belongs to a non-test login flow vs. a missing row. + f, err := h.d.LoginFlowPersister().GetLoginFlow(r.Context(), id) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + if !f.IsTest() { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound())) + return + } + if err := f.LoadTestContext(); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + if f.TestContext != nil && f.TestContext.DebugPayload != nil && + !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), f.CSRFToken) { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("missing or invalid CSRF token"))) + return + } + + if err := h.d.LoginFlowPersister().DeleteTestLoginFlow(r.Context(), id); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/selfservice/flow/login/test_handler_test.go b/selfservice/flow/login/test_handler_test.go new file mode 100644 index 0000000..46fb8ac --- /dev/null +++ b/selfservice/flow/login/test_handler_test.go @@ -0,0 +1,391 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlxx" +) + +// newRegistryWithOIDCProvider returns a fast test registry configured with a +// single generic OIDC provider whose ID is "google". The populator can find +// the provider when the admin-create handler invokes it. +func newRegistryWithOIDCProvider(t *testing.T) *driver.RegistryDefault { + t.Helper() + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://../../strategy/oidc/stub/registration.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, true)), + configx.WithValue( + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config.providers", + []map[string]any{{ + "provider": "generic", + "id": "google", + "label": "Google", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://../../strategy/oidc/stub/oidc.facebook.jsonnet", + }}, + ), + ) + return reg +} + +func doAdminPost(t *testing.T, adminURL string, body any) (*http.Response, []byte) { + t.Helper() + b, err := json.Marshal(body) + require.NoError(t, err) + res, err := testhelpers.NewTestClient(t).Post(adminURL+"/admin"+login.RouteAdminCreateTestFlow, "application/json", bytes.NewReader(b)) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + respBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, respBody +} + +func doDelete(t *testing.T, c *http.Client, u string) (*http.Response, []byte) { + t.Helper() + req, err := http.NewRequest(http.MethodDelete, u, nil) + require.NoError(t, err) + res, err := c.Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body +} + +// newSeedTestFlow creates a minimal test login flow directly via the +// persister to support cap-enforcement seeding. +func newSeedTestFlow(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, exp time.Duration) *login.Flow { + t.Helper() + f := &login.Flow{ + ID: x.NewUUID(), + Type: flow.TypeBrowser, + State: flow.StateChooseMethod, + RequestURL: "https://example.com/self-service/login/browser", + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(exp), + CSRFToken: nosurfx.FakeCSRFToken, + UI: container.New(""), + InternalContext: sqlxx.JSONRawMessage(`{}`), + } + require.NoError(t, f.SetTestContext(&login.TestContext{ProviderID: "google"})) + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + return f +} + +func TestHandler_AdminCreateTestLoginFlow_HappyPath(t *testing.T) { + t.Parallel() + reg := newRegistryWithOIDCProvider(t) + _, admin := testhelpers.NewKratosServer(t, reg) + + res, body := doAdminPost(t, admin.URL, map[string]string{"provider_id": "google"}) + require.Equal(t, http.StatusCreated, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "id").String()) + assert.Equal(t, "google", gjson.GetBytes(body, "test_context.provider_id").String()) + assert.Equal(t, string(flow.StateChooseMethod), gjson.GetBytes(body, "state").String()) + + // UI contains a provider submit node scoped to google. + found := false + for _, n := range gjson.GetBytes(body, "ui.nodes").Array() { + if n.Get("attributes.name").String() == "provider" && n.Get("attributes.value").String() == "google" { + found = true + break + } + } + assert.True(t, found, "expected provider=google submit node in UI: %s", body) +} + +// TestHandler_AdminCreateTestLoginFlow_Tracing asserts that the handler +// emits a span recording which strategy served the request and what the +// provider lookup saw. A production incident where the wrong strategy was +// selected (and thus an empty provider list was searched) was invisible in +// traces because this path had no instrumentation. +func TestHandler_AdminCreateTestLoginFlow_Tracing(t *testing.T) { + t.Parallel() + reg := newRegistryWithOIDCProvider(t) + + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + reg.SetTracer(otelx.NewNoop().WithOTLP(provider.Tracer("test"))) + + _, admin := testhelpers.NewKratosServer(t, reg) + + res, body := doAdminPost(t, admin.URL, map[string]string{"provider_id": "google"}) + require.Equal(t, http.StatusCreated, res.StatusCode, "%s", body) + res, body = doAdminPost(t, admin.URL, map[string]string{"provider_id": "nonesuch"}) + require.Equal(t, http.StatusNotFound, res.StatusCode, "%s", body) + + var spans []sdktrace.ReadOnlySpan + for _, sp := range recorder.Ended() { + if sp.Name() == "login.Handler.adminCreateTestLoginFlow" { + spans = append(spans, sp) + } + } + require.Len(t, spans, 2, "expected one handler span per request") + + attrs := func(sp sdktrace.ReadOnlySpan) map[attribute.Key]attribute.Value { + out := make(map[attribute.Key]attribute.Value, len(sp.Attributes())) + for _, kv := range sp.Attributes() { + out[kv.Key] = kv.Value + } + return out + } + + found := attrs(spans[0]) + assert.Equal(t, "oidc", found["test_login_flow.strategy"].AsString()) + assert.Equal(t, int64(1), found["test_login_flow.available_providers"].AsInt64()) + assert.True(t, found["test_login_flow.provider_found"].AsBool()) + + notFound := attrs(spans[1]) + assert.Equal(t, "oidc", notFound["test_login_flow.strategy"].AsString()) + assert.Equal(t, int64(1), notFound["test_login_flow.available_providers"].AsInt64()) + assert.False(t, notFound["test_login_flow.provider_found"].AsBool()) +} + +func TestHandler_AdminCreateTestLoginFlow_BadInput(t *testing.T) { + t.Parallel() + + reg := newRegistryWithOIDCProvider(t) + _, admin := testhelpers.NewKratosServer(t, reg) + + for _, tc := range []struct { + name string + body map[string]string + wantStatus int + }{ + {name: "empty provider_id", body: map[string]string{}, wantStatus: http.StatusBadRequest}, + {name: "unknown provider_id", body: map[string]string{"provider_id": "nonesuch"}, wantStatus: http.StatusNotFound}, + {name: "provider_id too long", body: map[string]string{"provider_id": strings.Repeat("a", 256)}, wantStatus: http.StatusBadRequest}, + } { + t.Run("case="+tc.name, func(t *testing.T) { + t.Parallel() + res, body := doAdminPost(t, admin.URL, tc.body) + assert.Equal(t, tc.wantStatus, res.StatusCode, "%s", body) + }) + } +} + +func TestHandler_DeleteTestLoginFlow(t *testing.T) { + t.Parallel() + + type setup func(t *testing.T, reg *driver.RegistryDefault) *login.Flow + + seedUncaptured := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedTestFlow(t, context.Background(), reg, time.Hour) + } + // seedCapturedMatching: captured flow whose CSRFToken matches what the + // fake CSRF generator returns — simulates the browser that holds the + // post-callback cookie. + seedCapturedMatching := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedCapturedTestFlow(t, context.Background(), reg, time.Hour, nosurfx.FakeCSRFToken) + } + // seedCapturedMismatch: captured flow with a CSRFToken that won't match, + // simulating a request without (or with the wrong) CSRF cookie. + seedCapturedMismatch := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedCapturedTestFlow(t, context.Background(), reg, time.Hour, "some-other-token") + } + + for _, tc := range []struct { + name string + setup setup + wantStatus int + wantDeleted bool + }{ + { + name: "uncaptured: 204", + setup: seedUncaptured, + wantStatus: http.StatusNoContent, + wantDeleted: true, + }, + { + name: "captured with mismatched CSRF token: 403", + setup: seedCapturedMismatch, + wantStatus: http.StatusForbidden, + }, + { + name: "captured with matching CSRF token: 204", + setup: seedCapturedMatching, + wantStatus: http.StatusNoContent, + wantDeleted: true, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + reg := newRegistryWithOIDCProvider(t) + public, _ := testhelpers.NewKratosServer(t, reg) + + f := tc.setup(t, reg) + u := fmt.Sprintf("%s%s?id=%s", public.URL, login.RouteDeleteTestFlow, f.ID) + + req, err := http.NewRequest(http.MethodDelete, u, nil) + require.NoError(t, err) + res, err := public.Client().Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, tc.wantStatus, res.StatusCode, "%s", body) + + _, getErr := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + if tc.wantDeleted { + assert.Error(t, getErr, "flow should be deleted") + } else { + assert.NoError(t, getErr, "flow should still exist") + } + }) + } +} + +func TestHandler_DeleteTestLoginFlow_NotATestFlow(t *testing.T) { + t.Parallel() + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + public, _ := testhelpers.NewKratosServer(t, reg) + + // Create a regular (non-test) login flow directly. + f := &login.Flow{ + ID: x.NewUUID(), + Type: flow.TypeBrowser, + State: flow.StateChooseMethod, + RequestURL: "https://example.com/self-service/login/browser", + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + CSRFToken: nosurfx.FakeCSRFToken, + UI: container.New(""), + InternalContext: sqlxx.JSONRawMessage(`{}`), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + + u := fmt.Sprintf("%s%s?id=%s", public.URL, login.RouteDeleteTestFlow, f.ID) + res, body := doDelete(t, public.Client(), u) + assert.Equal(t, http.StatusNotFound, res.StatusCode, "%s", body) +} + +func TestHandler_DeleteTestLoginFlow_InvalidUUID(t *testing.T) { + t.Parallel() + _, reg := pkg.NewFastRegistryWithMocks(t) + public, _ := testhelpers.NewKratosServer(t, reg) + + u := fmt.Sprintf("%s%s?id=not-a-uuid", public.URL, login.RouteDeleteTestFlow) + res, body := doDelete(t, public.Client(), u) + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", body) +} + +func TestHandler_GetTestLoginFlow(t *testing.T) { + t.Parallel() + + type setup func(t *testing.T, reg *driver.RegistryDefault) *login.Flow + + seedUncaptured := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedTestFlow(t, context.Background(), reg, time.Hour) + } + seedCapturedMatching := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedCapturedTestFlow(t, context.Background(), reg, time.Hour, nosurfx.FakeCSRFToken) + } + seedCapturedMismatch := func(t *testing.T, reg *driver.RegistryDefault) *login.Flow { + return newSeedCapturedTestFlow(t, context.Background(), reg, time.Hour, "some-other-token") + } + + for _, tc := range []struct { + name string + setup setup + wantStatus int + assertBody func(t *testing.T, body []byte) + }{ + { + name: "uncaptured: 200", + setup: seedUncaptured, + wantStatus: http.StatusOK, + assertBody: func(t *testing.T, body []byte) { + assert.Equal(t, "google", gjson.GetBytes(body, "test_context.provider_id").String()) + }, + }, + { + name: "captured with mismatched CSRF token: 403", + setup: seedCapturedMismatch, + wantStatus: http.StatusForbidden, + }, + { + name: "captured with matching CSRF token: 200", + setup: seedCapturedMatching, + wantStatus: http.StatusOK, + assertBody: func(t *testing.T, body []byte) { + assert.Equal(t, "alice@example.com", + gjson.GetBytes(body, "test_context.debug_payload.id_token_claims.sub").String()) + }, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + t.Parallel() + reg := newRegistryWithOIDCProvider(t) + public, _ := testhelpers.NewKratosServer(t, reg) + + f := tc.setup(t, reg) + u := fmt.Sprintf("%s%s?id=%s", public.URL, login.RouteGetFlow, f.ID) + + req, err := http.NewRequest(http.MethodGet, u, nil) + require.NoError(t, err) + res, err := public.Client().Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, tc.wantStatus, res.StatusCode, "%s", body) + if tc.assertBody != nil { + tc.assertBody(t, body) + } + }) + } +} + +// newSeedCapturedTestFlow creates a test login flow already advanced to the +// passed_challenge state with a DebugPayload populated, simulating what the +// OIDC callback would have persisted. csrfToken is written to f.CSRFToken so +// tests can simulate either the post-callback "matches the cookie" state or a +// mismatch. +func newSeedCapturedTestFlow(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, exp time.Duration, csrfToken string) *login.Flow { + t.Helper() + f := newSeedTestFlow(t, ctx, reg, exp) + f.CSRFToken = csrfToken + f.State = flow.StatePassedChallenge + require.NoError(t, f.SetTestContext(&login.TestContext{ + ProviderID: "google", + DebugPayload: &login.DebugPayload{ + IDTokenClaims: map[string]any{"sub": "alice@example.com"}, + }, + })) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(ctx, f)) + return f +} diff --git a/selfservice/flow/login/testsetup_test.go b/selfservice/flow/login/testsetup_test.go new file mode 100644 index 0000000..b9ad42b --- /dev/null +++ b/selfservice/flow/login/testsetup_test.go @@ -0,0 +1,20 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login_test + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" +) + +var returnToServer *httptest.Server + +func TestMain(m *testing.M) { + returnToServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + })) + os.Exit(m.Run()) +} diff --git a/selfservice/flow/logout/handler.go b/selfservice/flow/logout/handler.go new file mode 100644 index 0000000..9c40bbc --- /dev/null +++ b/selfservice/flow/logout/handler.go @@ -0,0 +1,375 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logout + +import ( + "net/http" + "net/url" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + + "go.opentelemetry.io/otel/trace" + + "github.com/pkg/errors" + + "github.com/ory/kratos/x/events" + + "github.com/ory/herodot" + "github.com/ory/x/decoderx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +const ( + RouteInitBrowserFlow = "/self-service/logout/browser" + RouteAPIFlow = "/self-service/logout/api" + RouteSubmitFlow = "/self-service/logout" +) + +type ( + dependencies interface { + httpx.WriterProvider + nosurfx.CSRFProvider + session.ManagementProvider + session.PersistenceProvider + errorx.ManagementProvider + config.Provider + } + HandlerProvider interface { + LogoutHandler() *Handler + } + Handler struct{ d dependencies } +) + +func NewHandler(d dependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterPublicRoutes(router *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteAPIFlow) + + router.GET(RouteInitBrowserFlow, h.createBrowserLogoutFlow) + router.DELETE(RouteAPIFlow, h.performNativeLogout) + router.GET(RouteSubmitFlow, h.updateLogoutFlow) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + admin.DELETE(RouteAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) +} + +// Logout Flow +// +// swagger:model logoutFlow +type logoutFlow struct { + // LogoutURL can be opened in a browser to sign the user out. + // + // format: uri + // required: true + LogoutURL string `json:"logout_url"` + + // LogoutToken can be used to perform logout using AJAX. + // + // required: true + LogoutToken string `json:"logout_token"` +} + +// Create Browser Logout Flow Parameters +// +// swagger:parameters createBrowserLogoutFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserLogoutFlow struct { + // HTTP Cookies + // + // If you call this endpoint from a backend, please include the + // original Cookie header in the request. + // + // in: header + // name: cookie + Cookie string `json:"cookie"` + + // Return to URL + // + // The URL to which the browser should be redirected to after the logout + // has been performed. + // + // in: query + ReturnTo string `json:"return_to"` +} + +// swagger:route GET /self-service/logout/browser frontend createBrowserLogoutFlow +// +// # Create a Logout URL for Browsers +// +// This endpoint initializes a browser-based user logout flow and a URL which can be used to log out the user. +// +// This endpoint is NOT INTENDED for API clients and only works +// with browsers (Chrome, Firefox, ...). For API clients you can +// call the `/self-service/logout/api` URL directly with the Ory Session Token. +// +// The URL is only valid for the currently signed in user. If no user is signed in, this endpoint returns +// a 401 error. +// +// When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: logoutFlow +// 400: errorGeneric +// 401: errorGeneric +// 500: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserLogoutFlow(w http.ResponseWriter, r *http.Request) { + sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + conf := h.d.Config() + + requestURL := x.RequestURL(r) + + var returnTo *url.URL + + if requestURL.Query().Get("return_to") != "" { + // Pre-validate the return to URL which is contained in the HTTP request. + returnTo, err = redir.SecureRedirectTo(r, + h.d.Config().SelfServiceFlowLogoutRedirectURL(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL.String()), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + } + + params := url.Values{"token": {sess.LogoutToken}} + + if returnTo != nil { + params.Set("return_to", returnTo.String()) + } + + h.d.Writer().Write(w, r, &logoutFlow{ + LogoutToken: sess.LogoutToken, + LogoutURL: urlx.CopyWithQuery(urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteSubmitFlow), params).String(), + }) +} + +// Perform Native Logout Parameters +// +// swagger:parameters performNativeLogout +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type performNativeLogout struct { + // in: body + // required: true + Body performNativeLogoutBody +} + +// Perform Native Logout Request Body +// +// swagger:model performNativeLogoutBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type performNativeLogoutBody struct { + // The Session Token + // + // Invalidate this session token. + // + // required: true + SessionToken string `json:"session_token"` +} + +// swagger:route DELETE /self-service/logout/api frontend performNativeLogout +// +// # Perform Logout for Native Apps +// +// Use this endpoint to log out an identity using an Ory Session Token. If the Ory Session Token was successfully +// revoked, the server returns a 204 No Content response. A 204 No Content response is also sent when +// the Ory Session Token has been revoked already before. +// +// If the Ory Session Token is malformed or does not exist a 403 Forbidden response will be returned. +// +// This endpoint does not remove any HTTP +// Cookies - use the Browser-Based Self-Service Logout Flow instead. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) performNativeLogout(w http.ResponseWriter, r *http.Request) { + var p performNativeLogoutBody + if err := decoderx.Decode(r, &p, + decoderx.HTTPJSONDecoder(), + decoderx.HTTPDecoderAllowedMethods("DELETE")); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + revoked, err := h.d.SessionPersister().RevokeSessionByToken(r.Context(), p.SessionToken) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("The provided Ory Session Token could not be found, is invalid, or otherwise malformed."))) + return + } + + h.d.Writer().WriteError(w, r, err) + return + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionRevoked(r.Context(), revoked.ID, revoked.IdentityID)) + + w.WriteHeader(http.StatusNoContent) +} + +// Update Logout Flow Parameters +// +// swagger:parameters updateLogoutFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateLogoutFlow struct { + // A Valid Logout Token + // + // If you do not have a logout token because you only have a session cookie, + // call `/self-service/logout/browser` to generate a URL for this endpoint. + // + // in: query + Token string `json:"token"` + + // The URL to return to after the logout was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route GET /self-service/logout frontend updateLogoutFlow +// +// # Update Logout Flow +// +// This endpoint logs out an identity in a self-service manner. +// +// If the `Accept` HTTP header is not set to `application/json`, the browser will be redirected (HTTP 303 See Other) +// to the `return_to` parameter of the initial request or fall back to `urls.default_return_to`. +// +// If the `Accept` HTTP header is set to `application/json`, a 204 No Content response +// will be sent on successful logout instead. +// +// This endpoint is NOT INTENDED for API clients and only works +// with browsers (Chrome, Firefox, ...). For API clients you can +// 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.com/docs/next/kratos/self-service/flows/user-logout). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 303: emptyResponse +// 204: emptyResponse +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) updateLogoutFlow(w http.ResponseWriter, r *http.Request) { + expected := r.URL.Query().Get("token") + if len(expected) == 0 { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("Please include a token in the URL query."))) + return + } + + sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing) + if err != nil { + // We could handle `session.ErrNoActiveSessionFound` gracefully with `h.completeLogout()` here but that would + // actually be an issue as it incorrectly indicates to clients that the session has been removed even if + // `RevokeSessionByToken` has not actually been called. + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if sess.LogoutToken != expected { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrForbidden().WithReason("Unable to log out because the logout token in the URL query does not match the session cookie."))) + return + } + + if err := h.d.SessionManager().PurgeFromRequest(r.Context(), w, r); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionRevoked(r.Context(), sess.ID, sess.IdentityID)) + + h.completeLogout(w, r) +} + +func (h *Handler) completeLogout(w http.ResponseWriter, r *http.Request) { + _ = h.d.CSRFHandler().RegenerateToken(w, r) + + ret, err := redir.SecureRedirectTo(r, h.d.Config().SelfServiceFlowLogoutRedirectURL(r.Context()), + redir.SecureRedirectUseSourceURL(r.RequestURI), + redir.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(r.Context())), + ) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + // Instruct the browser to clear cookies, storage, and caches for this origin. + // We only send the header over HTTPS because browsers ignore Clear-Site-Data + // on insecure origins, and gate it behind a config flag (off by default) so we + // do not clear data that other applications on the same origin may rely on. + if h.d.Config().SelfServiceFlowLogoutClearBrowserData(r.Context()) && x.RequestURL(r).Scheme == "https" { + w.Header().Set("Clear-Site-Data", `"cookies", "storage", "cache"`) + } + + if x.IsJSONRequest(r) { + w.WriteHeader(http.StatusNoContent) + return + } + + http.Redirect(w, r, ret.String(), http.StatusSeeOther) +} diff --git a/selfservice/flow/logout/handler_clearsitedata_test.go b/selfservice/flow/logout/handler_clearsitedata_test.go new file mode 100644 index 0000000..e0063ec --- /dev/null +++ b/selfservice/flow/logout/handler_clearsitedata_test.go @@ -0,0 +1,74 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logout_test + +import ( + "context" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" +) + +func TestLogoutClearSiteData(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + public, _, publicRouter, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + publicRouter.GET("/session/browser/set", func(w http.ResponseWriter, r *http.Request) { + testhelpers.MockSetSession(t, reg, conf)(w, r) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceLogoutBrowserDefaultReturnTo, public.URL+"/") + + // Resolve the submit URL and token for a fresh session, then issue the logout + // without following the redirect so we can inspect the 303 response headers. + logout := func(t *testing.T, forwardedProto string) *http.Response { + hc := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + + u, err := url.Parse(public.URL + "/self-service/logout/browser") + require.NoError(t, err) + body, res := testhelpers.HTTPRequestJSON(t, hc, "GET", u.String(), nil) + require.EqualValues(t, http.StatusOK, res.StatusCode) + logoutURL := gjson.GetBytes(body, "logout_url").String() + require.NotEmpty(t, logoutURL) + + hc.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } + req, err := http.NewRequest("GET", logoutURL, nil) + require.NoError(t, err) + if forwardedProto != "" { + req.Header.Set("X-Forwarded-Proto", forwardedProto) + } + out, err := hc.Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = out.Body.Close() }) + require.Equal(t, http.StatusSeeOther, out.StatusCode, "%s", x.MustReadAll(out.Body)) + return out + } + + t.Run("case=flag disabled does not set header", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceLogoutClearBrowserData, false) + res := logout(t, "https") + assert.Empty(t, res.Header.Get("Clear-Site-Data")) + }) + + t.Run("case=flag enabled over https sets header", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceLogoutClearBrowserData, true) + res := logout(t, "https") + assert.Equal(t, `"cookies", "storage", "cache"`, res.Header.Get("Clear-Site-Data")) + }) + + t.Run("case=flag enabled over http does not set header", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceLogoutClearBrowserData, true) + res := logout(t, "http") + assert.Empty(t, res.Header.Get("Clear-Site-Data")) + }) +} diff --git a/selfservice/flow/logout/handler_test.go b/selfservice/flow/logout/handler_test.go new file mode 100644 index 0000000..e6d7d0e --- /dev/null +++ b/selfservice/flow/logout/handler_test.go @@ -0,0 +1,305 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logout_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "testing" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestLogout(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + errTS := testhelpers.NewErrorTestServer(t, reg) + + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + public, _, publicRouter, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + publicRouter.GET("/session/browser/set", func(writer http.ResponseWriter, request *http.Request) { + testhelpers.MockSetSession(t, reg, conf)(writer, request) + }) + publicRouter.GET("/session/browser/get", func(w http.ResponseWriter, r *http.Request) { + sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + if err != nil { + reg.Writer().WriteError(w, r, err) + return + } + reg.Writer().Write(w, r, sess) + }) + publicRouter.POST("/csrf/check", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceLogoutBrowserDefaultReturnTo, public.URL+"/session/browser/get") + + t.Run("case=successful logout for API clients", func(t *testing.T) { + hc := testhelpers.NewDebugClient(t) + + sess := testhelpers.CreateSession(t, reg) + _, res := testhelpers.HTTPRequestJSON(t, hc, "DELETE", public.URL+"/self-service/logout/api", json.RawMessage(`{"session_token": "`+sess.Token+`"}`)) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + + actual, err := reg.SessionPersister().GetSession(ctx, sess.ID, session.ExpandNothing) + require.NoError(t, err) + assert.False(t, actual.IsActive()) + + // Retrying logout should not fail: + _, res = testhelpers.HTTPRequestJSON(t, hc, "DELETE", public.URL+"/self-service/logout/api", json.RawMessage(`{"session_token": "`+sess.Token+`"}`)) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("case=unsuccessful logout for API clients because session token is invalid", func(t *testing.T) { + hc := testhelpers.NewDebugClient(t) + + _, res := testhelpers.HTTPRequestJSON(t, hc, "DELETE", public.URL+"/self-service/logout/api", json.RawMessage(`{"session_token": "not a valid token"}`)) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + }) + + t.Run("case=unsuccessful logout for API clients because session token is missing", func(t *testing.T) { + hc := testhelpers.NewDebugClient(t) + + _, res := testhelpers.HTTPRequestJSON(t, hc, "DELETE", public.URL+"/self-service/logout/api", json.RawMessage(`{}`)) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + }) + + makeBrowserLogout := func(t *testing.T, hc *http.Client, u string) ([]byte, *http.Response) { + res, err := hc.Get(u) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + return x.MustReadAll(res.Body), res + } + + getLogoutUrl := func(t *testing.T, params url.Values) (hc *http.Client, logoutUrl string) { + hc = testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + + u, err := url.Parse(public.URL) + require.NoError(t, err) + u.Path = "/self-service/logout/browser" + if params != nil { + u.RawQuery = params.Encode() + } + + body, res := testhelpers.HTTPRequestJSON(t, hc, "GET", u.String(), nil) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + logoutUrl = gjson.GetBytes(body, "logout_url").String() + logoutToken := gjson.GetBytes(body, "logout_token").String() + + logoutUrlParsed, err := url.Parse(logoutUrl) + require.NoError(t, err) + + assert.Equalf(t, logoutUrlParsed.Query().Get("token"), logoutToken, "%s", body) + assert.Equalf(t, logoutUrlParsed.Path, "/self-service/logout", "%s", body) + return + } + + t.Run("case=successful logout for browser clients", func(t *testing.T) { + ensurePrincipalChange := func(t *testing.T, originalCookies []*http.Cookie) { + cj, err := cookiejar.New(nil) + require.NoError(t, err) + cj.SetCookies(urlx.ParseOrPanic(public.URL), originalCookies) + res, err := (&http.Client{Jar: cj, Transport: testhelpers.NewTestTransport(t)}).PostForm(public.URL+"/csrf/check", url.Values{}) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + body := x.MustReadAll(res.Body) + assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + hc, logoutUrl := getLogoutUrl(t, nil) + originalCookies := hc.Jar.Cookies(urlx.ParseOrPanic(public.URL)) + require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(public.URL))), "ory_kratos_session") + + body, res := makeBrowserLogout(t, hc, logoutUrl) + assert.EqualValues(t, public.URL+"/session/browser/get", res.Request.URL.String()) + require.NotContains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(public.URL))), "ory_kratos_session") + + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%s", body) + assert.EqualValues(t, "No active session was found in this request.", gjson.GetBytes(body, "error.reason").String()) + + // Logout means CSRF has also been changed. + ensurePrincipalChange(t, originalCookies) + }) + + t.Run("type=ajax", func(t *testing.T) { + hc, logoutUrl := getLogoutUrl(t, nil) + originalCookies := hc.Jar.Cookies(urlx.ParseOrPanic(public.URL)) + require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(public.URL))), "ory_kratos_session") + + body, res := testhelpers.HTTPRequestJSON(t, hc, "GET", logoutUrl, nil) + assert.EqualValues(t, logoutUrl, res.Request.URL.String()) + + assert.EqualValues(t, http.StatusNoContent, res.StatusCode, "%s", body) + require.NotContains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(public.URL))), "ory_kratos_session") + + // Logout means CSRF has also been changed. + ensurePrincipalChange(t, originalCookies) + }) + }) + + t.Run("case=calling submission without token but with session", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + hc := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + body, res := makeBrowserLogout(t, hc, public.URL+"/self-service/logout") + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, "Please include a token in the URL query.", gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("type=ajax", func(t *testing.T) { + hc := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + body, res := testhelpers.HTTPRequestJSON(t, hc, "GET", public.URL+"/self-service/logout", nil) + assert.EqualValues(t, public.URL+"/self-service/logout", res.Request.URL.String()) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode, "%s", body) + assert.EqualValues(t, "Please include a token in the URL query.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + }) + + t.Run("case=calling submission with token but without session", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + _, logoutUrl := getLogoutUrl(t, nil) + + body, res := makeBrowserLogout(t, testhelpers.NewTestClient(t), logoutUrl) + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, "No active session was found in this request.", gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("type=ajax", func(t *testing.T) { + _, logoutUrl := getLogoutUrl(t, nil) + + body, res := testhelpers.HTTPRequestJSON(t, testhelpers.NewTestClient(t), "GET", logoutUrl, nil) + assert.EqualValues(t, logoutUrl, res.Request.URL.String()) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%s", body) + assert.EqualValues(t, "No active session was found in this request.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + }) + + t.Run("case=calling submission with token but with session from another user", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + otherUser := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + _, logoutUrl := getLogoutUrl(t, nil) + + body, res := makeBrowserLogout(t, otherUser, logoutUrl) + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, "Unable to log out because the logout token in the URL query does not match the session cookie.", gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("type=ajax", func(t *testing.T) { + otherUser := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + _, logoutUrl := getLogoutUrl(t, nil) + + body, res := testhelpers.HTTPRequestJSON(t, otherUser, "GET", logoutUrl, nil) + assert.EqualValues(t, logoutUrl, res.Request.URL.String()) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "%s", body) + assert.EqualValues(t, "Unable to log out because the logout token in the URL query does not match the session cookie.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + }) + + t.Run("case=calling submission with invalid token", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + hc, logoutUrl := getLogoutUrl(t, nil) + + body, res := makeBrowserLogout(t, hc, logoutUrl+"invalid") + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, "Unable to log out because the logout token in the URL query does not match the session cookie.", gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("type=ajax", func(t *testing.T) { + hc, logoutUrl := getLogoutUrl(t, nil) + + body, res := testhelpers.HTTPRequestJSON(t, hc, "GET", logoutUrl+"invalid", nil) + assert.EqualValues(t, logoutUrl+"invalid", res.Request.URL.String()) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "%s", body) + assert.EqualValues(t, "Unable to log out because the logout token in the URL query does not match the session cookie.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + }) + + t.Run("case=calling browser init without session", func(t *testing.T) { + body, res := testhelpers.HTTPRequestJSON(t, testhelpers.NewTestClient(t), "GET", public.URL+"/self-service/logout/browser", nil) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + assert.EqualValues(t, "No active session was found in this request.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=init logout through browser does 303 redirect", func(t *testing.T) { + // init the logout + hc, logoutUrl := getLogoutUrl(t, nil) + // prevent the redirect, so we can get check the status code + hc.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + // submit the login + req, err := http.NewRequest("GET", logoutUrl, nil) + require.NoError(t, err) + + res, err := hc.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + }) + + t.Run("case=init logout with return_to should carry over return_to", func(t *testing.T) { + reg.Config().MustSet(context.Background(), config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + + hc, logoutUrl := getLogoutUrl(t, url.Values{"return_to": {"https://www.ory.sh"}}) + + logoutUrlParsed, err := url.Parse(logoutUrl) + require.NoError(t, err) + + assert.Equal(t, "https://www.ory.sh", logoutUrlParsed.Query().Get("return_to")) + + hc.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + body, res := makeBrowserLogout(t, hc, logoutUrl) + assert.EqualValues(t, http.StatusSeeOther, res.StatusCode, "%s", body) + assert.EqualValues(t, "https://www.ory.sh", res.Header.Get("Location")) + }) + + t.Run("case=init logout with return_to should not carry over return_to if not allowed", func(t *testing.T) { + hc := testhelpers.NewSessionClient(t, public.URL+"/session/browser/set") + + logoutUrl := public.URL + "/self-service/logout/browser?return_to=https://www.ory.com" + + r, err := http.NewRequest("GET", logoutUrl, nil) + require.NoError(t, err) + r.Header.Set("Accept", "application/json") + + resp, err := hc.Do(r) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.EqualValues(t, "Requested return_to URL \"https://www.ory.com\" is not allowed.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + assert.EqualValues(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + }) +} diff --git a/selfservice/flow/logout/stub/identity.schema.json b/selfservice/flow/logout/stub/identity.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/logout/stub/identity.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/name.go b/selfservice/flow/name.go new file mode 100644 index 0000000..1b766c6 --- /dev/null +++ b/selfservice/flow/name.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// FlowName is the flow name. +// +// The flow name can be one of: +// - 'login' +// - 'registration' +// - 'settings' +// - 'recovery' +// - 'verification' +// +// swagger:ignore +type FlowName string + +const ( + LoginFlow FlowName = "login" + RegistrationFlow FlowName = "registration" + SettingsFlow FlowName = "settings" + RecoveryFlow FlowName = "recovery" + VerificationFlow FlowName = "verification" +) + +func (t Type) String() string { + return string(t) +} diff --git a/selfservice/flow/nosurf.go b/selfservice/flow/nosurf.go new file mode 100644 index 0000000..62c0bbc --- /dev/null +++ b/selfservice/flow/nosurf.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "net/http" + + "github.com/ory/kratos/x/nosurfx" +) + +func GetCSRFToken(reg interface { + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider +}, w http.ResponseWriter, r *http.Request, p Type) string { + token := reg.GenerateCSRFToken(r) + if p != TypeBrowser { + return token + } + + if token == "" { + return reg.CSRFHandler().RegenerateToken(w, r) + } + + return token +} diff --git a/selfservice/flow/nosurf_test.go b/selfservice/flow/nosurf_test.go new file mode 100644 index 0000000..2922807 --- /dev/null +++ b/selfservice/flow/nosurf_test.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/nosurf" +) + +func TestGetCSRFToken(t *testing.T) { + noToken := &mockReg{ + presentToken: "", + regeneratedToken: "regenerated", + } + + tokenPresent := &mockReg{ + presentToken: "existing", + regeneratedToken: "regenerated", + } + + t.Run("case=no token, browser flow", func(t *testing.T) { + assert.Equal(t, "regenerated", GetCSRFToken(noToken, nil, nil, TypeBrowser)) + }) + + t.Run("case=token present, browser flow", func(t *testing.T) { + assert.Equal(t, "existing", GetCSRFToken(tokenPresent, nil, nil, TypeBrowser)) + }) + + t.Run("case=no token, api flow", func(t *testing.T) { + assert.Equal(t, "", GetCSRFToken(noToken, nil, nil, TypeAPI)) + }) + + t.Run("case=token present, api flow", func(t *testing.T) { + assert.Equal(t, "existing", GetCSRFToken(tokenPresent, nil, nil, TypeAPI)) + }) +} + +type mockReg struct { + presentToken, regeneratedToken string + + nosurf.Handler +} + +func (m *mockReg) GenerateCSRFToken(*http.Request) string { + return m.presentToken +} + +func (m *mockReg) CSRFHandler() nosurf.Handler { + return m +} + +func (m *mockReg) RegenerateToken(http.ResponseWriter, *http.Request) string { + return m.regeneratedToken +} diff --git a/selfservice/flow/organizations.go b/selfservice/flow/organizations.go new file mode 100644 index 0000000..a96bd6e --- /dev/null +++ b/selfservice/flow/organizations.go @@ -0,0 +1,32 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + "net/url" + + "github.com/gofrs/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// ParseOrganizationFromURLQuery is a helper function that parses the organization ID for a self-service flow from +// the URL query parameters. If the organization ID is not found in the URL query parameters, the function will return +// an NULL UUID. +func ParseOrganizationFromURLQuery(ctx context.Context, q url.Values) (orgID uuid.NullUUID) { + if rawOrg := q.Get("organization"); rawOrg != "" { + orgIDFromURL, err := uuid.FromString(rawOrg) + if err != nil { + trace.SpanFromContext(ctx).RecordError(err, trace.WithAttributes(attribute.String("organization", rawOrg))) + } else { + orgID = uuid.NullUUID{UUID: orgIDFromURL, Valid: true} + } + } + return +} + +type OrganizationImplementor interface { + SupportsOrganizations() bool +} diff --git a/selfservice/flow/organizations_test.go b/selfservice/flow/organizations_test.go new file mode 100644 index 0000000..eb52cfb --- /dev/null +++ b/selfservice/flow/organizations_test.go @@ -0,0 +1,45 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestParseOrganizationFromURLQuery(t *testing.T) { + tests := []struct { + name string + query url.Values + expected uuid.NullUUID + }{ + { + name: "valid organization ID", + query: url.Values{"organization": {"123e4567-e89b-12d3-a456-426614174000"}}, + expected: uuid.NullUUID{UUID: uuid.FromStringOrNil("123e4567-e89b-12d3-a456-426614174000"), Valid: true}, + }, + { + name: "invalid organization ID", + query: url.Values{"organization": {"invalid-uuid"}}, + expected: uuid.NullUUID{Valid: false}, + }, + { + name: "missing organization ID", + query: url.Values{}, + expected: uuid.NullUUID{Valid: false}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + result := ParseOrganizationFromURLQuery(ctx, tt.query) + assert.Equal(t, tt.expected, result) + }) + } +} 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 new file mode 100644 index 0000000..d55eea1 --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -0,0 +1,72 @@ +{ + "type": "api", + "request_url": "http:///", + "active": "link", + "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": "code", + "attributes": { + "name": "email", + "type": "email", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 4000001, + "text": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "type": "error", + "context": { + "reason": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method." + } + } + ] + }, + "state": "choose_method" +} diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json new file mode 100644 index 0000000..a9d446e --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json @@ -0,0 +1,6 @@ +{ + "code": 400, + "message": "The request was malformed or contained invalid parameters", + "reason": "You attempted recovery using not-valid, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "status": "Bad Request" +} 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 new file mode 100644 index 0000000..003aca9 --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -0,0 +1,72 @@ +{ + "type": "browser", + "request_url": "http:///", + "active": "link", + "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": "code", + "attributes": { + "name": "email", + "type": "email", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 4000001, + "text": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "type": "error", + "context": { + "reason": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method." + } + } + ] + }, + "state": "choose_method" +} 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 new file mode 100644 index 0000000..d55eea1 --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -0,0 +1,72 @@ +{ + "type": "api", + "request_url": "http:///", + "active": "link", + "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": "code", + "attributes": { + "name": "email", + "type": "email", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 4000001, + "text": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "type": "error", + "context": { + "reason": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method." + } + } + ] + }, + "state": "choose_method" +} diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json new file mode 100644 index 0000000..a9d446e --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json @@ -0,0 +1,6 @@ +{ + "code": 400, + "message": "The request was malformed or contained invalid parameters", + "reason": "You attempted recovery using not-valid, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "status": "Bad Request" +} 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 new file mode 100644 index 0000000..003aca9 --- /dev/null +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -0,0 +1,72 @@ +{ + "type": "browser", + "request_url": "http:///", + "active": "link", + "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": "code", + "attributes": { + "name": "email", + "type": "email", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 4000001, + "text": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method.", + "type": "error", + "context": { + "reason": "You attempted recovery using code, which is not enabled or does not exist. An administrator needs to enable this recovery method." + } + } + ] + }, + "state": "choose_method" +} diff --git a/selfservice/flow/recovery/error.go b/selfservice/flow/recovery/error.go new file mode 100644 index 0000000..d215137 --- /dev/null +++ b/selfservice/flow/recovery/error.go @@ -0,0 +1,187 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "net/http" + "net/url" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/gofrs/uuid" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" +) + +var ErrHookAbortFlow = errors.New("aborted recovery hook execution") + +func ErrAlreadyLoggedIn() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDAlreadyLoggedIn).WithReason("A valid session was detected and thus recovery is not possible.") +} + +type ( + errorHandlerDependencies interface { + clock.Provider + errorx.ManagementProvider + httpx.WriterProvider + logrusx.Provider + nosurfx.CSRFTokenGeneratorProvider + config.Provider + StrategyProvider + + FlowPersistenceProvider + } + + ErrorHandlerProvider interface { + RecoveryFlowErrorHandler() *ErrorHandler + } + + ErrorHandler struct { + d errorHandlerDependencies + } +) + +func NewErrorHandler(d errorHandlerDependencies) *ErrorHandler { + return &ErrorHandler{d: d} +} + +func (s *ErrorHandler) WriteFlowError( + w http.ResponseWriter, + r *http.Request, + f *Flow, + group node.UiNodeGroup, + recoveryErr error, +) { + logger := s.d.Logger(). + WithError(recoveryErr). + WithRequest(r). + WithField("recovery_flow", f.ToLoggerField()) + + logger. + Info("Encountered self-service recovery error.") + + if f == nil { + trace.SpanFromContext(r.Context()).AddEvent(events.NewRecoveryFailed(r.Context(), uuid.Nil, "", "", recoveryErr)) + s.forward(w, r, nil, recoveryErr) + return + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewRecoveryFailed(r.Context(), f.ID, string(f.Type), f.Active.String(), recoveryErr)) + + if expiredError := new(flow.ExpiredError); errors.As(recoveryErr, &expiredError) { + strategies, _, err := s.d.RecoveryStrategies(r.Context()).ActiveStrategies(f.Active.String()) + if err != nil { + strategies, _, err = s.d.GetActiveRecoveryStrategies(r.Context()) + // Can't retry the recovery if no primary strategy has been set + if err != nil { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + } + // create new flow because the old one is not valid + newFlow, err := FromOldFlow(s.d, s.d.Config().SelfServiceFlowRecoveryRequestLifespan(r.Context()), s.d.GenerateCSRFToken(r), r, strategies, *f) + if err != nil { + // failed to create a new session and redirect to it, handle that error as a new one + s.WriteFlowError(w, r, f, group, err) + return + } + + newFlow.UI.Messages.Add(text.NewErrorValidationRecoveryFlowExpired(s.d.Clock(), expiredError.ExpiredAt)) + if err := s.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), newFlow); err != nil { + s.forward(w, r, newFlow, err) + return + } + + if s.d.Config().UseContinueWithTransitions(r.Context()) { + switch { + case newFlow.Type.IsAPI(): + expiredError.FlowID = newFlow.ID + s.d.Writer().WriteError(w, r, expiredError.WithContinueWith(flow.NewContinueWithRecoveryUI(newFlow))) + case x.IsJSONRequest(r): + http.Redirect(w, r, urlx.CopyWithQuery( + urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), RouteGetFlow), + url.Values{"id": {newFlow.ID.String()}}, + ).String(), http.StatusSeeOther) + default: + http.Redirect(w, r, newFlow.AppendTo(s.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } + } else { + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "no_continue_with_transition_recovery_error_handler")) + + // We need to use the new flow, as that flow will be a browser flow. Bug fix for: + // + // https://github.com/ory/kratos/issues/2049!! + if newFlow.Type == flow.TypeAPI || x.IsJSONRequest(r) { + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + RouteGetFlow), url.Values{"id": {newFlow.ID.String()}}).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, newFlow.AppendTo(s.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } + } + return + } + + f.UI.ResetMessages() + if err := f.UI.ParseError(group, recoveryErr); err != nil { + s.forward(w, r, f, err) + return + } + + f.Active = sqlxx.NullString(group) + if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + s.forward(w, r, f, err) + return + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + return + } + + updatedFlow, innerErr := s.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), f.ID) + if innerErr != nil { + s.forward(w, r, updatedFlow, innerErr) + return + } + + s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(recoveryErr, http.StatusBadRequest), updatedFlow) +} + +func (s *ErrorHandler) forward(w http.ResponseWriter, r *http.Request, rr *Flow, err error) { + if rr == nil { + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if rr.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) + } else { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } +} diff --git a/selfservice/flow/recovery/error_test.go b/selfservice/flow/recovery/error_test.go new file mode 100644 index 0000000..0ced027 --- /dev/null +++ b/selfservice/flow/recovery/error_test.go @@ -0,0 +1,571 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/configx" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/jsonx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/urlx" +) + +func TestHandleError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceRecoveryEnabled: true, + config.ViperKeySelfServiceRecoveryUse: "code", + }), + ) + + public, _ := testhelpers.NewKratosServer(t, reg) + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + testhelpers.NewErrorTestServer(t, reg) + + h := reg.RecoveryFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var recoveryFlow *recovery.Flow + var flowError error + var methodName node.UiNodeGroup + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(w, r, recoveryFlow, methodName, flowError) + }) + + reset := func() { + recoveryFlow = nil + flowError = nil + methodName = "" + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *recovery.Flow { + req := &http.Request{URL: urlx.ParseOrPanic("/")} + s, _, err := reg.GetActiveRecoveryStrategies(context.Background()) + require.NoError(t, err) + f, err := recovery.NewFlow(reg, ttl, nosurfx.FakeCSRFToken, req, s, ft) + require.NoError(t, err) + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f)) + f, err = reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), f.ID) + require.NoError(t, err) + return f + } + + expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + anHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Contains(t, res.Request.URL.String(), public.URL+recovery.RouteGetFlow) + require.Equal(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), string(body)) + assert.NotEqual(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + + t.Run("case=fails if active strategy is disabled", func(t *testing.T) { + c, reg := pkg.NewVeryFastRegistryWithoutDB(t) + require.NoError(t, c.Set(context.Background(), "selfservice.methods.code.enabled", false)) + require.NoError(t, c.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code")) + _, _, err := reg.GetActiveRecoveryStrategies(context.Background()) + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = err + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("id", "expires_at", "issued_at", "ui.action", "ui.nodes.0.attributes.value")) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectRecoveryUI := func(t *testing.T) (*recovery.Flow, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()+"?flow=") + + rf, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return rf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.LinkGroup + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + methodName = node.LinkGroup + + lf, _ := expectRecoveryUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.LinkGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=new flow uses strategy of old flow", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "code"} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + assert.Equal(t, recoveryFlow.Active.String(), lf.Active.String()) + }) + + t.Run("case=new flow uses current strategy if strategy of old flow does not exist", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "not-valid"} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + assert.Equal(t, "code", lf.Active.String()) + }) + + t.Run("case=fails to retry flow if recovery strategy id is not valid", func(t *testing.T) { + t.Cleanup(func() { + reset() + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "code") + }) + + recoveryFlow = newFlow(t, 0, flow.TypeBrowser) + recoveryFlow.Active = "not-valid" + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "not-valid") + sse, _ := expectErrorUI(t) + snapshotx.SnapshotT(t, sse) + }) + }) +} + +func TestHandleError_WithContinueWith(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeyUseContinueWithTransitions: true, + config.ViperKeySelfServiceRecoveryEnabled: true, + config.ViperKeySelfServiceRecoveryUse: "code", + }), + ) + + public, _ := testhelpers.NewKratosServer(t, reg) + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + testhelpers.NewErrorTestServer(t, reg) + + h := reg.RecoveryFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var recoveryFlow *recovery.Flow + var flowError error + var methodName node.UiNodeGroup + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(w, r, recoveryFlow, methodName, flowError) + }) + + reset := func() { + recoveryFlow = nil + flowError = nil + methodName = "" + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *recovery.Flow { + req := &http.Request{URL: urlx.ParseOrPanic("/")} + s, _, err := reg.GetActiveRecoveryStrategies(context.Background()) + require.NoError(t, err) + f, err := recovery.NewFlow(reg, ttl, nosurfx.FakeCSRFToken, req, s, ft) + require.NoError(t, err) + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f)) + f, err = reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), f.ID) + require.NoError(t, err) + return f + } + + expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + anHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + switch tc.t { + case flow.TypeAPI: + require.Equal(t, http.StatusGone, res.StatusCode, "%s", body) + require.Len(t, gjson.GetBytes(body, "error.details.continue_with").Array(), 1, "%s", body) + require.Equal(t, "show_recovery_ui", gjson.GetBytes(body, "error.details.continue_with.0.action").String(), "%s", body) + id := gjson.GetBytes(body, "error.details.continue_with.0.flow.id").String() + res, err = ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, public.URL+recovery.RouteGetFlow+"?id="+id)) + require.NoError(t, err) + body = ioutilx.MustReadAll(res.Body) + case flow.TypeBrowser: + require.Contains(t, res.Request.URL.String(), public.URL+recovery.RouteGetFlow, "%s", body) + require.Equal(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + } + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), string(body)) + assert.NotEqual(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + + t.Run("case=fails if active strategy is disabled", func(t *testing.T) { + c, reg := pkg.NewVeryFastRegistryWithoutDB(t) + require.NoError(t, c.Set(context.Background(), "selfservice.methods.code.enabled", false)) + require.NoError(t, c.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code")) + _, _, err := reg.GetActiveRecoveryStrategies(context.Background()) + recoveryFlow = newFlow(t, time.Minute, tc.t) + flowError = err + methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("id", "expires_at", "issued_at", "ui.action", "ui.nodes.0.attributes.value")) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectRecoveryUI := func(t *testing.T) (*recovery.Flow, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()+"?flow=") + + rf, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return rf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.LinkGroup + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + methodName = node.LinkGroup + + lf, _ := expectRecoveryUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.LinkGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=new flow uses strategy of old flow", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "code"} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + assert.Equal(t, recoveryFlow.Active.String(), lf.Active.String()) + }) + + t.Run("case=new flow uses current strategy if strategy of old flow does not exist", func(t *testing.T) { + t.Cleanup(reset) + + recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "not-valid"} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + lf, _ := expectRecoveryUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID)) + assert.Equal(t, "code", lf.Active.String()) + }) + + t.Run("case=fails to retry flow if recovery strategy id is not valid", func(t *testing.T) { + t.Cleanup(func() { + reset() + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "code") + }) + + recoveryFlow = newFlow(t, 0, flow.TypeBrowser) + recoveryFlow.Active = "not-valid" + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "not-valid") + sse, _ := expectErrorUI(t) + snapshotx.SnapshotT(t, sse) + }) + }) +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go new file mode 100644 index 0000000..649421d --- /dev/null +++ b/selfservice/flow/recovery/flow.go @@ -0,0 +1,334 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "encoding/json" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/redir" + + "github.com/ory/pop/v6" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/clock" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +// A Recovery Flow +// +// This request is used when an identity wants to recover their account. +// +// We recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery) +// +// swagger:model recoveryFlow +type Flow struct { + // ID represents the request's unique ID. When performing the recovery flow, this + // represents the id in the recovery ui's query parameter: http://?request= + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type flow.Type `json:"type" db:"type" faker:"flow_type"` + + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting, + // a new request has to be initiated. + // + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the request occurred. + // + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // InternalContext stores internal-only data for this flow. + InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"` + + // ReturnTo contains the requested return_to URL. + ReturnTo string `json:"return_to,omitempty" db:"-"` + + // Active, if set, contains the recovery method that is being used. It is initially + // not set. + Active sqlxx.NullString `json:"active,omitempty" faker:"-" db:"active_method"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // 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. + // + // required: true + State State `json:"state" faker:"-" db:"state"` + + // CSRFToken contains the anti-csrf token associated with this request. + CSRFToken string `json:"-" db:"csrf_token"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // RecoveredIdentityID is a helper struct field for gobuffalo.pop. + RecoveredIdentityID uuid.NullUUID `json:"-" faker:"-" db:"recovered_identity_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + + // DangerousSkipCSRFCheck indicates whether anti CSRF measures should be enforced in this flow + // + // This is needed, because we can not enforce these measures, if the flow has been initialized by someone else than + // the user. + DangerousSkipCSRFCheck bool `json:"-" faker:"-" db:"skip_csrf_check"` + + // 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 = (*Flow)(nil) + +// flowDependencies are the dependencies NewFlow needs to construct a recovery +// flow: the configuration (for return-to validation) and the clock. The +// lifespan and CSRF token are passed explicitly because callers vary them +// (token-based lifespans, regenerated CSRF tokens). +type flowDependencies interface { + config.Provider + clock.Provider +} + +func NewFlow(reg flowDependencies, exp time.Duration, csrf string, r *http.Request, strategies Strategies, ft flow.Type) (*Flow, error) { + conf := reg.Config() + now := reg.Clock().Now().UTC() + id := x.NewUUID() + + // Pre-validate the return to URL which is contained in the HTTP request. + requestURL := x.RequestURL(r).String() + _, err := redir.SecureRedirectTo(r, + conf.SelfServiceBrowserDefaultReturnTo(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + return nil, err + } + + state := flow.StateChooseMethod + if conf.ChooseRecoveryAddress(r.Context()) { + state = flow.StateRecoveryAwaitingAddress + } else { + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "legacy_recovery_flow")) + } + + f := &Flow{ + ID: id, + ExpiresAt: now.Add(exp), + IssuedAt: now, + RequestURL: requestURL, + UI: &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), + }, + State: state, + CSRFToken: csrf, + Type: ft, + } + if err := f.setCourierBaseURL(x.BaseURLStringFromContext(r.Context())); err != nil { + return nil, err + } + + for _, strategy := range strategies { + if strategy.IsPrimary() { + f.Active = sqlxx.NullString(strategy.NodeGroup()) + } + if err := strategy.PopulateRecoveryMethod(r, f); err != nil { + return nil, err + } + } + + return f, nil +} + +func FromOldFlow(reg flowDependencies, exp time.Duration, csrf string, r *http.Request, strategies Strategies, of Flow) (*Flow, error) { + f := of.Type + // Using the same flow in the recovery/verification context can lead to using API flow in a verification/recovery email + if of.Type == flow.TypeAPI && of.Active.String() == string(RecoveryStrategyLink) { + f = flow.TypeBrowser + } + nf, err := NewFlow(reg, exp, csrf, r, strategies, f) + if err != nil { + return nil, err + } + + nf.RequestURL = of.RequestURL + return nf, nil +} + +func (f *Flow) GetType() flow.Type { return f.Type } +func (f *Flow) GetRequestURL() string { return f.RequestURL } +func (Flow) TableName() string { return "selfservice_recovery_flows" } +func (f Flow) GetID() uuid.UUID { return f.ID } +func (f *Flow) GetUI() *container.Container { return f.UI } +func (f *Flow) GetState() State { return f.State } +func (Flow) GetFlowName() flow.FlowName { return flow.RecoveryFlow } +func (f *Flow) SetState(state State) { f.State = state } +func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext } +func (f *Flow) SetInternalContext(c sqlxx.JSONRawMessage) { f.InternalContext = c } + +// EnsureInternalContext initializes InternalContext to an empty JSON object +// if it is missing or not valid JSON. Mirrors the registration / settings / +// login flow implementations. +func (f *Flow) EnsureInternalContext() { + if !gjson.ValidBytes(f.InternalContext) { + f.InternalContext = []byte("{}") + } +} + +// GetCourierBaseURL returns the base URL captured at flow init from the +// request context, or the empty string when nothing was captured (the email +// senders then fall back to Config.SelfPublicURL). +func (f *Flow) GetCourierBaseURL() string { + return gjson.GetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL).String() +} + +// setCourierBaseURL writes the captured base URL into InternalContext under +// the well-known key. Empty input is a no-op (preserves the fall-back +// path). Inputs longer than 8192 bytes are rejected — the same implicit +// ceiling the dedicated VARCHAR(8192) column used to enforce — so a +// pathological header cannot bloat the row. +func (f *Flow) setCourierBaseURL(s string) error { + if s == "" { + return nil + } + if len(s) > 8192 { + return nil + } + f.EnsureInternalContext() + out, err := sjson.SetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL, s) + if err != nil { + return errors.WithStack(err) + } + f.InternalContext = out + return nil +} + +// ShouldSkipSettingsFlow flow returns true iff. `skip_settings` was requested +// in the URL query parameters. +func (f *Flow) ShouldSkipSettingsFlow() bool { + u, err := url.Parse(f.RequestURL) + if err != nil { + return false + } + shouldSkip, _ := strconv.ParseBool(u.Query().Get("skip_settings")) + + return shouldSkip +} + +// SetShouldSkipSettingsFlow encodes shouldSkip in the flow's request URL. The +// setting can be retreived with `f.ShouldSkipSettingsFlow` +func (f *Flow) SetShouldSkipSettingsFlow(shouldSkip bool) error { + reqUrl, err := url.Parse(f.RequestURL) + if err != nil { + return errors.WithStack(err) + } + q := reqUrl.Query() + q.Set("skip_settings", strconv.FormatBool(shouldSkip)) + reqUrl.RawQuery = q.Encode() + f.RequestURL = reqUrl.String() + + return nil +} + +func (f *Flow) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} + +func (f *Flow) AppendTo(src *url.URL) *url.URL { + return urlx.CopyWithQuery(src, url.Values{"flow": {f.ID.String()}}) +} + +func (f *Flow) SetCSRFToken(token string) { + f.CSRFToken = token + f.UI.SetCSRF(token) +} + +func (f Flow) MarshalJSON() ([]byte, error) { + type local Flow + f.SetReturnTo() + return json.Marshal(local(f)) +} + +func (f *Flow) SetReturnTo() { + // Return to is already set, do not overwrite it. + if len(f.ReturnTo) > 0 { + return + } + if u, err := url.Parse(f.RequestURL); err == nil { + f.ReturnTo = u.Query().Get("return_to") + } +} + +func (f *Flow) AfterFind(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AfterSave(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) ToLoggerField() map[string]any { + if f == nil { + return map[string]any{} + } + return map[string]any{ + "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/flow_test.go b/selfservice/flow/recovery/flow_test.go new file mode 100644 index 0000000..9acfd87 --- /dev/null +++ b/selfservice/flow/recovery/flow_test.go @@ -0,0 +1,212 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery_test + +import ( + "context" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/pkg" + "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/link" + "github.com/ory/kratos/x" + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + "github.com/ory/x/urlx" +) + +func TestFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + must := func(r *recovery.Flow, err error) *recovery.Flow { + require.NoError(t, err) + return r + } + + u := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"} + for k, tc := range []struct { + r *recovery.Flow + expectErr bool + }{ + {r: must(recovery.NewFlow(reg, time.Hour, "", u, nil, flow.TypeBrowser))}, + {r: must(recovery.NewFlow(reg, -time.Hour, "", u, nil, flow.TypeBrowser)), expectErr: true}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + err := tc.r.Valid(clock.New()) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } + + assert.EqualValues(t, flow.StateChooseMethod, + must(recovery.NewFlow(reg, time.Hour, "", u, nil, flow.TypeBrowser)).State) + + t.Run("type=return_to", func(t *testing.T) { + _, err := recovery.NewFlow(reg, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, nil, flow.TypeBrowser) + require.Error(t, err) + + _, err = recovery.NewFlow(reg, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"}, nil, flow.TypeBrowser) + require.NoError(t, err) + }) +} + +func TestGetType(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=%s", ft), func(t *testing.T) { + r := &recovery.Flow{Type: ft} + assert.Equal(t, ft, r.GetType()) + }) + } +} + +func TestGetRequestURL(t *testing.T) { + expectedURL := "http://foo/bar/baz" + f := &recovery.Flow{RequestURL: expectedURL} + assert.Equal(t, expectedURL, f.GetRequestURL()) +} + +func TestNewFlow_capturesCourierBaseURL(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("nothing in context", func(t *testing.T) { + r := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"} + f, err := recovery.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "", f.GetCourierBaseURL()) + }) + + t.Run("BaseURL in request context is copied onto flow", func(t *testing.T) { + base := urlx.ParseOrPanic("https://customer.example.com/") + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), base)) + f, err := recovery.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, base.String(), f.GetCourierBaseURL()) + }) + + t.Run("http scheme is preserved (not coerced to https)", func(t *testing.T) { + base := urlx.ParseOrPanic("http://localhost:4000") + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), base)) + f, err := recovery.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", f.GetCourierBaseURL()) + }) +} + +func TestFlowEncodeJSON(t *testing.T) { + assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, &recovery.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, &recovery.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, recovery.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) +} + +func TestFromOldFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + r := http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"} + t.Run("strategy=code", func(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=original flow is %s", ft), func(t *testing.T) { + f, err := recovery.NewFlow(reg, 0, "csrf", &r, recovery.Strategies{code.NewStrategy(reg)}, ft) + require.NoError(t, err) + nF, err := recovery.FromOldFlow(reg, time.Duration(time.Hour), f.CSRFToken, &r, nil, *f) + require.NoError(t, err) + require.Equal(t, ft, nF.Type) + }) + } + }) + + t.Run("strategy=link", func(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=original flow is %s", ft), func(t *testing.T) { + f, err := recovery.NewFlow(reg, 0, "csrf", &r, recovery.Strategies{link.NewStrategy(reg)}, ft) + require.NoError(t, err) + nF, err := recovery.FromOldFlow(reg, time.Duration(time.Hour), f.CSRFToken, &r, nil, *f) + require.NoError(t, err) + require.Equal(t, flow.TypeBrowser, nF.Type) + }) + } + }) +} + +func TestFromOldFlow_capturesCourierBaseURL(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("uses the new request's context, not the old flow's value", func(t *testing.T) { + // Old flow was created at customer.example.com. + oldR := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("https://customer.example.com/"))) + oldF, err := recovery.NewFlow(reg, time.Hour, "csrf", oldR, nil, flow.TypeBrowser) + require.NoError(t, err) + require.Equal(t, "https://customer.example.com/", oldF.GetCourierBaseURL()) + + // New request comes through a different proxy / CNAME — we should + // reflect that, not silently inherit the old flow's captured URL. + newR := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("http://localhost:4000"))) + newF, err := recovery.FromOldFlow(reg, time.Hour, oldF.CSRFToken, newR, nil, *oldF) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", newF.GetCourierBaseURL()) + }) + + t.Run("empty when neither old nor new have a base URL", func(t *testing.T) { + r := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"} + oldF, err := recovery.NewFlow(reg, time.Hour, "csrf", r, nil, flow.TypeBrowser) + require.NoError(t, err) + newF, err := recovery.FromOldFlow(reg, time.Hour, oldF.CSRFToken, r, nil, *oldF) + require.NoError(t, err) + assert.Equal(t, "", newF.GetCourierBaseURL()) + }) +} + +// TestCourierBaseURLStoredInInternalContext locks down the storage +// location: the captured base URL must land in InternalContext under +// flow.InternalContextKeyCourierBaseURL, not in a dedicated column. If a +// future refactor moves it elsewhere, this fails — separately from the +// getter contract. +func TestCourierBaseURLStoredInInternalContext(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("https://customer.example.com/"))) + f, err := recovery.NewFlow(reg, time.Hour, "csrf", r, nil, flow.TypeBrowser) + require.NoError(t, err) + + got := gjson.GetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL).String() + assert.Equal(t, "https://customer.example.com/", got) +} + +func TestFlowDontOverrideReturnTo(t *testing.T) { + f := &recovery.Flow{ReturnTo: "/foo"} + f.SetReturnTo() + assert.Equal(t, "/foo", f.ReturnTo) + + f = &recovery.Flow{RequestURL: "https://foo.bar?return_to=/bar"} + f.SetReturnTo() + assert.Equal(t, "/bar", f.ReturnTo) +} diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go new file mode 100644 index 0000000..0f514aa --- /dev/null +++ b/selfservice/flow/recovery/handler.go @@ -0,0 +1,480 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" +) + +const ( + RouteInitBrowserFlow = "/self-service/recovery/browser" + RouteInitAPIFlow = "/self-service/recovery/api" + RouteGetFlow = "/self-service/recovery/flows" + + RouteSubmitFlow = "/self-service/recovery" +) + +type ( + HandlerProvider interface { + RecoveryHandler() *Handler + } + handlerDependencies interface { + clock.Provider + errorx.ManagementProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + session.HandlerProvider + StrategyProvider + FlowPersistenceProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + nosurfx.CSRFProvider + config.Provider + ErrorHandlerProvider + HookExecutorProvider + } + Handler struct { + d handlerDependencies + } +) + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) + h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) + + redirect := session.RedirectOnAuthenticated(h.d) + public.GET(RouteInitBrowserFlow, h.d.SessionHandler().IsNotAuthenticated(h.createBrowserRecoveryFlow, func(w http.ResponseWriter, r *http.Request) { + if x.IsJSONRequest(r) { + h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn())) + } else { + redirect(w, r) + } + })) + + public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsNotAuthenticated(h.createNativeRecoveryFlow, + session.RespondWithJSONErrorOnAuthenticated(h.d.Writer(), ErrAlreadyLoggedIn()))) + + public.GET(RouteGetFlow, h.getRecoveryFlow) + + public.GET(RouteSubmitFlow, h.updateRecoveryFlow) + public.POST(RouteSubmitFlow, h.updateRecoveryFlow) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) +} + +// swagger:route GET /self-service/recovery/api frontend createNativeRecoveryFlow +// +// # Create Recovery Flow for Native Apps +// +// This endpoint initiates a recovery flow for API clients such as mobile devices, smart TVs, and so on. +// +// If a valid provided session cookie or session token is provided, a 400 Bad Request error. +// +// On an existing recovery flow, use the `getRecoveryFlow` API endpoint. +// +// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server +// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make +// you vulnerable to a variety of CSRF attacks. +// +// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). +// +// More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). +// +// Schemes: http, https +// +// Responses: +// 200: recoveryFlow +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createNativeRecoveryFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowRecoveryEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Recovery is not allowed because it was disabled."))) + return + } + activeRecoveryStrategies, _, err := h.d.GetActiveRecoveryStrategies(r.Context()) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + f, err := NewFlow(h.d, h.d.Config().SelfServiceFlowRecoveryRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, activeRecoveryStrategies, flow.TypeAPI) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if err := h.d.RecoveryExecutor().PreRecoveryHook(w, r, f); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if err := h.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), f); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, f) +} + +// Create Browser Recovery Flow Parameters +// +// swagger:parameters createBrowserRecoveryFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserRecoveryFlow struct { + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // Skip redirection to the settings UI after the recovery flow was + // completed. Instead, the user will be redirected to the URL specified in + // `return_to` query parameter or the default return URL if `return_to` is + // not set. + // + // in: query + SkipSettings string `json:"skip_settings"` +} + +// swagger:route GET /self-service/recovery/browser frontend createBrowserRecoveryFlow +// +// # Create Recovery Flow for Browsers +// +// This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to +// `selfservice.flows.recovery.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session +// exists, the browser is returned to the configured return URL. +// +// If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects +// or a 400 bad request error if the user is already authenticated. +// +// This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. +// +// More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). +// +// Schemes: http, https +// +// Responses: +// 200: recoveryFlow +// 303: emptyResponse +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserRecoveryFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowRecoveryEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Recovery is not allowed because it was disabled."))) + return + } + activeRecoveryStrategies, _, err := h.d.GetActiveRecoveryStrategies(r.Context()) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + f, err := NewFlow(h.d, h.d.Config().SelfServiceFlowRecoveryRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, activeRecoveryStrategies, flow.TypeBrowser) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if err := h.d.RecoveryExecutor().PreRecoveryHook(w, r, f); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if err := h.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), f); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + redirTo := f.AppendTo(h.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String() + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), f, redirTo) +} + +// Get Recovery Flow Parameters +// +// swagger:parameters getRecoveryFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getRecoveryFlow struct { + // The Flow ID + // + // The value for this parameter comes from `request` URL Query parameter sent to your + // application (e.g. `/recovery?flow=abcde`). + // + // required: true + // in: query + FlowID string `json:"id"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route GET /self-service/recovery/flows frontend getRecoveryFlow +// +// # Get Recovery Flow +// +// This endpoint returns a recovery flow's context with, for example, error details and other information. +// +// Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. +// For AJAX requests you must ensure that cookies are included in the request or requests will fail. +// +// If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain +// and you need to forward the incoming HTTP Cookie header to this endpoint: +// +// ```js +// // pseudo-code example +// router.get('/recovery', async function (req, res) { +// const flow = await client.getRecoveryFlow(req.header('Cookie'), req.query['flow']) +// +// res.render('recovery', flow) +// }) +// ``` +// +// More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: recoveryFlow +// 404: errorGeneric +// 410: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) getRecoveryFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowRecoveryEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Recovery is not allowed because it was disabled."))) + return + } + + rid := x.ParseUUID(r.URL.Query().Get("id")) + f, err := h.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), rid) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + // Browser flows must include the CSRF token + // + // Resolves: https://github.com/ory/kratos/issues/1282 + if f.Type.IsBrowser() && !f.DangerousSkipCSRFCheck && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), f.CSRFToken) { + h.d.Writer().WriteError(w, r, nosurfx.CSRFErrorReason(r, h.d)) + return + } + + if f.ExpiresAt.Before(h.d.Clock().Now().UTC()) { + if f.Type == flow.TypeBrowser { + redirectURL := flow.GetFlowExpiredRedirectURL(r.Context(), h.d.Config(), RouteInitBrowserFlow, f.ReturnTo) + + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). + WithReason("The recovery flow has expired. Redirect the user to the recovery flow init endpoint to initialize a new recovery flow."). + WithDetail("redirect_to", redirectURL.String()). + WithDetail("return_to", f.ReturnTo))) + return + } + + h.d.Writer().WriteError(w, r, flow.NewFlowExpiredError(h.d.Clock(), f.ExpiresAt). + WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitAPIFlow).String())) + return + } + + h.d.Writer().Write(w, r, f) +} + +// Update Recovery Flow Parameters +// +// swagger:parameters updateRecoveryFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRecoveryFlow struct { + // The Recovery Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/recovery?flow=abcde`). + // + // required: true + // in: query + Flow string `json:"flow"` + + // Recovery Token + // + // The recovery token which completes the recovery request. If the token + // is invalid (e.g. expired) an error will be shown to the end-user. + // + // This parameter is usually set in a link and not used by any direct API call. + // + // in: query + Token string `json:"token" form:"token"` + + // in: body + // required: true + Body updateRecoveryFlowBody + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// Update Recovery Flow Request Body +// +// swagger:model updateRecoveryFlowBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRecoveryFlowBody struct{} + +// swagger:route POST /self-service/recovery frontend updateRecoveryFlow +// +// # Update Recovery Flow +// +// Use this endpoint to update a recovery flow. This endpoint +// behaves differently for API and browser flows and has several states: +// +// - `choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent +// and works with API- and Browser-initiated flows. +// - For API clients and Browser clients with HTTP Header `Accept: application/json` it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid. +// and a HTTP 303 See Other redirect with a fresh recovery flow if the flow was otherwise invalid (e.g. expired). +// - For Browser clients without HTTP Header `Accept` or with `Accept: text/*` it returns a HTTP 303 See Other redirect to the Recovery UI URL with the Recovery Flow ID appended. +// - `sent_email` is the success state after `choose_method` for the `link` method and allows the user to request another recovery email. It +// works for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state. +// - `passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow ("sending a recovery link") +// does not have any API capabilities. The server responds with a HTTP 303 See Other redirect either to the Settings UI URL +// (if the link was valid) and instructs the user to update their password, or a redirect to the Recover UI URL with +// a new Recovery Flow ID which contains an error message that the recovery link was invalid. +// +// More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: recoveryFlow +// 303: emptyResponse +// 400: recoveryFlow +// 410: errorGeneric +// 422: errorBrowserLocationChangeRequired +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request) { + rid, err := flow.GetFlowID(r) + if err != nil { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) + return + } + + f, err := h.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), rid) + if errors.Is(err, sqlcon.ErrNoRows()) { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, errors.WithStack(herodot.ErrNotFound().WithReasonf("The recovery request could not be found. Please restart the flow."))) + return + } else if err != nil { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) + return + } + + if err := f.Valid(h.d.Clock()); err != nil { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + var g node.UiNodeGroup + var found bool + for _, ss := range h.d.AllRecoveryStrategies() { + err := ss.Recover(w, r, f) + if errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) + return + } + + found = true + g = ss.NodeGroup() + break + } + + if !found { + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, errors.WithStack(schema.NewNoRecoveryStrategyResponsible())) + return + } + + // WARNING - just because no error was returned does not mean that the challenge was accepted. Instead, the + // success state is available as: + // + // if f.State == flow.StatePassedChallenge + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(h.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + return + } + + updatedFlow, err := h.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), f.ID) + if err != nil { + 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/handler_test.go b/selfservice/flow/recovery/handler_test.go new file mode 100644 index 0000000..05c3e96 --- /dev/null +++ b/selfservice/flow/recovery/handler_test.go @@ -0,0 +1,301 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "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/gjson" + + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" +) + +func init() { + corpx.RegisterFakes() +} + +func TestHandlerRedirectOnAuthenticated(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceRecoveryEnabled, true), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + + router := httprouterx.NewRouterPublic() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + redirTS := testhelpers.NewRedirTS(t, "already authenticated", conf) + + t.Run("does redirect to default on authenticated request", func(t *testing.T) { + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitBrowserFlow, nil)) + assert.Contains(t, res.Request.URL.String(), redirTS.URL, "%+v", res) + assert.EqualValues(t, "already authenticated", string(body)) + }) + + t.Run("does redirect to default on authenticated request", func(t *testing.T) { + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitAPIFlow, nil)) + assert.Contains(t, res.Request.URL.String(), recovery.RouteInitAPIFlow) + assert.EqualValues(t, text.ErrIDAlreadyLoggedIn, gjson.GetBytes(body, "error.id").Str) + assertx.EqualAsJSON(t, recovery.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw)) + }) +} + +func TestInitFlow(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(recovery.RecoveryStrategyLink, true)), + configx.WithValues(testhelpers.MethodEnableConfig(recovery.RecoveryStrategyCode, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + configx.WithValue(config.ViperKeySelfServiceRecoveryEnabled, true), + ) + + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + recoveryTS := testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + + assertion := func(body []byte, isForced, isApi bool) { + if isApi { + assert.Equal(t, "api", gjson.GetBytes(body, "type").String()) + } else { + assert.Equal(t, "browser", gjson.GetBytes(body, "type").String()) + } + } + + initAuthenticatedFlow := func(t *testing.T, isAPI, isSPA bool) (*http.Response, []byte) { + route := recovery.RouteInitBrowserFlow + if isAPI { + route = recovery.RouteInitAPIFlow + } + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + if isSPA { + req.Header.Set("Accept", "application/json") + } + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, req) + if isAPI { + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + } + return res, body + } + + initFlow := func(t *testing.T, isAPI bool) (*http.Response, []byte) { + route := recovery.RouteInitBrowserFlow + if isAPI { + route = recovery.RouteInitAPIFlow + } + c := publicTS.Client() + res, err := c.Get(publicTS.URL + route) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + initSPAFlow := func(t *testing.T, hc *http.Client, isSPA bool) (*http.Response, []byte) { + route := recovery.RouteInitBrowserFlow + c := publicTS.Client() + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + if isSPA { + req.Header.Set("Accept", "application/json") + } + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + t.Run("flow=api", func(t *testing.T) { + t.Run("case=creates a new flow on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, true) + assert.Contains(t, res.Request.URL.String(), recovery.RouteInitAPIFlow) + assertion(body, false, true) + }) + + t.Run("case=fails on authenticated request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, true, false) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, recovery.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + }) + + t.Run("flow=spa", func(t *testing.T) { + t.Run("case=creates a new flow on unauthenticated request", func(t *testing.T) { + res, body := initSPAFlow(t, testhelpers.NewTestClient(t), true) + assert.Contains(t, res.Request.URL.String(), recovery.RouteInitBrowserFlow) + assertion(body, false, false) + }) + + t.Run("case=fails on authenticated request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, false, true) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, recovery.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + }) + + t.Run("flow=browser", func(t *testing.T) { + t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, false) + assertion(body, false, false) + assert.Contains(t, res.Request.URL.String(), recoveryTS.URL) + }) + + t.Run("case=fails on authenticated request", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, false, false) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceBrowserDefaultReturnTo(t.Context()).Host) + }) + + t.Run("case=relative redirect when self-service recovery ui is a relative URL", func(t *testing.T) { + reg.Config().MustSet(ctx, config.ViperKeySelfServiceRecoveryUI, "/recovery-ts") + assert.Regexp( + t, + "^/recovery-ts.*$", + testhelpers.GetSelfServiceRedirectLocation(t, publicTS.URL+recovery.RouteInitBrowserFlow), + ) + }) + + t.Run("case=redirects with 303", func(t *testing.T) { + c := &http.Client{} + // don't get the reference, instead copy the values, so we don't alter the client directly. + *c = *publicTS.Client() + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", publicTS.URL+recovery.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + }) + }) +} + +func TestGetFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyLink), + map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyCode), + map[string]interface{}{"enabled": true}) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + returnToTS := testhelpers.NewRedirTS(t, "", conf) + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "", conf) + + setupRecoveryTS := func(t *testing.T, c *http.Client) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+recovery.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUI, ts.URL) + return ts + } + + t.Run("case=csrf cookie missing", func(t *testing.T) { + client := testhelpers.NewTestClient(t) + setupRecoveryTS(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + + assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=valid", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupRecoveryTS(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), gjson.GetBytes(body, "id").String(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), public.URL, "%s", body) + }) + + t.Run("case=expired", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupRecoveryTS(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + + // Expire the flow + f, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.RecoveryFlowPersister().UpdateRecoveryFlow(context.Background(), f)) + + res, body := testhelpers.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+f.ID.String()) + assert.EqualValues(t, http.StatusGone, res.StatusCode) + assert.Equal(t, public.URL+recovery.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) + }) + + t.Run("case=expired with return_to", func(t *testing.T) { + returnTo := returnToTS.URL + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + client := testhelpers.NewClientWithCookies(t) + setupRecoveryTS(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow+"?return_to="+returnTo) + + // Expire the flow + f, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.RecoveryFlowPersister().UpdateRecoveryFlow(context.Background(), f)) + + // Retrieve the flow and verify that return_to is in the response + getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, recovery.RouteGetFlow, f.ID, returnTo) + getBody := testhelpers.EasyGetBody(t, client, getURL) + assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) + + // submit the flow but it is expired + u := public.URL + recovery.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := client.PostForm(u, url.Values{"email": {"email@ory.sh"}, "csrf_token": {f.CSRFToken}, "method": {"link"}}) + require.NoError(t, err) + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + f, err = reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String())) + require.NoError(t, err) + assert.Equal(t, public.URL+recovery.RouteInitBrowserFlow+"?return_to="+returnTo, f.RequestURL) + }) + + t.Run("case=not found", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupRecoveryTS(t, client) + + res, _ := testhelpers.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+x.NewUUID().String()) + assert.EqualValues(t, http.StatusNotFound, res.StatusCode) + }) +} diff --git a/selfservice/flow/recovery/hook.go b/selfservice/flow/recovery/hook.go new file mode 100644 index 0000000..5562f4e --- /dev/null +++ b/selfservice/flow/recovery/hook.go @@ -0,0 +1,134 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "context" + "fmt" + "net/http" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" +) + +type ( + PreHookExecutor interface { + ExecuteRecoveryPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error + + PostHookExecutor interface { + ExecutePostRecoveryHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error + } + PostHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error + + HooksProvider interface { + PreRecoveryHooks(ctx context.Context) ([]PreHookExecutor, error) + PostRecoveryHooks(ctx context.Context) ([]PostHookExecutor, error) + } +) + +func PostHookRecoveryExecutorNames(e []PostHookExecutor) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func (f PreHookExecutorFunc) ExecuteRecoveryPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + return f(w, r, a) +} + +func (f PostHookExecutorFunc) ExecutePostRecoveryHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error { + return f(w, r, a, s) +} + +type ( + executorDependencies interface { + config.Provider + identity.ManagementProvider + identity.ValidationProvider + session.PersistenceProvider + HooksProvider + nosurfx.CSRFTokenGeneratorProvider + logrusx.Provider + httpx.WriterProvider + } + + HookExecutor struct { + d executorDependencies + } + + HookExecutorProvider interface { + RecoveryExecutor() *HookExecutor + } +) + +func NewHookExecutor(d executorDependencies) *HookExecutor { + return &HookExecutor{ + d: d, + } +} + +func (e *HookExecutor) PostRecoveryHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error { + logger := e.d.Logger(). + WithRequest(r) + + if s.Identity != nil { + logger = logger.WithField("identity_id", s.Identity.ID) + } + + logger.Debug("Running ExecutePostRecoveryHooks.") + hooks, err := e.d.PostRecoveryHooks(r.Context()) + if err != nil { + return err + } + for k, executor := range hooks { + if err := executor.ExecutePostRecoveryHook(w, r, a, s); err != nil { + var traits identity.Traits + if s.Identity != nil { + traits = s.Identity.Traits + } + return flow.HandleHookError(w, r, a, traits, node.LinkGroup, err, e.d, e.d) + } + + logger. + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", PostHookRecoveryExecutorNames(hooks)). + Debug("ExecutePostRecoveryHook completed successfully.") + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewRecoverySucceeded(r.Context(), a.ID, s.Identity.ID, string(a.Type), a.Active.String())) + + logger.Debug("Post recovery execution hooks completed successfully.") + + return nil +} + +func (e *HookExecutor) PreRecoveryHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + hooks, err := e.d.PreRecoveryHooks(r.Context()) + if err != nil { + return err + } + for _, executor := range hooks { + if err := executor.ExecuteRecoveryPreHook(w, r, a); err != nil { + return err + } + } + + return nil +} diff --git a/selfservice/flow/recovery/hook_test.go b/selfservice/flow/recovery/hook_test.go new file mode 100644 index 0000000..6af77a3 --- /dev/null +++ b/selfservice/flow/recovery/hook_test.go @@ -0,0 +1,119 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery_test + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gobuffalo/httptest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/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/x" + "github.com/ory/kratos/x/nosurfx" +) + +func TestRecoveryExecutor(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + s := recovery.Strategies{code.NewStrategy(reg)} + + newServer := func(t *testing.T, i *identity.Identity, ft flow.Type) *httptest.Server { + router := http.NewServeMux() + router.HandleFunc("GET /recovery/pre", func(w http.ResponseWriter, r *http.Request) { + a, err := recovery.NewFlow(reg, time.Minute, nosurfx.FakeCSRFToken, r, s, ft) + require.NoError(t, err) + if testhelpers.SelfServiceHookErrorHandler(t, w, r, recovery.ErrHookAbortFlow, reg.RecoveryExecutor().PreRecoveryHook(w, r, a)) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.HandleFunc("GET /recovery/post", func(w http.ResponseWriter, r *http.Request) { + a, err := recovery.NewFlow(reg, time.Minute, nosurfx.FakeCSRFToken, r, s, ft) + require.NoError(t, err) + s, err := testhelpers.NewActiveSession(r, + reg, + i, + 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")) + } + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + return ts + } + + t.Run("method=PostRecoveryHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, _ := testhelpers.SelfServiceMakeHookRequest(t, ts, "/recovery/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), + []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, _ := testhelpers.SelfServiceMakeHookRequest(t, ts, "/recovery/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), + []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostRecoveryHook": "abort"}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, body := testhelpers.SelfServiceMakeHookRequest(t, ts, "/recovery/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "", body) + }) + }) + + for _, kind := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run("type="+string(kind)+"/method=PreRecoveryHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceRecoveryBeforeHooks, + testhelpers.SelfServiceMakeRecoveryPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, nil, kind) + }, + conf, + )) + } +} diff --git a/selfservice/flow/recovery/persistence.go b/selfservice/flow/recovery/persistence.go new file mode 100644 index 0000000..f634bc6 --- /dev/null +++ b/selfservice/flow/recovery/persistence.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type ( + FlowPersister interface { + CreateRecoveryFlow(context.Context, *Flow) error + GetRecoveryFlow(ctx context.Context, id uuid.UUID) (*Flow, error) + UpdateRecoveryFlow(context.Context, *Flow) error + DeleteExpiredRecoveryFlows(context.Context, time.Time, int) error + } + FlowPersistenceProvider interface { + RecoveryFlowPersister() FlowPersister + } +) diff --git a/selfservice/flow/recovery/state.go b/selfservice/flow/recovery/state.go new file mode 100644 index 0000000..96ab9d2 --- /dev/null +++ b/selfservice/flow/recovery/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import "github.com/ory/kratos/selfservice/flow" + +// Recovery Flow State +// +// 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. +// +// swagger:model recoveryFlowState +type State = flow.State diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go new file mode 100644 index 0000000..6086910 --- /dev/null +++ b/selfservice/flow/recovery/strategy.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package recovery + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/ui/node" +) + +//swagger:enum RecoveryMethod +type RecoveryMethod string + +const ( + RecoveryStrategyLink RecoveryMethod = "link" + RecoveryStrategyCode RecoveryMethod = "code" +) + +type ( + Strategy interface { + RecoveryStrategyID() string + IsPrimary() bool + NodeGroup() node.UiNodeGroup + PopulateRecoveryMethod(*http.Request, *Flow) error + Recover(w http.ResponseWriter, r *http.Request, f *Flow) (err error) + } + Strategies []Strategy + StrategyProvider interface { + AllRecoveryStrategies() Strategies + RecoveryStrategies(ctx context.Context) Strategies + GetActiveRecoveryStrategies(ctx context.Context) (active Strategies, primary Strategy, err error) + } +) + +func (s Strategies) ActiveStrategies(id string) (active Strategies, primary Strategy, err error) { + ids := make([]string, len(s)) + for k, ss := range s { + ids[k] = ss.RecoveryStrategyID() + if ss.RecoveryStrategyID() == id || !ss.IsPrimary() { + active = append(active, ss) + if ss.IsPrimary() { + primary = ss + } + } + } + + if primary == nil { + return nil, nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("unable to find strategy for %s have %v", id, ids)) + } + + return active, primary, nil +} diff --git a/selfservice/flow/recovery/stub/identity.schema.json b/selfservice/flow/recovery/stub/identity.schema.json new file mode 100644 index 0000000..c465b28 --- /dev/null +++ b/selfservice/flow/recovery/stub/identity.schema.json @@ -0,0 +1,46 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + } + } +} diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go new file mode 100644 index 0000000..cac8abd --- /dev/null +++ b/selfservice/flow/recovery/test/persistence.go @@ -0,0 +1,169 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlcon" +) + +func TestFlowPersister(ctx context.Context, p interface { + persistence.Persister +}, +) func(t *testing.T) { + clearids := func(r *recovery.Flow) { + r.ID = uuid.UUID{} + } + + return func(t *testing.T) { + nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=should error when the recovery request does not exist", func(t *testing.T) { + _, err := p.GetRecoveryFlow(ctx, x.NewUUID()) + require.Error(t, err) + }) + + newFlow := func(t *testing.T) *recovery.Flow { + var r recovery.Flow + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + r.State = flow.StateShowForm + return &r + } + + t.Run("case=should create a new recovery request", func(t *testing.T) { + r := newFlow(t) + err := p.CreateRecoveryFlow(ctx, r) + require.NoError(t, err, "%#v", err) + require.Equal(t, nid, r.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetRecoveryFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r recovery.Flow + require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm + require.NoError(t, p.CreateRecoveryFlow(ctx, &r)) + require.Equal(t, nid, r.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetRecoveryFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create and fetch a recovery request", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateRecoveryFlow(ctx, expected) + require.NoError(t, err) + require.Equal(t, nid, expected.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetRecoveryFlow(ctx, expected.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + actual, err := p.GetRecoveryFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assertx.EqualAsJSON(t, expected.UI, actual.UI, "expected:\t%s\nactual:\t%s", expected.UI, actual.UI) + }) + + t.Run("case=should create and update a recovery request", func(t *testing.T) { + expected := newFlow(t) + expected.UI.Nodes = node.Nodes{} + expected.UI.Nodes.Append(node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + expected.UI.Nodes.Append(node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + err := p.CreateRecoveryFlow(ctx, expected) + require.NoError(t, err) + + expected.UI.Action = "/new-action" + expected.UI.Nodes.Append( + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + }))) + + expected.RequestURL = "/new-request-url" + + t.Run("fail to find on other network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + require.ErrorIs(t, other.UpdateRecoveryFlow(ctx, expected), sqlcon.ErrNoRows()) + + actual, err := p.GetRecoveryFlow(ctx, expected.ID) + require.NoError(t, err) + assert.NotEqual(t, "/new-request-url", actual.RequestURL) + }) + + require.NoError(t, p.UpdateRecoveryFlow(ctx, expected)) + + actual, err := p.GetRecoveryFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.Equal(t, "/new-action", actual.UI.Action) + assert.Equal(t, "/new-request-url", actual.RequestURL) + assertx.EqualAsJSON(t, node.Nodes{ + // v0.5: {Name: "zab", Type: "zab", Pattern: "zab"}, + node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + // v0.5: {Name: "zab", Type: "bar", Pattern: "baz"}, + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + })), + }, actual.UI.Nodes) + }) + + t.Run("case=should not cause data loss when updating a request without changes", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateRecoveryFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetRecoveryFlow(ctx, expected.ID) + require.NoError(t, err) + + require.NoError(t, p.UpdateRecoveryFlow(ctx, actual)) + + actual, err = p.GetRecoveryFlow(ctx, expected.ID) + require.NoError(t, err) + assertx.EqualAsJSON(t, expected.UI, actual.UI) + }) + + t.Run("case=handle network reference issues", func(t *testing.T) { + }) + } +} diff --git a/selfservice/flow/registration/.snapshots/TestSortNodes-case=1.json.json b/selfservice/flow/registration/.snapshots/TestSortNodes-case=1.json.json new file mode 100644 index 0000000..242f890 --- /dev/null +++ b/selfservice/flow/registration/.snapshots/TestSortNodes-case=1.json.json @@ -0,0 +1,81 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "YTc3djZwaWpsZTFha3UyNHRlMDMyaTRxaHMxMWVmcmk=", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.username", + "type": "text", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.foobar", + "type": "text", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/flow/registration/decoder.go b/selfservice/flow/registration/decoder.go new file mode 100644 index 0000000..0aa186d --- /dev/null +++ b/selfservice/flow/registration/decoder.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "net/http" + "net/url" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/x/decoderx" +) + +func DecodeBody(p interface{}, r *http.Request, schema []byte, ds *url.URL) error { + raw, err := sjson.SetBytes(schema, + "properties.traits.$ref", ds.String()+"#/properties/traits") + if err != nil { + return errors.WithStack(err) + } + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(raw) + if err != nil { + return errors.WithStack(err) + } + + return decoderx.Decode(r, p, compiler, decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat()) +} diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go new file mode 100644 index 0000000..da70c66 --- /dev/null +++ b/selfservice/flow/registration/error.go @@ -0,0 +1,218 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "net/http" + + "github.com/gofrs/uuid" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" + "github.com/ory/kratos/x/redir" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/x" +) + +var ErrHookAbortFlow = errors.New("aborted registration hook execution") + +func ErrAlreadyLoggedIn() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDAlreadyLoggedIn).WithError("you are already logged in").WithReason("A valid session was detected and thus registration is not possible.") +} + +func ErrRegistrationDisabled() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDSelfServiceFlowDisabled).WithError("registration flow disabled").WithReason("Registration is not allowed because it was disabled.") +} + +type ( + errorHandlerDependencies interface { + clock.Provider + errorx.ManagementProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + config.Provider + hydra.Provider + + sessiontokenexchange.PersistenceProvider + FlowPersistenceProvider + HandlerProvider + } + + ErrorHandlerProvider interface{ RegistrationFlowErrorHandler() *ErrorHandler } + + ErrorHandler struct { + d errorHandlerDependencies + } +) + +func NewErrorHandler(d errorHandlerDependencies) *ErrorHandler { + return &ErrorHandler{d: d} +} + +func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r *http.Request, f *Flow, err error) (*flow.ExpiredError, error) { + errExpired := new(flow.ExpiredError) + if !errors.As(err, &errExpired) { + return nil, nil + } + // create new flow because the old one is not valid + newFlow, err := s.d.RegistrationHandler().FromOldFlow(w, r, *f) + if err != nil { + return nil, err + } + + newFlow.UI.Messages.Add(text.NewErrorValidationRegistrationFlowExpired(s.d.Clock(), errExpired.ExpiredAt)) + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), newFlow); err != nil { + return nil, err + } + + return errExpired.WithFlow(newFlow), nil +} + +func (s *ErrorHandler) WriteFlowError( + w http.ResponseWriter, + r *http.Request, + f *Flow, + ct identity.CredentialsType, + group node.UiNodeGroup, + err error, +) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.registration.ErrorHandler.WriteFlowError", + trace.WithAttributes( + attribute.String("error", err.Error()), + )) + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + if dup := new(identity.ErrDuplicateCredentials); errors.As(err, &dup) { + err = schema.NewDuplicateCredentialsError(dup) + } + + logger := s.d.Logger(). + WithError(err). + WithRequest(r). + WithField("registration_flow", f.ToLoggerField()) + + logger.Info("Encountered self-service flow error.") + + if f == nil { + trace.SpanFromContext(r.Context()).AddEvent(events.NewRegistrationFailed(r.Context(), uuid.Nil, "", "", err)) + s.forward(w, r, nil, err) + return + } + span.SetAttributes(attribute.String("flow_id", f.ID.String())) + trace.SpanFromContext(r.Context()).AddEvent(events.NewRegistrationFailed(r.Context(), f.ID, string(f.Type), ct.String(), err)) + + if expired, inner := s.PrepareReplacementForExpiredFlow(w, r, f, err); inner != nil { + s.forward(w, r, f, err) + return + } else if expired != nil { + if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, expired) + } else { + http.Redirect(w, r, expired.GetFlow().AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(r.Context())).String(), http.StatusSeeOther) + } + return + } + + f.UI.ResetMessages() + if err := f.UI.ParseError(group, err); err != nil { + s.forward(w, r, f, err) + return + } + + ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + s.forward(w, r, f, err) + return + } + + if err := SortNodes(r.Context(), f.UI.Nodes, ds.String()); err != nil { + s.forward(w, r, f, err) + return + } + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); err != nil { + s.forward(w, r, f, err) + return + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(r.Context())).String(), http.StatusFound) + return + } + if codes, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); group == node.OpenIDConnectGroup && f.Type == flow.TypeAPI && hasCode { + returnTo, err := redir.SecureRedirectTo(r, + s.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()), + f.SecureRedirectToOpts(r.Context(), s.d)..., + ) + if err != nil { + s.forward(w, r, f, errors.WithStack(err)) + return + } + q := returnTo.Query() + q.Set("code", codes.ReturnToCode) + q.Set("flow", f.ID.String()) + returnTo.RawQuery = q.Encode() + http.Redirect(w, r, returnTo.String(), http.StatusSeeOther) + return + } + + updatedFlow, innerErr := s.d.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), f.ID) + if innerErr != nil { + s.forward(w, r, updatedFlow, innerErr) + return + } + + // If the flow has an OAuth2LoginChallenge, we try to fetch the login request from Hydra because we return the flow object as JSON. + if updatedFlow.OAuth2LoginChallenge != "" { + hlr, err := s.d.Hydra().GetLoginRequest(ctx, string(updatedFlow.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. + s.forward(w, r, updatedFlow, err) + return + } + updatedFlow.HydraLoginRequest = hlr + } + + s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), updatedFlow) +} + +func (s *ErrorHandler) forward(w http.ResponseWriter, r *http.Request, rr *Flow, err error) { + if rr == nil { + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if rr.Type == flow.TypeAPI { + s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) + } else { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } +} diff --git a/selfservice/flow/registration/error_test.go b/selfservice/flow/registration/error_test.go new file mode 100644 index 0000000..6d42df2 --- /dev/null +++ b/selfservice/flow/registration/error_test.go @@ -0,0 +1,309 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/configx" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "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/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +type opts struct { + oauth2LoginChallenge string +} + +func withLoginChallenge(challenge string) func(*opts) { + return func(o *opts) { + o.oauth2LoginChallenge = challenge + } +} + +func TestHandleError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceRegistrationEnabled, true), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + reg.SetHydra(hydra.NewFake()) + + public, _ := testhelpers.NewKratosServer(t, reg) + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + testhelpers.NewErrorTestServer(t, reg) + + h := reg.RegistrationFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var registrationFlow *registration.Flow + var flowError error + var group node.UiNodeGroup + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(w, r, registrationFlow, "", group, flowError) + }) + + reset := func() { + registrationFlow = nil + flowError = nil + group = "" + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type, options ...func(*opts)) *registration.Flow { + req := &http.Request{URL: urlx.ParseOrPanic("/")} + f, err := registration.NewFlow(reg, req, ft) + require.NoError(t, err) + opts := &opts{} + for _, o := range options { + o(opts) + } + if opts.oauth2LoginChallenge != "" { + f.OAuth2LoginChallenge = sqlxx.NullString(opts.oauth2LoginChallenge) + } + for _, s := range reg.RegistrationStrategies(context.Background()) { + var populateErr error + switch strategy := s.(type) { + case registration.FormHydrator: + switch { + case conf.SelfServiceFlowRegistrationTwoSteps(ctx): + populateErr = strategy.PopulateRegistrationMethodProfile(req, f) + default: + populateErr = strategy.PopulateRegistrationMethod(req, f) + } + case registration.UnifiedFormHydrator: + populateErr = strategy.PopulateRegistrationMethod(req, f) + default: + populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A registration strategy was expected to implement one of the interfaces UnifiedFormHydrator or FormHydrator but did not.")) + } + require.NoError(t, populateErr) + } + + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(context.Background(), f)) + return f + } + + expectErrorUI := func(t *testing.T) (interface{}, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + anHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, tc.t) + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusGone, res.StatusCode, "%+v\n\t%s", res.Request, body) + + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.GetBytes(body, "use_flow_id").String()) + assert.Equal(t, text.ErrIDSelfServiceFlowExpired, gjson.GetBytes(body, "error.id").String(), "%s", body) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, registrationFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + + t.Run("case=refuses to parse oauth2 login challenge when Hydra is not configured", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, flow.TypeBrowser, withLoginChallenge(hydra.FakeInvalidLoginChallenge)) + group = node.PasswordGroup + flowError = herodot.ErrBadRequest().WithReason("missing field 'password' fake error") + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%+v\n\t%s", res.Request, body) + require.Equal(t, "Unable to get OAuth 2.0 Login Challenge.", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=fetches hydra login request", func(t *testing.T) { + t.Cleanup(reset) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, nil) + }) + + registrationFlow = newFlow(t, time.Minute, flow.TypeBrowser, withLoginChallenge(hydra.FakeValidLoginChallenge)) + group = node.PasswordGroup + flowError = herodot.ErrBadRequest().WithReason("missing field 'password' fake error") + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%+v\n\t%s", res.Request, body) + require.NotEmpty(t, gjson.GetBytes(body, "oauth2_login_request").Value(), "%s", body) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectRegistrationUI := func(t *testing.T) (*registration.Flow, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRegistrationUI(ctx).String()+"?flow=") + + rf, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return rf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = ®istration.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + group = node.PasswordGroup + + lf, _ := expectRegistrationUI(t) + require.Len(t, lf.UI.Messages, 1) + assert.Equal(t, int(text.ErrorValidationRegistrationFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + group = node.PasswordGroup + + lf, _ := expectRegistrationUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + registrationFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + group = node.PasswordGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + }) +} diff --git a/selfservice/flow/registration/fixtures/sort.schema.json b/selfservice/flow/registration/fixtures/sort.schema.json new file mode 100644 index 0000000..4e7acec --- /dev/null +++ b/selfservice/flow/registration/fixtures/sort.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + }, + "foobar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/registration/fixtures/sort/1.json b/selfservice/flow/registration/fixtures/sort/1.json new file mode 100644 index 0000000..a4f7da2 --- /dev/null +++ b/selfservice/flow/registration/fixtures/sort/1.json @@ -0,0 +1,81 @@ +[ + { + "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", + "value": "YTc3djZwaWpsZTFha3UyNHRlMDMyaTRxaHMxMWVmcmk=" + }, + "group": "default", + "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/flow/registration/flow.go b/selfservice/flow/registration/flow.go new file mode 100644 index 0000000..cbad8f2 --- /dev/null +++ b/selfservice/flow/registration/flow.go @@ -0,0 +1,308 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +// swagger:model registrationFlow +type Flow struct { + // ID represents the flow's unique ID. When performing the registration flow, this + // represents the id in the registration ui's query parameter: http:///?flow= + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id"` + + // Ory OAuth 2.0 Login Challenge. + // + // This value is set using the `login_challenge` query parameter of the registration and login endpoints. + // If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + OAuth2LoginChallenge sqlxx.NullString `json:"oauth2_login_challenge,omitempty" faker:"-" db:"oauth2_login_challenge_data"` + + // HydraLoginRequest is an optional field whose presence indicates that Kratos + // is being used as an identity provider in a Hydra OAuth2 flow. Kratos + // populates this field by retrieving its value from Hydra and it is used by + // the login and consent UIs. + HydraLoginRequest *hydraclientgo.OAuth2LoginRequest `json:"oauth2_login_request,omitempty" faker:"-" db:"-"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type flow.Type `json:"type" db:"type" faker:"flow_type"` + + // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, + // a new flow has to be initiated. + // + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the flow occurred. + // + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // InternalContext stores internal context used by internals - for example MFA keys. + InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + // + // required: true + RequestURL string `json:"request_url" faker:"url" db:"request_url"` + + // ReturnTo contains the requested return_to URL. + ReturnTo string `json:"return_to,omitempty" db:"-"` + + // ReturnToVerification contains the redirect URL for the verification flow. + ReturnToVerification string `json:"-" db:"-"` + + // Active, if set, contains the registration method that is being used. It is initially + // not set. + Active identity.CredentialsType `json:"active,omitempty" faker:"identity_credentials_type" db:"active_method"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // CSRFToken contains the anti-csrf token associated with this flow. Only set for browser flows. + CSRFToken string `json:"-" db:"csrf_token"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + 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 + // + // 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:"-" ` + + // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. + // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", + // and only on creating the flow. + SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - 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. + // required: true + State State `json:"state" faker:"-" db:"state"` + + // only used internally + IDToken string `json:"-" faker:"-" db:"-"` + // Only used internally + RawIDTokenNonce string `json:"-" db:"-"` + + // IdentitySchema optionally holds the ID of the identity schema that is used + // for this flow. This value can be set by the user when creating the flow and + // should be retained when the flow is saved or converted to another flow. + IdentitySchema flow.IdentitySchema `json:"identity_schema,omitempty" faker:"-" db:"identity_schema_id"` +} + +var _ flow.Flow = new(Flow) + +// flowDependencies are the dependencies NewFlow needs to construct a +// registration flow: the configuration (for the lifespan and return-to +// validation), the clock, and the CSRF token generator. +type flowDependencies interface { + config.Provider + clock.Provider + nosurfx.CSRFTokenGeneratorProvider +} + +func NewFlow(reg flowDependencies, r *http.Request, ft flow.Type) (*Flow, error) { + conf := reg.Config() + now := reg.Clock().Now().UTC() + exp := conf.SelfServiceFlowRegistrationRequestLifespan(r.Context()) + id := x.NewUUID() + + // Pre-validate the return to URL which is contained in the HTTP request. + requestURL := x.RequestURL(r).String() + _, err := redir.SecureRedirectTo(r, + conf.SelfServiceBrowserDefaultReturnTo(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + return nil, err + } + + hlc, err := hydra.GetLoginChallengeID(conf, r) + if err != nil { + return nil, err + } + + identitySchema := "" + if requestedSchema := r.URL.Query().Get("identity_schema"); requestedSchema != "" { + identitySchema, err = conf.SelfServiceFlowIdentitySchema(r.Context(), requestedSchema) + if err != nil { + return nil, err + } + } + + f := &Flow{ + ID: id, + OAuth2LoginChallenge: hlc, + ExpiresAt: now.Add(exp), + IssuedAt: now, + RequestURL: requestURL, + UI: &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), + }, + CSRFToken: reg.GenerateCSRFToken(r), + Type: ft, + InternalContext: []byte("{}"), + State: flow.StateChooseMethod, + IdentitySchema: flow.IdentitySchema(identitySchema), + } + if err := flow.SetRequestBaseURL(f, x.BaseURLStringFromContext(r.Context())); err != nil { + return nil, err + } + return f, nil +} + +func (Flow) TableName() string { return "selfservice_registration_flows" } +func (f Flow) GetID() uuid.UUID { return f.ID } +func (f *Flow) AppendTo(src *url.URL) *url.URL { return flow.AppendFlowTo(src, f.ID) } +func (f *Flow) GetType() flow.Type { return f.Type } +func (f *Flow) GetRequestURL() string { return f.RequestURL } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext } +func (f *Flow) SetInternalContext(bytes sqlxx.JSONRawMessage) { f.InternalContext = bytes } +func (f *Flow) GetUI() *container.Container { return f.UI } +func (f *Flow) GetState() State { return f.State } +func (Flow) GetFlowName() flow.FlowName { return flow.RegistrationFlow } +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) GetOAuth2LoginChallenge() sqlxx.NullString { return f.OAuth2LoginChallenge } +func (f *Flow) GetHydraLoginRequest() *hydraclientgo.OAuth2LoginRequest { return f.HydraLoginRequest } + +func (f *Flow) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} + +func (f *Flow) EnsureInternalContext() { + if !gjson.ParseBytes(f.InternalContext).IsObject() { + f.InternalContext = []byte("{}") + } +} + +const internalContextAccountLinkingPath = "converted_for_account_linking" + +// MarkAsAccountLinking records in the flow's internal context that this +// registration flow was created internally by converting a login flow to +// perform account linking. Such flows may exist while registration is +// disabled, but must never create a new identity in that case. +func (f *Flow) MarkAsAccountLinking() { + f.EnsureInternalContext() + // The path is a constant and the internal context is guaranteed to be a + // JSON object at this point, so this cannot fail. + if bytes, err := sjson.SetBytes(f.InternalContext, internalContextAccountLinkingPath, true); err == nil { + f.InternalContext = bytes + } +} + +// IsAccountLinking returns true if this registration flow was created +// internally by converting a login flow to perform account linking. +func (f *Flow) IsAccountLinking() bool { + return gjson.GetBytes(f.InternalContext, internalContextAccountLinkingPath).Bool() +} + +func (f Flow) MarshalJSON() ([]byte, error) { + type local Flow + f.SetReturnTo() + return json.Marshal(local(f)) +} + +func (f *Flow) SetReturnTo() { + // Return to is already set, do not overwrite it. + if len(f.ReturnTo) > 0 { + return + } + if u, err := url.Parse(f.RequestURL); err == nil { + f.ReturnTo = u.Query().Get("return_to") + } +} + +func (f *Flow) AfterFind(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AfterSave(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +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) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (opts []redir.SecureRedirectOption) { + return []redir.SecureRedirectOption{ + redir.SecureRedirectReturnTo(f.ReturnTo), + redir.SecureRedirectUseSourceURL(f.RequestURL), + redir.SecureRedirectAllowURLs(cfg.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(cfg.Config().SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowRegistrationReturnTo(ctx, f.Active.String())), + } +} + +func (f *Flow) ToLoggerField() map[string]any { + if f == nil { + return map[string]any{} + } + return map[string]any{ + "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/flow_test.go b/selfservice/flow/registration/flow_test.go new file mode 100644 index 0000000..0bd642b --- /dev/null +++ b/selfservice/flow/registration/flow_test.go @@ -0,0 +1,171 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration_test + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/x" + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + "github.com/ory/x/urlx" +) + +func TestFakeFlow(t *testing.T) { + var r registration.Flow + require.NoError(t, faker.FakeData(&r)) + + assert.Equal(t, uuid.Nil, r.ID) + assert.NotEmpty(t, r.IssuedAt) + assert.NotEmpty(t, r.ExpiresAt) + assert.NotEmpty(t, r.RequestURL) + assert.NotEmpty(t, r.Active) +} + +func TestNewFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + t.Run("case=0", func(t *testing.T) { + r, err := registration.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/"), + Host: "ory.sh", TLS: &tls.ConnectionState{}, + }, flow.TypeBrowser) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeBrowser, r.Type) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) + + t.Run("type=return_to", func(t *testing.T) { + _, err := registration.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, flow.TypeBrowser) + require.Error(t, err) + + _, err = registration.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + }) + + t.Run("case=1", func(t *testing.T) { + r, err := registration.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/?refresh=true"), + Host: "ory.sh", + }, flow.TypeAPI) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeAPI, r.Type) + assert.Equal(t, "http://ory.sh/?refresh=true", r.RequestURL) + }) + + t.Run("case=2", func(t *testing.T) { + r, err := registration.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("https://ory.sh/"), + Host: "ory.sh", + }, flow.TypeBrowser) + + require.NoError(t, err) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) + + t.Run("should parse login_challenge when Hydra is configured", func(t *testing.T) { + _, err := registration.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("https://ory.sh/?login_challenge=badee1"), Host: "ory.sh"}, flow.TypeBrowser) + require.Error(t, err) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://hydra") + + r, err := registration.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("https://ory.sh/?login_challenge=8aadcb8fc1334186a84c4da9813356d9"), Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "8aadcb8fc1334186a84c4da9813356d9", string(r.OAuth2LoginChallenge)) + }) +} + +func TestFlow(t *testing.T) { + r := ®istration.Flow{ID: x.NewUUID()} + assert.Equal(t, r.ID, r.GetID()) + + t.Run("case=expired", func(t *testing.T) { + for _, tc := range []struct { + r *registration.Flow + valid bool + }{ + { + r: ®istration.Flow{ExpiresAt: time.Now().Add(time.Hour), IssuedAt: time.Now().Add(-time.Minute)}, + valid: true, + }, + {r: ®istration.Flow{ExpiresAt: time.Now().Add(-time.Hour), IssuedAt: time.Now().Add(-time.Minute)}}, + } { + if tc.valid { + require.NoError(t, tc.r.Valid(clock.New())) + } else { + require.Error(t, tc.r.Valid(clock.New())) + } + } + }) +} + +func TestGetType(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=%s", ft), func(t *testing.T) { + r := ®istration.Flow{Type: ft} + assert.Equal(t, ft, r.GetType()) + }) + } +} + +func TestGetRequestURL(t *testing.T) { + expectedURL := "http://foo/bar/baz" + f := ®istration.Flow{RequestURL: expectedURL} + assert.Equal(t, expectedURL, f.GetRequestURL()) +} + +func TestFlowEncodeJSON(t *testing.T) { + assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, ®istration.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, ®istration.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, registration.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) +} + +func TestFlowDontOverrideReturnTo(t *testing.T) { + f := ®istration.Flow{ReturnTo: "/foo"} + f.SetReturnTo() + assert.Equal(t, "/foo", f.ReturnTo) + + f = ®istration.Flow{RequestURL: "https://foo.bar?return_to=/bar"} + f.SetReturnTo() + assert.Equal(t, "/bar", f.ReturnTo) +} + +func TestFlowAccountLinkingMarker(t *testing.T) { + t.Parallel() + + f := ®istration.Flow{} + assert.False(t, f.IsAccountLinking()) + + registration.WithFlowAccountLinking()(f) + assert.True(t, f.IsAccountLinking()) + + // The marker must survive an internal-context round trip, because the + // flow is persisted between requests. + var copied registration.Flow + copied.SetInternalContext(f.GetInternalContext()) + assert.True(t, copied.IsAccountLinking()) +} diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go new file mode 100644 index 0000000..565062a --- /dev/null +++ b/selfservice/flow/registration/handler.go @@ -0,0 +1,760 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "net/http" + "net/url" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "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/events" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +const ( + RouteInitBrowserFlow = "/self-service/registration/browser" + RouteInitAPIFlow = "/self-service/registration/api" + + RouteGetFlow = "/self-service/registration/flows" + + RouteSubmitFlow = "/self-service/registration" +) + +type ( + handlerDependencies interface { + clock.Provider + config.Provider + errorx.ManagementProvider + hydra.Provider + session.HandlerProvider + session.ManagementProvider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + StrategyProvider + HookExecutorProvider + FlowPersistenceProvider + ErrorHandlerProvider + sessiontokenexchange.PersistenceProvider + logrusx.Provider + } + HandlerProvider interface { + RegistrationHandler() *Handler + } + Handler struct { + d handlerDependencies + } +) + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) + h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) + + public.GET(RouteInitBrowserFlow, h.createBrowserRegistrationFlow) + public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsNotAuthenticated(h.createNativeRegistrationFlow, + session.RespondWithJSONErrorOnAuthenticated(h.d.Writer(), errors.WithStack(ErrAlreadyLoggedIn())))) + + public.GET(RouteGetFlow, h.getRegistrationFlow) + + public.POST(RouteSubmitFlow, h.d.SessionHandler().IsNotAuthenticated(h.updateRegistrationFlow, h.onAuthenticated)) + public.GET(RouteSubmitFlow, h.d.SessionHandler().IsNotAuthenticated(h.updateRegistrationFlow, h.onAuthenticated)) +} + +func (h *Handler) onAuthenticated(w http.ResponseWriter, r *http.Request) { + handler := session.RedirectOnAuthenticated(h.d) + if x.IsJSONRequest(r) { + handler = session.RespondWithJSONErrorOnAuthenticated(h.d.Writer(), ErrAlreadyLoggedIn()) + } + + handler(w, r) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) + admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) +} + +type FlowOption func(f *Flow) + +func WithFlowReturnTo(returnTo string) FlowOption { + return func(f *Flow) { + f.ReturnTo = returnTo + } +} + +func WithFlowOAuth2LoginChallenge(loginChallenge string) FlowOption { + return func(f *Flow) { + f.OAuth2LoginChallenge = sqlxx.NullString(loginChallenge) + } +} + +func WithFlowIdentitySchema(schema string) FlowOption { + return func(f *Flow) { + f.IdentitySchema = flow.IdentitySchema(schema) + } +} + +func WithFlowAccountLinking() FlowOption { + return func(f *Flow) { + f.MarkAsAccountLinking() + } +} + +func (h *Handler) NewRegistrationFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...FlowOption) (*Flow, error) { + f, err := NewFlow(h.d, r, ft) + if err != nil { + return nil, err + } + for _, o := range opts { + o(f) + } + + // Flows that were converted from a login flow for account linking may be + // created even when registration is disabled. PostRegistrationHook + // guarantees they can only link to an existing identity, never create one. + if !h.d.Config().SelfServiceFlowRegistrationEnabled(r.Context()) && !f.IsAccountLinking() { + return nil, errors.WithStack(ErrRegistrationDisabled()) + } + + if ft == flow.TypeAPI && r.URL.Query().Get("return_session_token_exchange_code") == "true" { + e, err := h.d.SessionTokenExchangePersister().CreateSessionTokenExchanger(r.Context(), f.ID) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err)) + } + f.SessionTokenExchangeCode = e.InitCode + } + + for _, s := range h.d.RegistrationStrategies(r.Context(), PrepareOrganizations(r, f)...) { + var populateErr error + + switch strategy := s.(type) { + case FormHydrator: + if h.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) { + populateErr = strategy.PopulateRegistrationMethodProfile(r, f) + } else { + populateErr = strategy.PopulateRegistrationMethod(r, f) + } + case UnifiedFormHydrator: + populateErr = strategy.PopulateRegistrationMethod(r, f) + default: + populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A registration strategy was expected to implement one of the interfaces UnifiedFormHydrator or FormHydrator but did not.")) + } + + if populateErr != nil { + return nil, populateErr + } + } + + ds, err := f.IdentitySchema.URL(r.Context(), h.d.Config()) + if err != nil { + return nil, err + } + + if err := SortNodes(r.Context(), f.UI.Nodes, ds.String()); err != nil { + return nil, err + } + + if err := h.d.RegistrationExecutor().PreRegistrationHook(w, r, f); err != nil { + return nil, err + } + + if err := h.d.RegistrationFlowPersister().CreateRegistrationFlow(r.Context(), f); err != nil { + return nil, err + } + + span := trace.SpanFromContext(r.Context()) + span.AddEvent(events.NewRegistrationInitiated(r.Context(), f.ID, string(ft), f.OrganizationID)) + + return f, nil +} + +func (h *Handler) FromOldFlow(w http.ResponseWriter, r *http.Request, of Flow) (*Flow, error) { + nf, err := h.NewRegistrationFlow(w, r, of.Type) + if err != nil { + return nil, err + } + + nf.RequestURL = of.RequestURL + nf.IdentitySchema = of.IdentitySchema + return nf, nil +} + +// swagger:route GET /self-service/registration/api frontend createNativeRegistrationFlow +// +// # Create Registration Flow for Native Apps +// +// This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on. +// +// If a valid provided session cookie or session token is provided, a 400 Bad Request error +// will be returned unless the URL query parameter `?refresh=true` is set. +// +// To fetch an existing registration flow call `/self-service/registration/flows?flow=`. +// +// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server +// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make +// you vulnerable to a variety of CSRF attacks. +// +// In the case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// +// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Schemes: http, https +// +// Responses: +// 200: registrationFlow +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createNativeRegistrationFlow(w http.ResponseWriter, r *http.Request) { + a, err := h.NewRegistrationFlow(w, r, flow.TypeAPI) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, a) +} + +// Create Native Registration Flow Parameters +// +// swagger:parameters createNativeRegistrationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createNativeRegistrationFlow struct { + // EnableSessionTokenExchangeCode requests the login flow to include a code that can be used to retrieve the session token + // after the login flow has been completed. + // + // in: query + EnableSessionTokenExchangeCode bool `json:"return_session_token_exchange_code"` + + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // An optional organization ID that should be used to register this user. + // This parameter is only effective in the Ory Network. + // + // required: false + // in: query + Organization string `json:"organization"` + + // An optional identity schema to use for the registration flow. + // + // required: false + // in: query + IdentitySchema string `json:"identity_schema"` +} + +// Create Browser Registration Flow Parameters +// +// swagger:parameters createBrowserRegistrationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserRegistrationFlow struct { + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // Ory OAuth 2.0 Login Challenge. + // + // If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider. + // + // The value for this parameter comes from `login_challenge` URL Query parameter sent to your + // application (e.g. `/registration?login_challenge=abcde`). + // + // This feature is compatible with Ory Hydra when not running on the Ory Network. + // + // required: false + // in: query + LoginChallenge string `json:"login_challenge"` + + // The URL to return the browser to after the verification flow was completed. + // + // After the registration flow is completed, the user will be sent a verification email. + // Upon completing the verification flow, this URL will be used to override the default + // `selfservice.flows.verification.after.default_redirect_to` value. + // + // required: false + // in: query + AfterVerificationReturnTo string `json:"after_verification_return_to"` + + // An optional organization ID that should be used to register this user. + // This parameter is only effective in the Ory Network. + // + // required: false + // in: query + Organization string `json:"organization"` + + // An optional identity schema to use for the registration flow. + // + // required: false + // in: query + IdentitySchema string `json:"identity_schema"` +} + +// swagger:route GET /self-service/registration/browser frontend createBrowserRegistrationFlow +// +// # Create Registration Flow for Browsers +// +// This endpoint initializes a browser-based user registration flow. This endpoint will set the appropriate +// cookies and anti-CSRF measures required for browser-based flows. +// +// If this endpoint is opened as a link in the browser, it will be redirected to +// `selfservice.flows.registration.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session +// exists already, the browser will be redirected to `urls.default_redirect_url`. +// +// If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// +// If this endpoint is called via an AJAX request, the response contains the registration flow without a redirect. +// +// This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Schemes: http, https +// +// Produces: +// - application/json +// +// Responses: +// 200: registrationFlow +// 303: emptyResponse +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserRegistrationFlow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + a, err := h.NewRegistrationFlow(w, r, flow.TypeBrowser) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + var ( + hydraLoginRequest *hydraclientgo.OAuth2LoginRequest + hydraLoginChallenge sqlxx.NullString + ) + if r.URL.Query().Has("login_challenge") { + var err error + hydraLoginChallenge, err = hydra.GetLoginChallengeID(h.d.Config(), r) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + hydraLoginRequest, err = h.d.Hydra().GetLoginRequest(ctx, hydraLoginChallenge.String()) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + // on OAuth2 flows, we need to use the RequestURL + // as the ReturnTo URL. + // This is because a user might want to switch between + // 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()) { + q := r.URL.Query() + // replace the return_to query parameter + q.Set("return_to", hydraLoginRequest.RequestUrl) + r.URL.RawQuery = q.Encode() + } + } + + // Only session columns (identity ID, session ID, AMR) are read below, so skip loading the + // devices and the identity. + if sess, err := h.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandNothing, identity.ExpandNothing); err == nil { + if hydraLoginRequest != nil { + if hydraLoginRequest.GetSkip() { + rt, err := h.d.Hydra().AcceptLoginRequest(r.Context(), + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(hydraLoginChallenge), + IdentityID: sess.IdentityID.String(), + SessionID: sess.ID.String(), + AuthenticationMethods: sess.AMR, + }) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), 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))) + return + } + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), err, returnTo.String()) + return + } + + // hydra indicates that we cannot skip the login request + // so we must perform the login flow. + // we directly go to the login handler from here + // copy over any query parameters, such as `return_to` and `login_challenge` + loginURL := urlx.CopyWithQuery(urlx.AppendPaths(h.d.Config().SelfPublicURL(ctx), "/self-service/login/browser"), x.RequestURL(r).Query()) + http.Redirect( + w, + r, + loginURL.String(), + http.StatusFound, + ) + return + } + + if x.IsJSONRequest(r) { + h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn())) + return + } + + returnTo, redirErr := redir.SecureRedirectTo( + r, h.d.Config().SelfServiceBrowserDefaultReturnTo(ctx), + redir.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(ctx)), + redir.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), + ) + if redirErr != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, redirErr) + return + } + + http.Redirect(w, r, returnTo.String(), http.StatusSeeOther) + return + } + + redirTo := a.AppendTo(h.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), a, redirTo) +} + +// Get Registration Flow Parameters +// +// swagger:parameters getRegistrationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getRegistrationFlow struct { + // The Registration Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/registration?flow=abcde`). + // + // required: true + // in: query + ID string `json:"id"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route GET /self-service/registration/flows frontend getRegistrationFlow +// +// # Get Registration Flow +// +// This endpoint returns a registration flow's context with, for example, error details and other information. +// +// Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. +// For AJAX requests you must ensure that cookies are included in the request or requests will fail. +// +// If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain +// and you need to forward the incoming HTTP Cookie header to this endpoint: +// +// ```js +// // pseudo-code example +// router.get('/registration', async function (req, res) { +// const flow = await client.getRegistrationFlow(req.header('cookie'), req.query['flow']) +// +// res.render('registration', flow) +// }) +// ``` +// +// This request may fail due to several reasons. The `error.id` can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `self_service_flow_expired`: The flow is expired and you should request a new one. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: registrationFlow +// 403: errorGeneric +// 404: errorGeneric +// 410: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) getRegistrationFlow(w http.ResponseWriter, r *http.Request) { + ar, err := h.d.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("id"))) + if err != nil { + // A flow that does not exist reports a disabled registration to not + // leak flow existence. Any other error is returned as-is, so that an + // infrastructure failure is not misreported as a disabled + // registration. + if errors.Is(err, sqlcon.ErrNoRows()) && !h.d.Config().SelfServiceFlowRegistrationEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(ErrRegistrationDisabled())) + return + } + + h.d.Writer().WriteError(w, r, err) + return + } + + // When registration is disabled, only flows that were converted from a + // login flow for account linking may be fetched. + if !ar.IsAccountLinking() && !h.d.Config().SelfServiceFlowRegistrationEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(ErrRegistrationDisabled())) + return + } + + // Browser flows must include the CSRF token + // + // Resolves: https://github.com/ory/kratos/issues/1282 + if ar.Type == flow.TypeBrowser && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), ar.CSRFToken) { + h.d.Writer().WriteError(w, r, nosurfx.CSRFErrorReason(r, h.d)) + return + } + + if ar.ExpiresAt.Before(h.d.Clock().Now()) { + if ar.Type == flow.TypeBrowser { + redirectURL := flow.GetFlowExpiredRedirectURL(r.Context(), h.d.Config(), RouteInitBrowserFlow, ar.ReturnTo) + + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone().WithID(text.ErrIDSelfServiceFlowExpired). + WithReason("The registration flow has expired. Redirect the user to the registration flow init endpoint to initialize a new registration flow."). + WithDetail("redirect_to", redirectURL.String()). + WithDetail("return_to", ar.ReturnTo))) + return + } + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone().WithID(text.ErrIDSelfServiceFlowExpired). + WithReason("The registration flow has expired. Call the registration flow init API endpoint to initialize a new registration flow."). + WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitAPIFlow).String()))) + return + } + + if ar.OAuth2LoginChallenge != "" { + hlr, err := h.d.Hydra().GetLoginRequest(r.Context(), 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) + return + } + ar.HydraLoginRequest = hlr + } + + h.d.Writer().Write(w, r, ar) +} + +// Update Registration Flow Parameters +// +// swagger:parameters updateRegistrationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRegistrationFlow struct { + // The Registration Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/registration?flow=abcde`). + // + // required: true + // in: query + Flow string `json:"flow"` + + // in: body + // required: true + Body updateRegistrationFlowBody + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// Update Registration Request Body +// +// swagger:model updateRegistrationFlowBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRegistrationFlowBody struct{} + +// swagger:route POST /self-service/registration frontend updateRegistrationFlow +// +// # Update Registration Flow +// +// Use this endpoint to complete a registration flow by sending an identity's traits and password. This endpoint +// behaves differently for API and browser flows. +// +// API flows expect `application/json` to be sent in the body and respond with +// - HTTP 200 and a application/json body with the created identity success - if the session hook is configured the +// `session` and `session_token` will also be included; +// - HTTP 410 if the original flow expired with the appropriate error messages set and optionally a `use_flow_id` parameter in the body; +// - HTTP 400 on form validation errors. +// +// Browser flows expect a Content-Type of `application/x-www-form-urlencoded` or `application/json` to be sent in the body and respond with +// - a HTTP 303 redirect to the post/after registration URL or the `return_to` value if it was set and if the registration succeeded; +// - a HTTP 303 redirect to the registration UI URL with the flow ID containing the validation errors otherwise. +// +// Browser flows with an accept header of `application/json` will not redirect but instead respond with +// - HTTP 200 and a application/json body with the signed in identity and a `Set-Cookie` header on success; +// - HTTP 303 redirect to a fresh login flow if the original flow expired with the appropriate error messages set; +// - HTTP 400 on form validation errors. +// +// If this endpoint is called with `Accept: application/json` in the header, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_already_available`: The user is already signed in. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// - `browser_location_change_required`: Usually sent when an AJAX request indicates that the browser needs to open a specific URL. +// Most likely used in Social Sign In flows. +// +// More information can be found at [Ory Kratos User Login](https://www.ory.com/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.com/docs/kratos/self-service/flows/user-registration). +// +// Schemes: http, https +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Responses: +// 200: successfulNativeRegistration +// 303: emptyResponse +// 400: registrationFlow +// 410: errorGeneric +// 422: errorBrowserLocationChangeRequired +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) updateRegistrationFlow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctx = semconv.ContextWithAttributes( + ctx, + attribute.String(events.AttributeKeySelfServiceStrategyUsed.String(), "registration"), + attribute.String(events.AttributeKeySelfServiceFlowName.String(), "registration"), + ) + r = r.WithContext(ctx) + + rid, err := flow.GetFlowID(r) + if err != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, nil, "", node.DefaultGroup, err) + return + } + + f, err := h.d.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), rid) + if err != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, nil, "", node.DefaultGroup, err) + return + } + + if err := h.d.SessionManager().SessionActiveForRequest(r.Context(), r); err == nil { + if f.Type == flow.TypeBrowser { + http.Redirect(w, r, h.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()).String(), http.StatusSeeOther) + return + } + + h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn())) + return + } + + if err := f.Valid(h.d.Clock()); err != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, err) + return + } + + i := identity.NewIdentity(f.IdentitySchema.ID(ctx, h.d.Config())) + var s Strategy + for _, ss := range h.d.AllRegistrationStrategies() { + if err := ss.Register(w, r, f, i); errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, ss.ID(), ss.NodeGroup(), err) + return + } + + s = ss + break + } + + if s == nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, "", node.DefaultGroup, errors.WithStack(schema.NewNoRegistrationStrategyResponsible())) + return + } + + if err := h.d.RegistrationExecutor().PostRegistrationHook(w, r, f, i, session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + }); err != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, s.ID(), s.NodeGroup(), err) + return + } +} diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go new file mode 100644 index 0000000..3a79e15 --- /dev/null +++ b/selfservice/flow/registration/handler_test.go @@ -0,0 +1,696 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "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/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/password" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/urlx" +) + +func init() { + corpx.RegisterFakes() +} + +func TestHandlerRedirectOnAuthenticated(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + fakeHydra := hydra.NewFake() + reg.SetHydra(fakeHydra) + + router := httprouterx.NewRouterPublic() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + // Set it first as otherwise it will overwrite the ViperKeySelfServiceBrowserDefaultReturnTo key; + returnToTS := testhelpers.NewRedirTS(t, "return_to", conf) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToTS.URL}) + + redirTS := testhelpers.NewRedirTS(t, "already authenticated", conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + t.Run("does redirect to default on authenticated request", func(t *testing.T) { + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow, nil)) + assert.Contains(t, res.Request.URL.String(), redirTS.URL) + assert.EqualValues(t, "already authenticated", string(body)) + }) + + t.Run("does redirect to default on authenticated request", func(t *testing.T) { + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitAPIFlow, nil)) + assert.Contains(t, res.Request.URL.String(), registration.RouteInitAPIFlow) + assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw)) + }) + + t.Run("does redirect to return_to url on authenticated request", func(t *testing.T) { + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?return_to="+returnToTS.URL, nil)) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + assert.EqualValues(t, "return_to", string(body)) + }) + + t.Run("oauth2 with session and skip=false is redirected to login", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + + fakeHydra.RequestURL = "https://www.ory.com/oauth2/auth?audience=&client_id=foo&login_verifier=" + fakeHydra.Skip = false + + client := testhelpers.NewClientWithCookies(t) + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + _, res := testhelpers.MockMakeAuthenticatedRequestWithClient(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil), client) + assert.Contains(t, res.Header.Get("location"), login.RouteInitBrowserFlow) + }) + + t.Run("oauth2 with session and skip=true is accepted", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + + fakeHydra.Skip = true + fakeHydra.RequestURL = "https://www.ory.com/oauth2/auth?audience=&client_id=foo&login_verifier=" + + client := testhelpers.NewClientWithCookies(t) + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + _, res := testhelpers.MockMakeAuthenticatedRequestWithClient(t, reg, conf, router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil), client) + assert.Contains(t, res.Header.Get("location"), hydra.FakePostLoginURL) + }) +} + +func TestInitFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), + map[string]interface{}{"enabled": true}) + + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + registrationTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + returnToTS := testhelpers.NewRedirTS(t, "return_to", conf) + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL) + + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/registration.schema.json"}, + {ID: "email", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://./stub/registration.phone.schema.json", SelfserviceSelectable: true}, + {ID: "not-allowed", URL: "file://./stub/registration.schema.json"}, + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "email") + errTS := testhelpers.NewErrorTestServer(t, reg) + + assertion := func(body []byte, isForced, isApi bool) { + if isApi { + assert.Equal(t, "api", gjson.GetBytes(body, "type").String()) + } else { + assert.Equal(t, "browser", gjson.GetBytes(body, "type").String()) + } + } + + initAuthenticatedFlow := func(t *testing.T, isAPI bool, isSPA bool) (*http.Response, []byte) { + route := registration.RouteInitBrowserFlow + if isAPI { + route = registration.RouteInitAPIFlow + } + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + if isSPA { + req.Header.Set("Accept", "application/json") + } + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router, req) + if isAPI { + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + } + return res, body + } + + initFlowWithAccept := func(t *testing.T, query url.Values, isAPI bool, accept string) (*http.Response, []byte) { + route := registration.RouteInitBrowserFlow + if isAPI { + route = registration.RouteInitAPIFlow + } + c := publicTS.Client() + req, err := http.NewRequest("GET", publicTS.URL+route+"?"+query.Encode(), nil) + require.NoError(t, err) + if accept != "" { + req.Header.Set("Accept", accept) + } + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + initFlow := func(t *testing.T, query url.Values, isAPI bool) (*http.Response, []byte) { + return initFlowWithAccept(t, query, isAPI, "") + } + + initSPAFlow := func(t *testing.T) (*http.Response, []byte) { + return initFlowWithAccept(t, url.Values{}, false, "application/json") + } + + t.Run("suite=identity schema in query", func(t *testing.T) { + for _, tc := range []struct { + name string + query url.Values + wantErr bool + assert func(*testing.T, []byte) + }{{ + name: "not-allowed", + query: url.Values{"identity_schema": {"not-allowed"}}, + wantErr: true, + }, { + name: "not-found", + query: url.Values{"identity_schema": {"not-found"}}, + wantErr: true, + }, { + name: "phone", + query: url.Values{"identity_schema": {"phone"}}, + assert: func(t *testing.T, body []byte) { + assert.True(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.phone)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email)").Exists(), "%s", body) + }, + }, { + name: "email", + query: url.Values{"identity_schema": {"email"}}, + assert: func(t *testing.T, body []byte) { + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.phone)").Exists(), "%s", body) + assert.True(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email)").Exists(), "%s", body) + }, + }} { + t.Run("case="+tc.name, func(t *testing.T) { + t.Run("flow=api", func(t *testing.T) { + res, body := initFlow(t, tc.query, true) + if tc.wantErr { + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + return + } + tc.assert(t, body) + }) + + t.Run("flow=browser", func(t *testing.T) { + res, body := initFlow(t, tc.query, false) + if tc.wantErr { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + assert.EqualValues(t, "Bad Request", gjson.GetBytes(body, "status").String(), "%s", body) + return + } + tc.assert(t, body) + }) + }) + } + }) + + t.Run("flow=api", func(t *testing.T) { + t.Run("case=creates a new flow on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{}, true) + assert.Contains(t, res.Request.URL.String(), registration.RouteInitAPIFlow) + assertion(body, false, true) + assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + }) + + t.Run("case=returns a session token 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(), registration.RouteInitAPIFlow) + assertion(body, false, true) + assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + }) + + t.Run("case=fails on authenticated request", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, true, false) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + }) + + t.Run("flow=browser", func(t *testing.T) { + t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) { + res, body := initFlow(t, url.Values{}, false) + assertion(body, false, false) + assert.Contains(t, res.Request.URL.String(), registrationTS.URL) + assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + }) + + t.Run("case=never returns a session token exchange code", func(t *testing.T) { + _, body := initFlow(t, urlx.ParseOrPanic("/?return_session_token_exchange_code=true").Query(), false) + assertion(body, false, false) + assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + }) + + t.Run("case=makes request with JSON", func(t *testing.T) { + res, body := initSPAFlow(t) + assertion(body, false, false) + assert.NotContains(t, res.Request.URL.String(), registrationTS.URL) + }) + + t.Run("case=redirects when already authenticated", func(t *testing.T) { + res, _ := initAuthenticatedFlow(t, false, false) + assert.Contains(t, res.Request.URL.String(), returnToTS.URL) + }) + + t.Run("case=responds with error if already authenticated and SPA", func(t *testing.T) { + res, body := initAuthenticatedFlow(t, false, true) + assert.NotContains(t, res.Request.URL.String(), returnToTS.URL) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("case=relative redirect when self-service registration ui is a relative URL", func(t *testing.T) { + reg.Config().MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, "/registration-ts") + assert.Regexp( + t, + "^/registration-ts.*$", + testhelpers.GetSelfServiceRedirectLocation(t, publicTS.URL+registration.RouteInitBrowserFlow), + ) + }) + + t.Run("case=redirects with 303", func(t *testing.T) { + c := &http.Client{} + // don't get the reference, instead copy the values, so we don't alter the client directly. + *c = *publicTS.Client() + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", publicTS.URL+registration.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + }) + }) +} + +func TestDisabledFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), + map[string]interface{}{"enabled": true}) + + publicTS, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + + makeRequest := func(t *testing.T, route string, isSPA bool) (*http.Response, []byte) { + c := publicTS.Client() + req, err := http.NewRequest("GET", publicTS.URL+route, nil) + require.NoError(t, err) + + if isSPA { + req.Header.Set("Accept", "application/json") + } + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + t.Run("flow=api", func(t *testing.T) { + t.Run("case=init fails when flow disabled", func(t *testing.T) { + res, body := makeRequest(t, registration.RouteInitAPIFlow, false) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("case=get flow fails when flow disabled", func(t *testing.T) { + res, body := makeRequest(t, registration.RouteGetFlow+"?id="+x.NewUUID().String(), false) + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + assert.EqualValues(t, registration.ErrRegistrationDisabled().ReasonField, gjson.GetBytes(body, "reason").String(), "%s", body) + }) + }) + + t.Run("flow=browser", func(t *testing.T) { + t.Run("case=init responds with error if flow disabled and SPA", func(t *testing.T) { + res, body := makeRequest(t, registration.RouteInitBrowserFlow, true) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("case=get flow responds with error if flow disabled and SPA", func(t *testing.T) { + res, body := makeRequest(t, registration.RouteGetFlow+"?id="+x.NewUUID().String(), true) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + }) +} + +func TestAccountLinkingFlowWithRegistrationDisabled(t *testing.T) { + t.Parallel() + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), + map[string]interface{}{"enabled": true}) + + publicTS, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + + newAccountLinkingFlow := func(t *testing.T) *registration.Flow { + req := httptest.NewRequest("GET", publicTS.URL+registration.RouteInitAPIFlow, nil) + f, err := reg.RegistrationHandler().NewRegistrationFlow(httptest.NewRecorder(), req, flow.TypeAPI, + registration.WithFlowAccountLinking()) + require.NoError(t, err) + return f + } + + t.Run("case=creating an account linking flow succeeds", func(t *testing.T) { + f := newAccountLinkingFlow(t) + assert.True(t, f.IsAccountLinking()) + }) + + t.Run("case=creating a regular flow still fails", func(t *testing.T) { + req := httptest.NewRequest("GET", publicTS.URL+registration.RouteInitAPIFlow, nil) + _, err := reg.RegistrationHandler().NewRegistrationFlow(httptest.NewRecorder(), req, flow.TypeAPI) + require.ErrorContains(t, err, registration.ErrRegistrationDisabled().Error()) + }) + + t.Run("case=fetching an account linking flow succeeds", func(t *testing.T) { + f := newAccountLinkingFlow(t) + + res, err := publicTS.Client().Get(publicTS.URL + registration.RouteGetFlow + "?id=" + f.ID.String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equal(t, f.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) + }) + + t.Run("case=fetching a regular flow still fails", func(t *testing.T) { + // A regular flow can exist while registration is disabled if it was + // created before registration was disabled. + req := httptest.NewRequest("GET", publicTS.URL+registration.RouteInitAPIFlow, nil) + f, err := registration.NewFlow(reg, req, flow.TypeAPI) + require.NoError(t, err) + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, f)) + + res, err := publicTS.Client().Get(publicTS.URL + registration.RouteGetFlow + "?id=" + f.ID.String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + assert.EqualValues(t, registration.ErrRegistrationDisabled().ReasonField, gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("case=fetching a flow that does not exist still fails", func(t *testing.T) { + // Reporting "registration disabled" instead of "not found" keeps the + // existence of a flow from leaking. + res, err := publicTS.Client().Get(publicTS.URL + registration.RouteGetFlow + "?id=" + x.NewUUID().String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + assert.EqualValues(t, registration.ErrRegistrationDisabled().ReasonField, gjson.GetBytes(body, "reason").String(), "%s", body) + }) + + t.Run("case=fetching a flow does not report a broken database as registration disabled", func(t *testing.T) { + // This case needs a registry of its own because it breaks the database. + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + + publicTS, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + + // Drop the table the flow lookup reads so that it fails with an + // infrastructure error instead of "not found". The error persister + // keeps working, so a forward to the error UI stays observable. + require.NoError(t, reg.Persister().GetConnection(ctx). + RawQuery("DROP TABLE "+registration.Flow{}.TableName()).Exec()) + + res, err := publicTS.Client().Get(publicTS.URL + registration.RouteGetFlow + "?id=" + x.NewUUID().String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotContains(t, res.Request.URL.String(), errTS.URL, + "a database failure must not be forwarded to the error UI as a registration error: %s", body) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "%s", body) + }) +} + +func TestGetFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + returnToTS := testhelpers.NewRedirTS(t, "return_to", conf) + + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "email", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://./stub/registration.phone.schema.json", SelfserviceSelectable: true}, + {ID: "not-allowed", URL: "file://./stub/registration.schema.json"}, + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "email") + + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), + map[string]interface{}{"enabled": true}) + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "", conf) + + setupRegistrationUI := func(t *testing.T, c *http.Client) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, ts.URL) + return ts + } + + t.Run("case=valid", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + + assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), gjson.GetBytes(body, "id").String(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), public.URL, "%s", body) + }) + + t.Run("case=csrf cookie missing", func(t *testing.T) { + client := testhelpers.NewTestClient(t) + _ = setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + + assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=expired", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + + // Expire the flow + f, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), f)) + + res, body := testhelpers.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+f.ID.String()) + assert.EqualValues(t, http.StatusGone, res.StatusCode) + assert.Equal(t, public.URL+registration.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) + }) + + t.Run("case=expired with return_to and identity_schema", func(t *testing.T) { + returnTo := returnToTS.URL + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + + client := testhelpers.NewClientWithCookies(t) + setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow+ + "?return_to="+returnTo+ + "&identity_schema=email") + + // Expire the flow + f, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), f)) + + // Retrieve the flow and verify that return_to is in the response + getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, registration.RouteGetFlow, f.ID, returnTo) + getBody := testhelpers.EasyGetBody(t, client, getURL) + assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) + + // submit the flow but it is expired + u := public.URL + registration.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := client.PostForm(u, url.Values{"method": {"password"}, "csrf_token": {f.CSRFToken}, "password": {"password"}, "traits.email": {"email@ory.sh"}}) + require.NoError(t, err) + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + f, err = reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String())) + require.NoError(t, err) + assert.Equal(t, public.URL+registration.RouteInitBrowserFlow+"?return_to="+returnTo+"&identity_schema=email", f.RequestURL) + }) + + t.Run("case=not found", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + setupRegistrationUI(t, client) + + res, _ := testhelpers.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+x.NewUUID().String()) + assert.EqualValues(t, http.StatusNotFound, res.StatusCode) + }) +} + +// This test verifies that the password method is still executed even if the +// oidc strategy is ordered before the password strategy +// when submitting the form with both `method=password` and `provider=google`. +func TestOIDCStrategyOrder(t *testing.T) { + t.Logf("This test has been set up to validate the current incorrect `oidc` behaviour. When submitting the form, the `oidc` strategy is executed first, even if the method is set to `password`.") + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + // reorder the strategies + reg.WithSelfserviceStrategies(t, []any{ + oidc.NewStrategy(reg, oidc.ForCredentialType(identity.CredentialsTypeOIDC)), + password.NewStrategy(reg), + }) + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), + map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC), + map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), + map[string]interface{}{"passwordless_enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{ + { + ID: "google", + Provider: "google", + ClientID: "1234", + ClientSecret: "1234", + }, + }}) + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "", conf) + + setupRegistrationUI := func(t *testing.T, c *http.Client) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, ts.URL) + return ts + } + + t.Run("case=accept `password` method while including `provider:google`", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + + flow := gjson.GetBytes(body, "id").String() + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + email := faker.Email() + payload := json.RawMessage(`{"traits": {"email": "` + email + `"},"method": "password","password": "asdasdasdsa21312@#!@%","provider": "google","csrf_token": "` + csrfToken + `"}`) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, public.URL+registration.RouteSubmitFlow+"?flow="+flow, bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", ioutilx.MustReadAll(resp.Body)) + + verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.AddressTypeEmail, email) + require.NoError(t, err) + require.Equal(t, strings.ToLower(email), verifiableAddress.Value) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, verifiableAddress.IdentityID) + require.NoError(t, err) + require.NotNil(t, id.ID) + + _, ok := id.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok) + }) + + t.Run("case=accept oidc flow with just `provider:google`", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupRegistrationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + + flow := gjson.GetBytes(body, "id").String() + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + + payload := json.RawMessage(`{"provider": "google","csrf_token": "` + csrfToken + `"}`) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, public.URL+registration.RouteSubmitFlow+"?flow="+flow, bytes.NewBuffer(payload)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Containsf(t, + gjson.GetBytes(b, "error.reason").String(), + "In order to complete this flow please redirect the browser to: https://accounts.google.com/o/oauth2/v2/auth", + "%s", b, + ) + }) +} diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go new file mode 100644 index 0000000..f25c923 --- /dev/null +++ b/selfservice/flow/registration/hook.go @@ -0,0 +1,396 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +type ( + PreHookExecutor interface { + ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error + + PostHookPostPersistExecutor interface { + ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error + } + PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error + + PostHookPrePersistExecutor interface { + ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error + } + PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error + + HooksProvider interface { + PreRegistrationHooks(ctx context.Context) ([]PreHookExecutor, error) + PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookPrePersistExecutor, error) + PostRegistrationPostPersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookPostPersistExecutor, error) + } +) + +func ExecutorNames[T any](e []T) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func (f PreHookExecutorFunc) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + return f(w, r, a) +} + +func (f PostHookPostPersistExecutorFunc) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error { + return f(w, r, a, s) +} + +func (f PostHookPrePersistExecutorFunc) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error { + return f(w, r, a, i) +} + +type ( + executorDependencies interface { + config.Provider + identity.ManagementProvider + identity.PrivilegedPoolProvider + identity.ValidationProvider + login.FlowPersistenceProvider + login.StrategyProvider + session.PersistenceProvider + session.ManagementProvider + HooksProvider + FlowPersistenceProvider + hydra.Provider + nosurfx.CSRFTokenGeneratorProvider + httpx.ClientProvider + logrusx.Provider + httpx.WriterProvider + otelx.Provider + sessiontokenexchange.PersistenceProvider + } + HookExecutor struct { + d executorDependencies + } + HookExecutorProvider interface { + RegistrationExecutor() *HookExecutor + } +) + +func NewHookExecutor(d executorDependencies) *HookExecutor { + return &HookExecutor{d: d} +} + +// PostRegistrationHook finalizes a successful registration. The +// authMethod argument describes how the user authenticated during +// registration and is recorded on the resulting session verbatim. For +// OIDC registrations it carries the upstream acr/amr claims and the +// provider-computed AAL; for other strategies it is typically +// `{Method: strategy.ID(), AAL: AuthenticatorAssuranceLevel1}`. +func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, registrationFlow *Flow, i *identity.Identity, authMethod session.AuthenticationMethod) (err error) { + ct := authMethod.Method + ctx := r.Context() + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostRegistrationHook") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("Running PostRegistrationPrePersistHooks.") + preHooks, err := e.d.PostRegistrationPrePersistHooks(ctx, ct) + if err != nil { + return err + } + for k, executor := range preHooks { + if err := executor.ExecutePostRegistrationPrePersistHook(w, r, registrationFlow, i); err != nil { + if errors.Is(err, ErrHookAbortFlow) { + e.d.Logger(). + WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", ExecutorNames(preHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("A ExecutePostRegistrationPrePersistHook hook aborted early.") + return nil + } + + e.d.Logger(). + WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", ExecutorNames(preHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + WithError(err). + Error("ExecutePostRegistrationPostPersistHook hook failed with an error.") + + traits := i.Traits + return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d) + } + + e.d.Logger().WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", ExecutorNames(preHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("ExecutePostRegistrationPrePersistHook completed successfully.") + } + + // We need to make sure that the identity has a valid schema before passing it down to the identity pool. + if err := e.d.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + if registrationFlow.IsAccountLinking() && !e.d.Config().SelfServiceFlowRegistrationEnabled(ctx) { + // This flow was created internally from a login flow to link a new + // credential to an existing identity while registration is disabled. + // It must never create a new identity, so require that a conflicting + // identity exists before attempting to persist. The create below then + // fails with a unique violation, which triggers the account linking + // conversion back to a login flow. + if _, _, _, err := e.d.IdentityManager().ConflictingIdentity(ctx, i); err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + return errors.WithStack(ErrRegistrationDisabled()) + } + return err + } + } + + // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which + // would imply that the identity has to exist already. + if err := e.d.IdentityManager().Create(ctx, i); err != nil { + if errors.Is(err, sqlcon.ErrUniqueViolation()) { + strategy, err := e.d.AllLoginStrategies().Strategy(ct) + if err != nil { + return err + } + + if strategy, ok := strategy.(login.LinkableStrategy); ok { + duplicateIdentifier, err := e.getDuplicateIdentifier(ctx, i) + if err != nil { + return err + } + + if err := strategy.SetDuplicateCredentials( + registrationFlow, + duplicateIdentifier, + i.Credentials[ct], + authMethod.Provider, + ); err != nil { + return err + } + } + } + + // Restore submitted trait values to the flow UI so they are preserved in the error response. + for _, n := range container.NewFromJSON("", node.DefaultGroup, json.RawMessage(i.Traits), "traits").Nodes { + registrationFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + + return err + } + + // At this point the identity is already created and will not be rolled back, so + // we want all PostPersist hooks to be able to continue even when the client cancels the request. + ctx = context.WithoutCancel(ctx) + r = r.WithContext(ctx) + + // Verify the redirect URL before we do any other processing. + c := e.d.Config() + returnTo, err := redir.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(ctx), + redir.SecureRedirectReturnTo(registrationFlow.ReturnTo), + redir.SecureRedirectUseSourceURL(registrationFlow.RequestURL), + redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowRegistrationReturnTo(ctx, ct.String())), + ) + if err != nil { + return err + } + + span.SetAttributes(otelx.StringAttrs(map[string]string{ + "return_to": returnTo.String(), + "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.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + Info("A new identity has registered using self-service registration.") + + span.AddEvent(events.NewRegistrationSucceeded(ctx, registrationFlow.ID, i.ID, string(registrationFlow.Type), ct.String(), authMethod.Provider)) + + s := session.NewInactiveSession() + s.CompletedLoginForMethod(authMethod) + if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil { + return err + } + + // We persist the session here so that subsequent hooks (like verification) can use it. + if err := e.d.SessionPersister().UpsertSession(ctx, s); err != nil { + return err + } + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("Running PostRegistrationPostPersistHooks.") + postHooks, err := e.d.PostRegistrationPostPersistHooks(ctx, ct) + if err != nil { + return err + } + for k, executor := range postHooks { + if err := executor.ExecutePostRegistrationPostPersistHook(w, r, registrationFlow, 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", ExecutorNames(postHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("A ExecutePostRegistrationPostPersistHook hook aborted early.") + + span.SetAttributes(attribute.String("redirect_reason", "aborted by hook"), attribute.String("executor", fmt.Sprintf("%T", executor))) + + return nil + } + + e.d.Logger(). + WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", ExecutorNames(postHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + WithError(err). + Error("ExecutePostRegistrationPostPersistHook hook failed with an error.") + + span.SetAttributes(attribute.String("redirect_reason", "hook error"), attribute.String("executor", fmt.Sprintf("%T", executor))) + + traits := i.Traits + return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d) + } + + e.d.Logger().WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", ExecutorNames(postHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", ct). + Debug("ExecutePostRegistrationPostPersistHook completed successfully.") + } + + e.d.Logger(). + WithRequest(r). + WithField("flow_method", ct). + WithField("identity_id", i.ID). + Debug("Post registration execution hooks completed successfully.") + + if registrationFlow.Type == flow.TypeAPI || x.IsJSONRequest(r) { + span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI))) + + if registrationFlow.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, registrationFlow, s.ID, ct.ToUiNodeGroup()); err != nil { + return errors.WithStack(err) + } else if handled { + return nil + } + + e.d.Writer().Write(w, r, &APIFlowResponse{ + Identity: i, + ContinueWith: registrationFlow.ContinueWith(), + }) + return nil + } + + finalReturnTo := returnTo.String() + if registrationFlow.OAuth2LoginChallenge != "" { + if registrationFlow.ReturnToVerification != "" { + // Special case: If Kratos is used as a login UI *and* we want to show the verification UI, + // redirect to the verification URL first and then return to Hydra. + finalReturnTo = registrationFlow.ReturnToVerification + } else { + callbackURL, err := e.d.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(registrationFlow.OAuth2LoginChallenge), + IdentityID: i.ID.String(), + SessionID: s.ID.String(), + AuthenticationMethods: s.AMR, + }) + if err != nil { + return err + } + finalReturnTo = callbackURL + } + span.SetAttributes(attribute.String("redirect_reason", "oauth2 login challenge")) + } else if registrationFlow.ReturnToVerification != "" { + finalReturnTo = registrationFlow.ReturnToVerification + span.SetAttributes(attribute.String("redirect_reason", "verification requested")) + } + span.SetAttributes(attribute.String("return_to", finalReturnTo)) + + redir.ContentNegotiationRedirection(w, r, s.Declassified(), e.d.Writer(), finalReturnTo) + return nil +} + +func (e *HookExecutor) getDuplicateIdentifier(ctx context.Context, i *identity.Identity) (string, error) { + _, id, _, err := e.d.IdentityManager().ConflictingIdentity(ctx, i) + if err != nil { + return "", err + } + return id, nil +} + +func (e *HookExecutor) PreRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + hooks, err := e.d.PreRegistrationHooks(r.Context()) + if err != nil { + return err + } + for _, executor := range hooks { + if err := executor.ExecuteRegistrationPreHook(w, r, a); err != nil { + return err + } + } + + return nil +} diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go new file mode 100644 index 0000000..72b126a --- /dev/null +++ b/selfservice/flow/registration/hook_test.go @@ -0,0 +1,404 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration_test + +import ( + "context" + "fmt" + "net/http" + nethttptest "net/http/httptest" + "net/url" + "testing" + + "github.com/gobuffalo/httptest" + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func TestRegistrationExecutor(t *testing.T) { + t.Parallel() + ctx := context.Background() + + for _, strategy := range identity.AllCredentialTypes { + strategy := strategy.String() + + t.Run("strategy="+strategy, func(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.SetHydra(hydra.NewFake()) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL) + + newServer := func(t *testing.T, i *identity.Identity, ft flow.Type, flowCallbacks ...func(*registration.Flow)) *httptest.Server { + router := http.NewServeMux() + + handleErr := testhelpers.SelfServiceHookRegistrationErrorHandler + router.HandleFunc("GET /registration/pre", func(w http.ResponseWriter, r *http.Request) { + f, err := registration.NewFlow(reg, r, ft) + require.NoError(t, err) + if handleErr(t, w, r, reg.RegistrationHookExecutor().PreRegistrationHook(w, r, f)) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.HandleFunc("GET /registration/post", func(w http.ResponseWriter, r *http.Request) { + if i == nil { + i = testhelpers.SelfServiceHookFakeIdentity(t) + } + regFlow, err := registration.NewFlow(reg, r, ft) + require.NoError(t, err) + regFlow.RequestURL = x.RequestURL(r).String() + for _, callback := range flowCallbacks { + callback(regFlow) + } + _ = handleErr(t, w, r, reg.RegistrationHookExecutor().PostRegistrationHook(w, r, regFlow, i, session.AuthenticationMethod{ + Method: identity.CredentialsType(strategy), + AAL: identity.AuthenticatorAssuranceLevel1, + })) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + return ts + } + + makeRequestPost := testhelpers.SelfServiceMakeRegistrationPostHookRequest + viperSetPost := testhelpers.SelfServiceHookRegistrationViperSetPost + t.Run("method=PostRegistrationHook", func(t *testing.T) { + t.Run("case=pass without hooks", 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, false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, res.Request.URL.String()) + + 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 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{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.JSONEq(t, fmt.Sprintf(`[{"action":"redirect_browser_to","redirect_browser_to":"%s"}]`, returnToServer.URL), gjson.Get(body, "continue_with").Raw) + + 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(`{}`)}}) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, res.Request.URL.String()) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostRegistrationPrePersistHook": "abort"}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + + res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equal(t, "", body) + + _, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing) + require.Error(t, err) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL}) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{"return_to": {returnToServer.URL + "/kratos"}}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t, conf, strategy, returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL + "/kratos"}) + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectTo(t, conf, returnToServer.URL+"/not-kratos") + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t, conf, strategy, returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + 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(`{}`)}}) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL, res.Request.URL.String()) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeAPI), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) + + t.Run("case=pass without hooks for browser flow with application/json", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), true, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + assert.Empty(t, gjson.Get(body, "session.token")) + assert.Empty(t, gjson.Get(body, "session_token")) + }) + + t.Run("case=should redirect to verification UI if show_verification_ui hook is set", func(t *testing.T) { + verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg) + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + { + "hook": hook.KeyVerificationUI, + }, + }) + i := testhelpers.SelfServiceHookFakeIdentity(t) + i.Traits = identity.Traits(`{"email": "verifiable@ory.sh"}`) + + res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), verificationTS.URL) + assert.NotEmpty(t, res.Request.URL.Query().Get("flow")) + }) + + t.Run("case=should redirect to verification UI if there is a login_challenge", func(t *testing.T) { + verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg) + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{{ + "hook": hook.KeyVerificationUI, + }}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + i.Traits = identity.Traits(`{"email": "verifiable-valid-login_challenge@ory.sh"}`) + + withOAuthChallenge := func(f *registration.Flow) { + f.OAuth2LoginChallenge = hydra.FakeValidLoginChallenge + } + res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser, withOAuthChallenge), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), verificationTS.URL) + flowID := res.Request.URL.Query().Get("flow") + require.NotEmpty(t, flowID) + flow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, uuid.Must(uuid.FromString(flowID))) + require.NoError(t, err) + assert.Equal(t, hydra.FakeValidLoginChallenge, flow.OAuth2LoginChallenge.String()) + }) + + t.Run("case=should redirect to first verification UI if show_verification_ui hook is set and multiple verifiable addresses", func(t *testing.T) { + verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg) + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + { + "hook": hook.KeyVerificationUI, + }, + }) + + i := testhelpers.SelfServiceHookFakeIdentity(t) + i.SchemaID = testhelpers.UseIdentitySchema(t, conf, "file://./stub/registration-multi-email.schema.json") + i.Traits = identity.Traits(`{"emails": ["one@ory.sh", "two@ory.sh"]}`) + + res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{}) + require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), verificationTS.URL) + assert.NotEmpty(t, res.Request.URL.Query().Get("flow")) + }) + + t.Run("case=should still sent session if show_verification_ui is set after session hook", func(t *testing.T) { + verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg) + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + { + "hook": hook.KeyVerificationUI, + }, + { + "hook": hook.KeySessionIssuer, + }, + }) + + i := testhelpers.SelfServiceHookFakeIdentity(t) + i.Traits = identity.Traits(`{"email": "verifiable4@ory.sh"}`) + + jar := testhelpers.EasyCookieJar(t, nil) + s := newServer(t, i, flow.TypeBrowser) + s.Client().Jar = jar + res, _ := makeRequestPost(t, s, false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), verificationTS.URL) + assert.NotEmpty(t, res.Request.URL.Query().Get("flow")) + u, err := url.Parse(s.URL) + require.NoError(t, err) + cookies := jar.Cookies(u) + require.Len(t, cookies, 1) + assert.Equal(t, "ory_kratos_session", cookies[0].Name) + }) + }) + + t.Run("case=preserve trait values on duplicate credential error", func(t *testing.T) { + t.Parallel() + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + tosSchema := testhelpers.UseIdentitySchema(t, conf, "file://./stub/registration-tos.schema.json") + + // Create an existing identity with a known email so the next registration triggers a unique violation. + existing := &identity.Identity{ + SchemaID: tosSchema, + Traits: identity.Traits(`{"email":"duplicate@ory.sh"}`), + State: identity.StateActive, + } + existing.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"duplicate@ory.sh"}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$abc$def"}`), + }) + require.NoError(t, reg.IdentityManager().Create(context.Background(), existing)) + + // Build a new identity with the same email and tos=true. + duplicate := testhelpers.SelfServiceHookFakeIdentity(t) + duplicate.SchemaID = tosSchema + duplicate.Traits = identity.Traits(`{"email":"duplicate@ory.sh","tos":true}`) + duplicate.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"duplicate@ory.sh"}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$abc$def"}`), + }) + + regFlow, err := registration.NewFlow(reg, nethttptest.NewRequest("GET", returnToServer.URL, nil), flow.TypeBrowser) + require.NoError(t, err) + regFlow.RequestURL = returnToServer.URL + + // Add a checkbox node for traits.tos (simulating PopulateRegistrationMethod from the profile strategy). + regFlow.UI.Nodes.Upsert(node.NewInputField("traits.tos", false, node.DefaultGroup, node.InputAttributeTypeCheckbox)) + + err = reg.RegistrationHookExecutor().PostRegistrationHook( + nethttptest.NewRecorder(), nethttptest.NewRequest("GET", returnToServer.URL, nil), + regFlow, duplicate, session.AuthenticationMethod{ + Method: identity.CredentialsTypePassword, + AAL: identity.AuthenticatorAssuranceLevel1, + }, + ) + require.Error(t, err) + + // The TOS checkbox node must retain the submitted value (true) from the identity traits. + tosNode := regFlow.UI.Nodes.Find("traits.tos") + require.NotNil(t, tosNode, "traits.tos node must exist in the flow UI") + assert.Equal(t, true, tosNode.GetValue(), "traits.tos must be true after duplicate credential error") + }) + + for _, kind := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run("type="+string(kind)+"/method=PreRegistrationHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceRegistrationBeforeHooks, + testhelpers.SelfServiceMakeRegistrationPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, nil, kind) + }, + conf, + )) + } + }) + } +} + +func TestPostRegistrationHookAccountLinkingOnly(t *testing.T) { + t.Parallel() + ctx := t.Context() + + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.SetHydra(hydra.NewFake()) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, false) + + newIdentityWithEmail := func(email string) *identity.Identity { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email":"` + email + `"}`) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$fakefakefakefakefakefa"}`), + }) + return i + } + + runPostRegistrationHook := func(t *testing.T, i *identity.Identity) error { + r := httptest.NewRequest("GET", "/registration/post", nil) + r = r.WithContext(ctx) + regFlow, err := registration.NewFlow(reg, r, flow.TypeAPI) + require.NoError(t, err) + registration.WithFlowAccountLinking()(regFlow) + + return reg.RegistrationHookExecutor().PostRegistrationHook(httptest.NewRecorder(), r, regFlow, i, session.AuthenticationMethod{ + Method: identity.CredentialsTypePassword, + AAL: identity.AuthenticatorAssuranceLevel1, + }) + } + + t.Run("case=rejects a new identity when registration is disabled", func(t *testing.T) { + email := "linking-guard-new@ory.sh" + err := runPostRegistrationHook(t, newIdentityWithEmail(email)) + require.ErrorContains(t, err, registration.ErrRegistrationDisabled().Error()) + + _, _, err = reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, email) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + t.Run("case=proceeds into the duplicate conflict when an identity exists", func(t *testing.T) { + email := "linking-guard-existing@ory.sh" + existing := newIdentityWithEmail(email) + require.NoError(t, reg.IdentityManager().Create(ctx, existing)) + + err := runPostRegistrationHook(t, newIdentityWithEmail(email)) + // The guard must let the create attempt happen so that the unique + // violation triggers the account linking conversion. + require.ErrorIs(t, err, sqlcon.ErrUniqueViolation()) + }) +} diff --git a/selfservice/flow/registration/oragnizations.go b/selfservice/flow/registration/oragnizations.go new file mode 100644 index 0000000..58ae459 --- /dev/null +++ b/selfservice/flow/registration/oragnizations.go @@ -0,0 +1,29 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "net/http" + + "github.com/ory/kratos/selfservice/flow" +) + +var organizationFilter = []StrategyFilter{func(s Strategy) bool { + a, b := s.(flow.OrganizationImplementor) + return b && a.SupportsOrganizations() +}} + +func PrepareOrganizations(r *http.Request, f *Flow) []StrategyFilter { + if f.OrganizationID.Valid { + return organizationFilter + } + + orgID := flow.ParseOrganizationFromURLQuery(r.Context(), r.URL.Query()) + if !orgID.Valid { + return []StrategyFilter{} + } + + f.OrganizationID = orgID + return organizationFilter +} diff --git a/selfservice/flow/registration/organizations_test.go b/selfservice/flow/registration/organizations_test.go new file mode 100644 index 0000000..0ae4848 --- /dev/null +++ b/selfservice/flow/registration/organizations_test.go @@ -0,0 +1,56 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestPrepareOrganizations(t *testing.T) { + tests := []struct { + name string + flow *Flow + query url.Values + expectedFilter []StrategyFilter + }{ + { + name: "valid organization ID in flow", + flow: &Flow{OrganizationID: uuid.NullUUID{UUID: uuid.FromStringOrNil("123e4567-e89b-12d3-a456-426614174000"), Valid: true}}, + query: url.Values{}, + expectedFilter: organizationFilter, + }, + { + name: "valid organization ID in query", + flow: &Flow{}, + query: url.Values{"organization": {"123e4567-e89b-12d3-a456-426614174000"}}, + expectedFilter: organizationFilter, + }, + { + name: "invalid organization ID in query", + flow: &Flow{}, + query: url.Values{"organization": {"invalid-uuid"}}, + expectedFilter: []StrategyFilter{}, + }, + { + name: "missing organization ID", + flow: &Flow{}, + query: url.Values{}, + expectedFilter: []StrategyFilter{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &http.Request{URL: &url.URL{RawQuery: tt.query.Encode()}} + result := PrepareOrganizations(req.WithContext(context.Background()), tt.flow) + assert.Equal(t, tt.expectedFilter, result) + }) + } +} diff --git a/selfservice/flow/registration/persistence.go b/selfservice/flow/registration/persistence.go new file mode 100644 index 0000000..14ae943 --- /dev/null +++ b/selfservice/flow/registration/persistence.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type FlowPersister interface { + UpdateRegistrationFlow(context.Context, *Flow) error + CreateRegistrationFlow(context.Context, *Flow) error + GetRegistrationFlow(context.Context, uuid.UUID) (*Flow, error) + DeleteExpiredRegistrationFlows(context.Context, time.Time, int) error +} + +type FlowPersistenceProvider interface { + RegistrationFlowPersister() FlowPersister +} diff --git a/selfservice/flow/registration/session.go b/selfservice/flow/registration/session.go new file mode 100644 index 0000000..ed7047b --- /dev/null +++ b/selfservice/flow/registration/session.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" +) + +// The Response for Registration Flows via API +// +// swagger:model successfulNativeRegistration +type APIFlowResponse struct { + // The Session Token + // + // This field is only set when the session hook is configured as a post-registration hook. + // + // 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! + Token string `json:"session_token,omitempty"` + + // The Session + // + // This field is only set when the session hook is configured as a post-registration hook. + // + // The session contains information about the user, the session device, and so on. + // This is only available for API flows, not for Browser flows! + Session *session.Session `json:"session,omitempty"` + + // The Identity + // + // The identity that just signed up. + // + // required: true + Identity *identity.Identity `json:"identity"` + + // 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/registration/sort.go b/selfservice/flow/registration/sort.go new file mode 100644 index 0000000..0b25b38 --- /dev/null +++ b/selfservice/flow/registration/sort.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + + "github.com/ory/kratos/ui/node" +) + +func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { + return n.SortBySchema(ctx, + node.SortBySchema(schemaRef), + node.SortByGroups([]node.UiNodeGroup{ + node.OpenIDConnectGroup, + node.SAMLGroup, + node.DefaultGroup, + node.WebAuthnGroup, + node.PasskeyGroup, + node.CodeGroup, + node.PasswordGroup, + node.CaptchaGroup, + node.ProfileGroup, + }), + node.SortUpdateOrder(node.PasswordLoginOrder), + node.SortUseOrderAppend([]string{ + // WebAuthn + node.WebAuthnRemove, + node.WebAuthnRegisterDisplayName, + node.WebAuthnRegister, + }), + ) +} diff --git a/selfservice/flow/registration/sort_test.go b/selfservice/flow/registration/sort_test.go new file mode 100644 index 0000000..bf78968 --- /dev/null +++ b/selfservice/flow/registration/sort_test.go @@ -0,0 +1,44 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "embed" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/ui/node" + "github.com/ory/x/snapshotx" +) + +//go:embed fixtures/sort/* +var sortFixtures embed.FS + +func TestSortNodes(t *testing.T) { + ctx := context.Background() + + // TODO add more test cases. + entries, err := sortFixtures.ReadDir("fixtures/sort") + require.NoError(t, err) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + t.Run("case="+entry.Name(), func(t *testing.T) { + toSort, err := sortFixtures.ReadFile("fixtures/sort/" + entry.Name()) + require.NoError(t, err) + + var n node.Nodes + require.NoError(t, json.Unmarshal(toSort, &n)) + require.NoError(t, SortNodes(ctx, n, "file://fixtures/sort.schema.json")) + + snapshotx.SnapshotT(t, n) + }) + } +} diff --git a/selfservice/flow/registration/state.go b/selfservice/flow/registration/state.go new file mode 100644 index 0000000..be50551 --- /dev/null +++ b/selfservice/flow/registration/state.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import "github.com/ory/kratos/selfservice/flow" + +// State represents the state of this request: +// +// - 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. +// +// swagger:model registrationFlowState +type State = flow.State diff --git a/selfservice/flow/registration/strategy.go b/selfservice/flow/registration/strategy.go new file mode 100644 index 0000000..2a8df00 --- /dev/null +++ b/selfservice/flow/registration/strategy.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "context" + "net/http" + + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" +) + +type Strategy interface { + ID() identity.CredentialsType + NodeGroup() node.UiNodeGroup + Register(w http.ResponseWriter, r *http.Request, f *Flow, i *identity.Identity) (err error) +} + +type Strategies []Strategy + +func (s Strategies) Strategy(id identity.CredentialsType) (Strategy, error) { + ids := make([]identity.CredentialsType, len(s)) + for k, ss := range s { + ids[k] = ss.ID() + if ss.ID() == id { + return ss, nil + } + } + + return nil, errors.Errorf(`unable to find strategy for %s have %v`, id, ids) +} + +func (s Strategies) MustStrategy(id identity.CredentialsType) Strategy { + strategy, err := s.Strategy(id) + if err != nil { + panic(err) + } + return strategy +} + +type StrategyFilter func(strategy Strategy) bool + +type StrategyProvider interface { + RegistrationStrategies(ctx context.Context, filters ...StrategyFilter) Strategies + AllRegistrationStrategies() Strategies +} diff --git a/selfservice/flow/registration/strategy_form_hydrator.go b/selfservice/flow/registration/strategy_form_hydrator.go new file mode 100644 index 0000000..b1d0353 --- /dev/null +++ b/selfservice/flow/registration/strategy_form_hydrator.go @@ -0,0 +1,34 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "encoding/json" + stderr "errors" + "net/http" +) + +var ErrBreakRegistrationPopulate = stderr.New("skip rest of registration form population") + +type UnifiedFormHydrator interface { + PopulateRegistrationMethod(r *http.Request, sr *Flow) error +} + +type FormHydratorOptions struct { + WithTraits json.RawMessage +} + +type FormHydratorModifier func(o *FormHydratorOptions) + +func WithTraits(traits json.RawMessage) FormHydratorModifier { + return func(o *FormHydratorOptions) { + o.WithTraits = traits + } +} + +type FormHydrator interface { + UnifiedFormHydrator + PopulateRegistrationMethodCredentials(r *http.Request, sr *Flow, options ...FormHydratorModifier) error + PopulateRegistrationMethodProfile(r *http.Request, sr *Flow, options ...FormHydratorModifier) error +} diff --git a/selfservice/flow/registration/stub/fake-session.schema.json b/selfservice/flow/registration/stub/fake-session.schema.json new file mode 100644 index 0000000..bbcc9f4 --- /dev/null +++ b/selfservice/flow/registration/stub/fake-session.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/flow/registration/stub/identity.schema.json b/selfservice/flow/registration/stub/identity.schema.json new file mode 100644 index 0000000..10f59c4 --- /dev/null +++ b/selfservice/flow/registration/stub/identity.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + + } + } + } +} diff --git a/selfservice/flow/registration/stub/login.schema.json b/selfservice/flow/registration/stub/login.schema.json new file mode 100644 index 0000000..d358bd7 --- /dev/null +++ b/selfservice/flow/registration/stub/login.schema.json @@ -0,0 +1,17 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/registration/stub/registration-multi-email.schema.json b/selfservice/flow/registration/stub/registration-multi-email.schema.json new file mode 100644 index 0000000..442f278 --- /dev/null +++ b/selfservice/flow/registration/stub/registration-multi-email.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/flow/registration/stub/registration-tos.schema.json b/selfservice/flow/registration/stub/registration-tos.schema.json new file mode 100644 index 0000000..2eee540 --- /dev/null +++ b/selfservice/flow/registration/stub/registration-tos.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://example.com/registration-tos.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 + } + } + } + }, + "tos": { + "type": "boolean", + "title": "Accept Terms of Service" + } + } + } + } +} diff --git a/selfservice/flow/registration/stub/registration.phone.schema.json b/selfservice/flow/registration/stub/registration.phone.schema.json new file mode 100644 index 0000000..5813569 --- /dev/null +++ b/selfservice/flow/registration/stub/registration.phone.schema.json @@ -0,0 +1,33 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone-label": { + "type": "string" + }, + "phone": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "sms" + } + }, + "verification": { + "via": "sms" + } + } + } + } + } + } +} diff --git a/selfservice/flow/registration/stub/registration.schema.json b/selfservice/flow/registration/stub/registration.schema.json new file mode 100644 index 0000000..e591660 --- /dev/null +++ b/selfservice/flow/registration/stub/registration.schema.json @@ -0,0 +1,33 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/flow/registration/stub/updated.schema.json b/selfservice/flow/registration/stub/updated.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/registration/stub/updated.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/registration/test/persistence.go b/selfservice/flow/registration/test/persistence.go new file mode 100644 index 0000000..c21a3b1 --- /dev/null +++ b/selfservice/flow/registration/test/persistence.go @@ -0,0 +1,197 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/ui/node" + + "github.com/ory/x/assertx" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { + var clearids = func(r *registration.Flow) { + r.ID = uuid.UUID{} + } + + return func(t *testing.T) { + _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=should error when the registration flow does not exist", func(t *testing.T) { + _, err := p.GetRegistrationFlow(ctx, x.NewUUID()) + require.Error(t, err) + }) + + var newFlow = func(t *testing.T) *registration.Flow { + var r registration.Flow + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + return &r + } + + t.Run("case=should create a new registration flow and properly set IDs", func(t *testing.T) { + r := newFlow(t) + err := p.CreateRegistrationFlow(ctx, r) + require.NoError(t, err, "%#v", err) + + assert.NotEqual(t, uuid.Nil, r.ID) + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r registration.Flow + require.NoError(t, faker.FakeData(&r)) + require.NoError(t, p.CreateRegistrationFlow(ctx, &r)) + }) + + t.Run("case=should create and fetch a registration flow", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateRegistrationFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetRegistrationFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Active, actual.Active) + assertx.EqualAsJSON(t, expected.UI, actual.UI, "expected:\t%s\nactual:\t%s", expected.UI, actual.UI) + }) + + t.Run("case=should not cause data loss when updating a request without changes", func(t *testing.T) { + expected := newFlow(t) + expected.UI.Nodes = node.Nodes{} + expected.UI.Nodes.Append(node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + expected.UI.Nodes.Append(node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + err := p.CreateRegistrationFlow(ctx, expected) + require.NoError(t, err) + + expected.UI.Action = "/new-action" + expected.UI.Nodes.Append( + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + }))) + + expected.RequestURL = "/new-request-url" + require.NoError(t, p.UpdateRegistrationFlow(ctx, expected)) + + actual, err := p.GetRegistrationFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.Equal(t, "/new-action", actual.UI.Action) + assert.Equal(t, "/new-request-url", actual.RequestURL) + assertx.EqualAsJSON(t, node.Nodes{ + // v0.5: {Name: "zab", Type: "zab", Pattern: "zab"}, + node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + // v0.5: {Name: "zab", Type: "bar", Pattern: "baz"}, + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + })), + }, actual.UI.Nodes) + }) + + t.Run("case=should create and update and properly deal with internal context", func(t *testing.T) { + for k, tc := range []struct { + in []byte + expect string + }{ + {in: []byte("[]"), expect: "{}"}, + {expect: "{}"}, + {in: []byte("null"), expect: "{}"}, + {in: []byte(`{"foo":"bar"}`), expect: `{"foo":"bar"}`}, + } { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + r := newFlow(t) + r.InternalContext = tc.in + require.NoError(t, p.CreateRegistrationFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + + r.InternalContext = tc.in + require.NoError(t, p.UpdateRegistrationFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + }) + } + }) + + t.Run("case=network", func(t *testing.T) { + id := x.NewUUID() + nid, p := testhelpers.NewNetwork(t, ctx, p) + + t.Run("sets id on creation", func(t *testing.T) { + now := time.Now() + expected := ®istration.Flow{ID: id, ExpiresAt: now.Add(time.Hour), IssuedAt: now} + require.NoError(t, p.CreateRegistrationFlow(ctx, expected)) + assert.EqualValues(t, id, expected.ID) + assert.EqualValues(t, nid, expected.NID) + + actual, err := p.GetRegistrationFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not update id", func(t *testing.T) { + expected, err := p.GetRegistrationFlow(ctx, id) + require.NoError(t, err) + require.NoError(t, p.UpdateRegistrationFlow(ctx, expected)) + + actual, err := p.GetRegistrationFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + }) + + t.Run("can not update on another network", func(t *testing.T) { + expected, err := p.GetRegistrationFlow(ctx, id) + require.NoError(t, err) + + _, other := testhelpers.NewNetwork(t, ctx, p) + + expected.RequestURL = "updated" + require.Error(t, other.UpdateRegistrationFlow(ctx, expected)) + + _, err = other.GetRegistrationFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + actual, err := p.GetRegistrationFlow(ctx, id) + require.NoError(t, err) + require.NotEqual(t, "updated", actual.RequestURL) + }) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetRegistrationFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + } +} diff --git a/selfservice/flow/registration/testsetup_test.go b/selfservice/flow/registration/testsetup_test.go new file mode 100644 index 0000000..f7061fe --- /dev/null +++ b/selfservice/flow/registration/testsetup_test.go @@ -0,0 +1,20 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration_test + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" +) + +var returnToServer *httptest.Server + +func TestMain(m *testing.M) { + returnToServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + })) + os.Exit(m.Run()) +} diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go new file mode 100644 index 0000000..51fdc88 --- /dev/null +++ b/selfservice/flow/request.go @@ -0,0 +1,120 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "context" + _ "embed" + "net/http" + "strings" + + "github.com/ory/kratos/x/nosurfx" + + "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" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/nosurf" +) + +//go:embed .schema/method.schema.json +var methodSchema []byte + +func ErrOriginHeaderNeedsBrowserFlow() *herodot.DefaultError { + return herodot.ErrBadRequest(). + WithReasonf(`The HTTP Request Header included the "Origin" key, indicating that this request was made as part of an AJAX request in a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) +} + +func ErrCookieHeaderNeedsBrowserFlow() *herodot.DefaultError { + return herodot.ErrBadRequest(). + WithReasonf(`The HTTP Request Header included the "Cookie" key, indicating that this request was made by a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) +} + +func EnsureCSRF( + reg config.Provider, + r *http.Request, + flowType Type, + disableAPIFlowEnforcement bool, + generator func(r *http.Request) string, + actual string, +) error { + switch flowType { + case TypeAPI: + if disableAPIFlowEnforcement { + return nil + } + + // API Based flows to not require anti-CSRF tokens because we can not leverage a session, making this + // endpoint pointless. + + // Let's ensure that no-one mistakenly makes an AJAX request using the API flow. + if r.Header.Get("Origin") != "" { + return errors.WithStack(ErrOriginHeaderNeedsBrowserFlow()) + } + + // Workaround for Cloudflare setting cookies that we can't control. + // https://developers.cloudflare.com/fundamentals/reference/policies-compliances/cloudflare-cookies/ + var cookies []string + for _, c := range r.Cookies() { + if !strings.HasPrefix(c.Name, "__cf") && !strings.HasPrefix(c.Name, "_cf") && !strings.HasPrefix(c.Name, "cf_") { + cookies = append(cookies, c.Name) + } + } + + if len(cookies) > 0 { + return errors.WithStack(ErrCookieHeaderNeedsBrowserFlow().WithDetail("found cookies", cookies)) + } + + return nil + default: + if !nosurf.VerifyToken(generator(r), actual) { + return errors.WithStack(nosurfx.CSRFErrorReason(r, reg)) + } + } + + return nil +} + +func MethodEnabledAndAllowedFromRequest(r *http.Request, flow FlowName, expected string, d interface { + config.Provider +}, +) error { + var method struct { + Method string `json:"method" form:"method"` + } + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(methodSchema) + if err != nil { + return errors.WithStack(err) + } + + if err := decoderx.Decode(r, &method, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "PUT", "PATCH", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return errors.WithStack(err) + } + + return MethodEnabledAndAllowed(r.Context(), flow, expected, method.Method, d) +} + +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) + } + + if !d.Config().SelfServiceStrategy(ctx, expected).Enabled { + return errors.WithStack(herodot.ErrNotFound().WithReason(strategy.EndpointDisabledMessage)) + } + + return nil +} diff --git a/selfservice/flow/request_test.go b/selfservice/flow/request_test.go new file mode 100644 index 0000000..7332ac5 --- /dev/null +++ b/selfservice/flow/request_test.go @@ -0,0 +1,103 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/stretchr/testify/assert" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + + "github.com/stretchr/testify/require" +) + +func TestVerifyRequest(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + require.EqualError(t, flow.EnsureCSRF(reg, &http.Request{}, flow.TypeBrowser, false, nosurfx.FakeCSRFTokenGenerator, "not_csrf_token"), nosurfx.ErrInvalidCSRFToken().Error()) + require.NoError(t, flow.EnsureCSRF(reg, &http.Request{}, flow.TypeBrowser, false, nosurfx.FakeCSRFTokenGenerator, nosurfx.FakeCSRFToken), nil) + require.NoError(t, flow.EnsureCSRF(reg, &http.Request{}, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, "")) + require.EqualError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Origin": {"https://www.ory.sh"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), flow.ErrOriginHeaderNeedsBrowserFlow().Error()) + require.EqualError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"cookie=ory"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), flow.ErrCookieHeaderNeedsBrowserFlow().Error(), "should error because of cookie=ory") + + err := flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"cookie1=cookievalue", "cookie2=cookievalue"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, "") + var he herodot.DetailsCarrier + require.ErrorAs(t, err, &he) + cs, ok := he.Details()["found cookies"].([]string) + require.True(t, ok) + require.ElementsMatch(t, cs, []string{"cookie1", "cookie2"}) + + // Cloudflare + require.NoError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"__cflb=0pg1RtZzPoPDprTf8gX3TJm8XF5hKZ4pZV74UCe7", "_cfuvid=blub", "cf_clearance=bla"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), "should ignore Cloudflare cookies") + require.NoError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"__cflb=0pg1RtZzPoPDprTf8gX3TJm8XF5hKZ4pZV74UCe7; __cfruid=0pg1RtZzPoPDprTf8gX3TJm8XF5hKZ4pZV74UCe7"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), "should ignore Cloudflare cookies") + require.EqualError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"__cflb=0pg1RtZzPoPDprTf8gX3TJm8XF5hKZ4pZV74UCe7; __cfruid=0pg1RtZzPoPDprTf8gX3TJm8XF5hKZ4pZV74UCe7; some_cookie=some_value"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), flow.ErrCookieHeaderNeedsBrowserFlow().Error(), "should error because of some_cookie") + require.EqualError(t, flow.EnsureCSRF(reg, &http.Request{ + Header: http.Header{"Cookie": {"some_cookie=some_value"}}, + }, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), flow.ErrCookieHeaderNeedsBrowserFlow().Error(), "should error because of some_cookie") + require.NoError(t, flow.EnsureCSRF(reg, &http.Request{}, flow.TypeAPI, false, nosurfx.FakeCSRFTokenGenerator, ""), "no cookie, no error") +} + +func TestMethodEnabledAndAllowed(t *testing.T) { + ctx := context.Background() + conf, d := pkg.NewFastRegistryWithMocks(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := flow.MethodEnabledAndAllowedFromRequest(r, flow.LoginFlow, "password", d); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(ts.Close) + + t.Run("allowed", func(t *testing.T) { + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"password"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("unknown", func(t *testing.T) { + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"other"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "is not responsible for this request") + }) + + t.Run("disabled", func(t *testing.T) { + require.NoError(t, conf.Set(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, "password"), false)) + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"password"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "The requested resource could not be found") + }) +} diff --git a/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_API-description=success.json b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_API-description=success.json new file mode 100644 index 0000000..981faf3 --- /dev/null +++ b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_API-description=success.json @@ -0,0 +1,158 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "email", + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + } +] diff --git a/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_SPA-description=success.json b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_SPA-description=success.json new file mode 100644 index 0000000..981faf3 --- /dev/null +++ b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_SPA-description=success.json @@ -0,0 +1,158 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "email", + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + } +] diff --git a/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_browser-description=success.json b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_browser-description=success.json new file mode 100644 index 0000000..981faf3 --- /dev/null +++ b/selfservice/flow/settings/.snapshots/TestHandler-case=multi-schema_endpoint=init-description=init_a_flow_as_browser-description=success.json @@ -0,0 +1,158 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "email", + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + } +] diff --git a/selfservice/flow/settings/.snapshots/TestHandler-endpoint=init-description=init_a_flow_as_API-description=success.json b/selfservice/flow/settings/.snapshots/TestHandler-endpoint=init-description=init_a_flow_as_API-description=success.json new file mode 100644 index 0000000..981faf3 --- /dev/null +++ b/selfservice/flow/settings/.snapshots/TestHandler-endpoint=init-description=init_a_flow_as_API-description=success.json @@ -0,0 +1,158 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "email", + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + } +] diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go new file mode 100644 index 0000000..40b7fed --- /dev/null +++ b/selfservice/flow/settings/error.go @@ -0,0 +1,287 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + "net/http" + "net/url" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x/events" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/swagger" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/urlx" +) + +var ErrHookAbortFlow = errors.New("aborted settings hook execution") + +type ( + errorHandlerDependencies interface { + clock.Provider + config.Provider + errorx.ManagementProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + + HandlerProvider + FlowPersistenceProvider + schema.IdentitySchemaProvider + } + + ErrorHandlerProvider interface{ SettingsFlowErrorHandler() *ErrorHandler } + + ErrorHandler struct { + d errorHandlerDependencies + } +) + +// Is sent when a privileged session is required to perform the settings update. +// +// swagger:model needsPrivilegedSessionError +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type needsPrivilegedSessionError struct { + Error swagger.GenericError `json:"error"` + + // Points to where to redirect the user to next. + // + // required: true + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +// FlowNeedsReAuth is sent when a privileged session is required to perform the settings update. +type FlowNeedsReAuth struct { + *herodot.DefaultError `json:"error"` + + // Points to where to redirect the user to next. + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +func (e *FlowNeedsReAuth) EnhanceJSONError() interface{} { + return e +} + +func NewFlowNeedsReAuth() *FlowNeedsReAuth { + return &FlowNeedsReAuth{ + DefaultError: herodot.ErrForbidden().WithID(text.ErrIDNeedsPrivilegedSession). + WithReasonf("The login session is too old and thus not allowed to update these fields. Please re-authenticate."), + } +} + +func NewErrorHandler(d errorHandlerDependencies) *ErrorHandler { + return &ErrorHandler{d: d} +} + +func (s *ErrorHandler) reauthenticate( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + f *Flow, + err *FlowNeedsReAuth, +) { + returnTo := urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), r.URL.Path), r.URL.Query()) + + params := url.Values{} + params.Set("refresh", "true") + params.Set("return_to", returnTo.String()) + + redirectTo := urlx.AppendPaths(urlx.CopyWithQuery(s.d.Config().SelfPublicURL(ctx), params), login.RouteInitBrowserFlow).String() + err.RedirectBrowserTo = redirectTo + if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + + http.Redirect(w, r, redirectTo, http.StatusSeeOther) +} + +func (s *ErrorHandler) PrepareReplacementForExpiredFlow(ctx context.Context, w http.ResponseWriter, r *http.Request, f *Flow, id *identity.Identity, sess *session.Session, err error) (*flow.ExpiredError, error) { + e := new(flow.ExpiredError) + if !errors.As(err, &e) { + return nil, nil + } + + // create new flow because the old one is not valid + a, err := s.d.SettingsHandler().FromOldFlow(ctx, w, r, id, sess, *f) + if err != nil { + return nil, err + } + + a.UI.Messages.Add(text.NewErrorValidationSettingsFlowExpired(s.d.Clock(), e.ExpiredAt)) + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, a); err != nil { + return nil, err + } + + return e.WithFlow(a), nil +} + +func (s *ErrorHandler) WriteFlowError( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + group node.UiNodeGroup, + f *Flow, + id *identity.Identity, + sess *session.Session, + err error, +) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.ErrorHandler.WriteFlowError", + trace.WithAttributes( + attribute.String("error", err.Error()), + )) + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + logger := s.d.Logger(). + WithError(err). + WithRequest(r). + WithField("settings_flow", f.ToLoggerField()) + + logger.Info("Encountered self-service settings error.") + + shouldRespondWithJSON := x.IsJSONRequest(r) + if f != nil { + span.SetAttributes(attribute.String("flow_id", f.ID.String())) + if f.Type == flow.TypeAPI { + shouldRespondWithJSON = true + } + } + + if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) { + if shouldRespondWithJSON { + s.d.Writer().WriteError(w, r, err) + } else { + u := urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), login.RouteInitBrowserFlow) + http.Redirect(w, r, u.String(), http.StatusSeeOther) + } + return + } + + if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { + if shouldRespondWithJSON { + s.d.Writer().WriteError(w, r, aalErr) + } else { + http.Redirect(w, r, aalErr.RedirectTo, http.StatusSeeOther) + } + return + } + + if f == nil { + trace.SpanFromContext(ctx).AddEvent(events.NewSettingsFailed(ctx, uuid.Nil, "", "", err)) + s.forward(ctx, w, r, nil, err) + return + } + trace.SpanFromContext(ctx).AddEvent(events.NewSettingsFailed(ctx, f.ID, string(f.Type), f.Active.String(), err)) + + if expired, inner := s.PrepareReplacementForExpiredFlow(ctx, w, r, f, id, sess, err); inner != nil { + s.forward(ctx, w, r, f, err) + return + } else if expired != nil { + if id == nil { + s.forward(ctx, w, r, f, err) + return + } + + if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, expired) + } else { + http.Redirect(w, r, expired.GetFlow().AppendTo(s.d.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther) + } + return + } + + if errors.Is(err, flow.ErrStrategyAsksToReturnToUI) { + if shouldRespondWithJSON { + s.d.Writer().Write(w, r, f) + } else { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther) + } + return + } + + if e := new(FlowNeedsReAuth); errors.As(err, &e) { + s.reauthenticate(ctx, w, r, f, e) + return + } + + if err := f.UI.ParseError(group, err); err != nil { + s.forward(ctx, w, r, f, err) + return + } + + // Lookup the schema from the loaded configuration. This local schema + // URL is needed for sorting the UI nodes, instead of the public URL. + schemas, err := s.d.IdentityTraitsSchemas(ctx) + if err != nil { + s.forward(ctx, w, r, f, err) + return + } + + schema, err := schemas.GetByID(id.SchemaID) + if err != nil { + s.forward(ctx, w, r, f, err) + return + } + + if err := sortNodes(ctx, f.UI.Nodes, schema.RawURL); err != nil { + s.forward(ctx, w, r, f, err) + return + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil { + s.forward(ctx, w, r, f, err) + return + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther) + return + } + + updatedFlow, innerErr := s.d.SettingsFlowPersister().GetSettingsFlow(ctx, f.ID) + if innerErr != nil { + s.forward(ctx, w, r, updatedFlow, innerErr) + } + + s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), updatedFlow) +} + +func (s *ErrorHandler) forward(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *Flow, err error) { + if rr == nil { + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + s.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + if rr.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) + } else { + s.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + } +} diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go new file mode 100644 index 0000000..27c11d5 --- /dev/null +++ b/selfservice/flow/settings/error_test.go @@ -0,0 +1,420 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/go-faker/faker/v4" + "github.com/gobuffalo/httptest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "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/x/assertx" + "github.com/ory/x/clock" + "github.com/ory/x/contextx" + "github.com/ory/x/urlx" +) + +func TestHandleError(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + public, _ := testhelpers.NewKratosServer(t, reg) + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + h := reg.SettingsFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var settingsFlow *settings.Flow + var flowError error + var flowMethod node.UiNodeGroup + var id identity.Identity + require.NoError(t, faker.FakeData(&id)) + id.SchemaID = "default" + id.State = identity.StateActive + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &id)) + + req := httptest.NewRequest("GET", "/sessions/whoami", nil).WithContext(contextx.WithConfigValue(ctx, config.ViperKeySessionLifespan, time.Hour)) + + // This needs an authenticated client in order to call the RouteGetFlow endpoint + s, err := testhelpers.NewActiveSession(req, reg, &id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + c := testhelpers.NewHTTPClientWithSessionToken(ctx, t, reg, s) + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(ctx, w, r, flowMethod, settingsFlow, &id, s, flowError) + }) + + router.HandleFunc("GET /fake-redirect", func(w http.ResponseWriter, r *http.Request) { + _, _, err := reg.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser) + require.NoError(t, err) + }) + + reset := func() { + settingsFlow = nil + flowError = nil + flowMethod = node.DefaultGroup + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *settings.Flow { + req := &http.Request{URL: urlx.ParseOrPanic("/")} + f, err := settings.NewFlow(reg, req, &id, ft) + require.NoError(t, err) + + for _, s := range reg.SettingsStrategies(context.Background()) { + require.NoError(t, s.PopulateSettingsMethod(ctx, req, &id, f)) + } + + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(context.Background(), f)) + return f + } + + expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + expiredAnHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + flowMethod = settings.StrategyProfile + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, tc.t) + flowError = flow.NewFlowExpiredError(clock.New(), expiredAnHourAgo) + flowMethod = settings.StrategyProfile + + res, err := c.Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Contains(t, res.Request.URL.String(), ts.URL+"/error") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, http.StatusGone, res.StatusCode, "%+v\n\t%s", res.Request, body) + + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.GetBytes(body, "use_flow_id").String()) + // "since" and "error.reason" both derive from time.Since(at), recomputed when the expected error is + // built here — after the response was generated. Across a rounding boundary the two timings disagree, + // so exclude both from the exact comparison and verify the reason template separately. + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(clock.New(), expiredAnHourAgo), json.RawMessage(body), []string{"since", "error.reason", "redirect_browser_to", "use_flow_id"}) + assert.Regexp(t, `^The self-service flow expired \d+\.\d{2} minutes ago, initialize a new one\.$`, gjson.GetBytes(body, "error.reason").String()) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, settingsFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=return to UI error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = flow.ErrStrategyAsksToReturnToUI + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, settingsFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=no active session", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = errors.WithStack(session.NewErrNoActiveSessionFound()) + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusUnauthorized, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, session.NewErrNoActiveSessionFound().Reason(), gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=aal too low", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = errors.WithStack(session.NewErrAALNotSatisfied("a")) + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusForbidden, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assertx.EqualAsJSON(t, session.NewErrAALNotSatisfied("a"), json.RawMessage(body)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectSettingsUI := func(t *testing.T) (*settings.Flow, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowSettingsUI(ctx).String()+"?flow=") + + sf, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return sf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = &settings.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), expiredAnHourAgo) + flowMethod = settings.StrategyProfile + + lf, _ := expectSettingsUI(t) + require.Len(t, lf.UI.Messages, 1) + assert.Equal(t, int(text.ErrorValidationSettingsFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=return to ui error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = flow.ErrStrategyAsksToReturnToUI + flowMethod = settings.StrategyProfile + + lf, _ := expectSettingsUI(t) + assert.EqualValues(t, settingsFlow.ID, lf.ID) + }) + + t.Run("case=no active session error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = errors.WithStack(session.NewErrNoActiveSessionFound()) + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Contains(t, res.Request.URL.String(), loginTS.URL) + + lf, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + assert.Equal(t, identity.AuthenticatorAssuranceLevel1, lf.RequestedAAL) + }) + + t.Run("case=aal too low", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + settingsFlow.IdentityID = id.ID + flowError = errors.WithStack(session.NewErrAALNotSatisfied(conf.SelfServiceFlowLoginUI(ctx).String())) + flowMethod = settings.StrategyProfile + + client := &http.Client{} + *client = *ts.Client() + + // disable redirects + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + res, err := client.Get(ts.URL + "/error") + require.NoError(t, err) + + loc, err := res.Location() + require.NoError(t, err) + + // we should end up at the login URL since the NewALLNotSatisfied takes in a redirect to URL. + assert.Contains(t, loc.String(), loginTS.URL) + + // test the JSON resoponse as well + request, err := http.NewRequest("GET", ts.URL+"/error", nil) + require.NoError(t, err) + + request.Header.Add("Accept", "application/json") + + res, err = client.Do(request) + require.NoError(t, err) + + // the body should contain the reason for the error + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + require.NotEmpty(t, gjson.GetBytes(body, "error.reason").String(), "%s", body) + // We end up at the error endpoint with an aal2 error message because ts.client has no session. + assert.Equal(t, session.NewErrAALNotSatisfied("").Reason(), gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=session old error", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{urlx.AppendPaths(conf.SelfPublicURL(ctx), "/error").String()}) + t.Cleanup(reset) + + settingsFlow = &settings.Flow{Type: flow.TypeBrowser} + flowError = settings.NewFlowNeedsReAuth() + flowMethod = settings.StrategyProfile + + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + flowMethod = settings.StrategyProfile + + lf, _ := expectSettingsUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=inaccessible public URL", func(t *testing.T) { + t.Cleanup(reset) + + // Since WriteFlowError is invoked directly by the /error handler above, + // manipulate the schema's URL directly to a bad URL. + id.SchemaURL = "http://some.random.url" + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + flowMethod = settings.StrategyProfile + + lf, _ := expectSettingsUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + flowMethod = settings.StrategyProfile + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + }) +} diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go new file mode 100644 index 0000000..6d12199 --- /dev/null +++ b/selfservice/flow/settings/flow.go @@ -0,0 +1,261 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/redir" + "github.com/ory/pop/v6" + "github.com/ory/x/clock" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +// Flow represents a Settings Flow +// +// This flow is used when an identity wants to update settings +// (e.g. profile data, passwords, ...) in a selfservice manner. +// +// We recommend reading the [User Settings Documentation](../self-service/flows/user-settings) +// +// swagger:model settingsFlow +type Flow struct { + // ID represents the flow's unique ID. When performing the settings flow, this + // represents the id in the settings ui's query parameter: http://?flow= + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type flow.Type `json:"type" db:"type" faker:"flow_type"` + + // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting, + // a new flow has to be initiated. + // + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the flow occurred. + // + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // ReturnTo contains the requested return_to URL. + ReturnTo string `json:"return_to,omitempty" db:"-"` + + // Active, if set, contains the registration method that is being used. It is initially + // not set. + Active sqlxx.NullString `json:"active,omitempty" db:"active_method"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // Identity contains the identity's data in raw form. + // + // If `state` is `success` this will be the updated identity! + // + // required: true + Identity *identity.Identity `json:"identity" faker:"identity" db:"-" belongs_to:"identities" fk_id:"IdentityID"` + + // 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. + // + // required: true + State State `json:"state" faker:"-" db:"state"` + + // InternalContext stores internal context used by internals - for example MFA keys. + InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"` + + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"` + + // 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. + // + // 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 = (*Flow)(nil) + _ flow.InternalContexter = (*Flow)(nil) +) + +// flowDependencies are the dependencies NewFlow needs to construct a settings +// flow: the configuration (for the lifespan and return-to validation) and the +// clock. +type flowDependencies interface { + config.Provider + clock.Provider +} + +func MustNewFlow(reg flowDependencies, r *http.Request, i *identity.Identity, ft flow.Type) *Flow { + f, err := NewFlow(reg, r, i, ft) + if err != nil { + panic(err) + } + return f +} + +func NewFlow(reg flowDependencies, r *http.Request, i *identity.Identity, ft flow.Type) (*Flow, error) { + conf := reg.Config() + now := reg.Clock().Now().UTC() + exp := conf.SelfServiceFlowSettingsFlowLifespan(r.Context()) + id := x.NewUUID() + + // Pre-validate the return to URL which is contained in the HTTP request. + requestURL := x.RequestURL(r).String() + _, err := redir.SecureRedirectTo(r, + conf.SelfServiceBrowserDefaultReturnTo(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + return nil, err + } + + f := &Flow{ + ID: id, + ExpiresAt: now.Add(exp), + IssuedAt: now, + RequestURL: requestURL, + IdentityID: i.ID, + Identity: i, + Type: ft, + State: flow.StateShowForm, + UI: &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), + }, + InternalContext: []byte("{}"), + } + if err := flow.SetRequestBaseURL(f, x.BaseURLStringFromContext(r.Context())); err != nil { + return nil, err + } + return f, nil +} + +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext } +func (f *Flow) SetInternalContext(message sqlxx.JSONRawMessage) { f.InternalContext = message } +func (f *Flow) GetType() flow.Type { return f.Type } +func (f *Flow) GetRequestURL() string { return f.RequestURL } +func (Flow) TableName() string { return "selfservice_settings_flows" } +func (f Flow) GetID() uuid.UUID { return f.ID } +func (f *Flow) AppendTo(src *url.URL) *url.URL { return flow.AppendFlowTo(src, f.ID) } +func (f *Flow) GetUI() *container.Container { return f.UI } +func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems } +func (f *Flow) GetState() State { return f.State } +func (Flow) GetFlowName() flow.FlowName { return flow.SettingsFlow } +func (f *Flow) SetState(state State) { f.State = state } +func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload } + +func (f *Flow) Valid(c clock.Clock, s *session.Session) error { + if f.ExpiresAt.Before(c.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + + if f.IdentityID != s.Identity.ID { + 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 +} + +func (f *Flow) EnsureInternalContext() { + if !gjson.ParseBytes(f.InternalContext).IsObject() { + f.InternalContext = []byte("{}") + } +} + +func (f Flow) MarshalJSON() ([]byte, error) { + type local Flow + f.SetReturnTo() + return json.Marshal(local(f)) +} + +func (f *Flow) SetReturnTo() { + // Return to is already set, do not overwrite it. + if len(f.ReturnTo) > 0 { + return + } + if u, err := url.Parse(f.RequestURL); err == nil { + f.ReturnTo = u.Query().Get("return_to") + } +} + +func (f *Flow) AfterFind(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AfterSave(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AddContinueWith(c flow.ContinueWith) { + f.ContinueWithItems = append(f.ContinueWithItems, c) +} + +func (f *Flow) ToLoggerField() map[string]any { + if f == nil { + return map[string]any{} + } + return map[string]any{ + "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/flow_test.go b/selfservice/flow/settings/flow_test.go new file mode 100644 index 0000000..a4a0f79 --- /dev/null +++ b/selfservice/flow/settings/flow_test.go @@ -0,0 +1,187 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + + "github.com/ory/kratos/pkg" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/urlx" + + "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/x" +) + +func TestFakeFlow(t *testing.T) { + var r settings.Flow + require.NoError(t, faker.FakeData(&r)) + + assert.Equal(t, uuid.Nil, r.ID) + assert.NotEmpty(t, r.IssuedAt) + assert.NotEmpty(t, r.ExpiresAt) + assert.NotEmpty(t, r.RequestURL) + assert.NotEmpty(t, r.Active) +} + +func TestNewFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + id := &identity.Identity{ID: x.NewUUID()} + t.Run("case=0", func(t *testing.T) { + r, err := settings.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("/"), + Host: "ory.sh", TLS: &tls.ConnectionState{}}, id, flow.TypeBrowser) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeBrowser, r.Type) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) + + t.Run("type=return_to", func(t *testing.T) { + _, err := settings.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, id, flow.TypeBrowser) + require.Error(t, err) + + _, err = settings.NewFlow(reg, &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"}, id, flow.TypeBrowser) + require.NoError(t, err) + }) + + t.Run("case=1", func(t *testing.T) { + r, err := settings.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("/?refresh=true"), + Host: "ory.sh"}, id, flow.TypeAPI) + + require.NoError(t, err) + assert.True(t, r.ExpiresAt.After(r.IssuedAt)) + assert.Equal(t, flow.TypeAPI, r.Type) + assert.Equal(t, "http://ory.sh/?refresh=true", r.RequestURL) + }) + + t.Run("case=2", func(t *testing.T) { + r, err := settings.NewFlow(reg, &http.Request{ + URL: urlx.ParseOrPanic("https://ory.sh/"), + Host: "ory.sh"}, id, flow.TypeBrowser) + + require.NoError(t, err) + assert.Equal(t, "https://ory.sh/", r.RequestURL) + }) +} + +func TestFlow(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + alice := x.NewUUID() + malice := x.NewUUID() + for k, tc := range []struct { + r *settings.Flow + exp time.Duration + s *session.Session + expectErr bool + }{ + { + r: settings.MustNewFlow( + reg, + &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, + &identity.Identity{ID: alice}, + flow.TypeBrowser), + exp: time.Hour, + + s: &session.Session{Identity: &identity.Identity{ID: alice}}, + }, + { + r: settings.MustNewFlow( + reg, + &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, + &identity.Identity{ID: alice}, + flow.TypeBrowser), + exp: time.Hour, + + s: &session.Session{Identity: &identity.Identity{ID: malice}}, + expectErr: true, + }, + { + r: settings.MustNewFlow( + reg, + &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, + &identity.Identity{ID: alice}, + flow.TypeBrowser), + exp: -time.Hour, + + s: &session.Session{Identity: &identity.Identity{ID: alice}}, + expectErr: true, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + tc.r.ExpiresAt = tc.r.IssuedAt.Add(tc.exp) + err := tc.r.Valid(clock.New(), tc.s) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestGetType(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=%s", ft), func(t *testing.T) { + r := &settings.Flow{Type: ft} + assert.Equal(t, ft, r.GetType()) + }) + } +} + +func TestGetRequestURL(t *testing.T) { + expectedURL := "http://foo/bar/baz" + f := &settings.Flow{RequestURL: expectedURL} + assert.Equal(t, expectedURL, f.GetRequestURL()) +} + +func TestEnsureInternalContext(t *testing.T) { + f := new(settings.Flow) + f.EnsureInternalContext() + assert.Equal(t, "{}", string(f.InternalContext)) +} + +func TestFlowEncodeJSON(t *testing.T) { + assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, &settings.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, &settings.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, settings.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) +} + +func TestFlowDontOverrideReturnTo(t *testing.T) { + f := &settings.Flow{ReturnTo: "/foo"} + f.SetReturnTo() + assert.Equal(t, "/foo", f.ReturnTo) + + f = &settings.Flow{RequestURL: "https://foo.bar?return_to=/bar"} + f.SetReturnTo() + assert.Equal(t, "/bar", f.ReturnTo) +} diff --git a/selfservice/flow/settings/handler.go b/selfservice/flow/settings/handler.go new file mode 100644 index 0000000..8198fdd --- /dev/null +++ b/selfservice/flow/settings/handler.go @@ -0,0 +1,708 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + "net/http" + "net/url" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "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/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" +) + +const ( + RouteInitBrowserFlow = "/self-service/settings/browser" + RouteInitAPIFlow = "/self-service/settings/api" + RouteGetFlow = "/self-service/settings/flows" + + RouteSubmitFlow = "/self-service/settings" + + // RouteWellKnownChangePassword implements the W3C "change password URL" + // well-known location. Browsers and password managers use it to deep-link + // users to the page where they can change their password. + // + // See https://w3c.github.io/webappsec-change-password-url/. + RouteWellKnownChangePassword = "/.well-known/change-password" + + ContinuityPrefix = "ory_kratos_settings" +) + +func ContinuityKey(id string) string { + return ContinuityPrefix + "_" + id +} + +type ( + handlerDependencies interface { + clock.Provider + nosurfx.CSRFProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + + config.Provider + + session.HandlerProvider + session.ManagementProvider + + identity.ValidationProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + + errorx.ManagementProvider + + continuity.ManagementProvider + + x.CookieProvider + + ErrorHandlerProvider + FlowPersistenceProvider + StrategyProvider + HookExecutorProvider + nosurfx.CSRFTokenGeneratorProvider + + schema.IdentitySchemaProvider + + login.HandlerProvider + } + HandlerProvider interface { + SettingsHandler() *Handler + } + Handler struct { + d handlerDependencies + csrf nosurfx.CSRFToken + } +) + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d, csrf: nosurf.Token} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) + h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) + + public.GET(RouteInitBrowserFlow, h.d.SessionHandler().IsAuthenticated(h.createBrowserSettingsFlow, func(w http.ResponseWriter, r *http.Request) { + if x.IsJSONRequest(r) { + h.d.Writer().WriteError(w, r, session.NewErrNoActiveSessionFound()) + } else { + loginFlowUrl := h.d.Config().SelfPublicURL(r.Context()).JoinPath(login.RouteInitBrowserFlow).String() + redirectUrl, err := redir.TakeOverReturnToParameter(r.URL.String(), loginFlowUrl) + if err != nil { + http.Redirect(w, r, h.d.Config().SelfServiceFlowLoginUI(r.Context()).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, redirectUrl, http.StatusSeeOther) + } + } + })) + + public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsAuthenticated(h.createNativeSettingsFlow, nil)) + public.GET(RouteGetFlow, h.d.SessionHandler().IsAuthenticated(h.getSettingsFlow, OnUnauthenticated(h.d))) + + public.POST(RouteSubmitFlow, h.d.SessionHandler().IsAuthenticated(h.updateSettingsFlow, OnUnauthenticated(h.d))) + public.GET(RouteSubmitFlow, h.d.SessionHandler().IsAuthenticated(h.updateSettingsFlow, OnUnauthenticated(h.d))) + + public.GET(RouteWellKnownChangePassword, h.redirectToChangePassword) +} + +// swagger:route GET /.well-known/change-password frontend getWellKnownChangePassword +// +// # Change Password URL +// +// This endpoint implements the W3C "change password URL" well-known location by +// redirecting the browser to the configured settings UI. Password managers follow +// this redirect to take users straight to the page where they can change their +// password. +// +// Schemes: http, https +// +// Responses: +// 303: emptyResponse +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) redirectToChangePassword(w http.ResponseWriter, r *http.Request) { + to := h.d.Config().SelfServiceFlowSettingsUI(r.Context()).String() + if to == "" { + to = h.d.Config().SelfPublicURL(r.Context()).JoinPath(RouteInitBrowserFlow).String() + } + http.Redirect(w, r, to, http.StatusSeeOther) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + + admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) + + admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) +} + +func (h *Handler) NewFlow(ctx context.Context, w http.ResponseWriter, r *http.Request, i *identity.Identity, s *session.Session, ft flow.Type) (_ *Flow, err error) { + ctx, span := h.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.Handler.NewFlow") + defer otelx.End(span, &err) + + f, err := NewFlow(h.d, r, i, ft) + if err != nil { + return nil, err + } + + if err := h.d.SettingsHookExecutor().PreSettingsHook(ctx, w, r, f, s); err != nil { + return nil, err + } + + filters := PrepareOrganizations(r, f, i, h.d.Config().Organizations(ctx)) + + cookieStore := continuity.NewCookieReferenceStore(h.d.ContinuityCookieManager(ctx)) + for _, strategy := range h.d.SettingsStrategies(ctx, filters...) { + if err := h.d.ContinuityManager().Abort(ctx, w, r, ContinuityKey(strategy.SettingsStrategyID()), cookieStore); err != nil { + return nil, err + } + + if err := strategy.PopulateSettingsMethod(ctx, r, i, f); err != nil { + return nil, err + } + } + + ds, err := h.d.Config().IdentityTraitsSchemaURL(ctx, i.SchemaID) + if err != nil { + return nil, err + } + + if err := sortNodes(r.Context(), f.UI.Nodes, ds.String()); err != nil { + return nil, err + } + + if err := h.d.SettingsFlowPersister().CreateSettingsFlow(r.Context(), f); err != nil { + return nil, err + } + + return f, nil +} + +func (h *Handler) FromOldFlow(ctx context.Context, w http.ResponseWriter, r *http.Request, i *identity.Identity, s *session.Session, of Flow) (*Flow, error) { + nf, err := h.NewFlow(ctx, w, r, i, s, of.Type) + if err != nil { + return nil, err + } + + nf.RequestURL = of.RequestURL + return nf, nil +} + +// Create Native Settings Flow Parameters +// +// swagger:parameters createNativeSettingsFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createNativeSettingsFlow struct { + // The Session Token of the Identity performing the settings flow. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // An optional organization ID that scopes the settings flow to providers of that organization. + // This parameter is only effective in the Ory Network. + // + // required: false + // in: query + Organization string `json:"organization"` +} + +// swagger:route GET /self-service/settings/api frontend createNativeSettingsFlow +// +// # Create Settings Flow for Native Apps +// +// This endpoint initiates a settings flow for API clients such as mobile devices, smart TVs, and so on. +// You must provide a valid Ory Kratos Session Token for this endpoint to respond with HTTP 200 OK. +// +// To fetch an existing settings flow call `/self-service/settings/flows?flow=`. +// +// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server +// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make +// you vulnerable to a variety of CSRF attacks. +// +// Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator +// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn +// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user +// to sign in with the second factor or change the configuration. +// +// In the case of an error, the `error.id` of the JSON response body can be one of: +// +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `session_inactive`: No Ory Session was found - sign in a user first. +// +// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). +// +// More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). +// +// Schemes: http, https +// +// Responses: +// 200: settingsFlow +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createNativeSettingsFlow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s, err := h.d.SessionManager().FetchFromRequestContext(ctx, r) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if err := h.d.SessionManager().DoesSessionSatisfy(ctx, s, h.d.Config().SelfServiceSettingsRequiredAAL(ctx)); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + f, err := h.NewFlow(ctx, w, r, s.Identity, s, flow.TypeAPI) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, f) +} + +// Create Browser Settings Flow Parameters +// +// swagger:parameters createBrowserSettingsFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserSettingsFlow struct { + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` + + // An optional organization ID that scopes the settings flow to providers of that organization. + // This parameter is only effective in the Ory Network. + // + // required: false + // in: query + Organization string `json:"organization"` +} + +// swagger:route GET /self-service/settings/browser frontend createBrowserSettingsFlow +// +// # Create Settings Flow for Browsers +// +// This endpoint initializes a browser-based user settings flow. Once initialized, the browser will be redirected to +// `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid +// Ory Kratos Session Cookie is included in the request, a login flow will be initialized. +// +// If this endpoint is opened as a link in the browser, it will be redirected to +// `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid user session +// was set, the browser will be redirected to the login endpoint. +// +// If this endpoint is called via an AJAX request, the response contains the settings flow without any redirects +// or a 401 forbidden error if no valid session was set. +// +// Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator +// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn +// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user +// to sign in with the second factor (happens automatically for server-side browser flows) or change the configuration. +// +// If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `session_inactive`: No Ory Session was found - sign in a user first. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// +// This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. +// +// More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). +// +// Schemes: http, https +// +// Responses: +// 200: settingsFlow +// 303: emptyResponse +// 400: errorGeneric +// 401: errorGeneric +// 403: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserSettingsFlow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s, err := h.d.SessionManager().FetchFromRequestContext(ctx, r) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + var managerOptions []session.ManagerOptions + requestURL := x.RequestURL(r) + if requestURL.Query().Get("return_to") != "" { + managerOptions = append(managerOptions, session.WithRequestURL(requestURL.String())) + } + + if err := h.d.SessionManager().DoesSessionSatisfy(ctx, s, h.d.Config().SelfServiceSettingsRequiredAAL(ctx), managerOptions...); err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, nil, nil, nil, err) + return + } + + f, err := h.NewFlow(ctx, w, r, s.Identity, s, flow.TypeBrowser) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + redirTo := f.AppendTo(h.d.Config().SelfServiceFlowSettingsUI(ctx)).String() + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), f, redirTo) +} + +// Get Settings Flow +// +// swagger:parameters getSettingsFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getSettingsFlow struct { + // ID is the Settings Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/settings?flow=abcde`). + // + // required: true + // in: query + ID string `json:"id"` + + // The Session Token + // + // When using the SDK in an app without a browser, please include the + // session token here. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// swagger:route GET /self-service/settings/flows frontend getSettingsFlow +// +// # Get Settings Flow +// +// When accessing this endpoint through Ory Kratos' Public API you must ensure that either the Ory Kratos Session Cookie +// or the Ory Kratos Session Token are set. +// +// Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator +// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn +// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user +// to sign in with the second factor or change the configuration. +// +// You can access this endpoint without credentials when using Ory Kratos' Admin API. +// +// If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `session_inactive`: No Ory Session was found - sign in a user first. +// - `security_identity_mismatch`: The flow was interrupted with `session_refresh_required` but apparently some other +// identity logged in instead. +// +// More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: settingsFlow +// 401: errorGeneric +// 403: errorGeneric +// 404: errorGeneric +// 410: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) getSettingsFlow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + rid := x.ParseUUID(r.URL.Query().Get("id")) + pr, err := h.d.SettingsFlowPersister().GetSettingsFlow(ctx, rid) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + sess, err := h.d.SessionManager().FetchFromRequestContext(ctx, r) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if pr.IdentityID != sess.Identity.ID { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden(). + WithID(text.ErrIDInitiatedBySomeoneElse). + WithReasonf("The request was made for another identity and has been blocked for security reasons."))) + return + } + + // we cannot redirect back to the request URL (/self-service/settings/flows?id=...) since it would just redirect + // to a page displaying raw JSON to the client (browser), which is not what we want. + // Let's rather carry over the flow ID as a query parameter and redirect to the settings UI URL. + requestURL := urlx.CopyWithQuery(h.d.Config().SelfServiceFlowSettingsUI(ctx), url.Values{"flow": {rid.String()}}) + if err := h.d.SessionManager().DoesSessionSatisfy(ctx, sess, h.d.Config().SelfServiceSettingsRequiredAAL(ctx), session.WithRequestURL(requestURL.String())); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if pr.ExpiresAt.Before(h.d.Clock().Now().UTC()) { + if pr.Type == flow.TypeBrowser { + redirectURL := flow.GetFlowExpiredRedirectURL(ctx, h.d.Config(), RouteInitBrowserFlow, pr.ReturnTo) + + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). + WithReason("The settings flow has expired. Redirect the user to the settings flow init endpoint to initialize a new settings flow."). + WithDetail("redirect_to", redirectURL.String()). + WithDetail("return_to", pr.ReturnTo))) + return + } + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). + WithReason("The settings flow has expired. Call the settings flow init API endpoint to initialize a new settings flow."). + WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(ctx), RouteInitAPIFlow).String()))) + return + } + + h.d.Writer().Write(w, r, pr) +} + +// Update Settings Flow Parameters +// +// swagger:parameters updateSettingsFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateSettingsFlow struct { + // The Settings Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/settings?flow=abcde`). + // + // required: true + // in: query + Flow string `json:"flow"` + + // in: body + // required: true + Body updateSettingsFlowBody + + // The Session Token of the Identity performing the settings flow. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// Update Settings Flow Request Body +// +// swagger:model updateSettingsFlowBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateSettingsFlowBody struct{} + +// swagger:route POST /self-service/settings frontend updateSettingsFlow +// +// # Complete Settings Flow +// +// Use this endpoint to complete a settings flow by sending an identity's updated password. This endpoint +// behaves differently for API and browser flows. +// +// API-initiated flows expect `application/json` to be sent in the body and respond with +// - HTTP 200 and an application/json body with the session token on success; +// - HTTP 303 redirect to a fresh settings flow if the original flow expired with the appropriate error messages set; +// - HTTP 400 on form validation errors. +// - HTTP 401 when the endpoint is called without a valid session token. +// - HTTP 403 when `selfservice.flows.settings.privileged_session_max_age` was reached or the session's AAL is too low. +// Implies that the user needs to re-authenticate. +// +// Browser flows without HTTP Header `Accept` or with `Accept: text/*` respond with +// - a HTTP 303 redirect to the post/after settings URL or the `return_to` value if it was set and if the flow succeeded; +// - a HTTP 303 redirect to the Settings UI URL with the flow ID containing the validation errors otherwise. +// - a HTTP 303 redirect to the login endpoint when `selfservice.flows.settings.privileged_session_max_age` was reached or the session's AAL is too low. +// +// Browser flows with HTTP Header `Accept: application/json` respond with +// - HTTP 200 and a application/json body with the signed in identity and a `Set-Cookie` header on success; +// - HTTP 303 redirect to a fresh login flow if the original flow expired with the appropriate error messages set; +// - HTTP 401 when the endpoint is called without a valid session cookie. +// - HTTP 403 when the page is accessed without a session cookie or the session's AAL is too low. +// - HTTP 400 on form validation errors. +// +// Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator +// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn +// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user +// to sign in with the second factor (happens automatically for server-side browser flows) or change the configuration. +// +// If this endpoint is called with a `Accept: application/json` HTTP header, the response contains the flow without a redirect. In the +// case of an error, the `error.id` of the JSON response body can be one of: +// +// - `session_refresh_required`: The identity requested to change something that needs a privileged session. Redirect +// the identity to the login init endpoint with query parameters `?refresh=true&return_to=`, +// or initiate a refresh login flow otherwise. +// - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. +// - `session_inactive`: No Ory Session was found - sign in a user first. +// - `security_identity_mismatch`: The flow was interrupted with `session_refresh_required` but apparently some other +// identity logged in instead. +// - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! +// - `browser_location_change_required`: Usually sent when an AJAX request indicates that the browser needs to open a specific URL. +// 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). +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Security: +// sessionToken: +// +// Schemes: http, https +// +// Responses: +// 200: settingsFlow +// 303: emptyResponse +// 400: settingsFlow +// 401: errorGeneric +// 403: errorGeneric +// 410: errorGeneric +// 422: errorBrowserLocationChangeRequired +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) updateSettingsFlow(w http.ResponseWriter, r *http.Request) { + var ( + err error + ctx = r.Context() + ) + + ctx, span := h.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.Handler.updateSettingsFlow") + defer otelx.End(span, &err) + + rid, err := GetFlowID(r) + if err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, nil, nil, nil, err) + return + } + + f, err := h.d.SettingsFlowPersister().GetSettingsFlow(ctx, rid) + if errors.Is(err, sqlcon.ErrNoRows()) { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, nil, nil, nil, errors.WithStack(herodot.ErrNotFound().WithReasonf("The settings request could not be found. Please restart the flow."))) + return + } else if err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, nil, nil, nil, err) + return + } + + ss, err := h.d.SessionManager().FetchFromRequestContext(ctx, r) + if err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, nil, nil, err) + return + } + + requestURL := x.RequestURL(r).String() + if err := h.d.SessionManager().DoesSessionSatisfy(ctx, ss, h.d.Config().SelfServiceSettingsRequiredAAL(ctx), session.WithRequestURL(requestURL)); err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, nil, nil, err) + return + } + + if err := f.Valid(h.d.Clock(), ss); err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, ss.Identity, ss, err) + return + } + + var s string + var updateContext *UpdateContext + for _, strat := range h.d.AllSettingsStrategies() { + uc, err := strat.Settings(ctx, w, r, f, ss) + if errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, strat.NodeGroup(), f, ss.Identity, ss, err) + return + } + + s = strat.SettingsStrategyID() + updateContext = uc + break + } + + if updateContext == nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, ss.Identity, ss, errors.WithStack(schema.NewNoSettingsStrategyResponsible())) + return + } + + i, err := updateContext.GetIdentityToUpdate() + if err != nil { + // An identity to update must always be present. + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, ss.Identity, ss, err) + return + } + + if err := h.d.SettingsHookExecutor().PostSettingsHook(ctx, w, r, s, updateContext, i); err != nil { + h.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, node.DefaultGroup, f, ss.Identity, ss, err) + return + } +} diff --git a/selfservice/flow/settings/handler_change_password_test.go b/selfservice/flow/settings/handler_change_password_test.go new file mode 100644 index 0000000..a13e8d7 --- /dev/null +++ b/selfservice/flow/settings/handler_change_password_test.go @@ -0,0 +1,33 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" +) + +func TestWellKnownChangePassword(t *testing.T) { + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsURL, "https://ui.example.com/settings") + + public, _ := testhelpers.NewKratosServer(t, reg) + + hc := &http.Client{Transport: testhelpers.NewTestTransport(t), CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} + res, err := hc.Get(public.URL + "/.well-known/change-password") + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + + assert.Equal(t, http.StatusSeeOther, res.StatusCode) + assert.Equal(t, "https://ui.example.com/settings", res.Header.Get("Location")) +} diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go new file mode 100644 index 0000000..67c68b2 --- /dev/null +++ b/selfservice/flow/settings/handler_test.go @@ -0,0 +1,750 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/snapshotx" + + "github.com/ory/x/assertx" + + "github.com/ory/kratos/session" + + "github.com/gofrs/uuid" + + kratos "github.com/ory/kratos/pkg/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/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/x" +) + +func init() { + corpx.RegisterFakes() +} + +func TestHandler(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + {ID: "not-default", URL: "file://./stub/multi-email.schema.json"}, + }) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + _ = testhelpers.NewSettingsUITestServer(t, conf) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + + primaryIdentity := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), primaryIdentity)) + primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, primaryIdentity) + otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(ctx, t, reg) + + newExpiredFlow := func() *settings.Flow { + f, err := settings.NewFlow(reg, + &http.Request{URL: urlx.ParseOrPanic(publicTS.URL + login.RouteInitBrowserFlow)}, + primaryIdentity, flow.TypeBrowser) + + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Minute) + return f + } + + assertion := func(t *testing.T, body []byte, isApi bool) { + if isApi { + assert.Equal(t, "api", gjson.GetBytes(body, "type").String()) + } else { + assert.Equal(t, "browser", gjson.GetBytes(body, "type").String()) + } + } + + type options struct { + query url.Values + } + type initAuthOptions func(*options) + + WithInitAuthQuery := func(q url.Values) initAuthOptions { + return func(o *options) { + o.query = q + } + } + + initAuthenticatedFlow := func(t *testing.T, hc *http.Client, isAPI bool, isSPA bool, opts ...initAuthOptions) (*http.Response, []byte) { + op := new(options) + for _, o := range opts { + o(op) + } + route := settings.RouteInitBrowserFlow + if isAPI { + route = settings.RouteInitAPIFlow + } + reqURL, err := url.Parse(publicTS.URL + route) + require.NoError(t, err) + reqURL.RawQuery = op.query.Encode() + + req := testhelpers.NewTestHTTPRequest(t, "GET", reqURL.String(), nil) + if isSPA || isAPI { + req.Header.Set("Accept", "application/json") + } + res, err := hc.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + if isAPI { + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + } + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body + } + + initFlow := func(t *testing.T, hc *http.Client, isAPI bool, opts ...initAuthOptions) (*http.Response, []byte) { + return initAuthenticatedFlow(t, hc, isAPI, false, opts...) + } + + initSPAFlow := func(t *testing.T, hc *http.Client, opts ...initAuthOptions) (*http.Response, []byte) { + return initAuthenticatedFlow(t, hc, false, true, opts...) + } + + aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, &identity.Identity{ + State: identity.StateActive, + Traits: []byte(`{"email":"foo@bar"}`), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), Identifiers: []string{"foo@bar"}}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), Identifiers: []string{"foo@bar"}}, + }, + }) + + t.Run("endpoint=init", func(t *testing.T) { + t.Run("description=init a flow as API", func(t *testing.T) { + t.Run("description=without privileges", func(t *testing.T) { + res, body := initFlow(t, testhelpers.NewTestClient(t), true) + assert.Equalf(t, http.StatusUnauthorized, res.StatusCode, "%s", body) + assert.Equalf(t, text.ErrIDNoActiveSession, gjson.GetBytes(body, "error.id").String(), "%s", body) + }) + + t.Run("description=success", func(t *testing.T) { + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + res, body := initFlow(t, user1, true) + assert.Contains(t, res.Request.URL.String(), settings.RouteInitAPIFlow) + assertion(t, body, true) + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").Raw), snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + + t.Run("description=can not init if identity has aal2 but session has aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + res, body := initFlow(t, aal2Identity, true) + assert.Equalf(t, http.StatusForbidden, res.StatusCode, "%s", body) + assertx.EqualAsJSON(t, + session.NewErrAALNotSatisfied(publicTS.URL+"/self-service/login/browser?aal=aal2"), + json.RawMessage(body)) + }) + }) + + t.Run("description=init a flow as browser", func(t *testing.T) { + t.Run("case=unauthorized users are redirected to login preserving redirect_to param", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + returnTo := "?return_to=validRedirect" + req, err := http.NewRequest("GET", publicTS.URL+settings.RouteInitBrowserFlow+returnTo, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + location, err := res.Location() + require.NoError(t, err) + require.Equal(t, publicTS.URL+login.RouteInitBrowserFlow+returnTo, location.String()) + }) + + t.Run("case=unauthorized users are redirected to login", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + location, err := res.Location() + require.NoError(t, err) + require.Equal(t, publicTS.URL+login.RouteInitBrowserFlow, location.String()) + }) + + t.Run("description=success", func(t *testing.T) { + user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(ctx, t, reg) + res, body := initFlow(t, user1, false) + assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowSettingsUI(ctx).String()) + assertion(t, body, false) + }) + + t.Run("description=can not init if identity has aal2 but session has aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + res, body := initFlow(t, aal2Identity, false) + assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowLoginUI(ctx).String()) + assert.EqualValues(t, "Please complete the second authentication challenge.", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("description=settings return_to is persisted through aal2 flow", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + res, _ := initFlow(t, aal2Identity, false, WithInitAuthQuery(url.Values{"return_to": {"https://www.ory.com"}})) + assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowLoginUI(ctx).String()) + + lf, err := reg.LoginFlowPersister().GetLoginFlow(ctx, uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + + reqURL, err := url.Parse(lf.RequestURL) + require.NoError(t, err) + + // the return_to of login will be set to the full URL of the settings URL with its query parameters + settingsURL, err := url.Parse(publicTS.URL + settings.RouteInitBrowserFlow) + require.NoError(t, err) + q := settingsURL.Query() + q.Set("return_to", "https://www.ory.com") + settingsURL.RawQuery = q.Encode() + + assert.Equal(t, settingsURL.String(), reqURL.Query().Get("return_to")) + }) + + t.Run("case=redirects with 303", func(t *testing.T) { + c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(ctx, t, reg) + // prevent the redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := c.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + location, err := res.Location() + require.NoError(t, err) + require.Contains(t, location.String(), conf.SelfServiceFlowSettingsUI(ctx).String()) + }) + }) + + t.Run("description=init a flow as SPA", func(t *testing.T) { + t.Run("description=without privileges", func(t *testing.T) { + res, body := initSPAFlow(t, testhelpers.NewTestClient(t)) + assert.Equalf(t, http.StatusUnauthorized, res.StatusCode, "%s", body) + assert.Equalf(t, text.ErrIDNoActiveSession, gjson.GetBytes(body, "error.id").String(), "%s", body) + }) + + t.Run("description=success", func(t *testing.T) { + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + res, body := initSPAFlow(t, user1) + assert.Contains(t, res.Request.URL.String(), settings.RouteInitBrowserFlow) + assertion(t, body, false) + }) + + 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(ctx, t, reg, &identity.Identity{ + State: identity.StateActive, + Traits: []byte(`{"email":"` + email + `"}`), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), Identifiers: []string{email}}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), Identifiers: []string{email}}, + }, + }) + res, body := initSPAFlow(t, user1) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + + url := url.URL{ + Scheme: conf.SelfPublicURL(ctx).Scheme, + Host: conf.SelfPublicURL(ctx).Host, + Path: login.RouteInitBrowserFlow, + } + q := url.Query() + q.Add("aal", "aal2") + url.RawQuery = q.Encode() + + assertx.EqualAsJSON(t, session.NewErrAALNotSatisfied(url.String()), json.RawMessage(body)) + }) + + 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(ctx, t, reg, &identity.Identity{ + State: identity.StateActive, + Traits: []byte(`{"email":"` + email + `"}`), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), Identifiers: []string{email}}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), Identifiers: []string{email}}, + }, + }) + res, body := initSPAFlow(t, user1, WithInitAuthQuery(url.Values{"return_to": {"https://www.ory.com/"}})) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + + returnToURL := gjson.GetBytes(body, "redirect_browser_to").String() + require.NotEmpty(t, returnToURL) + + reqURL, err := url.Parse(gjson.GetBytes(body, "redirect_browser_to").String()) + require.NoError(t, err) + + settingsURL, err := url.Parse(publicTS.URL + settings.RouteInitBrowserFlow) + require.NoError(t, err) + + q := settingsURL.Query() + q.Add("return_to", "https://www.ory.com/") + settingsURL.RawQuery = q.Encode() + + assert.Equal(t, settingsURL.String(), reqURL.Query().Get("return_to")) + }) + }) + }) + + t.Run("case=multi-schema_endpoint=init", func(t *testing.T) { + t.Run("description=init a flow as API", func(t *testing.T) { + t.Run("description=success", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + }) + + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + + // set the default schema to something else than the default + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "not-default") + res, body := initFlow(t, user1, true) + assert.Contains(t, res.Request.URL.String(), settings.RouteInitAPIFlow) + assertion(t, body, true) + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").Raw), snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + }) + + t.Run("description=init a flow as browser", func(t *testing.T) { + t.Run("description=success", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + }) + + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + + // set the default schema to something else than the default + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "not-default") + + res, body := initFlow(t, user1, false) + assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowSettingsUI(ctx).String()) + assertion(t, body, false) + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").Raw), snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + }) + + t.Run("description=init a flow as SPA", func(t *testing.T) { + t.Run("description=success", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + }) + + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + + // set the default schema to something else than the default + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "not-default") + + res, body := initSPAFlow(t, user1) + assert.Contains(t, res.Request.URL.String(), settings.RouteInitBrowserFlow) + assertion(t, body, false) + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").Raw), snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + }) + }) + + 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() + require.Error(t, err) + + require.IsTypef(t, new(kratos.GenericOpenAPIError), err, "%T", err) + assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(err.(*kratos.GenericOpenAPIError).Body(), "error.code").Int()) + }) + + t.Run("description=fetching an expired flow returns 410", func(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() + require.Error(t, err) + + require.IsTypef(t, new(kratos.GenericOpenAPIError), err, "%T", err) + assert.Equal(t, int64(http.StatusGone), gjson.GetBytes(err.(*kratos.GenericOpenAPIError).Body(), "error.code").Int()) + }) + + t.Run("case=expired with return_to", func(t *testing.T) { + returnTo := "https://www.ory.com" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + + client := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + body := testhelpers.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo) + + // Expire the flow + f, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(context.Background(), f)) + + // Retrieve the flow and verify that return_to is in the response + getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", publicTS.URL, settings.RouteGetFlow, f.ID, returnTo) + getBody := testhelpers.EasyGetBody(t, client, getURL) + assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) + + // submit the flow but it is expired + u := publicTS.URL + settings.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := client.PostForm(u, url.Values{"method": {"password"}, "csrf_token": {"csrf"}, "password": {"password"}}) + require.NoError(t, err) + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + f, err = reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String())) + require.NoError(t, err) + assert.Equal(t, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo, f.RequestURL) + }) + + 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(ctx, t, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + + res, err := user1.Get(publicTS.URL + settings.RouteInitAPIFlow) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + body := ioutilx.MustReadAll(res.Body) + id := gjson.GetBytes(body, "id") + require.NotEmpty(t, id) + + res, err = user2.Get(publicTS.URL + settings.RouteGetFlow + "?id=" + id.String()) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + require.EqualValues(t, res.StatusCode, http.StatusForbidden) + body = ioutilx.MustReadAll(res.Body) + assert.Containsf(t, gjson.GetBytes(body, "error.reason").String(), "The request was made for another identity and has been blocked for security reasons", "%s", body) + }) + + t.Run("type=browser", func(t *testing.T) { + res, err := primaryUser.Get(publicTS.URL + settings.RouteInitBrowserFlow) + require.NoError(t, err) + + rid := res.Request.URL.Query().Get("flow") + require.NotEmpty(t, rid) + + _, _, 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") + }) + }) + + t.Run("description=can not fetch if identity has aal2 but session has aal1", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + + res, body := initFlow(t, aal2Identity, false) + require.Equal(t, http.StatusOK, res.StatusCode) + + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + flowID := gjson.GetBytes(body, "id").String() + settingsURL := publicTS.URL + settings.RouteGetFlow + "?id=" + flowID + res, err := aal2Identity.Get(settingsURL) + require.NoError(t, err) + body = ioutilx.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + url := url.URL{ + Scheme: conf.SelfPublicURL(ctx).Scheme, + Host: conf.SelfPublicURL(ctx).Host, + Path: login.RouteInitBrowserFlow, + } + + returnTo := conf.SelfServiceFlowSettingsUI(context.Background()) + rq := returnTo.Query() + // the flow ID should be perserved within the return_to query parameter of the login flow + rq.Set("flow", flowID) + returnTo.RawQuery = rq.Encode() + + q := url.Query() + q.Set("aal", "aal2") + q.Set("return_to", returnTo.String()) + url.RawQuery = q.Encode() + + require.EqualValues(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSON(t, session.NewErrAALNotSatisfied(url.String()), json.RawMessage(body)) + }) + }) + + t.Run("endpoint=submit", func(t *testing.T) { + t.Run("description=can not submit if identity has aal2 but session has aal1", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + + res, body := initFlow(t, aal2Identity, false) + require.Equal(t, http.StatusOK, res.StatusCode) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, aal2Identity, `{"method":"not-exists"}`) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "Please complete the second authentication challenge.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + }) + + t.Run("type=spa", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + + res, body := initFlow(t, aal2Identity, false) + require.Equal(t, http.StatusOK, res.StatusCode) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + actual, res := testhelpers.SettingsMakeRequest(t, false, true, &f, aal2Identity, `{"method":"not-exists"}`) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + + url := url.URL{ + Scheme: conf.SelfPublicURL(ctx).Scheme, + Host: conf.SelfPublicURL(ctx).Host, + Path: login.RouteInitBrowserFlow, + } + q := url.Query() + q.Set("aal", "aal2") + q.Set("return_to", publicTS.URL+"/self-service/settings?flow="+f.GetId()) + url.RawQuery = q.Encode() + + assert.Equal(t, url.String(), gjson.Get(actual, "redirect_browser_to").String(), actual) + }) + + t.Run("type=api", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + }) + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + + res, body := initFlow(t, aal2Identity, true) + require.Equal(t, http.StatusOK, res.StatusCode) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + actual, res := testhelpers.SettingsMakeRequest(t, true, false, &f, aal2Identity, `{"method":"not-exists"}`) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + + url := url.URL{ + Scheme: conf.SelfPublicURL(ctx).Scheme, + Host: conf.SelfPublicURL(ctx).Host, + Path: login.RouteInitBrowserFlow, + } + + q := url.Query() + q.Set("aal", "aal2") + q.Set("return_to", publicTS.URL+"/self-service/settings?flow="+f.GetId()) + url.RawQuery = q.Encode() + assert.Equal(t, url.String(), gjson.Get(actual, "redirect_browser_to").String(), actual) + }) + }) + + 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(ctx, t, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, 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.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(ctx, t, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(ctx, t, 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.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(ctx, t, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(ctx, t, 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.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("description=submit - kratos session cookie issued", func(t *testing.T) { + t.Run("type=spa", func(t *testing.T) { + _, body := initFlow(t, primaryUser, false) + var f kratos.SettingsFlow + 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"}`, nosurfx.FakeCSRFToken)) + 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) + }) + + t.Run("type=browser", func(t *testing.T) { + _, body := initFlow(t, primaryUser, false) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, primaryUser, `method=profile&traits.numby=15&csrf_token=`+nosurfx.FakeCSRFToken) + assert.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) + }) + }) + }) + + t.Run("case=multi-schema_endpoint=submit", func(t *testing.T) { + t.Run("description=fail to submit form with invalid data", func(t *testing.T) { + for _, tc := range []struct { + name string + isAPI bool + isSPA bool + }{ + { + name: "api", + isAPI: true, + }, { + name: "spa", + isSPA: true, + }, { + name: "browser", + }, + } { + t.Run("type="+tc.name, func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + }) + + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + + // set the default schema to something else than the default + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "not-default") + + _, body := initFlow(t, user1, true) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + _, res := testhelpers.SettingsMakeRequest(t, tc.isAPI, tc.isSPA, &f, user1, `{"should_long_string":"tooshort"}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + } + }) + + t.Run("description=submit - kratos session cookie issued", func(t *testing.T) { + t.Run("type=spa", func(t *testing.T) { + _, body := initFlow(t, primaryUser, false) + var f kratos.SettingsFlow + 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"}`, nosurfx.FakeCSRFToken)) + 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) + }) + + t.Run("type=browser", func(t *testing.T) { + _, body := initFlow(t, primaryUser, false) + var f kratos.SettingsFlow + require.NoError(t, json.Unmarshal(body, &f)) + + actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, primaryUser, `method=profile&traits.numby=15&csrf_token=`+nosurfx.FakeCSRFToken) + assert.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) + }) + }) + }) + + 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(ctx, t, reg) + res, _ := initFlow(t, user1, false) + assert.Regexp( + t, + "^/settings-ts.*$", + res.Header.Get("Location"), + ) + }) +} diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go new file mode 100644 index 0000000..a87babd --- /dev/null +++ b/selfservice/flow/settings/hook.go @@ -0,0 +1,558 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + stderrors "errors" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + + "github.com/ory/pop/v6" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/session" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + + "github.com/ory/kratos/schema" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/transaction" +) + +type ( + PreHookExecutorParams struct { + Flow *Flow + Session *session.Session + } + PreHookExecutor interface { + ExecuteSettingsPreHook(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error + } + PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error + + PostHookPrePersistExecutorParams struct { + Flow *Flow + Identity *identity.Identity + Session *session.Session + } + PostHookPrePersistExecutor interface { + ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error + } + PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error + + PostHookPostPersistExecutorParams struct { + Flow *Flow + Previous *identity.Identity + Updated *identity.Identity + Session *session.Session + } + PostHookPostPersistExecutor interface { + ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error + } + PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error + + HooksProvider interface { + PreSettingsHooks(ctx context.Context) ([]PreHookExecutor, error) + PostSettingsPrePersistHooks(ctx context.Context, settingsType string) ([]PostHookPrePersistExecutor, error) + PostSettingsPostPersistHooks(ctx context.Context, settingsType string) ([]PostHookPostPersistExecutor, error) + } + + executorDependencies interface { + identity.ManagementProvider + identity.ValidationProvider + identity.PoolProvider + identity.PrivilegedPoolProvider + identity.PendingTraitsChangePersistenceProvider + session.ManagementProvider + session.PersistenceProvider + config.Provider + + HandlerProvider + HooksProvider + FlowPersistenceProvider + + nosurfx.CSRFTokenGeneratorProvider + logrusx.Provider + httpx.WriterProvider + otelx.Provider + transaction.PersistenceProvider + } + HookExecutor struct { + d executorDependencies + } + HookExecutorProvider interface { + SettingsHookExecutor() *HookExecutor + } +) + +func (f PreHookExecutorFunc) ExecuteSettingsPreHook(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error { + return f(w, r, params) +} + +func (f PostHookPrePersistExecutorFunc) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error { + return f(w, r, params) +} + +func (f PostHookPostPersistExecutorFunc) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error { + return f(w, r, params) +} + +func PostHookPostPersistExecutorNames(e []PostHookPostPersistExecutor) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func PostHookPrePersistExecutorNames(e []PostHookPrePersistExecutor) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func NewHookExecutor(d executorDependencies) *HookExecutor { + return &HookExecutor{d: d} +} + +type PostSettingsHookOption func(o *postSettingsHookOptions) + +type postSettingsHookOptions struct { + cb func(ctxUpdate *UpdateContext) error +} + +func WithCallback(cb func(ctxUpdate *UpdateContext) error) func(o *postSettingsHookOptions) { + return func(o *postSettingsHookOptions) { + o.cb = cb + } +} + +func (e *HookExecutor) handleSettingsError(_ context.Context, _ http.ResponseWriter, r *http.Request, settingsType string, f *Flow, i *identity.Identity, flowError error) error { + if f != nil { + if i != nil { + var group node.UiNodeGroup + switch settingsType { + case "password": + group = node.PasswordGroup + case "oidc": + group = node.OpenIDConnectGroup + } + + cont, err := container.NewFromStruct("", group, i.Traits, "traits") + if err != nil { + e.d.Logger().WithError(err).Error("could not update flow UI") + return err + } + + for _, n := range cont.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(e.d.GenerateCSRFToken(r)) + } + } + + return flowError +} + +func (e *HookExecutor) PostSettingsHook(ctx context.Context, w http.ResponseWriter, r *http.Request, settingsType string, ctxUpdate *UpdateContext, i *identity.Identity, opts ...PostSettingsHookOption) (err error) { + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.PostSettingsHook") + defer otelx.End(span, &err) + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + WithField("flow_method", settingsType). + Debug("Running PostSettingsPrePersistHooks.") + + // Verify the redirect URL before we do any other processing. + c := e.d.Config() + returnTo, err := redir.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(ctx), + redir.SecureRedirectUseSourceURL(ctxUpdate.Flow.RequestURL), + redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo( + e.d.Config().SelfServiceFlowSettingsReturnTo(ctx, settingsType, + ctxUpdate.Flow.AppendTo(e.d.Config().SelfServiceFlowSettingsUI(ctx)))), + ) + if err != nil { + return err + } + + hookOptions := new(postSettingsHookOptions) + for _, f := range opts { + f(hookOptions) + } + + preHooks, err := e.d.PostSettingsPrePersistHooks(ctx, settingsType) + if err != nil { + return err + } + for k, executor := range preHooks { + logFields := logrus.Fields{ + "executor": fmt.Sprintf("%T", executor), + "executor_position": k, + "executors": PostHookPrePersistExecutorNames(preHooks), + "identity_id": i.ID, + "flow_method": settingsType, + } + + if err := executor.ExecuteSettingsPrePersistHook(w, r, PostHookPrePersistExecutorParams{ + Flow: ctxUpdate.Flow, + Identity: i, + Session: ctxUpdate.Session, + }); err != nil { + if errors.Is(err, ErrHookAbortFlow) { + e.d.Logger().WithRequest(r).WithFields(logFields). + Debug("A ExecuteSettingsPrePersistHook hook aborted early.") + return nil + } + var group node.UiNodeGroup + switch settingsType { + case "password": + group = node.PasswordGroup + case "oidc": + group = node.OpenIDConnectGroup + } + return flow.HandleHookError(w, r, ctxUpdate.Flow, i.Traits, group, err, e.d, e.d) + } + + e.d.Logger().WithRequest(r).WithFields(logFields).Debug("ExecuteSettingsPrePersistHook completed successfully.") + } + + options := []identity.ManagerOption{identity.ManagerExposeValidationErrorsForInternalTypeAssertion} + if e.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + options = append(options, identity.ManagerAllowWriteProtectedTraits) + } + if len(ctxUpdate.excludeCredentialTypesFromUpdate) > 0 { + options = append(options, identity.ManagerWithoutCredentialTypes(ctxUpdate.excludeCredentialTypesFromUpdate...)) + } + + if err := e.d.IdentityManager().Update(ctx, i, options...); err != nil { + if errors.Is(err, identity.ErrProtectedFieldModified()) { + e.d.Logger().WithError(err).Debug("Modifying protected field requires re-authentication.") + return errors.WithStack(NewFlowNeedsReAuth()) + } + if errors.Is(err, sqlcon.ErrUniqueViolation()) { + return schema.NewDuplicateCredentialsError(err) + } + return err + } + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + Debug("An identity's settings have been updated.") + + ctxUpdate.UpdateIdentity(i) + ctxUpdate.Flow.State = flow.StateSuccess + if hookOptions.cb != nil { + if err := hookOptions.cb(ctxUpdate); err != nil { + return err + } + } + + newFlow, err := e.d.SettingsHandler().NewFlow(ctx, w, r, i, ctxUpdate.Session, ctxUpdate.Flow.Type) + if err != nil { + return err + } + + ctxUpdate.Flow.UI = newFlow.UI + ctxUpdate.Flow.UI.ResetMessages() + ctxUpdate.Flow.UI.AddMessage(node.DefaultGroup, text.NewInfoSelfServiceSettingsUpdateSuccess()) + ctxUpdate.Flow.InternalContext = newFlow.InternalContext + if err := e.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { + return err + } + + postHooks, err := e.d.PostSettingsPostPersistHooks(ctx, settingsType) + if err != nil { + return err + } + previous := ctxUpdate.GetSessionIdentity() + for k, executor := range postHooks { + if err := executor.ExecuteSettingsPostPersistHook(w, r, PostHookPostPersistExecutorParams{ + Flow: ctxUpdate.Flow, + Previous: previous, + Updated: i, + Session: ctxUpdate.Session, + }); err != nil { + if errors.Is(err, ErrHookAbortFlow) { + e.d.Logger(). + WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", PostHookPostPersistExecutorNames(postHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", settingsType). + Debug("A ExecuteSettingsPostPersistHook hook aborted early.") + return nil + } + return e.handleSettingsError(ctx, w, r, settingsType, ctxUpdate.Flow, i, err) + } + + e.d.Logger().WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", PostHookPostPersistExecutorNames(postHooks)). + WithField("identity_id", i.ID). + WithField("flow_method", settingsType). + Debug("ExecuteSettingsPostPersistHook completed successfully.") + } + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + WithField("flow_method", settingsType). + Debug("Completed all PostSettingsPrePersistHooks and PostSettingsPostPersistHooks.") + + trace.SpanFromContext(ctx).AddEvent(events.NewSettingsSucceeded( + ctx, ctxUpdate.Flow.ID, i.ID, string(ctxUpdate.Flow.Type), settingsType)) + + if ctxUpdate.Flow.Type == flow.TypeAPI { + updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ctxUpdate.Flow.ID) + if err != nil { + return err + } + // 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. + updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems + + e.d.Writer().Write(w, r, updatedFlow) + return nil + } + + if err := e.d.SessionManager().IssueCookie(ctx, w, r, ctxUpdate.Session); err != nil { + return errors.WithStack(err) + } + + if x.IsJSONRequest(r) { + updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ctxUpdate.Flow.ID) + if err != nil { + return err + } + // 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) + return nil + } + + redir.ContentNegotiationRedirection(w, r, i.CopyWithoutCredentials(), e.d.Writer(), returnTo.String()) + return nil +} + +// settingsTypePendingTraitsChange is the settings strategy the verify_new_address +// hook is allowed to be configured under. The JSON config schema +// (selfServiceAfterSettingsProfileMethod) only admits verify_new_address inside +// the profile hook list, so a pending traits change can only ever originate +// from the profile strategy. +const settingsTypePendingTraitsChange = "profile" + +// ErrPendingTraitsChangeSessionInvalid is returned by ApplyPendingTraitsChange +// when the session that created the pending change is missing, revoked, +// expired, disabled, or belongs to a different tenant. Callers should treat +// this as "the verification attempt is invalid" — the change is not applied. +var ErrPendingTraitsChangeSessionInvalid = stderrors.New("pending traits change session is no longer valid") + +// ApplyPendingTraitsChange commits a pending traits change to the identity and +// fires the "after profile settings, post-persist" hook chain. It is the +// single entry point verification strategies use to resolve a pending change +// created by a settings flow. +// +// The originating session is validated inside the same transaction that +// applies the change, so a session that is revoked between the caller's +// token/code consumption and this call is still rejected. If validation +// succeeds, the loaded session is reused for the hook chain — no second +// GetSession round-trip. +// +// Returns ErrPendingTraitsChangeSessionInvalid when the session is not usable. +// Returns identity.ErrConcurrentModification when the identity's traits have +// drifted since the change was proposed. +func (e *HookExecutor) ApplyPendingTraitsChange( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ptc *identity.PendingTraitsChange, +) (i *identity.Identity, err error) { + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.ApplyPendingTraitsChange") + defer otelx.End(span, &err) + + // Reject PTCs whose FK references were nulled or never written. The + // session FK is ON DELETE SET NULL and the origin-flow FK is ON + // DELETE CASCADE, so a NULL here means either a revoked session or + // a writer that failed to set the link. + if !ptc.SessionID.Valid || !ptc.OriginSettingsFlowID.Valid { + return nil, errors.WithStack(ErrPendingTraitsChangeSessionInvalid) + } + + var ( + sess *session.Session + // Capture the pre-change identity before sess.Identity is overwritten + // below so post-persist hooks (notify_previous_addresses) can compute + // diffs without re-reading the row. + previous *identity.Identity + ) + err = e.d.TransactionalPersisterProvider().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) error { + // ExpandDefault loads the session's identity in the same query, + // so IsActive() below checks the identity's active state and we + // avoid a second round-trip to IdentityPool.GetIdentity for the + // hash comparison. + s, sErr := e.d.SessionPersister().GetSession(ctx, ptc.SessionID.UUID, session.ExpandDefault) + if sErr != nil { + if errors.Is(sErr, sqlcon.ErrNoRows()) { + return errors.WithStack(ErrPendingTraitsChangeSessionInvalid) + } + return sErr + } + if s.NID != ptc.NID || !s.IsActive() { + return errors.WithStack(ErrPendingTraitsChangeSessionInvalid) + } + // TODO(jonas): We can likely drop the identity id column entirely, since the session's identity is always loaded and we check the ID matches here. This would simplify the schema and remove a potential source of FK inconsistency. + if s.Identity == nil || s.Identity.ID != ptc.IdentityID { + return errors.WithStack(ErrPendingTraitsChangeSessionInvalid) + } + previous = s.Identity + sess = s + + if identity.HashTraits(json.RawMessage(s.Identity.Traits)) != ptc.OriginalTraitsHash { + return identity.ErrConcurrentModification + } + + if err := e.d.IdentityManager().UpdateTraits( + ctx, ptc.IdentityID, identity.Traits(ptc.ProposedTraits), + identity.ManagerAllowWriteProtectedTraits, + ); err != nil { + return err + } + + // The PTC Status column is informational only; the real + // single-submit guard is the verification flow state machine + // (StatePassedChallenge) plus atomic token/code consumption. + // We intentionally do not write Status here. + + // Re-read the identity after UpdateTraits so the returned + // snapshot reflects the freshly-committed state, including any + // VerifiableAddress row UpdateTraits may have created for the + // new address. + updated, err := e.d.IdentityPool().GetIdentity(ctx, ptc.IdentityID, identity.ExpandDefault) + if err != nil { + return err + } + + for idx := range updated.VerifiableAddresses { + a := &updated.VerifiableAddresses[idx] + if a.Value == ptc.NewAddressValue && a.Via == ptc.NewAddressVia { + a.Verified = true + verifiedAt := sqlxx.NullTime(time.Now().UTC()) + a.VerifiedAt = &verifiedAt + a.Status = identity.VerifiableAddressStatusCompleted + if err := e.d.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, a, "verified", "verified_at", "status"); err != nil { + return err + } + break + } + } + + i = updated + // Keep Session.Identity in sync so post-persist webhook templates render the committed traits and the newly-verified address. + // TODO: pointer aliasing could cause issues if a downstream hook implementation modifies the identity in-place instead of treating it as immutable. We should either enforce immutability or document this behavior. + sess.Identity = updated + return nil + }) + if err != nil { + return nil, err + } + + // Fire the "profile" post-persist hooks. Load the originating settings flow + // so the hook chain receives a real Flow (honest ID, type, RequestURL) + // instead of a synthetic one. The FK is ON DELETE CASCADE, so if the flow + // was cleaned up the PTC was already deleted and we would not reach this + // call. Apply has already committed, so a hook error cannot roll the change + // back — we propagate errors so callers can surface them in the + // verification UI. + originFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ptc.OriginSettingsFlowID.UUID) + if err != nil { + return nil, err + } + postHooks, err := e.d.PostSettingsPostPersistHooks(ctx, settingsTypePendingTraitsChange) + if err != nil { + return nil, err + } + for k, executor := range postHooks { + logFields := logrus.Fields{ + "executor": fmt.Sprintf("%T", executor), + "executor_position": k, + "executors": PostHookPostPersistExecutorNames(postHooks), + "identity_id": i.ID, + "flow_method": settingsTypePendingTraitsChange, + } + if err := executor.ExecuteSettingsPostPersistHook(w, r, PostHookPostPersistExecutorParams{ + Flow: originFlow, + Previous: previous, + Updated: i, + Session: sess, + }); err != nil { + if errors.Is(err, ErrHookAbortFlow) { + e.d.Logger().WithRequest(r).WithFields(logFields). + Debug("A post-persist hook aborted the pending-traits-change continuation flow.") + return i, nil + } + return nil, err + } + e.d.Logger().WithRequest(r).WithFields(logFields). + Debug("ExecuteSettingsPostPersistHook completed successfully after pending traits change.") + } + + return i, nil +} + +func (e *HookExecutor) PreSettingsHook(ctx context.Context, w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) (err error) { + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.PreSettingsHook") + defer otelx.End(span, &err) + + hooks, err := e.d.PreSettingsHooks(ctx) + if err != nil { + return err + } + for _, executor := range hooks { + if err := executor.ExecuteSettingsPreHook(w, r, PreHookExecutorParams{ + Flow: a, + Session: s, + }); err != nil { + return err + } + } + + return nil +} diff --git a/selfservice/flow/settings/hook_pending_traits_change_test.go b/selfservice/flow/settings/hook_pending_traits_change_test.go new file mode 100644 index 0000000..ac53a61 --- /dev/null +++ b/selfservice/flow/settings/hook_pending_traits_change_test.go @@ -0,0 +1,289 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" +) + +// applyPTCFixture holds a wired-up registry plus a minimal set of persisted +// objects (identity, session, settings flow, verification flow, PTC) for +// unit-testing HookExecutor.ApplyPendingTraitsChange. Subtests mutate the +// PTC before calling ApplyPendingTraitsChange to exercise specific guard paths. +type applyPTCFixture struct { + reg *driver.RegistryDefault + identity *identity.Identity + sess *session.Session + flow *settings.Flow + ptc *identity.PendingTraitsChange +} + +// newApplyPTCFixture constructs a fresh, isolated fixture for each subtest. +// All objects are persisted to the SQLite test database. +func newApplyPTCFixture(t *testing.T) *applyPTCFixture { + t.Helper() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + // Create an identity whose traits will be the baseline for the PTC. + i := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"original@example.com"}`), + SchemaID: "default", + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, i, identity.ManagerAllowWriteProtectedTraits)) + + // Create an active session linked to the identity. + sess, err := testhelpers.NewActiveSession( + httptest.NewRequest(http.MethodGet, "/", nil), + reg, i, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, + ) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Create a settings flow that acts as the origin for the PTC. + f := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: "http://localhost/settings", + IdentityID: i.ID, + Identity: i, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + // Create a verification flow — the PTC table has a FK on verification_flow_id + // (ON DELETE CASCADE), so we must persist a real verification flow row. + vf, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest(http.MethodGet, "/verification", nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + vf.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, vf)) + + // Build a PTC that is valid in every field; subtests nil out individual + // fields to trigger the specific guard under test. + sessID := sess.ID + originFlowID := f.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: i.ID, + NewAddressValue: "new@example.com", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(i.Traits)), + ProposedTraits: json.RawMessage(`{"email":"new@example.com"}`), + VerificationFlowID: vf.ID, + Status: identity.PendingTraitsChangeStatusPending, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true}, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + return &applyPTCFixture{ + reg: reg, + identity: i, + sess: sess, + flow: f, + ptc: ptc, + } +} + +func TestApplyPendingTraitsChange_NilOriginSettingsFlowID(t *testing.T) { + t.Parallel() + fx := newApplyPTCFixture(t) + + // Simulate a PTC whose OriginSettingsFlowID was never set (malformed writer) + // or was cleared before the apply call. + fx.ptc.OriginSettingsFlowID = uuid.NullUUID{} + + req := httptest.NewRequest(http.MethodGet, "/verify", nil) + rw := httptest.NewRecorder() + + _, err := fx.reg.SettingsHookExecutor().ApplyPendingTraitsChange(req.Context(), rw, req, fx.ptc) + require.Error(t, err) + assert.ErrorIs(t, err, settings.ErrPendingTraitsChangeSessionInvalid) +} + +// Ensure the fixture itself is valid: a fully-populated PTC must not be +// rejected. This guards against regressions in the fixture setup. +func TestApplyPendingTraitsChange_ValidPTC(t *testing.T) { + t.Parallel() + fx := newApplyPTCFixture(t) + + req := httptest.NewRequest(http.MethodGet, "/verify", nil) + rw := httptest.NewRecorder() + + updated, err := fx.reg.SettingsHookExecutor().ApplyPendingTraitsChange(req.Context(), rw, req, fx.ptc) + require.NoError(t, err) + require.NotNil(t, updated) +} + +func TestApplyPendingTraitsChange_NilSessionID(t *testing.T) { + t.Parallel() + fx := newApplyPTCFixture(t) + + fx.ptc.SessionID = uuid.NullUUID{} + + req := httptest.NewRequest(http.MethodGet, "/verify", nil) + rw := httptest.NewRecorder() + + _, err := fx.reg.SettingsHookExecutor().ApplyPendingTraitsChange(req.Context(), rw, req, fx.ptc) + require.Error(t, err) + assert.ErrorIs(t, err, settings.ErrPendingTraitsChangeSessionInvalid) +} + +func TestApplyPendingTraitsChange_NIDMismatch(t *testing.T) { + t.Parallel() + fx := newApplyPTCFixture(t) + + // Simulate a cross-tenant PTC: the PTC's NID differs from the session's NID. + fx.ptc.NID = uuid.Must(uuid.NewV4()) + + req := httptest.NewRequest(http.MethodGet, "/verify", nil) + rw := httptest.NewRecorder() + + _, err := fx.reg.SettingsHookExecutor().ApplyPendingTraitsChange(req.Context(), rw, req, fx.ptc) + require.Error(t, err) + assert.ErrorIs(t, err, settings.ErrPendingTraitsChangeSessionInvalid) +} + +// TestApplyPendingTraitsChange_WebhookReceivesOriginFlow asserts that the +// post-persist hook chain is invoked with the originating settings flow +// (not a synthetic one), so webhook payloads carry the real flow ID, +// type, and request URL. +func TestApplyPendingTraitsChange_WebhookReceivesOriginFlow(t *testing.T) { + t.Parallel() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + // Set up a recording webhook target that captures the raw body. + receivedCh := make(chan []byte, 1) + webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + select { + case receivedCh <- body: + default: + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(webhookServer.Close) + + // Register a web_hook as a settings-profile post-persist hook. + // The jsonnet body template `function(ctx) ctx` forwards the entire context, + // which includes the flow object with its ID, type, and request_url. + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, "profile"), []map[string]any{ + { + "hook": "web_hook", + "config": map[string]any{ + "url": webhookServer.URL, + "method": "POST", + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + }, + }) + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, "profile"), []map[string]any{}) + }) + + i := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"webhooktest@example.com"}`), + SchemaID: "default", + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, i, identity.ManagerAllowWriteProtectedTraits)) + + sess, err := testhelpers.NewActiveSession( + httptest.NewRequest(http.MethodGet, "/", nil), + reg, i, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, + ) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + originFlow := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: "http://localhost/settings?origin=real", + IdentityID: i.ID, + Identity: i, + Type: flow.TypeAPI, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, originFlow)) + + vf, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest(http.MethodGet, "/verification", nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + vf.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, vf)) + + sessID := sess.ID + originFlowID := originFlow.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: i.ID, + NewAddressValue: "webhooktest-new@example.com", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(i.Traits)), + ProposedTraits: json.RawMessage(`{"email":"webhooktest-new@example.com"}`), + VerificationFlowID: vf.ID, + Status: identity.PendingTraitsChangeStatusPending, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true}, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + req := httptest.NewRequest(http.MethodGet, "/verify", nil) + rw := httptest.NewRecorder() + _, err = reg.SettingsHookExecutor().ApplyPendingTraitsChange(req.Context(), rw, req, ptc) + require.NoError(t, err) + + var receivedBody []byte + select { + case receivedBody = <-receivedCh: + case <-time.After(5 * time.Second): + t.Fatal("settings post-persist webhook was not invoked after pending traits change applied") + } + + // The webhook payload should carry the real origin settings flow's ID and + // request URL — not values from a synthetic flow. + assert.Contains(t, string(receivedBody), originFlow.ID.String(), + "webhook payload should contain the real origin settings flow ID") + assert.Contains(t, string(receivedBody), "http://localhost/settings?origin=real", + "webhook payload should contain the real origin settings flow RequestURL") +} diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go new file mode 100644 index 0000000..346b82b --- /dev/null +++ b/selfservice/flow/settings/hook_test.go @@ -0,0 +1,197 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/tidwall/gjson" + + "github.com/gobuffalo/httptest" + + "github.com/stretchr/testify/assert" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/x" +) + +func TestSettingsExecutor(t *testing.T) { + ctx := context.Background() + for _, strategy := range []string{ + identity.CredentialsTypePassword.String(), + settings.StrategyProfile, + } { + t.Run("strategy="+strategy, func(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL) + + reg.WithHooks(map[string]driver.NewHookFn{ + "err": func(c config.SelfServiceHook, _ driver.Registry) interface{} { + return &hook.Error{Config: c.Config} + }, + }) + + newServer := func(t *testing.T, i *identity.Identity, ft flow.Type) *httptest.Server { + t.Helper() + router := http.NewServeMux() + handleErr := testhelpers.SelfServiceHookSettingsErrorHandler + router.HandleFunc("GET /settings/pre", func(w http.ResponseWriter, r *http.Request) { + if i == nil { + i = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) + } + sess, _ := testhelpers.NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + + f, err := settings.NewFlow(reg, r, sess.Identity, ft) + require.NoError(t, err) + if handleErr(t, w, r, reg.SettingsHookExecutor().PreSettingsHook(r.Context(), w, r, f, sess)) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.HandleFunc("GET /settings/post", func(w http.ResponseWriter, r *http.Request) { + if i == nil { + i = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) + } + sess, _ := testhelpers.NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + + a, err := settings.NewFlow(reg, r, sess.Identity, ft) + require.NoError(t, err) + a.RequestURL = x.RequestURL(r).String() + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(r.Context(), a)) + _ = handleErr(t, w, r, reg.SettingsHookExecutor(). + PostSettingsHook(ctx, w, r, strategy, &settings.UpdateContext{Flow: a, Session: sess}, i)) + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + return ts + } + + makeRequestPost := testhelpers.SelfServiceMakeSettingsPostHookRequest + viperSetPost := func(strategy string, c []config.SelfServiceHook) { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, strategy), c) + } + + uiTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + t.Cleanup(uiTS.Close) + uiURL := uiTS.URL + "/user/settings" + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsURL, uiURL) + + t.Run("method=PostSettingsHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + 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{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + 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)) + + viperSetPost(strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), uiURL) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + viperSetPost(strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteSettingsPrePersistHook": "abort"}`)}}) + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Equal(t, "", body) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL}) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, "https://www.ory.com") + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{"return_to": {returnToServer.URL + "/kratos"}}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, "https://www.ory.com/not-kratos") + testhelpers.SelfServiceHookSettingsSetDefaultRedirectToStrategy(t, conf, strategy, returnToServer.URL+"/kratos") + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String()) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + viperSetPost(strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), uiURL) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + viperSetPost(strategy, nil) + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeAPI), true, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) + + t.Run("case=pass without hooks for browser flow with application/json", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), true, url.Values{}) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) + }) + + for _, kind := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run("type="+string(kind)+"/method=PreSettingsHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceSettingsBeforeHooks, + testhelpers.SelfServiceMakeSettingsPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, nil, kind) + }, + conf, + )) + } + }) + } +} diff --git a/selfservice/flow/settings/notify_previous_addresses_integration_test.go b/selfservice/flow/settings/notify_previous_addresses_integration_test.go new file mode 100644 index 0000000..401419d --- /dev/null +++ b/selfservice/flow/settings/notify_previous_addresses_integration_test.go @@ -0,0 +1,98 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "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/ui/container" + "github.com/ory/kratos/x" +) + +// TestNotifyPreviousAddresses_Integration exercises the post-persist hook +// against a real registry and courier persister without going through the +// HTTP layer. This catches wiring regressions between the hook, the identity +// manager, the courier, and the template loader that a pure-unit test for +// the hook cannot detect. +func TestNotifyPreviousAddresses_Integration(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + t.Run("case=direct persist queues notification to removed address", func(t *testing.T) { + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = identity.Traits(`{"email":"old@example.com"}`) + original.VerifiableAddresses = []identity.VerifiableAddress{ + {Value: "old@example.com", Via: identity.AddressTypeEmail, Verified: true}, + } + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + + proposed := *original + proposed.Traits = identity.Traits(`{"email":"new@example.com"}`) + proposed.VerifiableAddresses = []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, Verified: false}, + } + + f := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().Add(time.Hour), + IssuedAt: time.Now(), + RequestURL: "http://foo.com/settings", + IdentityID: proposed.ID, + Identity: &proposed, + Type: flow.TypeBrowser, + UI: &container.Container{Method: "POST"}, + InternalContext: []byte("{}"), + } + sess := &session.Session{Identity: original} + + h := hook.NewNotifyPreviousAddresses(reg, &hook.NotifyPreviousAddressesConfig{ + Recipients: hook.RecipientsRemoved, + }) + + r := httptest.NewRequest("POST", "/", nil).WithContext(ctx) + w := httptest.NewRecorder() + + require.NoError(t, h.ExecuteSettingsPostPersistHook(w, r, settings.PostHookPostPersistExecutorParams{ + Flow: f, + Previous: original, + Updated: &proposed, + Session: sess, + })) + + messages, err := reg.CourierPersister().NextMessages(ctx, 10) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.Equal(t, "old@example.com", messages[0].Recipient) + assert.Equal(t, courier.MessageTypeEmail, messages[0].Type) + assert.Contains(t, messages[0].Subject, "address") + }) + + t.Run("case=registry wires hook when configured via SelfServiceHook", func(t *testing.T) { + executor := reg.HookNotifyPreviousAddresses(&hook.NotifyPreviousAddressesConfig{ + Recipients: hook.RecipientsAllVerified, + }) + require.NotNil(t, executor) + + var _ settings.PostHookPostPersistExecutor = executor + }) +} diff --git a/selfservice/flow/settings/organizations.go b/selfservice/flow/settings/organizations.go new file mode 100644 index 0000000..4ed0fed --- /dev/null +++ b/selfservice/flow/settings/organizations.go @@ -0,0 +1,113 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "net/http" + "slices" + "strings" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/x" +) + +var organizationFilter = []StrategyFilter{func(s Strategy) bool { + a, b := s.(flow.OrganizationImplementor) + return b && a.SupportsOrganizations() +}} + +// PrepareOrganizations resolves the org id from the identity (winner) or the +// `?organization=` query param, sets it on the flow, and returns a filter +// that keeps only org-implementing strategies. Returns an empty filter when +// no org id applies — the flow then proceeds as a normal (non-org) settings +// flow. +// +// The identity is the authoritative source: an identity already bound to an +// org cannot be silently re-bound, and the session cache must not be relied +// on because it can lag behind a just-persisted org binding (PostSettingsHook +// rebuilds the flow with the freshly linked identity). +// +// The query param is free-form caller input, so it is only honored when one +// of the identity's email addresses is under the organization's configured +// domains. Without that gate, any authenticated identity could render an +// org-scoped flow for every org in the project and enumerate its SSO +// providers. The org hooks block self-service registration and profile +// updates under a claimed domain, so a matching address implies the domain +// owner provisioned the account. +// +// When the resolved org id does not match any configured organization, the +// flow falls back to the unscoped path. An identity may carry an +// organization_id for an organization that has since been deleted; scoping +// to a dangling org would render no nodes and trap chained flows such as +// recovery -> settings. +func PrepareOrganizations(r *http.Request, f *Flow, i *identity.Identity, configuredOrgs []config.Organization) []StrategyFilter { + if f.OrganizationID.Valid { + return organizationFilter + } + + if i != nil && i.OrganizationID.Valid { + if _, ok := findConfiguredOrganization(i.OrganizationID.UUID, configuredOrgs); !ok { + return []StrategyFilter{} + } + f.OrganizationID = i.OrganizationID + return organizationFilter + } + + orgID := flow.ParseOrganizationFromURLQuery(r.Context(), r.URL.Query()) + if !orgID.Valid { + return []StrategyFilter{} + } + + org, ok := findConfiguredOrganization(orgID.UUID, configuredOrgs) + if !ok { + return []StrategyFilter{} + } + + if !identityEmailMatchesOrganizationDomains(i, org) { + return []StrategyFilter{} + } + + f.OrganizationID = orgID + return organizationFilter +} + +func findConfiguredOrganization(id uuid.UUID, configured []config.Organization) (config.Organization, bool) { + idx := slices.IndexFunc(configured, func(o config.Organization) bool { + return o.ID == id + }) + if idx < 0 { + return config.Organization{}, false + } + return configured[idx], true +} + +// identityEmailMatchesOrganizationDomains reports whether one of the +// identity's email addresses is under one of the organization's domains. The +// normalization mirrors the org email-domain matching in the cloud module's +// organization hook. +func identityEmailMatchesOrganizationDomains(i *identity.Identity, org config.Organization) bool { + if i == nil { + return false + } + // TODO(jonas): This should actually use the `organization.via` field of the identity schema. + for _, addr := range i.VerifiableAddresses { + if addr.Via != identity.AddressTypeEmail { + continue + } + _, domain, ok := strings.Cut(x.NormalizeEmailIdentifier(addr.Value), "@") + if !ok || domain == "" { + continue + } + if slices.ContainsFunc(org.Domains, func(orgDomain string) bool { + return strings.EqualFold(domain, x.NormalizeOtherIdentifier(orgDomain)) + }) { + return true + } + } + return false +} diff --git a/selfservice/flow/settings/organizations_test.go b/selfservice/flow/settings/organizations_test.go new file mode 100644 index 0000000..d125cb6 --- /dev/null +++ b/selfservice/flow/settings/organizations_test.go @@ -0,0 +1,157 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/settings" +) + +func TestPrepareOrganizations(t *testing.T) { + ctx := context.Background() + orgQuery := uuid.Must(uuid.NewV4()) + orgIdentity := uuid.Must(uuid.NewV4()) + configured := []config.Organization{ + {ID: orgQuery, Domains: []string{"corp.example"}}, + {ID: orgIdentity, Domains: []string{"other-corp.example"}}, + } + + // identityWithEmail returns an unbound identity carrying a single email + // verifiable address, mirroring what the session loader hydrates via + // identity.ExpandDefault. + identityWithEmail := func(email string) *identity.Identity { + return &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: email, Via: identity.AddressTypeEmail}, + }, + } + } + + t.Run("returns empty filter when no org id is set", func(t *testing.T) { + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser", nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("user@corp.example"), configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("uses query param when the identity's email is under the org's domains", func(t *testing.T) { + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("user@corp.example"), configured) + assert.Len(t, filters, 1) + assert.Equal(t, orgQuery, f.OrganizationID.UUID) + }) + + t.Run("domain match is case-insensitive", func(t *testing.T) { + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("User@CORP.Example"), configured) + assert.Len(t, filters, 1) + assert.Equal(t, orgQuery, f.OrganizationID.UUID) + }) + + t.Run("subdomain of a configured domain is not a match", func(t *testing.T) { + // Domain comparison is an exact match, not a suffix match: an email + // under sub.corp.example must not satisfy an org that claims + // corp.example. Suffix matching would let anyone controlling an + // arbitrary subdomain-style address scope flows to the org. + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("user@sub.corp.example"), configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("query param with a non-matching email domain falls back to unscoped", func(t *testing.T) { + // The query parameter is free-form caller input. Without a domain + // match it must not scope the flow — otherwise any authenticated + // identity could enumerate the SSO providers of every org in the + // project. + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("user@elsewhere.example"), configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("query param with an identity without email addresses falls back to unscoped", func(t *testing.T) { + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, &identity.Identity{}, configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("non-email addresses do not count as a domain match", func(t *testing.T) { + f := &settings.Flow{} + i := &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "user@corp.example", Via: identity.AddressTypeSMS}, + }, + } + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, i, configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("nil identity cannot be scoped via the query param", func(t *testing.T) { + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, nil, configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("identity org id overrides query param without a domain check", func(t *testing.T) { + // An identity already bound to an org cannot be silently re-bound to + // the org carried by the query string. Membership is proven by the + // binding itself, so no email-domain match is required. + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+orgQuery.String(), nil).WithContext(ctx) + i := &identity.Identity{OrganizationID: uuid.NullUUID{UUID: orgIdentity, Valid: true}} + filters := settings.PrepareOrganizations(r, f, i, configured) + assert.Len(t, filters, 1) + assert.Equal(t, orgIdentity, f.OrganizationID.UUID) + }) + + t.Run("preserves an already-set flow org id", func(t *testing.T) { + f := &settings.Flow{OrganizationID: uuid.NullUUID{UUID: orgQuery, Valid: true}} + r := httptest.NewRequest("GET", "/self-service/settings/browser", nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, &identity.Identity{}, configured) + assert.Len(t, filters, 1) + assert.Equal(t, orgQuery, f.OrganizationID.UUID) + }) + + t.Run("identity org id pointing at a deleted org falls back to unscoped", func(t *testing.T) { + // An identity bound to an organization that has since been deleted + // must not trap the user in an org-scoped flow with no nodes — + // notably the recovery -> settings hand-off relies on the password + // node being rendered for offboarded users. + f := &settings.Flow{} + r := httptest.NewRequest("GET", "/self-service/settings/browser", nil).WithContext(ctx) + i := &identity.Identity{OrganizationID: uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true}} + filters := settings.PrepareOrganizations(r, f, i, configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) + + t.Run("query param org id that is not configured falls back to unscoped", func(t *testing.T) { + f := &settings.Flow{} + unknown := uuid.Must(uuid.NewV4()) + r := httptest.NewRequest("GET", "/self-service/settings/browser?organization="+unknown.String(), nil).WithContext(ctx) + filters := settings.PrepareOrganizations(r, f, identityWithEmail("user@corp.example"), configured) + assert.Empty(t, filters) + assert.False(t, f.OrganizationID.Valid) + }) +} diff --git a/selfservice/flow/settings/persistence.go b/selfservice/flow/settings/persistence.go new file mode 100644 index 0000000..1737941 --- /dev/null +++ b/selfservice/flow/settings/persistence.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type ( + FlowPersister interface { + CreateSettingsFlow(context.Context, *Flow) error + GetSettingsFlow(ctx context.Context, id uuid.UUID) (*Flow, error) + UpdateSettingsFlow(context.Context, *Flow) error + DeleteExpiredSettingsFlows(context.Context, time.Time, int) error + } + FlowPersistenceProvider interface { + SettingsFlowPersister() FlowPersister + } +) diff --git a/selfservice/flow/settings/sort.go b/selfservice/flow/settings/sort.go new file mode 100644 index 0000000..e6b3de4 --- /dev/null +++ b/selfservice/flow/settings/sort.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + + "github.com/ory/kratos/ui/node" +) + +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.ProfileGroup, + node.PasswordGroup, + node.OpenIDConnectGroup, + node.LookupGroup, + node.WebAuthnGroup, + node.PasskeyGroup, + node.TOTPGroup, + }), + node.SortStableGroups(node.DeviceAuthnGroup), + node.SortUseOrderAppend([]string{ + // Lookup + node.LookupReveal, + node.LookupRegenerate, + node.LookupCodes, + node.LookupConfirm, + + // WebAuthn + node.WebAuthnRemove, + node.WebAuthnRegisterDisplayName, + node.WebAuthnRegister, + + // Passkey + node.PasskeyRemove, + node.PasskeyRegister, + + // TOTP + node.TOTPQR, + node.TOTPSecretKey, + node.TOTPUnlink, + node.TOTPCode, + }), + ) +} diff --git a/selfservice/flow/settings/state.go b/selfservice/flow/settings/state.go new file mode 100644 index 0000000..21cd22f --- /dev/null +++ b/selfservice/flow/settings/state.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import "github.com/ory/kratos/selfservice/flow" + +// 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. +// +// swagger:model settingsFlowState +type State = flow.State diff --git a/selfservice/flow/settings/strategy.go b/selfservice/flow/settings/strategy.go new file mode 100644 index 0000000..d7be7f3 --- /dev/null +++ b/selfservice/flow/settings/strategy.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "context" + "net/http" + "reflect" + + "github.com/ory/kratos/session" + + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" +) + +const ( + StrategyProfile = "profile" +) + +var pkgName = reflect.TypeOf(Strategies{}).PkgPath() + +type Strategy interface { + SettingsStrategyID() string + NodeGroup() node.UiNodeGroup + PopulateSettingsMethod(context.Context, *http.Request, *identity.Identity, *Flow) error + Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *Flow, s *session.Session) (*UpdateContext, error) +} + +type Strategies []Strategy + +func (s Strategies) Strategy(id string) (Strategy, error) { + ids := make([]string, len(s)) + for k, ss := range s { + ids[k] = ss.SettingsStrategyID() + if ss.SettingsStrategyID() == id { + return ss, nil + } + } + + return nil, errors.Errorf(`unable to find strategy for %s have %v`, id, ids) +} + +func (s Strategies) MustStrategy(id string) Strategy { + strategy, err := s.Strategy(id) + if err != nil { + panic(err) + } + return strategy +} + +// StrategyFilter selects which Settings strategies to include when populating a flow. +type StrategyFilter func(strategy Strategy) bool + +type StrategyProvider interface { + SettingsStrategies(ctx context.Context, filters ...StrategyFilter) Strategies + AllSettingsStrategies() Strategies +} diff --git a/selfservice/flow/settings/strategy_helper.go b/selfservice/flow/settings/strategy_helper.go new file mode 100644 index 0000000..f27af8f --- /dev/null +++ b/selfservice/flow/settings/strategy_helper.go @@ -0,0 +1,131 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "net/http" + "runtime/debug" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "github.com/ory/herodot" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +var ( + ErrContinuePreviousAction = errors.New("found prior action") +) + +type UpdatePayload interface { + GetFlowID() uuid.UUID + SetFlowID(id uuid.UUID) +} + +type UpdateContext struct { + Session *session.Session + Flow *Flow + toUpdate *identity.Identity + + // excludeCredentialTypesFromUpdate lists credential types the + // post-settings identity persist must not touch; strategies that persist + // those credentials themselves (e.g. through a locked credential-config + // update) set this via ExcludeCredentialTypesFromUpdate. + excludeCredentialTypesFromUpdate []identity.CredentialsType +} + +func (c *UpdateContext) UpdateIdentity(i *identity.Identity) { + c.toUpdate = i +} + +// ExcludeCredentialTypesFromUpdate marks the given credential types as +// self-persisted: the post-settings identity persist leaves their rows +// untouched. +func (c *UpdateContext) ExcludeCredentialTypesFromUpdate(cts ...identity.CredentialsType) { + c.excludeCredentialTypesFromUpdate = append(c.excludeCredentialTypesFromUpdate, cts...) +} + +func (c *UpdateContext) GetIdentityToUpdate() (*identity.Identity, error) { + if c.toUpdate == nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Could not find a identity to update.")) + } + return c.toUpdate, nil +} + +func (c UpdateContext) GetSessionIdentity() *identity.Identity { + if c.Session == nil { + return nil + } + return c.Session.Identity +} + +func PrepareUpdate(d interface { + logrusx.Provider + continuity.ManagementProvider + x.CookieProvider +}, w http.ResponseWriter, r *http.Request, f *Flow, ss *session.Session, name string, payload UpdatePayload) (*UpdateContext, error) { + payload.SetFlowID(f.ID) + c := &UpdateContext{Session: ss, Flow: f} + if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { + return c, nil + } + + cookieStore := continuity.NewCookieReferenceStore(d.ContinuityCookieManager(r.Context())) + if _, err := d.ContinuityManager().Continue(r.Context(), w, r, name, cookieStore, ContinuityOptions(payload, ss.Identity)...); err == nil { + if payload.GetFlowID() == f.ID { + return c, ErrContinuePreviousAction + } + d.Logger(). + WithField("package", pkgName). + WithField("stack_trace", string(debug.Stack())). + WithField("expected_request_id", payload.GetFlowID()). + WithField("actual_request_id", f.ID). + Debug("Flow ID from continuity manager does not match Flow ID from request.") + return c, nil + } else if !errors.Is(err, continuity.ErrNotResumable()) { + return new(UpdateContext), err + } + + return c, nil +} + +func ContinuityOptions(p interface{}, i *identity.Identity) []continuity.ManagerOption { + return []continuity.ManagerOption{ + continuity.WithPayload(p), + continuity.WithIdentity(i), + continuity.WithLifespan(time.Minute * 15), + } +} + +func GetFlowID(r *http.Request) (uuid.UUID, error) { + rid := x.ParseUUID(r.URL.Query().Get("flow")) + if rid == uuid.Nil { + return rid, errors.WithStack(herodot.ErrBadRequest().WithReasonf("The flow query parameter is missing or malformed.")) + } + return rid, nil +} + +func OnUnauthenticated(reg interface { + config.Provider + httpx.WriterProvider +}) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + handler := session.RedirectOnUnauthenticated(reg.Config().SelfServiceFlowLoginUI(r.Context()).String()) + if x.IsJSONRequest(r) { + handler = session.RespondWithJSONErrorOnAuthenticated(reg.Writer(), herodot.ErrUnauthorized().WithReasonf("A valid Ory Session Cookie or Ory Session Token is missing.")) + } + + handler(w, r) + } +} diff --git a/selfservice/flow/settings/strategy_helper_test.go b/selfservice/flow/settings/strategy_helper_test.go new file mode 100644 index 0000000..84d9b74 --- /dev/null +++ b/selfservice/flow/settings/strategy_helper_test.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" +) + +func TestGetIdentityToUpdate(t *testing.T) { + c := new(UpdateContext) + _, err := c.GetIdentityToUpdate() + require.Error(t, err) + + expected := &identity.Identity{ID: x.NewUUID()} + c.UpdateIdentity(expected) + + actual, err := c.GetIdentityToUpdate() + require.NoError(t, err) + require.Equal(t, expected, actual) +} diff --git a/selfservice/flow/settings/stub/identity.schema.json b/selfservice/flow/settings/stub/identity.schema.json new file mode 100644 index 0000000..beacf8b --- /dev/null +++ b/selfservice/flow/settings/stub/identity.schema.json @@ -0,0 +1,48 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "name": { + "type": "string", + "format": "email", + "minLength": 10 + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + } + } +} diff --git a/selfservice/flow/settings/stub/multi-email.schema.json b/selfservice/flow/settings/stub/multi-email.schema.json new file mode 100644 index 0000000..442f278 --- /dev/null +++ b/selfservice/flow/settings/stub/multi-email.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go new file mode 100644 index 0000000..facf88e --- /dev/null +++ b/selfservice/flow/settings/test/persistence.go @@ -0,0 +1,260 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/x/sqlcon" + + "github.com/ory/x/assertx" + + "github.com/ory/kratos/ui/node" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func clearids(r *settings.Flow) { + r.ID = uuid.Nil + r.Identity.ID = uuid.Nil + r.IdentityID = uuid.Nil +} + +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { + return func(t *testing.T) { + _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + 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) + }) + + newFlow := func(t *testing.T) *settings.Flow { + var r settings.Flow + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + require.NoError(t, p.CreateIdentity(ctx, r.Identity)) + require.NotZero(t, r.Identity.ID) + r.IdentityID = r.Identity.ID + r.State = flow.StateShowForm + return &r + } + + t.Run("case=should create a new settings request", func(t *testing.T) { + r := newFlow(t) + err := p.CreateSettingsFlow(ctx, r) + require.NoError(t, err, "%#v", err) + + t.Run("can not fetch on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetSettingsFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create and update and properly deal with internal context", func(t *testing.T) { + for k, tc := range []struct { + in []byte + expect string + }{ + {in: []byte("[]"), expect: "{}"}, + {expect: "{}"}, + {in: []byte("null"), expect: "{}"}, + {in: []byte(`{"foo":"bar"}`), expect: `{"foo":"bar"}`}, + } { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + r := newFlow(t) + r.InternalContext = tc.in + require.NoError(t, p.CreateSettingsFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + + r.InternalContext = tc.in + require.NoError(t, p.UpdateSettingsFlow(ctx, r)) + assert.Equal(t, tc.expect, string(r.InternalContext)) + }) + } + }) + + t.Run("case=should ensure that internal context is an object", func(t *testing.T) { + r := newFlow(t) + r.InternalContext = []byte("null") + require.NoError(t, p.CreateSettingsFlow(ctx, r)) + assert.Equal(t, "{}", string(r.InternalContext)) + + r.InternalContext = nil + require.NoError(t, p.UpdateSettingsFlow(ctx, r)) + assert.Equal(t, "{}", string(r.InternalContext)) + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r settings.Flow + require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm + require.NoError(t, p.CreateIdentity(ctx, r.Identity)) + require.NoError(t, p.CreateSettingsFlow(ctx, &r)) + + require.NotEmpty(t, r.Identity) + t.Run("can not fetch on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetSettingsFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create and fetch a settings request", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateSettingsFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetSettingsFlow(ctx, expected.ID) + require.NoError(t, err) + + factual, _ := json.Marshal(actual.UI) + fexpected, _ := json.Marshal(expected.UI) + + require.NotEmpty(t, actual.UI.Action) + assert.EqualValues(t, expected.ID, actual.ID) + assert.JSONEq(t, string(fexpected), string(factual)) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Identity.ID, actual.Identity.ID) + assert.EqualValues(t, expected.Identity.Traits, actual.Identity.Traits) + assert.EqualValues(t, expected.Identity.SchemaID, actual.Identity.SchemaID) + assert.Empty(t, actual.Identity.Credentials) + }) + + t.Run("case=should fail to create if identity does not exist", func(t *testing.T) { + var expected settings.Flow + require.NoError(t, faker.FakeData(&expected)) + clearids(&expected) + expected.Identity = nil + expected.IdentityID = uuid.Nil + expected.State = flow.StateShowForm + err := p.CreateSettingsFlow(ctx, &expected) + require.Errorf(t, err, "%+v", expected) + }) + + t.Run("case=should create and update a settings request", func(t *testing.T) { + expected := newFlow(t) + expected.UI.Nodes = node.Nodes{} + expected.UI.Nodes.Append(node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + expected.UI.Nodes.Append(node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + err := p.CreateSettingsFlow(ctx, expected) + require.NoError(t, err) + + expected.UI.Action = "/new-action" + expected.UI.Nodes.Append( + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + }))) + + expected.RequestURL = "/new-request-url" + require.NoError(t, p.UpdateSettingsFlow(ctx, expected)) + + actual, err := p.GetSettingsFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.Equal(t, "/new-action", actual.UI.Action) + assert.Equal(t, "/new-request-url", actual.RequestURL) + assertx.EqualAsJSON(t, node.Nodes{ + // v0.5: {Name: "zab", Type: "zab", Pattern: "zab"}, + node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + // v0.5: {Name: "zab", Type: "bar", Pattern: "baz"}, + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + })), + }, actual.UI.Nodes) + }) + + t.Run("case=network", func(t *testing.T) { + id := x.NewUUID() + iid := x.NewUUID() + nid, p := testhelpers.NewNetwork(t, ctx, p) + + require.NoError(t, p.CreateIdentity(ctx, &identity.Identity{ID: iid})) + + t.Run("sets id on creation", func(t *testing.T) { + expected := &settings.Flow{ID: id, IdentityID: iid, State: flow.StateShowForm, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} + require.NoError(t, p.CreateSettingsFlow(ctx, expected)) + assert.EqualValues(t, id, expected.ID) + assert.EqualValues(t, nid, expected.NID) + + actual, err := p.GetSettingsFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + + _, err = p.GetSettingsFlow(ctx, id) + require.NoError(t, err) + }) + + t.Run("can not update id", func(t *testing.T) { + expected, err := p.GetSettingsFlow(ctx, id) + require.NoError(t, err) + + _, other := testhelpers.NewNetwork(t, ctx, p) + + expected.RequestURL = "updated" + require.ErrorIs(t, other.UpdateSettingsFlow(ctx, expected), sqlcon.ErrNoRows()) + + actual, err := p.GetSettingsFlow(ctx, id) + require.NoError(t, err) + assert.EqualValues(t, id, actual.ID) + assert.EqualValues(t, nid, actual.NID) + require.NotEqual(t, "updated", actual.RequestURL) + }) + + t.Run("can not get on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetSettingsFlow(ctx, id) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + t.Run("network isolation", func(t *testing.T) { + nid2, _ := testhelpers.NewNetwork(t, ctx, p) + nid1, p := testhelpers.NewNetwork(t, ctx, p) + + iid1, iid2 := x.NewUUID(), x.NewUUID() + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid1, nid1, time.Now(), time.Now()).Exec()) + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid2, nid2, time.Now(), time.Now()).Exec()) + + sid1, sid2 := x.NewUUID(), x.NewUUID() + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO selfservice_settings_flows (id, nid, identity_id, ui, created_at, updated_at, expires_at, request_url, internal_context) VALUES (?, ?, ?, '{}', ?, ?, ?, '', '{}')", sid1, nid1, iid1, time.Now(), time.Now(), time.Now().Add(time.Hour)).Exec()) + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO selfservice_settings_flows (id, nid, identity_id, ui, created_at, updated_at, expires_at, request_url, internal_context) VALUES (?, ?, ?, '{}', ?, ?, ?, '', '{}')", sid2, nid2, iid2, time.Now(), time.Now(), time.Now().Add(time.Hour)).Exec()) + + _, err := p.GetSettingsFlow(ctx, sid1) + require.NoError(t, err) + _, err = p.GetSettingsFlow(ctx, sid2) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + } +} diff --git a/selfservice/flow/settings/testsetup_test.go b/selfservice/flow/settings/testsetup_test.go new file mode 100644 index 0000000..d9601dd --- /dev/null +++ b/selfservice/flow/settings/testsetup_test.go @@ -0,0 +1,20 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package settings_test + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" +) + +var returnToServer *httptest.Server + +func TestMain(m *testing.M) { + returnToServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + })) + os.Exit(m.Run()) +} diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go new file mode 100644 index 0000000..010b86e --- /dev/null +++ b/selfservice/flow/state.go @@ -0,0 +1,128 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "strings" + + "github.com/pkg/errors" +) + +// Flow State +// +// 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. +// - show_form: a form is shown to the user to perform the flow +// - success: the flow has been completed successfully +// +// swagger:enum selfServiceFlowState +type State string + +// #nosec G101 -- only a key constant +// Define the various states for all flows. +// Recovery flows for V2 have different states (see below). +const ( + StateChooseMethod State = "choose_method" + // Note: this state should actually be called `StateMessageSent`, + // where a 'Message' is a code or link sent to an address (e.g. `email`, `sms`, etc). + StateEmailSent State = "sent_email" + StatePassedChallenge State = "passed_challenge" + StateShowForm State = "show_form" + StateSuccess State = "success" + + // Recovery V2. + // The initial state is different from `StateChooseMethod` to distinguish recovery v1 vs v2. + // This avoids the issue of the feature flag being toggled while some recovery flows are on-going. + // This would lead to an inconsistent state machine/logic for these flows. + StateRecoveryAwaitingAddress State = "recovery_awaiting_address" + StateRecoveryAwaitingAddressChoice State = "recovery_awaiting_address_choice" + StateRecoveryAwaitingAddressConfirm State = "recovery_confirming_address" + StateRecoveryAwaitingCode State = "recovery_awaiting_code" + // The final success state is the same as in Recovery V1 (`passed_challenge`). + // Since this is the terminal state, it is not affected by toggling the feature flag. + + // State machine diagrams: + // - ./state_recovery_v1.mermaid + // - ./state_recovery_v2.mermaid +) + +var states = []State{ + StateChooseMethod, + StateEmailSent, + StatePassedChallenge, +} + +func indexOf(current State) int { + for k, s := range states { + if s == current { + return k + } + } + return 0 +} + +func HasReachedState(expected, actual State) bool { + return indexOf(actual) >= indexOf(expected) +} + +func IsStateRecoveryV2(state State) bool { + return strings.HasPrefix(state.String(), "recovery_") +} + +func NextState(current State) State { + if current == StatePassedChallenge { + return StatePassedChallenge + } + + return states[indexOf(current)+1] +} + +// For some reason using sqlxx.NullString as the State type does not work here. +// Reimplementing the Scanner interface on type State does work and allows +// the state to be NULL in the database. + +// MarshalJSON returns m as the JSON encoding of m. +func (ns State) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ns)) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *State) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal(data, (*string)(ns))) +} + +// Scan implements the Scanner interface. +func (ns *State) Scan(value interface{}) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *ns = State(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (ns State) Value() (driver.Value, error) { + if len(ns) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(ns)}.Value() +} + +// String implements the Stringer interface. +func (ns State) String() string { + return string(ns) +} diff --git a/selfservice/flow/state_recovery_v1.mermaid b/selfservice/flow/state_recovery_v1.mermaid new file mode 100644 index 0000000..fe96be4 --- /dev/null +++ b/selfservice/flow/state_recovery_v1.mermaid @@ -0,0 +1,12 @@ +stateDiagram-v2 + [*] --> choose_method + choose_method --> sent_email: provided an existing email address + sent_email --> sent_email: clicked 'resend code' + choose_method --> sent_email: provided a non existing email address - pretend we sent a code + sent_email --> passed_challenge: provided valid code + passed_challenge --> [*] + + note right of sent_email + If the email exists, a recovery code is sent to it. + Otherwise, an email mentioning that this is an unknown address may be sent depending on the configuration. + end note diff --git a/selfservice/flow/state_recovery_v2.mermaid b/selfservice/flow/state_recovery_v2.mermaid new file mode 100644 index 0000000..4b59b86 --- /dev/null +++ b/selfservice/flow/state_recovery_v2.mermaid @@ -0,0 +1,22 @@ +stateDiagram-v2 + [*] --> recovery_awaiting_address + + recovery_awaiting_address --> recovery_awaiting_address_choice: provided any address which exists + recovery_awaiting_address --> recovery_awaiting_code: provided any address which does not exist - pretend we sent a code + recovery_awaiting_address --> recovery_awaiting_code: provided any address & auto-picked the only existing address + recovery_awaiting_address_choice --> recovery_confirming_address: chose a masked address + recovery_confirming_address --> recovery_awaiting_address_choice : choose different address +recovery_awaiting_address_choice --> recovery_awaiting_code: chose a masked address & it is the one provided initially (do not ask again for the full address) + recovery_confirming_address --> recovery_awaiting_code: provided the full address corresponding to the masked address + recovery_awaiting_code --> recovery_awaiting_code: clicked 'resend code' + recovery_awaiting_code --> passed_challenge: provided valid code + recovery_awaiting_code --> recovery_awaiting_address_choice : choose different address + + passed_challenge --> [*] + + + note right of recovery_awaiting_code + If the address exists, a recovery code is sent to it. + Otherwise, an email mentioning that this is an unknown address may be sent depending on the configuration + end note + diff --git a/selfservice/flow/state_test.go b/selfservice/flow/state_test.go new file mode 100644 index 0000000..349e042 --- /dev/null +++ b/selfservice/flow/state_test.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestState(t *testing.T) { + assert.EqualValues(t, StateEmailSent, NextState(StateChooseMethod)) + assert.EqualValues(t, StatePassedChallenge, NextState(StateEmailSent)) + assert.EqualValues(t, StatePassedChallenge, NextState(StatePassedChallenge)) + + assert.True(t, HasReachedState(StatePassedChallenge, StatePassedChallenge)) + assert.False(t, HasReachedState(StatePassedChallenge, StateEmailSent)) + assert.False(t, HasReachedState(StateEmailSent, StateChooseMethod)) +} diff --git a/selfservice/flow/type.go b/selfservice/flow/type.go new file mode 100644 index 0000000..4206344 --- /dev/null +++ b/selfservice/flow/type.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// Type is the flow type. +// +// The flow type can either be `api` or `browser`. +// +// swagger:model selfServiceFlowType +type Type string + +const ( + TypeAPI Type = "api" + TypeBrowser Type = "browser" +) + +func (t Type) IsBrowser() bool { + return t == TypeBrowser +} + +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/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json b/selfservice/flow/verification/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json new file mode 100644 index 0000000..f47dfa7 --- /dev/null +++ b/selfservice/flow/verification/.snapshots/TestHandleError-flow=browser-case=fails_to_retry_flow_if_recovery_strategy_id_is_not_valid.json @@ -0,0 +1,6 @@ +{ + "code": 400, + "message": "The request was malformed or contained invalid parameters", + "reason": "The active verification strategy not-valid is not enabled. Please enable it in the configuration.", + "status": "Bad Request" +} diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go new file mode 100644 index 0000000..8a4eb9a --- /dev/null +++ b/selfservice/flow/verification/error.go @@ -0,0 +1,175 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "net/http" + "net/url" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "github.com/gofrs/uuid" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" + + "github.com/ory/x/sqlxx" + + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" +) + +var ErrHookAbortFlow = errors.New("aborted verification hook execution") + +type ( + errorHandlerDependencies interface { + clock.Provider + errorx.ManagementProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider + config.Provider + FlowPersistenceProvider + StrategyProvider + } + + ErrorHandlerProvider interface { + VerificationFlowErrorHandler() *ErrorHandler + } + + ErrorHandler struct { + d errorHandlerDependencies + } +) + +func NewErrorHandler(d errorHandlerDependencies) *ErrorHandler { + return &ErrorHandler{d: d} +} + +func (s *ErrorHandler) WriteFlowError( + w http.ResponseWriter, + r *http.Request, + f *Flow, + group node.UiNodeGroup, + err error, +) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.verification.ErrorHandler.WriteFlowError", + trace.WithAttributes( + attribute.String("error", err.Error()), + )) + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + logger := s.d.Logger(). + WithError(err). + WithRequest(r). + WithField("verification_flow", f.ToLoggerField()) + + logger. + Info("Encountered self-service verification error.") + + if f == nil { + trace.SpanFromContext(r.Context()).AddEvent(events.NewVerificationFailed(r.Context(), uuid.Nil, "", "", err)) + s.forward(w, r, nil, err) + return + } + span.SetAttributes(attribute.String("flow_id", f.ID.String())) + trace.SpanFromContext(r.Context()).AddEvent(events.NewVerificationFailed(r.Context(), f.ID, string(f.Type), f.Active.String(), err)) + + if e := new(flow.ExpiredError); errors.As(err, &e) { + strategies, _, err := s.d.VerificationStrategies(r.Context()).ActiveStrategies(f.Active.String()) + if err != nil { + strategies, _, err = s.d.GetActiveVerificationStrategies(r.Context()) + // Can't retry the verification if no primary strategy has been set + if err != nil { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + } + // create new flow because the old one is not valid + a, err := FromOldFlow(s.d, s.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), + s.d.CSRFHandler().RegenerateToken(w, r), r, strategies, f) + if err != nil { + // failed to create a new session and redirect to it, handle that error as a new one + s.WriteFlowError(w, r, f, group, err) + return + } + + a.UI.Messages.Add(text.NewErrorValidationVerificationFlowExpired(s.d.Clock(), e.ExpiredAt)) + if err := s.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), a); err != nil { + s.forward(w, r, a, err) + return + } + + // We need to use the new flow, as that flow will be a browser flow. Bug fix for: + // + // https://github.com/ory/kratos/issues/2049!! + if a.Type == flow.TypeAPI || x.IsJSONRequest(r) { + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + RouteGetFlow), url.Values{"id": {a.ID.String()}}).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, a.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) + } + return + } + + if err := f.UI.ParseError(group, err); err != nil { + s.forward(w, r, f, err) + return + } + + f.Active = sqlxx.NullString(group) + if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { + s.forward(w, r, f, err) + return + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) + return + } + + updatedFlow, innerErr := s.d.VerificationFlowPersister().GetVerificationFlow(r.Context(), f.ID) + if innerErr != nil { + s.forward(w, r, updatedFlow, innerErr) + return + } + + s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), updatedFlow) +} + +func (s *ErrorHandler) forward(w http.ResponseWriter, r *http.Request, rr *Flow, err error) { + if rr == nil { + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, err) + return + } + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + + if rr.Type == flow.TypeAPI || x.IsJSONRequest(r) { + s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) + } else { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } +} diff --git a/selfservice/flow/verification/error_test.go b/selfservice/flow/verification/error_test.go new file mode 100644 index 0000000..696f75c --- /dev/null +++ b/selfservice/flow/verification/error_test.go @@ -0,0 +1,255 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification_test + +import ( + "context" + "io" + "net/http" + "testing" + "time" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/gofrs/uuid" + + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + + "github.com/ory/kratos/ui/node" + + "github.com/gobuffalo/httptest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/assertx" + "github.com/ory/x/urlx" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" +) + +func TestHandleError(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + + public, _ := testhelpers.NewKratosServer(t, reg) + + router := http.NewServeMux() + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + testhelpers.NewVerificationUIFlowEchoServer(t, reg) + testhelpers.NewErrorTestServer(t, reg) + + h := reg.VerificationFlowErrorHandler() + sdk := testhelpers.NewSDKClient(public) + + var verificationFlow *verification.Flow + var flowError error + var methodName node.UiNodeGroup + router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) { + h.WriteFlowError(w, r, verificationFlow, methodName, flowError) + }) + + reset := func() { + verificationFlow = nil + flowError = nil + methodName = "" + } + + newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *verification.Flow { + t.Helper() + req := &http.Request{URL: urlx.ParseOrPanic("/")} + strategies, _, err := reg.GetActiveVerificationStrategies(context.Background()) + require.NoError(t, err) + f, err := verification.NewFlow(reg, ttl, nosurfx.FakeCSRFToken, req, strategies, ft) + require.NoError(t, err) + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) + f, err = reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), f.ID) + require.NoError(t, err) + return f + } + + expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) { + t.Helper() + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = 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() + require.NoError(t, err) + + return sse.Error, nil + } + + anHourAgo := time.Now().Add(-time.Hour) + + t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(verification.VerificationStrategyLink) + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=error with nil flow detects application/json", func(t *testing.T) { + t.Cleanup(reset) + + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(verification.VerificationStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Header.Get("Content-Type"), "application/json") + assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "system error") + }) + + for _, tc := range []struct { + n string + t flow.Type + }{ + {"api", flow.TypeAPI}, + {"spa", flow.TypeBrowser}, + } { + t.Run("flow="+tc.n, func(t *testing.T) { + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = newFlow(t, time.Minute, flow.TypeAPI) + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.UiNodeGroup(verification.VerificationStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Contains(t, res.Request.URL.String(), public.URL+verification.RouteGetFlow) + require.Equal(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationVerificationFlowExpired), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), string(body)) + assert.NotEqual(t, verificationFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = newFlow(t, time.Minute, tc.t) + flowError = schema.NewInvalidCredentialsError() + methodName = node.UiNodeGroup(verification.VerificationStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body) + assert.Equal(t, verificationFlow.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = newFlow(t, time.Minute, tc.t) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.UiNodeGroup(verification.VerificationStrategyLink) + + res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw) + }) + }) + } + + t.Run("flow=browser", func(t *testing.T) { + expectVerificationUI := func(t *testing.T) (*verification.Flow, *http.Response) { + res, err := ts.Client().Get(ts.URL + "/error") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()+"?flow=") + + vf, err := reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err) + return vf, res + } + + t.Run("case=expired error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = &verification.Flow{Type: flow.TypeBrowser} + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + methodName = node.LinkGroup + + lf, _ := expectVerificationUI(t) + require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf)) + assert.Equal(t, int(text.ErrorValidationVerificationFlowExpired), int(lf.UI.Messages[0].ID)) + }) + + t.Run("case=validation error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = schema.NewInvalidCredentialsError() + methodName = node.LinkGroup + + lf, _ := expectVerificationUI(t) + require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf)) + require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf)) + assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf)) + }) + + t.Run("case=generic error", func(t *testing.T) { + t.Cleanup(reset) + + verificationFlow = newFlow(t, time.Minute, flow.TypeBrowser) + flowError = herodot.ErrInternalServerError().WithReason("system error") + methodName = node.LinkGroup + + sse, _ := expectErrorUI(t) + assertx.EqualAsJSON(t, flowError, sse) + }) + + t.Run("case=fails to retry flow if recovery strategy id is not valid", func(t *testing.T) { + t.Cleanup(func() { + reset() + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationUse, "code") + }) + + verificationFlow = newFlow(t, 0, flow.TypeBrowser) + verificationFlow.Active = "not-valid" + flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo) + + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationUse, "not-valid") + sse, _ := expectErrorUI(t) + testhelpers.SnapshotTExcept(t, sse, nil) + }) + }) +} diff --git a/selfservice/flow/verification/fake_strategy.go b/selfservice/flow/verification/fake_strategy.go new file mode 100644 index 0000000..72943c4 --- /dev/null +++ b/selfservice/flow/verification/fake_strategy.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "context" + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/ui/node" +) + +type FakeStrategy struct{} + +var _ Strategy = new(FakeStrategy) +var _ PrimaryStrategy = new(FakeStrategy) + +func (f FakeStrategy) VerificationStrategyID() string { + return "fake" +} + +func (f FakeStrategy) IsPrimary() bool { + return true +} + +func (f FakeStrategy) VerificationNodeGroup() node.UiNodeGroup { + return "fake" +} + +func (f FakeStrategy) PopulateVerificationMethod(*http.Request, *Flow) error { + return nil +} + +func (f FakeStrategy) Verify(_ http.ResponseWriter, _ *http.Request, _ *Flow) (err error) { + return nil +} + +func (f FakeStrategy) SendVerificationCode(context.Context, *Flow, *identity.Identity, identity.VerifiableAddressLike) error { + return nil +} + +func (f FakeStrategy) NodeGroup() node.UiNodeGroup { + return "fake" +} diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go new file mode 100644 index 0000000..2354294 --- /dev/null +++ b/selfservice/flow/verification/flow.go @@ -0,0 +1,360 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + + "github.com/ory/kratos/x/redir" + + "github.com/ory/pop/v6" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/clock" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +// A Verification Flow +// +// Used to verify an out-of-band communication +// channel such as an email address or a phone number. +// +// For more information head over to: https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation +// +// swagger:model verificationFlow +type Flow struct { + // ID represents the request's unique ID. When performing the verification flow, this + // represents the id in the verify ui's query parameter: http://?request= + // + // type: string + // format: uuid + // required: true + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction. + // + // required: true + Type flow.Type `json:"type" db:"type" faker:"flow_type"` + + // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address, + // a new request has to be initiated. + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the request occurred. + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // RequestURL is the initial URL that was requested from Ory Kratos. It can be used + // to forward information contained in the URL's path or query for example. + RequestURL string `json:"request_url" db:"request_url"` + + // InternalContext stores internal-only data for this flow. + InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"` + + // ReturnTo contains the requested return_to URL. + ReturnTo string `json:"return_to,omitempty" db:"-"` + + // Active, if set, contains the registration method that is being used. It is initially + // not set. + Active sqlxx.NullString `json:"active,omitempty" faker:"-" db:"active_method"` + + // UI contains data which must be shown in the user interface. + // + // required: true + UI *container.Container `json:"ui" db:"ui"` + + // 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. + // + // required: true + State State `json:"state" faker:"-" db:"state"` + + // OAuth2LoginChallenge holds the login challenge originally set during the registration flow. + OAuth2LoginChallenge sqlxx.NullString `json:"-" db:"oauth2_login_challenge"` + OAuth2LoginChallengeParams + + // HydraLoginRequest is the OAuth2 login request behind OAuth2LoginChallenge. It is + // hydrated on demand when a message is sent for this flow so that courier templates + // can brand messages per OAuth2 client. It is neither persisted nor exposed via the API. + HydraLoginRequest *hydraclientgo.OAuth2LoginRequest `json:"-" faker:"-" db:"-"` + + // CSRFToken contains the anti-csrf token associated with this request. + CSRFToken string `json:"-" db:"csrf_token"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // 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 { + // SessionID holds the session id if set from a registraton hook. + SessionID uuid.NullUUID `json:"-" faker:"-" db:"session_id"` + + // IdentityID holds the identity id if set from a registraton hook. + IdentityID uuid.NullUUID `json:"-" faker:"-" db:"identity_id"` + + // AMR contains a list of authentication methods that were used to verify the + // session if set from a registration hook. + AMR session.AuthenticationMethods `db:"authentication_methods" json:"-"` +} + +var _ flow.Flow = (*Flow)(nil) + +// flowDependencies are the dependencies NewFlow needs to construct a +// verification flow: the configuration (for return-to validation) and the +// clock. The lifespan and CSRF token are passed explicitly because callers +// vary them (regenerated CSRF tokens, conditional tokens by flow type). +type flowDependencies interface { + config.Provider + clock.Provider +} + +func NewFlow(reg flowDependencies, exp time.Duration, csrf string, r *http.Request, strategies Strategies, ft flow.Type) (*Flow, error) { + conf := reg.Config() + now := reg.Clock().Now().UTC() + id := x.NewUUID() + + // Pre-validate the return to URL which is contained in the HTTP request. + requestURL := x.RequestURL(r).String() + _, err := redir.SecureRedirectTo(r, + conf.SelfServiceBrowserDefaultReturnTo(r.Context()), + redir.SecureRedirectUseSourceURL(requestURL), + redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())), + ) + if err != nil { + return nil, err + } + + f := &Flow{ + ID: id, + ExpiresAt: now.Add(exp), + IssuedAt: now, + RequestURL: requestURL, + UI: &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), + }, + CSRFToken: csrf, + State: flow.StateChooseMethod, + Type: ft, + } + if err := f.setCourierBaseURL(x.BaseURLStringFromContext(r.Context())); err != nil { + return nil, err + } + + for _, strategy := range strategies { + if ps, isPrimary := strategy.(PrimaryStrategy); isPrimary { + f.Active = sqlxx.NullString(ps.NodeGroup()) + } + if err := strategy.PopulateVerificationMethod(r, f); err != nil { + return nil, err + } + } + + return f, nil +} + +func FromOldFlow(reg flowDependencies, exp time.Duration, csrf string, r *http.Request, strategies Strategies, of *Flow) (*Flow, error) { + f := of.Type + // Using the same flow in the recovery/verification context can lead to using API flow in a verification/recovery email + if of.Type == flow.TypeAPI { + f = flow.TypeBrowser + } + nf, err := NewFlow(reg, exp, csrf, r, strategies, f) + if err != nil { + return nil, err + } + + nf.RequestURL = of.RequestURL + return nf, nil +} + +func NewPostHookFlow(reg flowDependencies, exp time.Duration, csrf string, r *http.Request, strategies Strategies, original flow.Flow) (*Flow, error) { + f, err := NewFlow(reg, exp, csrf, r, strategies, original.GetType()) + if err != nil { + return nil, err + } + f.TransientPayload = original.GetTransientPayload() + requestURL, err := url.ParseRequestURI(original.GetRequestURL()) + if err != nil { + requestURL = new(url.URL) + } + query := requestURL.Query() + // we need to keep the return_to in-tact if the `after_verification_return_to` is empty + // otherwise we take the `after_verification_return_to` query parameter over the current `return_to` + if afterVerificationReturn := query.Get("after_verification_return_to"); afterVerificationReturn != "" { + query.Set("return_to", afterVerificationReturn) + } + query.Del("after_verification_return_to") + requestURL.RawQuery = query.Encode() + f.RequestURL = requestURL.String() + if t, ok := original.(flow.OAuth2ChallengeProvider); ok { + f.OAuth2LoginChallenge = t.GetOAuth2LoginChallenge() + } + return f, nil +} + +func (f *Flow) GetType() flow.Type { return f.Type } +func (f *Flow) GetRequestURL() string { return f.RequestURL } +func (Flow) TableName() string { return "selfservice_verification_flows" } +func (f Flow) GetID() uuid.UUID { return f.ID } +func (f *Flow) GetState() State { return f.State } +func (Flow) GetFlowName() flow.FlowName { return flow.VerificationFlow } +func (f *Flow) SetState(state State) { f.State = state } +func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload } +func (f *Flow) GetOAuth2LoginChallenge() sqlxx.NullString { return f.OAuth2LoginChallenge } +func (f *Flow) GetHydraLoginRequest() *hydraclientgo.OAuth2LoginRequest { + return f.HydraLoginRequest +} +func (f *Flow) GetUI() *container.Container { return f.UI } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext } +func (f *Flow) SetInternalContext(c sqlxx.JSONRawMessage) { f.InternalContext = c } + +// EnsureInternalContext initializes InternalContext to an empty JSON object +// if it is missing or not valid JSON. Mirrors the registration / settings / +// login flow implementations. +func (f *Flow) EnsureInternalContext() { + if !gjson.ValidBytes(f.InternalContext) { + f.InternalContext = []byte("{}") + } +} + +// GetCourierBaseURL returns the base URL captured at flow init from the +// request context, or the empty string when nothing was captured (the email +// senders then fall back to Config.SelfPublicURL). +func (f *Flow) GetCourierBaseURL() string { + return gjson.GetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL).String() +} + +// setCourierBaseURL writes the captured base URL into InternalContext under +// the well-known key. Empty input is a no-op (preserves the fall-back +// path). Inputs longer than 8192 bytes are rejected — the same implicit +// ceiling the dedicated VARCHAR(8192) column used to enforce — so a +// pathological header cannot bloat the row. +func (f *Flow) setCourierBaseURL(s string) error { + if s == "" { + return nil + } + if len(s) > 8192 { + return nil + } + f.EnsureInternalContext() + out, err := sjson.SetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL, s) + if err != nil { + return errors.WithStack(err) + } + f.InternalContext = out + return nil +} + +func (f *Flow) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} + +func (f *Flow) AppendTo(src *url.URL) *url.URL { + values := src.Query() + values.Set("flow", f.ID.String()) + return urlx.CopyWithQuery(src, values) +} + +func (f *Flow) SetCSRFToken(token string) { + f.CSRFToken = token + f.UI.SetCSRF(token) +} + +func (f Flow) MarshalJSON() ([]byte, error) { + type local Flow + f.SetReturnTo() + return json.Marshal(local(f)) +} + +func (f *Flow) SetReturnTo() { + if u, err := url.Parse(f.RequestURL); err == nil { + f.ReturnTo = u.Query().Get("return_to") + } +} + +func (f *Flow) AfterFind(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +func (f *Flow) AfterSave(*pop.Connection) error { + f.SetReturnTo() + return nil +} + +// ContinueURL generates the URL to show on the continue screen after succesful verification +// +// It follows the following precedence: +// 1. If a `return_to` parameter has been passed to the flow's creation, is a valid URL and it's in the `selfservice.allowed_return_urls` that URL is returned +// 2. If `selfservice.flows.verification.after` is set, that URL is returned +// 3. As a fallback, the `selfservice.default_browser_return_url` URL is returned +func (f *Flow) ContinueURL(ctx context.Context, config *config.Config) *url.URL { + flowContinueURL := config.SelfServiceFlowVerificationReturnTo(ctx, config.SelfServiceBrowserDefaultReturnTo(ctx)) + + // Parse the flows request URL + verificationRequestURL, err := urlx.Parse(f.GetRequestURL()) + if err != nil { + // Return flow default, or global default return URL + return flowContinueURL + } + + verificationRequest := http.Request{URL: verificationRequestURL} + + returnTo, err := redir.SecureRedirectTo(&verificationRequest, flowContinueURL, + redir.SecureRedirectAllowSelfServiceURLs(config.SelfPublicURL(ctx)), + redir.SecureRedirectAllowURLs(config.SelfServiceBrowserAllowedReturnToDomains(ctx)), + ) + if err != nil { + // an error occured return flow default, or global default return URL + return flowContinueURL + } + return returnTo +} + +func (f *Flow) ToLoggerField() map[string]any { + if f == nil { + return map[string]any{} + } + return map[string]any{ + "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/flow_test.go b/selfservice/flow/verification/flow_test.go new file mode 100644 index 0000000..219cf44 --- /dev/null +++ b/selfservice/flow/verification/flow_test.go @@ -0,0 +1,327 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification_test + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/jsonx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/x" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/urlx" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" +) + +func TestFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + must := func(r *verification.Flow, err error) *verification.Flow { + require.NoError(t, err) + return r + } + + u := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"} + for k, tc := range []struct { + r *verification.Flow + expectErr bool + }{ + {r: must(verification.NewFlow(reg, time.Hour, "", u, nil, flow.TypeBrowser))}, + {r: must(verification.NewFlow(reg, -time.Hour, "", u, nil, flow.TypeBrowser)), expectErr: true}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + err := tc.r.Valid(clock.New()) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } + + t.Run("type=return_to", func(t *testing.T) { + _, err := verification.NewFlow(reg, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, nil, flow.TypeBrowser) + require.Error(t, err) + + _, err = verification.NewFlow(reg, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"}, nil, flow.TypeBrowser) + require.NoError(t, err) + }) + + assert.EqualValues(t, flow.StateChooseMethod, + must(verification.NewFlow(reg, time.Hour, "", u, nil, flow.TypeBrowser)).State) +} + +func TestGetType(t *testing.T) { + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=%s", ft), func(t *testing.T) { + r := &verification.Flow{Type: ft} + assert.Equal(t, ft, r.GetType()) + }) + } +} + +func TestGetRequestURL(t *testing.T) { + expectedURL := "http://foo/bar/baz" + f := &verification.Flow{RequestURL: expectedURL} + assert.Equal(t, expectedURL, f.GetRequestURL()) +} + +func TestNewFlow_capturesCourierBaseURL(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("nothing in context", func(t *testing.T) { + r := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"} + f, err := verification.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "", f.GetCourierBaseURL()) + }) + + t.Run("BaseURL in request context is copied onto flow", func(t *testing.T) { + base := urlx.ParseOrPanic("https://customer.example.com/") + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), base)) + f, err := verification.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, base.String(), f.GetCourierBaseURL()) + }) + + t.Run("http scheme is preserved (not coerced to https)", func(t *testing.T) { + base := urlx.ParseOrPanic("http://localhost:4000") + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), base)) + f, err := verification.NewFlow(reg, time.Hour, "", r, nil, flow.TypeBrowser) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", f.GetCourierBaseURL()) + }) +} + +func TestNewPostHookFlow(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + u := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"} + expectReturnTo := func(t *testing.T, originalFlowRequestQueryParams url.Values, expectedReturnTo string) { + originalFlow := registration.Flow{ + RequestURL: "http://foo.com/bar?" + originalFlowRequestQueryParams.Encode(), + } + t.Log(originalFlow.RequestURL) + f, err := verification.NewPostHookFlow(reg, time.Second, "", u, nil, &originalFlow) + require.NoError(t, err) + u, err := urlx.Parse(f.RequestURL) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(f.RequestURL, "http://foo.com/bar?")) + assert.Equal(t, "", u.Query().Get("after_verification_return_to")) + assert.Equal(t, expectedReturnTo, u.Query().Get("return_to")) + } + + t.Run("case=after_verification_return_to supplied", func(t *testing.T) { + expectedReturnTo := "http://foo.com/verification_callback" + expectReturnTo(t, url.Values{"after_verification_return_to": {expectedReturnTo}}, expectedReturnTo) + }) + + t.Run("case=return_to supplied", func(t *testing.T) { + expectReturnTo(t, url.Values{"return_to": {"http://foo.com/original_flow_callback"}}, "http://foo.com/original_flow_callback") + }) + + t.Run("case=return_to and after_verification_return_to supplied", func(t *testing.T) { + expectedReturnTo := "http://foo.com/verification_callback" + expectReturnTo(t, url.Values{ + "return_to": {"http://foo.com/original_flow_callback"}, + "after_verification_return_to": {expectedReturnTo}, + }, expectedReturnTo) + }) +} + +func TestFlowEncodeJSON(t *testing.T) { + assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, &verification.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, &verification.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) + assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, verification.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) +} + +func TestFromOldFlow_capturesCourierBaseURL(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + t.Run("uses the new request's context, not the old flow's value", func(t *testing.T) { + oldR := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("https://customer.example.com/"))) + oldF, err := verification.NewFlow(reg, time.Hour, "csrf", oldR, nil, flow.TypeBrowser) + require.NoError(t, err) + require.Equal(t, "https://customer.example.com/", oldF.GetCourierBaseURL()) + + newR := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("http://localhost:4000"))) + newF, err := verification.FromOldFlow(reg, time.Hour, oldF.CSRFToken, newR, nil, oldF) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", newF.GetCourierBaseURL()) + }) + + t.Run("empty when neither old nor new have a base URL", func(t *testing.T) { + r := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"} + oldF, err := verification.NewFlow(reg, time.Hour, "csrf", r, nil, flow.TypeBrowser) + require.NoError(t, err) + newF, err := verification.FromOldFlow(reg, time.Hour, oldF.CSRFToken, r, nil, oldF) + require.NoError(t, err) + assert.Equal(t, "", newF.GetCourierBaseURL()) + }) +} + +func TestNewPostHookFlow_capturesCourierBaseURL(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + // NewPostHookFlow is what the registration → auto-verification chain + // calls during the registration submit handler. The verification flow + // it creates needs CourierBaseURL captured from the *triggering* + // request's context (the registration submit, with the CNAME-aware + // X-Ory-Original-Host set by the cloud middleware) — not from the + // original flow's RequestURL. + t.Run("captures from triggering request context, ignoring original flow's URL", func(t *testing.T) { + original := registration.Flow{ + RequestURL: "https://" + "different.host/bar?return_to=https://elsewhere/path", + } + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("https://customer.example.com/"))) + f, err := verification.NewPostHookFlow(reg, time.Hour, "csrf", r, nil, &original) + require.NoError(t, err) + assert.Equal(t, "https://customer.example.com/", f.GetCourierBaseURL()) + }) + + t.Run("preserves http scheme through the post-hook chain", func(t *testing.T) { + // Regression test for x.WithBaseURL no longer coercing scheme to + // https. The Ory CLI / NextJS proxy case carries http://localhost. + original := registration.Flow{RequestURL: "http://foo.com/bar"} + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("http://localhost:4000"))) + f, err := verification.NewPostHookFlow(reg, time.Hour, "csrf", r, nil, &original) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", f.GetCourierBaseURL()) + }) + + t.Run("empty when triggering request has no base URL", func(t *testing.T) { + original := registration.Flow{RequestURL: "http://foo.com/bar"} + r := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"} + f, err := verification.NewPostHookFlow(reg, time.Hour, "csrf", r, nil, &original) + require.NoError(t, err) + assert.Equal(t, "", f.GetCourierBaseURL()) + }) +} + +// TestCourierBaseURLStoredInInternalContext locks down the storage +// location: the captured base URL must land in InternalContext under +// flow.InternalContextKeyCourierBaseURL, not in a dedicated column. If a +// future refactor moves it elsewhere, this fails — separately from the +// getter contract. +func TestCourierBaseURLStoredInInternalContext(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + r := (&http.Request{URL: urlx.ParseOrPanic("http://foo/bar"), Host: "foo"}). + WithContext(x.WithBaseURL(context.Background(), urlx.ParseOrPanic("https://customer.example.com/"))) + f, err := verification.NewFlow(reg, time.Hour, "csrf", r, nil, flow.TypeBrowser) + require.NoError(t, err) + + got := gjson.GetBytes(f.InternalContext, flow.InternalContextKeyCourierBaseURL).String() + assert.Equal(t, "https://customer.example.com/", got) +} + +func TestFromOldFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + r := http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=" + urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser").String()}, Host: "ory.sh"} + for _, ft := range []flow.Type{ + flow.TypeAPI, + flow.TypeBrowser, + } { + t.Run(fmt.Sprintf("case=original flow is %s", ft), func(t *testing.T) { + f, err := verification.NewFlow(reg, 0, "csrf", &r, nil, ft) + require.NoError(t, err) + nf, err := verification.FromOldFlow(reg, time.Duration(time.Hour), f.CSRFToken, &r, nil, f) + require.NoError(t, err) + require.Equal(t, flow.TypeBrowser, nf.Type) + }) + } +} + +func TestContinueURL(t *testing.T) { + const globalReturnTo = "https://ory.sh/global-return-to" + const localReturnTo = "https://ory.sh/local-return-to" + const flowReturnTo = "https://ory.sh/flow-return-to" + + for _, tc := range []struct { + desc string + prep func(conf *config.Config) + requestURL string + expect string + }{ + { + desc: "return_to has precedence over global return to", + prep: func(conf *config.Config) { + conf.MustSet(context.Background(), config.ViperKeyURLsAllowedReturnToDomains, []string{localReturnTo}) + }, + requestURL: fmt.Sprintf("http://kratos:4433/verification?return_to=%s", localReturnTo), + expect: localReturnTo, + }, + { + desc: "with return_to not allowed", + requestURL: fmt.Sprintf("http://kratos:4433/verification?return_to=%s", localReturnTo), + expect: globalReturnTo, + }, + { + desc: "with invalid request url", + requestURL: string([]byte{0x7f}), // 0x7f is an ASCII control char, and fails URL validation + expect: globalReturnTo, + }, + { + desc: "flow return to has precedence over global return to", + prep: func(conf *config.Config) { + conf.MustSet(context.Background(), config.ViperKeySelfServiceVerificationBrowserDefaultReturnTo, flowReturnTo) + }, + requestURL: "http://kratos:4433/verification", + expect: flowReturnTo, + }, + { + desc: "return_to has precedence over flow return to", + prep: func(conf *config.Config) { + conf.MustSet(context.Background(), config.ViperKeySelfServiceVerificationBrowserDefaultReturnTo, flowReturnTo) + conf.MustSet(context.Background(), config.ViperKeyURLsAllowedReturnToDomains, []string{localReturnTo}) + }, + requestURL: fmt.Sprintf("http://kratos:4433/verification?return_to=%s", localReturnTo), + expect: localReturnTo, + }, + } { + t.Run(fmt.Sprintf("case=%s", tc.desc), func(t *testing.T) { + conf := pkg.NewConfigurationWithDefaults(t) + conf.MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo) + if tc.prep != nil { + tc.prep(conf) + } + flow := verification.Flow{ + RequestURL: tc.requestURL, + } + + url := flow.ContinueURL(context.Background(), conf) + require.NotNil(t, url) + + require.Equal(t, tc.expect, url.String()) + }) + } +} diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go new file mode 100644 index 0000000..c25a2a2 --- /dev/null +++ b/selfservice/flow/verification/handler.go @@ -0,0 +1,542 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/nosurf" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" +) + +const ( + RouteInitBrowserFlow = "/self-service/verification/browser" + RouteInitAPIFlow = "/self-service/verification/api" + RouteGetFlow = "/self-service/verification/flows" + + RouteSubmitFlow = "/self-service/verification" +) + +type ( + HandlerProvider interface { + VerificationHandler() *Handler + } + handlerDependencies interface { + clock.Provider + errorx.ManagementProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + config.Provider + hydra.Provider + session.PersistenceProvider + session.ManagementProvider + + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + nosurfx.CSRFProvider + logrusx.Provider + + FlowPersistenceProvider + ErrorHandlerProvider + StrategyProvider + HookExecutorProvider + } + Handler struct { + d handlerDependencies + } +) + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) + h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) + + public.GET(RouteInitBrowserFlow, h.createBrowserVerificationFlow) + public.GET(RouteInitAPIFlow, h.createNativeVerificationFlow) + public.GET(RouteGetFlow, h.getVerificationFlow) + + public.POST(RouteSubmitFlow, h.updateVerificationFlow) + public.GET(RouteSubmitFlow, h.updateVerificationFlow) +} + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) + + admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) + admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) +} + +type FlowOption func(f *Flow) + +func WithFlowReturnTo(returnTo string) FlowOption { + return func(f *Flow) { + f.ReturnTo = returnTo + } +} + +func (h *Handler) NewVerificationFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...FlowOption) (*Flow, error) { + strategies, _, err := h.d.GetActiveVerificationStrategies(r.Context()) + if err != nil { + return nil, err + } + + f, err := NewFlow(h.d, h.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, strategies, ft) + if err != nil { + return nil, err + } + for _, o := range opts { + o(f) + } + + if err := h.d.VerificationExecutor().PreVerificationHook(w, r, f); err != nil { + return nil, err + } + + if err := h.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { + return nil, err + } + + return f, nil +} + +// Create Verification Flow Parameters for Native Apps +// +// swagger:parameters createNativeVerificationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createNativeVerificationFlow struct { + // A URL contained in the return_to key of the verification flow. + // This piece of data has no effect on the actual logic of the flow and is purely informational. + // + // in: query + ReturnTo string `json:"return_to"` +} + +// swagger:route GET /self-service/verification/api frontend createNativeVerificationFlow +// +// # Create Verification Flow for Native Apps +// +// This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on. +// +// To fetch an existing verification flow call `/self-service/verification/flows?flow=`. +// +// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server +// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make +// you vulnerable to a variety of CSRF attacks. +// +// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). +// +// More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). +// +// Schemes: http, https +// +// Responses: +// 200: verificationFlow +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createNativeVerificationFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) + return + } + + req, err := h.NewVerificationFlow(w, r, flow.TypeAPI) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, req) +} + +// Create Browser Verification Flow Parameters +// +// swagger:parameters createBrowserVerificationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createBrowserVerificationFlow struct { + // The URL to return the browser to after the flow was completed. + // + // in: query + ReturnTo string `json:"return_to"` +} + +// swagger:route GET /self-service/verification/browser frontend createBrowserVerificationFlow +// +// # Create Verification Flow for Browser Clients +// +// This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to +// `selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`. +// +// If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects. +// +// This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...). +// +// More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). +// +// Schemes: http, https +// +// Responses: +// 200: verificationFlow +// 303: emptyResponse +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) createBrowserVerificationFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) + return + } + + req, err := h.NewVerificationFlow(w, r, flow.TypeBrowser) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + redirTo := req.AppendTo(h.d.Config().SelfServiceFlowVerificationUI(r.Context())).String() + x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), req, redirTo) +} + +// Get Verification Flow Parameters +// +// swagger:parameters getVerificationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getVerificationFlow struct { + // The Flow ID + // + // The value for this parameter comes from `request` URL Query parameter sent to your + // application (e.g. `/verification?flow=abcde`). + // + // required: true + // in: query + FlowID string `json:"id"` + + // HTTP Cookies + // + // When using the SDK on the server side you must include the HTTP Cookie Header + // originally sent to your HTTP handler here. + // + // in: header + // name: Cookie + Cookie string `json:"cookie"` +} + +// swagger:route GET /self-service/verification/flows frontend getVerificationFlow +// +// # Get Verification Flow +// +// This endpoint returns a verification flow's context with, for example, error details and other information. +// +// Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. +// For AJAX requests you must ensure that cookies are included in the request or requests will fail. +// +// If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain +// and you need to forward the incoming HTTP Cookie header to this endpoint: +// +// ```js +// // pseudo-code example +// router.get('/recovery', async function (req, res) { +// const flow = await client.getVerificationFlow(req.header('cookie'), req.query['flow']) +// +// res.render('verification', flow) +// }) +// ``` +// +// More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: verificationFlow +// 403: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) getVerificationFlow(w http.ResponseWriter, r *http.Request) { + if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) + return + } + + rid := x.ParseUUID(r.URL.Query().Get("id")) + req, err := h.d.VerificationFlowPersister().GetVerificationFlow(r.Context(), rid) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + // Browser flows must include the CSRF token + // + // Resolves: https://github.com/ory/kratos/issues/1282 + if req.Type == flow.TypeBrowser && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), req.CSRFToken) { + h.d.Writer().WriteError(w, r, nosurfx.CSRFErrorReason(r, h.d)) + return + } + + if req.ExpiresAt.Before(h.d.Clock().Now().UTC()) { + if req.Type == flow.TypeBrowser { + redirectURL := flow.GetFlowExpiredRedirectURL(r.Context(), h.d.Config(), RouteInitBrowserFlow, req.ReturnTo) + + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). + WithReason("The verification flow has expired. Redirect the user to the verification flow init endpoint to initialize a new verification flow."). + WithDetail("redirect_to", redirectURL.String()). + WithDetail("return_to", req.ReturnTo))) + return + } + h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). + WithReason("The verification flow has expired. Call the verification flow init API endpoint to initialize a new verification flow."). + WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitAPIFlow).String()))) + return + } + + h.d.Writer().Write(w, r, req) +} + +// Update Verification Flow Parameters +// +// swagger:parameters updateVerificationFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateVerificationFlow struct { + // The Verification Flow ID + // + // The value for this parameter comes from `flow` URL Query parameter sent to your + // application (e.g. `/verification?flow=abcde`). + // + // required: true + // in: query + Flow string `json:"flow"` + + // Verification Token + // + // The verification token which completes the verification request. If the token + // is invalid (e.g. expired) an error will be shown to the end-user. + // + // This parameter is usually set in a link and not used by any direct API call. + // + // in: query + Token string `json:"token" form:"token"` + + // in: body + // required: true + Body updateVerificationFlowBody + + // HTTP Cookies + // + // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header + // sent by the client to your server here. This ensures that CSRF and session cookies are respected. + // + // in: header + // name: Cookie + Cookies string `json:"Cookie"` +} + +// Update Verification Flow Request Body +// +// swagger:model updateVerificationFlowBody +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateVerificationFlowBody struct{} + +// swagger:route POST /self-service/verification frontend updateVerificationFlow +// +// # Complete Verification Flow +// +// Use this endpoint to complete a verification flow. This endpoint +// behaves differently for API and browser flows and has several states: +// +// - `choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent +// and works with API- and Browser-initiated flows. +// - For API clients and Browser clients with HTTP Header `Accept: application/json` it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid +// and a HTTP 303 See Other redirect with a fresh verification flow if the flow was otherwise invalid (e.g. expired). +// - For Browser clients without HTTP Header `Accept` or with `Accept: text/*` it returns a HTTP 303 See Other redirect to the Verification UI URL with the Verification Flow ID appended. +// - `sent_email` is the success state after `choose_method` when using the `link` method and allows the user to request another verification email. It +// works for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state. +// - `passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow ("sending a verification link") +// does not have any API capabilities. The server responds with a HTTP 303 See Other redirect either to the Settings UI URL +// (if the link was valid) and instructs the user to update their password, or a redirect to the Verification UI URL with +// a new Verification Flow ID which contains an error message that the verification link was invalid. +// +// More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: verificationFlow +// 303: emptyResponse +// 400: verificationFlow +// 410: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request) { + rid, err := flow.GetFlowID(r) + if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) + return + } + + ctx := r.Context() + f, err := h.d.VerificationFlowPersister().GetVerificationFlow(ctx, rid) + if errors.Is(err, sqlcon.ErrNoRows()) { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, errors.WithStack(herodot.ErrNotFound().WithReasonf("The verification request could not be found. Please restart the flow."))) + return + } else if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) + return + } + + if err := f.Valid(h.d.Clock()); err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + var g node.UiNodeGroup + var found bool + for _, ss := range h.d.AllVerificationStrategies() { + // If a primary strategy is set, but it does not match the current strategy, that strategy is not responsible anyways. + if ps, isPrimary := ss.(PrimaryStrategy); isPrimary && f.Active.String() != "" && f.Active.String() != ps.VerificationStrategyID() { + continue + } + + err := ss.Verify(w, r, f) + if errors.Is(err, flow.ErrStrategyNotResponsible) { + continue + } else if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) + return + } + + found = true + g = ss.NodeGroup() + break + } + + if !found { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, errors.WithStack(schema.NewNoVerificationStrategyResponsible())) + return + } + + // API flows can receive requests from the browser, if the link strategy is used. + // However, x.IsBrowserRequest only checks for form submissions, not JSON requests made from a browser context + if x.IsBrowserRequest(r) || (f.Type == flow.TypeBrowser && x.IsJSONRequest(r)) { + // Special case: If we ended up here through a OAuth2 login challenge, we need to accept the login request + // and redirect back to the OAuth2 provider. + if flow.HasReachedState(flow.StatePassedChallenge, f.State) && f.OAuth2LoginChallenge.String() != "" { + if !f.IdentityID.Valid || !f.SessionID.Valid { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, + herodot.ErrBadRequest().WithReasonf("No session was found for this flow. Please retry the authentication.")) + return + } + + callbackURL, err := h.d.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(f.OAuth2LoginChallenge), + IdentityID: f.IdentityID.UUID.String(), + SessionID: f.SessionID.UUID.String(), + AuthenticationMethods: f.AMR, + }) + if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + sess, err := h.d.SessionPersister().GetSession(ctx, f.SessionID.UUID, session.ExpandDefault) + if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + err = h.d.SessionManager().IssueCookie(ctx, w, r, sess) + if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + if x.IsJSONRequest(r) { + // This intentionally works differently than the "form browser" flow, + // as it _does_ show the `verification success` UI, but the "Continue" + // button contains the link to the OAuth2 provider with the `login_verifier`. + continueNode := f.UI.Nodes.Find("continue") + if continueNode != nil { + if attr, ok := continueNode.Attributes.(*node.AnchorAttributes); ok { + attr.HREF = callbackURL + if err := h.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) + return + } + + h.d.Writer().Write(w, r, f) + return + } + } + + // The flow does not have the `continue` node, which is an unknown state. + // This should never happen. + } else { + http.Redirect(w, r, callbackURL, http.StatusSeeOther) + return + } + } else if x.IsBrowserRequest(r) { + http.Redirect(w, r, f.AppendTo(h.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + return + } + } + + updatedFlow, err := h.d.VerificationFlowPersister().GetVerificationFlow(ctx, f.ID) + if err != nil { + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, g, err) + return + } + + h.d.Writer().Write(w, r, updatedFlow) +} diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go new file mode 100644 index 0000000..195d6d5 --- /dev/null +++ b/selfservice/flow/verification/handler_test.go @@ -0,0 +1,317 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/gobuffalo/httptest" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +func TestGetFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(verification.VerificationStrategyLink), + map[string]interface{}{"enabled": true}) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "", conf) + + setupVerificationUI := func(t *testing.T, c *http.Client) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationUI, ts.URL) + return ts + } + + assertFlowPayload := func(t *testing.T, body []byte, isApi bool) { + if isApi { + assert.Equal(t, "api", gjson.GetBytes(body, "type").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "ui.fields.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) + } else { + assert.Equal(t, "browser", gjson.GetBytes(body, "type").String(), "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) + } + + assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), gjson.GetBytes(body, "id").String(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), public.URL, "%s", body) + } + + t.Run("case=valid", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteInitBrowserFlow) + require.NotEqualValues(t, res.Request.URL.String(), public.URL+verification.RouteInitBrowserFlow) + assertFlowPayload(t, body, false) + }) + + t.Run("type=spa", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + res, body := testhelpers.EasyGetJSON(t, client, public.URL+verification.RouteInitBrowserFlow) + require.EqualValues(t, res.Request.URL.String(), public.URL+verification.RouteInitBrowserFlow) + assertFlowPayload(t, body, false) + }) + + t.Run("type=api", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteInitAPIFlow) + assert.Len(t, res.Header.Get("Set-Cookie"), 0) + assertFlowPayload(t, body, true) + }) + }) + + t.Run("case=csrf cookie missing", func(t *testing.T) { + client := testhelpers.NewTestClient(t) + _ = setupVerificationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) + + assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) + }) + + t.Run("case=expired", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) + + // Expire the flow + f, err := reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.VerificationFlowPersister().UpdateVerificationFlow(context.Background(), f)) + + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+f.ID.String()) + assert.EqualValues(t, http.StatusGone, res.StatusCode) + assert.Equal(t, public.URL+verification.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) + }) + + t.Run("case=expired with return_to", func(t *testing.T) { + returnTo := "https://www.ory.com" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow+"?return_to="+returnTo) + + // Expire the flow + f, err := reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) + require.NoError(t, err) + f.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.VerificationFlowPersister().UpdateVerificationFlow(context.Background(), f)) + + // Retrieve the flow and verify that return_to is in the response + getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, verification.RouteGetFlow, f.ID, returnTo) + getBody := testhelpers.EasyGetBody(t, client, getURL) + assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) + + // submit the flow but it is expired + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := client.PostForm(u, url.Values{"method": {"link"}, "csrf_token": {f.CSRFToken}, "email": {"email@ory.sh"}}) + require.NoError(t, err) + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + f, err = reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String())) + require.NoError(t, err) + assert.Equal(t, public.URL+verification.RouteInitBrowserFlow+"?return_to="+returnTo, f.RequestURL) + }) + + t.Run("case=relative redirect when self-service verification ui is a relative URL", func(t *testing.T) { + ts, _ := testhelpers.NewKratosServer(t, reg) + reg.Config().MustSet(ctx, config.ViperKeySelfServiceVerificationUI, "/verification-ts") + assert.Regexp( + t, + "^/verification-ts.*$", + testhelpers.GetSelfServiceRedirectLocation(t, ts.URL+verification.RouteInitBrowserFlow), + ) + }) + + t.Run("case=not found", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + _ = setupVerificationUI(t, client) + + res, _ := testhelpers.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+x.NewUUID().String()) + assert.EqualValues(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("case=redirects with 303", func(t *testing.T) { + ts, _ := testhelpers.NewKratosServer(t, reg) + + // prevent the redirect + ts.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + req, err := http.NewRequest("GET", ts.URL+verification.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + res, err := ts.Client().Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + // here we check that the redirect status is 303 + require.Equal(t, http.StatusSeeOther, res.StatusCode) + }) +} + +func TestPostFlow(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.WithSelfserviceStrategies(t, []any{&verification.FakeStrategy{}}) + reg.SetHydra(hydra.NewFake()) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "", conf) + + t.Run("client=browser/case=valid", func(t *testing.T) { + f := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + Type: "browser", + ExpiresAt: time.Now().Add(1 * time.Hour), + IssuedAt: time.Now(), + State: flow.StateChooseMethod, + } + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + client := testhelpers.NewNoRedirectClientWithCookies(t) + + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + resp, err := client.PostForm(u, url.Values{"method": {"fake"}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusSeeOther, resp.StatusCode) + assert.Equal(t, conf.SelfServiceFlowVerificationUI(ctx).String()+"?flow="+f.ID.String(), resp.Header.Get("Location")) + }) + + t.Run("client=spa/case=valid", func(t *testing.T) { + f := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + Type: "browser", + ExpiresAt: time.Now().Add(1 * time.Hour), + IssuedAt: time.Now(), + State: flow.StateChooseMethod, + } + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + client := testhelpers.NewClientWithCookies(t) + + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + req, err := http.NewRequest("POST", u, strings.NewReader(`{"method": "fake"}`)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + resp, err := client.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("suite=with OIDC login challenge", func(t *testing.T) { + createFlow := func(t *testing.T) *verification.Flow { + t.Helper() + s := testhelpers.CreateSession(t, reg) + + f := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + Type: "browser", + ExpiresAt: time.Now().Add(1 * time.Hour), + IssuedAt: time.Now(), + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + OAuth2LoginChallengeParams: verification.OAuth2LoginChallengeParams{ + SessionID: uuid.NullUUID{UUID: s.ID, Valid: true}, + IdentityID: uuid.NullUUID{UUID: s.IdentityID, Valid: true}, + AMR: s.AMR, + }, + UI: &container.Container{ + Action: "http://action", + Nodes: []*node.Node{node.NewAnchorField("continue", "https://ory.sh", node.CodeGroup, text.NewInfoNodeLabelContinue())}, + }, + State: flow.StatePassedChallenge, + } + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + return f + } + t.Run("client=browser/case=succeeds with a session", func(t *testing.T) { + f := createFlow(t) + client := testhelpers.NewNoRedirectClientWithCookies(t) + + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + resp, err := client.PostForm(u, url.Values{"method": {"fake"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusSeeOther, resp.StatusCode) + assert.Equal(t, hydra.FakePostLoginURL, resp.Header.Get("Location")) + }) + t.Run("client=spa/case=succeeds with a session", func(t *testing.T) { + f := createFlow(t) + client := testhelpers.NewNoRedirectClientWithCookies(t) + + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + resp, err := client.Post(u, "application/json", strings.NewReader(`{"method": "fake"}`)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := x.MustReadAll(resp.Body) + assert.Equal(t, hydra.FakePostLoginURL, gjson.GetBytes(body, "ui.nodes.#(attributes.id==continue).attributes.href").String(), "%s", body) + }) + + t.Run("case=fails without a session", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(testhelpers.EasyGetBody(t, client, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationUI, ts.URL) + + f := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + Type: "browser", + ExpiresAt: time.Now().Add(1 * time.Hour), + IssuedAt: time.Now(), + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + State: flow.StateChooseMethod, + } + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + u := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + resp, err := client.PostForm(u, url.Values{"method": {"fake"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, f.ID.String(), resp.Request.URL.Query().Get("flow")) + }) + }) +} diff --git a/selfservice/flow/verification/hook.go b/selfservice/flow/verification/hook.go new file mode 100644 index 0000000..1f98b5a --- /dev/null +++ b/selfservice/flow/verification/hook.go @@ -0,0 +1,130 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "context" + "fmt" + "net/http" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" +) + +type ( + PreHookExecutor interface { + ExecuteVerificationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error + + PostHookExecutor interface { + ExecutePostVerificationHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error + } + PostHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error + + HooksProvider interface { + PostVerificationHooks(ctx context.Context) ([]PostHookExecutor, error) + PreVerificationHooks(ctx context.Context) ([]PreHookExecutor, error) + } +) + +func PostHookVerificationExecutorNames(e []PostHookExecutor) []string { + names := make([]string, len(e)) + for k, ee := range e { + names[k] = fmt.Sprintf("%T", ee) + } + return names +} + +func (f PreHookExecutorFunc) ExecuteVerificationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + return f(w, r, a) +} + +func (f PostHookExecutorFunc) ExecutePostVerificationHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error { + return f(w, r, a, i) +} + +type ( + executorDependencies interface { + config.Provider + identity.ManagementProvider + identity.ValidationProvider + session.PersistenceProvider + HooksProvider + nosurfx.CSRFTokenGeneratorProvider + logrusx.Provider + httpx.WriterProvider + } + + HookExecutor struct { + d executorDependencies + } + + HookExecutorProvider interface { + VerificationExecutor() *HookExecutor + } +) + +func NewHookExecutor(d executorDependencies) *HookExecutor { + return &HookExecutor{ + d: d, + } +} + +func (e *HookExecutor) PreVerificationHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + hooks, err := e.d.PreVerificationHooks(r.Context()) + if err != nil { + return err + } + for _, executor := range hooks { + if err := executor.ExecuteVerificationPreHook(w, r, a); err != nil { + return err + } + } + + return nil +} + +func (e *HookExecutor) PostVerificationHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error { + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + Debug("Running ExecutePostVerificationHooks.") + hooks, err := e.d.PostVerificationHooks(r.Context()) + if err != nil { + return err + } + for k, executor := range hooks { + if err := executor.ExecutePostVerificationHook(w, r, a, i); err != nil { + return flow.HandleHookError(w, r, a, i.Traits, node.LinkGroup, err, e.d, e.d) + } + + e.d.Logger().WithRequest(r). + WithField("executor", fmt.Sprintf("%T", executor)). + WithField("executor_position", k). + WithField("executors", PostHookVerificationExecutorNames(hooks)). + WithField("identity_id", i.ID). + Debug("ExecutePostVerificationHook completed successfully.") + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewVerificationSucceeded(r.Context(), a.ID, i.ID, string(a.Type), a.Active.String())) + + e.d.Logger(). + WithRequest(r). + WithField("identity_id", i.ID). + Debug("Post verification execution hooks completed successfully.") + + return nil +} diff --git a/selfservice/flow/verification/hook_test.go b/selfservice/flow/verification/hook_test.go new file mode 100644 index 0000000..405b30f --- /dev/null +++ b/selfservice/flow/verification/hook_test.go @@ -0,0 +1,111 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification_test + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/selfservice/flow/verification" + + "github.com/gobuffalo/httptest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/x" +) + +func TestVerificationExecutor(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + newServer := func(t *testing.T, i *identity.Identity, ft flow.Type) *httptest.Server { + router := http.NewServeMux() + router.HandleFunc("GET /verification/pre", func(w http.ResponseWriter, r *http.Request) { + strategies, _, err := reg.GetActiveVerificationStrategies(r.Context()) + require.NoError(t, err) + a, err := verification.NewFlow(reg, time.Minute, nosurfx.FakeCSRFToken, r, strategies, ft) + require.NoError(t, err) + if testhelpers.SelfServiceHookErrorHandler(t, w, r, verification.ErrHookAbortFlow, reg.VerificationExecutor().PreVerificationHook(w, r, a)) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.HandleFunc("GET /verification/post", func(w http.ResponseWriter, r *http.Request) { + strategies, _, err := reg.GetActiveVerificationStrategies(r.Context()) + require.NoError(t, err) + a, err := verification.NewFlow(reg, time.Minute, nosurfx.FakeCSRFToken, r, strategies, ft) + require.NoError(t, err) + a.RequestURL = x.RequestURL(r).String() + if testhelpers.SelfServiceHookErrorHandler(t, w, r, verification.ErrHookAbortFlow, reg.VerificationExecutor().PostVerificationHook(w, r, a, i)) { + _, _ = w.Write([]byte("ok")) + } + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + return ts + } + + t.Run("method=PostVerificationHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, _ := testhelpers.SelfServiceMakeHookRequest(t, ts, "/verification/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceVerificationAfter, config.HookGlobal), + []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, _ := testhelpers.SelfServiceMakeHookRequest(t, ts, "/verification/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceVerificationAfter, config.HookGlobal), + []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostVerificationHook": "abort"}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) + ts := newServer(t, i, flow.TypeBrowser) + + res, body := testhelpers.SelfServiceMakeHookRequest(t, ts, "/verification/post", false, url.Values{}) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "", body) + }) + + for _, kind := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run("type="+string(kind)+"/method=PreVerificationHook", testhelpers.TestSelfServicePreHook( + config.ViperKeySelfServiceVerificationBeforeHooks, + testhelpers.SelfServiceMakeVerificationPreHookRequest, + func(t *testing.T) *httptest.Server { + i := testhelpers.SelfServiceHookFakeIdentity(t) + return newServer(t, i, kind) + }, + conf, + )) + } + }) +} diff --git a/selfservice/flow/verification/persistence.go b/selfservice/flow/verification/persistence.go new file mode 100644 index 0000000..361bbb8 --- /dev/null +++ b/selfservice/flow/verification/persistence.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type ( + FlowPersistenceProvider interface { + VerificationFlowPersister() FlowPersister + } + FlowPersister interface { + CreateVerificationFlow(context.Context, *Flow) error + GetVerificationFlow(ctx context.Context, id uuid.UUID) (*Flow, error) + UpdateVerificationFlow(context.Context, *Flow) error + DeleteExpiredVerificationFlows(context.Context, time.Time, int) error + } +) diff --git a/selfservice/flow/verification/state.go b/selfservice/flow/verification/state.go new file mode 100644 index 0000000..84aded9 --- /dev/null +++ b/selfservice/flow/verification/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import "github.com/ory/kratos/selfservice/flow" + +// Verification Flow State +// +// 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. +// +// swagger:model verificationFlowState +type State = flow.State diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go new file mode 100644 index 0000000..65a3fdd --- /dev/null +++ b/selfservice/flow/verification/strategy.go @@ -0,0 +1,70 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package verification + +import ( + "context" + "net/http" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" +) + +//swagger:enum VerificationStrategy +type VerificationStrategy string + +const ( + VerificationStrategyLink VerificationStrategy = "link" + VerificationStrategyCode VerificationStrategy = "code" +) + +type ( + Strategy interface { + VerificationStrategyID() string + NodeGroup() node.UiNodeGroup + PopulateVerificationMethod(*http.Request, *Flow) error + Verify(w http.ResponseWriter, r *http.Request, f *Flow) (err error) + } + PrimaryStrategy interface { + Strategy + SendVerificationCode(context.Context, *Flow, *identity.Identity, identity.VerifiableAddressLike) error + } + Strategies []Strategy + StrategyProvider interface { + VerificationStrategies(ctx context.Context) Strategies + AllVerificationStrategies() Strategies + GetActiveVerificationStrategies(context.Context) (active Strategies, primary PrimaryStrategy, err error) + } +) + +func (s Strategies) ActiveStrategies(id string) (active Strategies, primary PrimaryStrategy, err error) { + ids := make([]string, len(s)) + activeStrategies := Strategies{} + for k, ss := range s { + ids[k] = ss.VerificationStrategyID() + if ps, isPrimary := ss.(PrimaryStrategy); ss.VerificationStrategyID() == id || !isPrimary { + activeStrategies = append(activeStrategies, ss) + if isPrimary { + primary = ps + } + } + } + + if primary == nil { + return nil, nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("unable to find strategy for %s have %v", id, ids)) + } + + return activeStrategies, primary, nil +} + +func (s Strategies) MustStrategy(id string) Strategy { + _, strategy, err := s.ActiveStrategies(id) + if err != nil { + panic(err) + } + return strategy +} diff --git a/selfservice/flow/verification/stub/extension/schema.json b/selfservice/flow/verification/stub/extension/schema.json new file mode 100644 index 0000000..4dc2cdb --- /dev/null +++ b/selfservice/flow/verification/stub/extension/schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/flow/verification/stub/identity.schema.json b/selfservice/flow/verification/stub/identity.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/flow/verification/stub/identity.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go new file mode 100644 index 0000000..ec88775 --- /dev/null +++ b/selfservice/flow/verification/test/persistence.go @@ -0,0 +1,165 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlcon" +) + +func TestFlowPersister(ctx context.Context, p interface { + persistence.Persister +}, +) func(t *testing.T) { + clearids := func(r *verification.Flow) { + r.ID = uuid.UUID{} + } + + return func(t *testing.T) { + nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + 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) + }) + + newFlow := func(t *testing.T) *verification.Flow { + var r verification.Flow + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + r.State = flow.StateChooseMethod + return &r + } + + t.Run("case=should create a new verification flow", func(t *testing.T) { + r := newFlow(t) + err := p.CreateVerificationFlow(ctx, r) + require.NoError(t, err, "%#v", err) + require.Equal(t, nid, r.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetVerificationFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r verification.Flow + require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateChooseMethod + require.NoError(t, p.CreateVerificationFlow(ctx, &r)) + require.Equal(t, nid, r.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetVerificationFlow(ctx, r.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=should create and fetch a verification request", func(t *testing.T) { + expected := newFlow(t) + require.NoError(t, p.CreateVerificationFlow(ctx, expected)) + require.Equal(t, nid, expected.NID) + + t.Run("fail to find on other network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetVerificationFlow(ctx, expected.ID) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + actual, err := p.GetVerificationFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assertx.EqualAsJSON(t, expected.UI, actual.UI, "expected:\t%s\nactual:\t%s", expected.UI, actual.UI) + }) + + t.Run("case=should create and update a verification request", func(t *testing.T) { + expected := newFlow(t) + expected.UI.Nodes = node.Nodes{} + expected.UI.Nodes.Append(node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + expected.UI.Nodes.Append(node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + }))) + + err := p.CreateVerificationFlow(ctx, expected) + require.NoError(t, err) + + expected.UI.Action = "/new-action" + expected.UI.Nodes.Append( + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + }))) + + expected.RequestURL = "/new-request-url" + + t.Run("fail to find on other network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + require.ErrorIs(t, other.UpdateVerificationFlow(ctx, expected), sqlcon.ErrNoRows()) + + actual, err := p.GetVerificationFlow(ctx, expected.ID) + require.NoError(t, err) + assert.NotEqual(t, "/new-request-url", actual.RequestURL) + }) + + require.NoError(t, p.UpdateVerificationFlow(ctx, expected)) + + actual, err := p.GetVerificationFlow(ctx, expected.ID) + require.NoError(t, err) + + assert.Equal(t, "/new-action", actual.UI.Action) + assert.Equal(t, "/new-request-url", actual.RequestURL) + assertx.EqualAsJSON(t, node.Nodes{ + // v0.5: {Name: "zab", Type: "zab", Pattern: "zab"}, + node.NewInputField("zab", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + node.NewInputField("foo", nil, node.DefaultGroup, "bar", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "baz" + })), + // v0.5: {Name: "zab", Type: "bar", Pattern: "baz"}, + node.NewInputField("zab", nil, node.DefaultGroup, "zab", node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "zab" + })), + }, actual.UI.Nodes) + }) + + t.Run("case=should not cause data loss when updating a request without changes", func(t *testing.T) { + expected := newFlow(t) + err := p.CreateVerificationFlow(ctx, expected) + require.NoError(t, err) + + actual, err := p.GetVerificationFlow(ctx, expected.ID) + require.NoError(t, err) + + require.NoError(t, p.UpdateVerificationFlow(ctx, actual)) + + actual, err = p.GetVerificationFlow(ctx, expected.ID) + require.NoError(t, err) + assertx.EqualAsJSON(t, expected.UI, actual.UI) + }) + } +} diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go new file mode 100644 index 0000000..daaf87a --- /dev/null +++ b/selfservice/flowhelpers/login.go @@ -0,0 +1,36 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flowhelpers + +import ( + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" +) + +// GuessForcedLoginIdentifier returns the identifier for login flows where the identity needs to refresh the session. +func GuessForcedLoginIdentifier(r *http.Request, d interface { + session.ManagementProvider + identity.PrivilegedPoolProvider +}, f interface { + 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.IsRefresh() { + // do nothing + } else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing); err != nil { + // do nothing + } else if id, err = d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), sess.IdentityID); err != nil { + // do nothing + } else if creds, ok = id.GetCredentials(ct); !ok { + // do nothing + } else if len(creds.Identifiers) == 0 { + // do nothing + } else { + identifier = creds.Identifiers[0] + } + return +} diff --git a/selfservice/flowhelpers/login_test.go b/selfservice/flowhelpers/login_test.go new file mode 100644 index 0000000..1661bda --- /dev/null +++ b/selfservice/flowhelpers/login_test.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flowhelpers_test + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flowhelpers" +) + +func TestGuessForcedLoginIdentifier(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + + i := identity.NewIdentity("") + ic := identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"foobar"}, + } + i.Credentials[identity.CredentialsTypePassword] = ic + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + req := httptest.NewRequest("GET", "/sessions/whoami", nil) + + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) + + r := httptest.NewRequest("GET", "/login", nil) + r.Header.Set("Authorization", "Bearer "+sess.Token) + + var f login.Flow + f.Refresh = true + + identifier, id, creds := flowhelpers.GuessForcedLoginIdentifier(r, reg, &f, identity.CredentialsTypePassword) + assert.Equal(t, "foobar", identifier) + assert.EqualValues(t, ic.Type, creds.Type) + assert.EqualValues(t, ic.Identifiers, creds.Identifiers) + assert.EqualValues(t, id.ID, id.ID) +} diff --git a/selfservice/flowhelpers/stub/login.schema.json b/selfservice/flowhelpers/stub/login.schema.json new file mode 100644 index 0000000..82f8581 --- /dev/null +++ b/selfservice/flowhelpers/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=body_is_empty.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=body_is_empty.json new file mode 100644 index 0000000..a1b587e --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=body_is_empty.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_admin_metadata.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_admin_metadata.json new file mode 100644 index 0000000..0658721 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_admin_metadata.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "useful": "admin metadata" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_external_id.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_external_id.json new file mode 100644 index 0000000..fea6fcb --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_external_id.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "updated-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_public_metadata.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_public_metadata.json new file mode 100644 index 0000000..7f9a2f0 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_public_metadata.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "useful": "metadata" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_recovery_addresses.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_recovery_addresses.json new file mode 100644 index 0000000..958062b --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_recovery_addresses.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@other-example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@other-example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_state.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_state.json new file mode 100644 index 0000000..1acf22c --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_state.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "inactive", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_traits.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_traits.json new file mode 100644 index 0000000..b167e35 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_traits.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@other-example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_verified_addresses.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_verified_addresses.json new file mode 100644 index 0000000..20c1603 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_has_updated_verified_addresses.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@other-example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@other-example.org", + "verified": false, + "via": "email", + "status": "", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty#01.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty#01.json new file mode 100644 index 0000000..5cc8256 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty#01.json @@ -0,0 +1,51 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty.json new file mode 100644 index 0000000..a1b587e --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=identity_is_present_but_empty.json @@ -0,0 +1,53 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "external_id": "original-external-id", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=unset_external_id.json b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=unset_external_id.json new file mode 100644 index 0000000..06206d1 --- /dev/null +++ b/selfservice/hook/.snapshots/TestWebHooks-update_identity_fields-case=update_identity_fields-case=unset_external_id.json @@ -0,0 +1,52 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "test" + ], + "config": { + "hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA" + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "default", + "schema_url": "file://stub/default.schema.json", + "state": "active", + "traits": { + "email": "some@example.org" + }, + "verifiable_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "verified": false, + "via": "email", + "status": "pending", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "recovery_addresses": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "value": "some@example.org", + "via": "email", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + ], + "metadata_public": { + "public": "data" + }, + "metadata_admin": { + "admin": "data" + }, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/selfservice/hook/error.go b/selfservice/hook/error.go new file mode 100644 index 0000000..58a8766 --- /dev/null +++ b/selfservice/hook/error.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/ui/node" + + "github.com/ory/kratos/identity" + "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" +) + +var ( + _ registration.PostHookPrePersistExecutor = new(Error) + _ registration.PostHookPostPersistExecutor = new(Error) + _ registration.PreHookExecutor = new(Error) + + _ login.PreHookExecutor = new(Error) + _ login.PostHookExecutor = new(Error) + _ login.PreHookExecutor = new(Error) + + _ settings.PostHookPostPersistExecutor = new(Error) + _ settings.PostHookPrePersistExecutor = new(Error) + _ settings.PreHookExecutor = new(Error) + + _ verification.PreHookExecutor = new(Error) + _ recovery.PreHookExecutor = new(Error) +) + +type Error struct { + Config json.RawMessage +} + +func (e Error) err(path string, abort error) error { + switch gjson.GetBytes(e.Config, path).String() { + case "err": + return errors.New("err") + case "abort": + return abort + } + return nil +} + +func (e Error) ExecuteSettingsPreHook(w http.ResponseWriter, r *http.Request, _ settings.PreHookExecutorParams) error { + return e.err("ExecuteSettingsPreHook", settings.ErrHookAbortFlow) +} + +func (e Error) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, _ settings.PostHookPrePersistExecutorParams) error { + return e.err("ExecuteSettingsPrePersistHook", settings.ErrHookAbortFlow) +} + +func (e Error) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, _ settings.PostHookPostPersistExecutorParams) error { + return e.err("ExecuteSettingsPostPersistHook", settings.ErrHookAbortFlow) +} + +func (e Error) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *login.Flow, s *session.Session) error { + return e.err("ExecuteLoginPostHook", login.ErrHookAbortFlow) +} + +func (e Error) ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *login.Flow) error { + return e.err("ExecuteLoginPreHook", login.ErrHookAbortFlow) +} + +func (e Error) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *registration.Flow) error { + return e.err("ExecuteRegistrationPreHook", registration.ErrHookAbortFlow) +} + +func (e Error) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { + return e.err("ExecutePostRegistrationPostPersistHook", registration.ErrHookAbortFlow) +} + +func (e Error) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, i *identity.Identity) error { + return e.err("ExecutePostRegistrationPrePersistHook", registration.ErrHookAbortFlow) +} + +func (e Error) ExecuteRecoveryPreHook(w http.ResponseWriter, r *http.Request, a *recovery.Flow) error { + return e.err("ExecuteRecoveryPreHook", recovery.ErrHookAbortFlow) +} + +func (e Error) ExecutePostRecoveryHook(w http.ResponseWriter, r *http.Request, a *recovery.Flow, s *session.Session) error { + return e.err("ExecutePostRecoveryHook", recovery.ErrHookAbortFlow) +} + +func (e Error) ExecuteVerificationPreHook(w http.ResponseWriter, r *http.Request, a *verification.Flow) error { + return e.err("ExecuteVerificationPreHook", verification.ErrHookAbortFlow) +} + +func (e Error) ExecutePostVerificationHook(w http.ResponseWriter, r *http.Request, a *verification.Flow, i *identity.Identity) error { + return e.err("ExecutePostVerificationHook", verification.ErrHookAbortFlow) +} diff --git a/selfservice/hook/hooks.go b/selfservice/hook/hooks.go new file mode 100644 index 0000000..24b41e3 --- /dev/null +++ b/selfservice/hook/hooks.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +const ( + KeySessionIssuer = "session" + KeySessionDestroyer = "revoke_active_sessions" + KeyWebHook = "web_hook" + KeyRequireVerifiedAddress = "require_verified_address" + KeyVerificationUI = "show_verification_ui" + KeyVerifier = "verification" + KeyVerifyNewAddress = "verify_new_address" + KeyNotifyPreviousAddresses = "notify_previous_addresses" +) 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 0000000..6111b48 --- /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/notify_previous_addresses.go b/selfservice/hook/notify_previous_addresses.go new file mode 100644 index 0000000..1a6f5bc --- /dev/null +++ b/selfservice/hook/notify_previous_addresses.go @@ -0,0 +1,148 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "net/http" + "slices" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +const ( + RecipientsRemoved = "removed" + RecipientsAllVerified = "all_verified" + RecipientsAll = "all" +) + +type ( + NotifyPreviousAddressesConfig struct { + Recipients string `json:"recipients"` + } + + notifyPreviousAddressesDependencies interface { + identity.ManagementProvider + config.Provider + logrusx.Provider + otelx.Provider + } + + NotifyPreviousAddresses struct { + r notifyPreviousAddressesDependencies + c *NotifyPreviousAddressesConfig + } +) + +var _ settings.PostHookPostPersistExecutor = new(NotifyPreviousAddresses) + +func NewNotifyPreviousAddresses(r notifyPreviousAddressesDependencies, c *NotifyPreviousAddressesConfig) *NotifyPreviousAddresses { + if c == nil { + c = &NotifyPreviousAddressesConfig{} + } + if c.Recipients == "" { + c.Recipients = RecipientsRemoved + } + return &NotifyPreviousAddresses{r: r, c: c} +} + +func (e *NotifyPreviousAddresses) ExecuteSettingsPostPersistHook( + _ http.ResponseWriter, r *http.Request, + params settings.PostHookPostPersistExecutorParams, +) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.NotifyPreviousAddresses.ExecuteSettingsPostPersistHook", func(ctx context.Context) error { + if params.Previous == nil || params.Updated == nil { + return nil + } + targets := computeNotifyTargets(e.c.Recipients, params.Previous, params.Updated) + targets = slices.DeleteFunc(targets, func(a identity.AddressRef) bool { + return a.Via != identity.AddressTypeEmail && a.Via != identity.AddressTypeSMS + }) + if len(targets) == 0 { + return nil + } + if err := e.r.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, targets, params.Updated); err != nil { + e.r.Logger().WithError(err). + WithField("count", len(targets)). + Warn("Failed to queue one or more verifiable-address-change notifications.") + // Persist already succeeded — never fail the flow on courier errors. + } + return nil + }) +} + +func computeNotifyTargets(mode string, original, proposed *identity.Identity) []identity.AddressRef { + if !addressesChanged(original.VerifiableAddresses, proposed.VerifiableAddresses) { + return nil + } + switch mode { + case RecipientsAllVerified: + out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses)) + for _, a := range original.VerifiableAddresses { + if a.Verified { + out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via}) + } + } + return out + case RecipientsAll: + out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses)) + for _, a := range original.VerifiableAddresses { + out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via}) + } + return out + default: // RecipientsRemoved + keep := map[string]struct{}{} + for _, a := range proposed.VerifiableAddresses { + keep[a.Via+"|"+a.Value] = struct{}{} + } + out := make([]identity.AddressRef, 0) + for _, a := range original.VerifiableAddresses { + if _, still := keep[a.Via+"|"+a.Value]; still { + continue + } + out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via}) + } + return out + } +} + +type set struct { + m map[string]struct{} +} + +func newSet() *set { + return &set{m: make(map[string]struct{})} +} + +func (s *set) add(via, value string) { + s.m[via+"|"+value] = struct{}{} +} + +func (s *set) contains(via, value string) bool { + _, ok := s.m[via+"|"+value] + return ok +} + +// addressesChanged reports whether two verifiable-address slices represent +// different (via,value) sets. Status or verification-flag flips alone are not +// "address changes" and must not trigger notifications. +func addressesChanged(a, b []identity.VerifiableAddress) bool { + if len(a) != len(b) { + return true + } + seen := newSet() + for _, x := range a { + seen.add(x.Via, x.Value) + } + for _, x := range b { + if !seen.contains(x.Via, x.Value) { + return true + } + } + return false +} diff --git a/selfservice/hook/notify_previous_addresses_test.go b/selfservice/hook/notify_previous_addresses_test.go new file mode 100644 index 0000000..1f725c9 --- /dev/null +++ b/selfservice/hook/notify_previous_addresses_test.go @@ -0,0 +1,195 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/session" +) + +func TestNotifyPreviousAddresses_PostPersist(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + mode string + previousAddresses []identity.VerifiableAddress + updatedAddresses []identity.VerifiableAddress + wantRecipients []string + }{ + { + name: "removed-only notifies the dropped address", + mode: hook.RecipientsRemoved, + previousAddresses: []identity.VerifiableAddress{ + {Value: "old@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "keep@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "keep@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "new@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: []string{"old@example.com"}, + }, + { + name: "all-verified notifies every previously verified address", + mode: hook.RecipientsAllVerified, + previousAddresses: []identity.VerifiableAddress{ + {Value: "v1@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "v2@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "unverified@example.com", Via: identity.AddressTypeEmail, Verified: false, Status: identity.VerifiableAddressStatusPending}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: []string{"v1@example.com", "v2@example.com"}, + }, + { + name: "all notifies every prior address regardless of verification", + mode: hook.RecipientsAll, + previousAddresses: []identity.VerifiableAddress{ + {Value: "verified@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "unverified@example.com", Via: identity.AddressTypeEmail, Verified: false, Status: identity.VerifiableAddressStatusPending}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: []string{"verified@example.com", "unverified@example.com"}, + }, + { + name: "unsupported via is filtered out", + mode: hook.RecipientsAll, + previousAddresses: []identity.VerifiableAddress{ + {Value: "ok@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + {Value: "pager://123", Via: "pager", Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: []string{"ok@example.com"}, + }, + { + name: "no diff queues no notifications", + mode: hook.RecipientsRemoved, + previousAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: nil, + }, + { + name: "all-verified does not notify when only non-address traits changed", + mode: hook.RecipientsAllVerified, + previousAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: nil, + }, + { + name: "all does not notify when only non-address traits changed", + mode: hook.RecipientsAll, + previousAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: nil, + }, + { + name: "verification status flip alone does not notify", + mode: hook.RecipientsAllVerified, + previousAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: false, Status: identity.VerifiableAddressStatusPending}, + }, + updatedAddresses: []identity.VerifiableAddress{ + {Value: "stable@example.com", Via: identity.AddressTypeEmail, Verified: true, Status: identity.VerifiableAddressStatusCompleted}, + }, + wantRecipients: nil, + }, + } { + tc := tc + t.Run("case="+tc.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify_single_email.schema.json") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + previous := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + previous.Traits = identity.Traits(`{"email":"new@example.com"}`) + previous.VerifiableAddresses = tc.previousAddresses + + updated := *previous + updated.VerifiableAddresses = tc.updatedAddresses + + h := hook.NewNotifyPreviousAddresses(reg, &hook.NotifyPreviousAddressesConfig{Recipients: tc.mode}) + + r := httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx) + w := httptest.NewRecorder() + require.NoError(t, h.ExecuteSettingsPostPersistHook(w, r, settings.PostHookPostPersistExecutorParams{ + Flow: &settings.Flow{InternalContext: []byte("{}")}, + Previous: previous, + Updated: &updated, + Session: &session.Session{Identity: &updated}, + })) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + if len(tc.wantRecipients) == 0 { + require.ErrorIs(t, err, courier.ErrQueueEmpty) + return + } + require.NoError(t, err) + require.Len(t, messages, len(tc.wantRecipients)) + gotRecipients := make([]string, len(messages)) + for k, m := range messages { + gotRecipients[k] = m.Recipient + assert.Equal(t, courier.MessageTypeEmail, m.Type) + } + assert.ElementsMatch(t, tc.wantRecipients, gotRecipients) + }) + } +} + +func TestNotifyPreviousAddresses_PostPersist_NilPrevious(t *testing.T) { + t.Parallel() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify_single_email.schema.json") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + updated := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + + h := hook.NewNotifyPreviousAddresses(reg, &hook.NotifyPreviousAddressesConfig{Recipients: hook.RecipientsRemoved}) + + r := httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx) + w := httptest.NewRecorder() + require.NoError(t, h.ExecuteSettingsPostPersistHook(w, r, settings.PostHookPostPersistExecutorParams{ + Flow: &settings.Flow{InternalContext: []byte("{}")}, + Previous: nil, + Updated: updated, + Session: &session.Session{Identity: updated}, + })) + + _, err := reg.CourierPersister().NextMessages(ctx, 10) + require.ErrorIs(t, err, courier.ErrQueueEmpty) +} diff --git a/selfservice/hook/password_migration_hook.go b/selfservice/hook/password_migration_hook.go new file mode 100644 index 0000000..c30a1e0 --- /dev/null +++ b/selfservice/hook/password_migration_hook.go @@ -0,0 +1,138 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "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/identity" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/x" + "github.com/ory/x/otelx" +) + +type ( + PasswordMigration struct { + deps webHookDependencies + conf *request.Config + } + PasswordMigrationRequest struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + Identity *identity.Identity `json:"-"` + } + PasswordMigrationResponse struct { + Status string `json:"status"` + } +) + +func NewPasswordMigrationHook(deps webHookDependencies, conf *request.Config) *PasswordMigration { + return &PasswordMigration{deps: deps, conf: conf} +} + +func (p *PasswordMigration) Execute(ctx context.Context, req *http.Request, flow flow.Flow, data *PasswordMigrationRequest) (err error) { + var ( + httpClient = p.deps.HTTPClient(ctx) + emitEvent = p.conf.EmitAnalyticsEvent == nil || *p.conf.EmitAnalyticsEvent // 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, x.NewUUID(), "password_migration_hook") + } + builder, err := request.NewBuilder(p.conf, p.deps) + if err != nil { + return errors.WithStack(err) + } + var whReq *retryablehttp.Request + if p.conf.TemplateURI == "" { + whReq, err = builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet + if err != nil { + return err + } + rawData, err := json.Marshal(data) + if err != nil { + return errors.WithStack(err) + } + if err = whReq.SetBody(rawData); err != nil { + return errors.WithStack(err) + } + } else { + type templateContextMerged struct { + templateContext + Password string `json:"password"` + Identifier string `json:"identifier"` + } + whReq, err = builder.BuildRequest(ctx, templateContextMerged{ + templateContext: templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: data.Identity, + }, + Password: data.Password, + Identifier: data.Identifier, + }) + if err != nil { + return err + } + } + + p.deps.Logger().WithRequest(whReq.Request).Info("Dispatching password migration hook") + + resp, err := httpClient.Do(whReq) + 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 func() { _ = 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/require_verified_address.go b/selfservice/hook/require_verified_address.go new file mode 100644 index 0000000..5a7b9cb --- /dev/null +++ b/selfservice/hook/require_verified_address.go @@ -0,0 +1,166 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "net/http" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlxx" + + "github.com/pkg/errors" + + "github.com/ory/kratos/ui/node" + + "github.com/ory/herodot" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" +) + +var _ login.PostHookExecutor = new(AddressVerifier) + +type ( + addressVerifierDependencies interface { + clock.Provider + config.Provider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + verification.StrategyProvider + verification.FlowPersistenceProvider + identity.PrivilegedPoolProvider + httpx.WriterProvider + otelx.Provider + } + AddressVerifier struct { + r addressVerifierDependencies + } +) + +func NewAddressVerifier(r addressVerifierDependencies) *AddressVerifier { + return &AddressVerifier{ + r: r, + } +} + +func (e *AddressVerifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) { + ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.do") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + // TODO remove once flag is removed. + if e.r.Config().UseLegacyRequireVerifiedLoginError(ctx) { + if f.Active != identity.CredentialsTypePassword { + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "legacy_require_verified_login_error")) + return nil + } + } + // END TODO + + if len(s.Identity.VerifiableAddresses) == 0 { + return errors.WithStack(herodot.ErrMisconfiguration().WithReason("A misconfiguration prevents login. Expected to find a verification address but this identity does not have one assigned.")) + } + + for _, va := range s.Identity.VerifiableAddresses { + if va.Verified { + return nil + } + } + + strategies, primaryStrategy, err := e.r.GetActiveVerificationStrategies(ctx) + if err != nil { + return err + } + + // TODO remove once flag is removed. + if e.r.Config().UseLegacyRequireVerifiedLoginError(ctx) { + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "legacy_require_verified_login_error")) + return login.ErrAddressNotVerified() + } + // END TODO + + i := s.Identity + for k := range i.VerifiableAddresses { + address := &i.VerifiableAddresses[k] + if address.Value == "" { + continue + } + + verificationFlow, err := verification.NewPostHookFlow(e.r, + e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx), + e.r.GenerateCSRFToken(r), r, strategies, f) + if err != nil { + return err + } + + verificationFlow.State = flow.StateEmailSent + for _, strategy := range strategies { + if ps, isPrimary := strategy.(verification.PrimaryStrategy); isPrimary { + verificationFlow.Active = sqlxx.NullString(ps.NodeGroup()) + } + if err := strategy.PopulateVerificationMethod(r, verificationFlow); err != nil { + return err + } + } + + // The code strategy's PopulateVerificationMethod sets the email "code sent" + // message unconditionally; override it with the message that matches the + // channel the code was sent over. The legacy link strategy sends a link, + // not a code, and sets no message — leave it untouched. + if primaryStrategy.NodeGroup() == node.CodeGroup { + verificationFlow.UI.Messages.Set(text.VerificationCodeSentMessage(address.Via)) + } + + verificationFlow.SessionID = uuid.NullUUID{UUID: s.ID, Valid: true} + verificationFlow.IdentityID = uuid.NullUUID{UUID: s.Identity.ID, Valid: true} + + verificationFlow.UI.Nodes.Append( + node.NewInputField(address.Via, address.Value, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + + if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil { + return err + } + + if err := primaryStrategy.SendVerificationCode(ctx, verificationFlow, i, address); err != nil { + return err + } + + address.Status = identity.VerifiableAddressStatusSent + if err := e.r.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status"); err != nil { + return err + } + + flowURL := verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String() + continueWith := flow.NewContinueWithVerificationUI(verificationFlow.ID, address.Value, flowURL) + f.AddContinueWith(continueWith) + + if x.IsJSONRequest(r) { + e.r.Writer().WriteErrorCode(w, r, http.StatusForbidden, flow.ErrorWithContinueWith(login.ErrAddressNotVerified(), continueWith)) + return errors.WithStack(login.ErrHookAbortFlow) + } + + if x.IsBrowserRequest(r) { + http.Redirect(w, r, flowURL, http.StatusSeeOther) + return errors.WithStack(login.ErrHookAbortFlow) + } + + return errors.WithStack(login.ErrHookAbortFlow) + } + + return login.ErrAddressNotVerified() +} diff --git a/selfservice/hook/require_verified_address_test.go b/selfservice/hook/require_verified_address_test.go new file mode 100644 index 0000000..11afd11 --- /dev/null +++ b/selfservice/hook/require_verified_address_test.go @@ -0,0 +1,287 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/hook" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/ui/node" + + "github.com/ory/herodot" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +func TestAddressVerifier(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.schema.json") + verifier := hook.NewAddressVerifier(reg) + + t.Run("legacy behavior", func(t *testing.T) { + // Mock legacy config + conf.MustSet(context.Background(), config.ViperKeyUseLegacyRequireVerifiedLoginError, true) + + for _, tc := range []struct { + flow *login.Flow + neverError bool + }{ + {&login.Flow{Active: identity.CredentialsTypePassword}, false}, + {&login.Flow{Active: identity.CredentialsTypeOIDC}, true}, + } { + t.Run(tc.flow.Active.String()+" flow", func(t *testing.T) { + for _, uc := range []struct { + name string + verifiableAddresses []identity.VerifiableAddress + expectedError error + }{ + { + name: "No Verification Address", + verifiableAddresses: []identity.VerifiableAddress{}, + expectedError: herodot.ErrMisconfiguration().WithReason("A misconfiguration prevents login. Expected to find a verification address but this identity does not have one assigned."), + }, + { + name: "Single Address Not Verified", + verifiableAddresses: []identity.VerifiableAddress{ + {ID: uuid.UUID{}, Verified: false}, + }, + expectedError: login.ErrAddressNotVerified(), + }, + { + name: "Single Address Verified", + verifiableAddresses: []identity.VerifiableAddress{ + {ID: uuid.UUID{}, Verified: true}, + }, + }, + { + name: "Multiple Addresses Verified", + verifiableAddresses: []identity.VerifiableAddress{ + {ID: uuid.UUID{}, Verified: true}, + {ID: uuid.UUID{}, Verified: true}, + }, + }, + { + name: "Multiple Addresses Not Verified", + verifiableAddresses: []identity.VerifiableAddress{ + {ID: uuid.UUID{}, Verified: false}, + {ID: uuid.UUID{}, Verified: false}, + }, + expectedError: login.ErrAddressNotVerified(), + }, + { + name: "One Address Verified And One Not", + verifiableAddresses: []identity.VerifiableAddress{ + {ID: uuid.UUID{}, Verified: true}, + {ID: uuid.UUID{}, Verified: false}, + }, + }, + } { + t.Run(uc.name, func(t *testing.T) { + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: &identity.Identity{ID: x.NewUUID(), VerifiableAddresses: uc.verifiableAddresses}, + } + err := verifier.ExecuteLoginPostHook(nil, httptest.NewRequest("GET", "http://example.com", nil), node.DefaultGroup, tc.flow, sessions) + if tc.neverError || uc.expectedError == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, uc.expectedError) + } + }) + } + }) + } + }) + + t.Run("current behavior", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/require_verified.schema.json") + // Mock non-legacy config + conf.MustSet(context.Background(), config.ViperKeyUseLegacyRequireVerifiedLoginError, false) + + mockRequest := httptest.NewRequest("GET", "http://example.com", nil) + + // Setup flow with identity addresses needing verification + loginFlow := &login.Flow{ + Active: identity.CredentialsTypePassword, + ID: x.NewUUID(), + } + + // Must persist the flow first for foreign key constraints + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), loginFlow)) + + t.Run("json request for unverified address", func(t *testing.T) { + // Mock JSON request + mockJSONReq := httptest.NewRequest("GET", "http://example.com", nil) + mockJSONReq.Header.Set("Content-Type", "application/json") + mockJSONReq.Header.Set("Accept", "application/json") + mockResponse := httptest.NewRecorder() + + // Create identity with valid values + identity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"user@example.com"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: "user@example.com", + Verified: false, + Via: identity.AddressTypeEmail, + }, + }, + } + + // Persist identity to satisfy foreign key constraints + require.NoError(t, reg.IdentityManager().Create(context.Background(), identity)) + + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: identity, + } + + // Expect verification flow creation and ErrHookAbortFlow + err := verifier.ExecuteLoginPostHook(mockResponse, mockJSONReq, node.DefaultGroup, loginFlow, sessions) + assert.ErrorIs(t, err, login.ErrHookAbortFlow) + + // Verify response contains continueWith and ErrAddressNotVerified + resp := mockResponse.Result() + body, _ := io.ReadAll(resp.Body) + + var responseErr *herodot.DefaultError + + // Check for required JSON fields + require.NoError(t, json.Unmarshal([]byte(gjson.GetBytes(body, "error").Raw), &responseErr)) + assert.Contains(t, responseErr.ReasonField, login.ErrAddressNotVerified().Reason(), "%s", string(body)) + + // Verify flow has continueWith added + var continueWith flow.ContinueWithVerificationUI + require.NoError(t, json.Unmarshal([]byte(gjson.GetBytes(body, "error.details.continue_with.0").Raw), &continueWith)) + + // Verify the continueWith fields are properly set + assert.NotEmpty(t, continueWith.Flow.ID) + assert.Contains(t, continueWith.Action, "show_verification_ui") + assert.Contains(t, continueWith.Flow.URL, reg.Config().SelfServiceFlowVerificationUI(context.Background()).String()) + assert.Equal(t, continueWith.Flow.VerifiableAddress, "user@example.com") + }) + + t.Run("browser request for unverified address", func(t *testing.T) { + // Mock browser request + mockBrowserReq := httptest.NewRequest("GET", "http://example.com", nil) + mockBrowserReq.Header.Set("Accept", "text/html") + mockResponse := httptest.NewRecorder() + + // Create new flow for this test case + browserFlow := &login.Flow{ + Active: identity.CredentialsTypePassword, + ID: x.NewUUID(), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), browserFlow)) + + // Create identity with valid values + identity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"user2@example.com"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: "user2@example.com", + Verified: false, + Via: identity.AddressTypeEmail, + }, + }, + } + require.NoError(t, reg.IdentityManager().Create(context.Background(), identity)) + + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: identity, + } + + // Expect verification flow creation and redirect + err := verifier.ExecuteLoginPostHook(mockResponse, mockBrowserReq, node.DefaultGroup, browserFlow, sessions) + assert.ErrorIs(t, err, login.ErrHookAbortFlow) + + // Verify redirect occurred + resp := mockResponse.Result() + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusSeeOther, resp.StatusCode) + assert.NotEmpty(t, resp.Header.Get("Location")) + }) + + t.Run("verified address skips verification", func(t *testing.T) { + // Create new flow for this test case + verifiedFlow := &login.Flow{ + Active: identity.CredentialsTypePassword, + ID: x.NewUUID(), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), verifiedFlow)) + + // Create identity with verified address + identity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"verified@example.com"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: "verified@example.com", + Verified: true, + Via: identity.AddressTypeEmail, + }, + }, + } + require.NoError(t, reg.IdentityManager().Create(context.Background(), identity)) + + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: identity, + } + + err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, verifiedFlow, sessions) + assert.NoError(t, err) + }) + + t.Run("no verifiable address", func(t *testing.T) { + // Create new flow for this test case + noAddressFlow := &login.Flow{ + Active: identity.CredentialsTypePassword, + ID: x.NewUUID(), + } + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), noAddressFlow)) + + // Create identity with no verifiable addresses + identity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{}`), + VerifiableAddresses: []identity.VerifiableAddress{}, + } + require.NoError(t, reg.IdentityManager().Create(context.Background(), identity)) + + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: identity, + } + + err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, noAddressFlow, sessions) + assert.ErrorIs(t, err, herodot.ErrMisconfiguration()) + }) + }) +} diff --git a/selfservice/hook/session_destroyer.go b/selfservice/hook/session_destroyer.go new file mode 100644 index 0000000..7503a2a --- /dev/null +++ b/selfservice/hook/session_destroyer.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "net/http" + + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/otelx" +) + +var ( + _ login.PostHookExecutor = new(SessionDestroyer) + _ recovery.PostHookExecutor = new(SessionDestroyer) + _ settings.PostHookPostPersistExecutor = new(SessionDestroyer) +) + +type ( + sessionDestroyerDependencies interface { + session.ManagementProvider + session.PersistenceProvider + } + SessionDestroyer struct { + r sessionDestroyerDependencies + } +) + +func NewSessionDestroyer(r sessionDestroyerDependencies) *SessionDestroyer { + return &SessionDestroyer{r: r} +} + +func (e *SessionDestroyer) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, _ *login.Flow, s *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.SessionDestroyer.ExecuteLoginPostHook", func(ctx context.Context) error { + if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(ctx, s.Identity.ID, s.ID); err != nil { + return err + } + return nil + }) +} + +func (e *SessionDestroyer) ExecutePostRecoveryHook(_ http.ResponseWriter, r *http.Request, _ *recovery.Flow, s *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.SessionDestroyer.ExecutePostRecoveryHook", func(ctx context.Context) error { + if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(ctx, s.Identity.ID, s.ID); err != nil { + return err + } + return nil + }) +} + +func (e *SessionDestroyer) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, r *http.Request, params settings.PostHookPostPersistExecutorParams) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.SessionDestroyer.ExecuteSettingsPostPersistHook", func(ctx context.Context) error { + if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(ctx, params.Updated.ID, params.Session.ID); err != nil { + return err + } + return nil + }) +} diff --git a/selfservice/hook/session_destroyer_test.go b/selfservice/hook/session_destroyer_test.go new file mode 100644 index 0000000..66123a6 --- /dev/null +++ b/selfservice/hook/session_destroyer_test.go @@ -0,0 +1,119 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "net/http" + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/ui/node" + + "github.com/go-faker/faker/v4" + "github.com/gobuffalo/httptest" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/session" +) + +func init() { + corpx.RegisterFakes() +} + +func TestSessionDestroyer(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/") + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/stub.schema.json") + + h := hook.NewSessionDestroyer(reg) + + for _, tc := range []struct { + name string + hook func(*identity.Identity) error + }{ + { + name: "ExecuteLoginPostHook", + hook: func(i *identity.Identity) error { + return h.ExecuteLoginPostHook( + httptest.NewRecorder(), + new(http.Request), + node.DefaultGroup, + nil, + &session.Session{Identity: i}, + ) + }, + }, + { + name: "ExecutePostRecoveryHook", + hook: func(i *identity.Identity) error { + return h.ExecutePostRecoveryHook( + httptest.NewRecorder(), + new(http.Request), + nil, + &session.Session{Identity: i}, + ) + }, + }, + { + name: "ExecuteSettingsPostPersistHook", + hook: func(i *identity.Identity) error { + return h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), + new(http.Request), + settings.PostHookPostPersistExecutorParams{ + Flow: nil, + Previous: i, + Updated: i, + Session: &session.Session{Identity: i}, + }, + ) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + + sessions := make([]session.Session, 5) + for k := range sessions { + s := sessions[k] // keep this for pointers' sake ;) + require.NoError(t, faker.FakeData(&s)) + s.IdentityID = uuid.Nil + s.Identity = &i + s.Active = true + + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), &s)) + sessions[k] = s + } + + for k := range sessions { + sess, err := reg.SessionPersister().GetSession(context.Background(), sessions[k].ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, sess.IsActive()) + } + + // Should revoke all the sessions. + require.NoError(t, tc.hook(&i)) + + for k := range sessions { + sess, err := reg.SessionPersister().GetSession(context.Background(), sessions[k].ID, session.ExpandNothing) + require.NoError(t, err) + assert.False(t, sess.IsActive()) + } + }) + } +} diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go new file mode 100644 index 0000000..82d3f59 --- /dev/null +++ b/selfservice/hook/session_issuer.go @@ -0,0 +1,160 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" + + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" +) + +var _ registration.PostHookPostPersistExecutor = new(SessionIssuer) + +type ( + sessionIssuerDependencies interface { + session.ManagementProvider + session.PersistenceProvider + sessiontokenexchange.PersistenceProvider + config.Provider + httpx.WriterProvider + hydra.Provider + } + SessionIssuerProvider interface { + HookSessionIssuer() *SessionIssuer + } + SessionIssuer struct { + r sessionIssuerDependencies + } +) + +func NewSessionIssuer(r sessionIssuerDependencies) *SessionIssuer { + return &SessionIssuer{r: r} +} + +func (e *SessionIssuer) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.SessionIssuer.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { + return e.executePostRegistrationPostPersistHook(w, r.WithContext(ctx), a, s) + }) +} + +func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { + if a.Type == flow.TypeAPI { + // 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. + if s.AuthenticatedVia(identity.CredentialsTypeOIDC) && a.IDToken == "" { + if handled, err := e.r.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, node.OpenIDConnectGroup); err != nil { + return errors.WithStack(err) + } else if handled { + // The redirect response has already been written. Abort the flow + // so the executor does not run its tail block, which would call + // MaybeRedirectAPICodeFlow a second time and issue a redundant + // (and slow) write to the GLOBAL session_token_exchanges table. + return errors.WithStack(registration.ErrHookAbortFlow) + } + } + + a.AddContinueWith(flow.NewContinueWithSetToken(s.Token)) + e.r.Writer().Write(w, r, ®istration.APIFlowResponse{ + Session: s, + Token: s.Token, + Identity: s.Identity, + ContinueWith: a.ContinueWithItems, + }) + + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowID: a.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + + return errors.WithStack(registration.ErrHookAbortFlow) + } + + // cookie is issued both for browser and for SPA flows + if err := e.r.SessionManager().IssueCookie(r.Context(), w, r, s); err != nil { + return err + } + + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowID: a.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + + // SPA flows additionally send the session + if x.IsJSONRequest(r) { + if err := e.acceptLoginChallenge(r.Context(), a, s, s.Identity); err != nil { + return err + } + e.r.Writer().Write(w, r, ®istration.APIFlowResponse{ + Session: s, + Identity: s.Identity, + ContinueWith: a.ContinueWithItems, + }) + return errors.WithStack(registration.ErrHookAbortFlow) + } + + return nil +} + +func (e *SessionIssuer) acceptLoginChallenge(ctx context.Context, registrationFlow *registration.Flow, s *session.Session, i *identity.Identity) error { + // If Kratos is used as a Hydra login provider, we need to redirect back to Hydra by using the continue_with items + // with the post login challenge URL as the body. + // We only do this if the flow did not create a verification flow (e.g. verification is disabled or not active due to it being a code flow). + // Since the session issuer hook must be the last hook in the flow, we can safely assume that the verification flow was already added (if it was) + if registrationFlow.OAuth2LoginChallenge != "" && !willVerificationFollow(registrationFlow) { + postChallengeURL, err := e.r.Hydra().AcceptLoginRequest(ctx, + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(registrationFlow.OAuth2LoginChallenge), + IdentityID: i.ID.String(), + SessionID: s.ID.String(), + AuthenticationMethods: s.AMR, + }) + if err != nil { + return err + } + cw := []flow.ContinueWith{} + for _, i := range registrationFlow.ContinueWithItems { + // Filter any continueWithRedirectBrowserTo items out of the list + // We will add a new one at the end of the flow + // as the OAuth2 login challenge should be the last step in the flow + if i.GetAction() != string(flow.ContinueWithActionRedirectBrowserToString) { + cw = append(cw, i) + } + } + registrationFlow.ContinueWithItems = append(cw, flow.NewContinueWithRedirectBrowserTo(postChallengeURL)) + } + return nil +} + +// willVerificationFollow returns true if the flow's continue with items contain a verification UI. +func willVerificationFollow(f *registration.Flow) bool { + for _, i := range f.ContinueWithItems { + if i.GetAction() == string(flow.ContinueWithActionShowVerificationUIString) { + return true + } + } + return false +} diff --git a/selfservice/hook/session_issuer_test.go b/selfservice/hook/session_issuer_test.go new file mode 100644 index 0000000..996d3e2 --- /dev/null +++ b/selfservice/hook/session_issuer_test.go @@ -0,0 +1,355 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/pkg/testhelpers" + + "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/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/randx" +) + +func TestSessionIssuer(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.SetHydra(hydra.NewFake()) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/") + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/stub.schema.json") + + var r http.Request + h := hook.NewSessionIssuer(reg) + + t.Run("method=sign-up", func(t *testing.T) { + t.Run("flow=browser", func(t *testing.T) { + w := httptest.NewRecorder() + s := testhelpers.CreateSession(t, reg) + f := ®istration.Flow{Type: flow.TypeBrowser} + + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(w, &r, + f, &session.Session{ID: s.ID, Identity: s.Identity, Token: randx.MustString(12, randx.AlphaLowerNum)})) + + require.Empty(t, f.ContinueWithItems) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID, got.ID) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.Contains(t, w.Header().Get("Set-Cookie"), config.DefaultSessionCookieName) + }) + + t.Run("flow=api", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + f := ®istration.Flow{Type: flow.TypeAPI} + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Len(t, f.ContinueWithItems, 1) + + st := f.ContinueWithItems[0] + require.IsType(t, &flow.ContinueWithSetOrySessionToken{}, st) + assert.NotEmpty(t, st.(*flow.ContinueWithSetOrySessionToken).OrySessionToken) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.Empty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Equal(t, got.Token, gjson.GetBytes(body, "session_token").String()) + }) + + t.Run("flow=spa", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + f := ®istration.Flow{Type: flow.TypeBrowser} + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Empty(t, f.ContinueWithItems) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.NotEmpty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Empty(t, gjson.GetBytes(body, "session_token").String()) + }) + + t.Run("flow=api with oidc code exchange", func(t *testing.T) { + // A native API OIDC flow with a session token exchange code redirects + // via MaybeRedirectAPICodeFlow. Once the redirect is written, the hook + // must abort the flow (ErrHookAbortFlow) so the executor's tail block + // does not call MaybeRedirectAPICodeFlow a second time and issue a + // redundant write to the GLOBAL session_token_exchanges table. + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + s.CompletedLoginFor(identity.CredentialsTypeOIDC, identity.AuthenticatorAssuranceLevel1) + f := ®istration.Flow{ID: x.NewUUID(), Type: flow.TypeAPI} + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + ex, err := reg.SessionTokenExchangePersister().CreateSessionTokenExchanger(ctx, f.ID) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "https://localhost/self-service/methods/oidc/callback/test", nil).WithContext(ctx) + req.Header.Set("Accept", "application/json") + err = h.ExecutePostRegistrationPostPersistHook(w, req, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + + // The redirect carries the return-to code, proving the OIDC handled + // branch ran before aborting. + assert.Equal(t, http.StatusSeeOther, w.Code) + assert.Contains(t, w.Header().Get("Location"), "code="+ex.ReturnToCode) + + // The exchanger was bound to the session exactly once. + got, _, err := reg.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID) + require.NoError(t, err) + assert.Equal(t, ex.ReturnToCode, got.ReturnToCode) + exchanged, err := reg.SessionTokenExchangePersister().GetExchangerFromCode(ctx, ex.InitCode, ex.ReturnToCode) + require.NoError(t, err) + assert.Equal(t, s.ID, exchanged.SessionID.UUID) + }) + }) + + t.Run("method=sign-up with oauth2", func(t *testing.T) { + t.Run("flow=browser", func(t *testing.T) { + w := httptest.NewRecorder() + s := testhelpers.CreateSession(t, reg) + f := ®istration.Flow{ + Type: flow.TypeBrowser, + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + } + + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(w, &r, + f, &session.Session{ID: s.ID, Identity: s.Identity, Token: randx.MustString(12, randx.AlphaLowerNum)})) + + require.Empty(t, f.ContinueWithItems) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID, got.ID) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.Contains(t, w.Header().Get("Set-Cookie"), config.DefaultSessionCookieName) + }) + + t.Run("flow=api", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + f := ®istration.Flow{ + Type: flow.TypeAPI, + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Len(t, f.ContinueWithItems, 1) + + st := f.ContinueWithItems[0] + require.IsType(t, &flow.ContinueWithSetOrySessionToken{}, st) + assert.NotEmpty(t, st.(*flow.ContinueWithSetOrySessionToken).OrySessionToken) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.Empty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Equal(t, got.Token, gjson.GetBytes(body, "session_token").String()) + }) + + t.Run("flow=spa", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + f := ®istration.Flow{ + Type: flow.TypeBrowser, + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithRedirectBrowserTo("https://ory.sh")}, + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Len(t, f.ContinueWithItems, 1) + require.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, f.ContinueWithItems[0].GetAction()) + require.EqualValues(t, hydra.FakePostLoginURL, f.ContinueWithItems[0].(*flow.ContinueWithRedirectBrowserTo).RedirectTo) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.NotEmpty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Empty(t, gjson.GetBytes(body, "session_token").String()) + }) + }) + + t.Run("method=sign-up with oauth2 and verification enabled", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + vf := &verification.Flow{ + ID: x.NewUUID(), + } + f := ®istration.Flow{ + Type: flow.TypeBrowser, + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithVerificationUI(vf.ID, "some@ory.sh", "")}, + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Len(t, f.ContinueWithItems, 1) + require.EqualValues(t, flow.ContinueWithActionShowVerificationUIString, f.ContinueWithItems[0].GetAction()) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.NotEmpty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Empty(t, gjson.GetBytes(body, "session_token").String()) + }) + + t.Run("method=sign-up with oauth2 and some other continue with item", func(t *testing.T) { + w := httptest.NewRecorder() + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + s := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now().UTC(), + } + vf := &recovery.Flow{ + ID: x.NewUUID(), + } + f := ®istration.Flow{ + Type: flow.TypeBrowser, + OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithRecoveryUI(vf)}, + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s) + require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err) + require.Len(t, f.ContinueWithItems, 2) + require.EqualValues(t, flow.ContinueWithActionShowRecoveryUIString, f.ContinueWithItems[0].GetAction()) + require.EqualValues(t, hydra.FakePostLoginURL, f.ContinueWithItems[1].(*flow.ContinueWithRedirectBrowserTo).RedirectTo) + + got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID.String(), got.ID.String()) + assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute))) + + assert.NotEmpty(t, w.Header().Get("Set-Cookie")) + body := w.Body.Bytes() + assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String()) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String()) + assert.Empty(t, gjson.GetBytes(body, "session_token").String()) + }) +} diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go new file mode 100644 index 0000000..f533d5b --- /dev/null +++ b/selfservice/hook/show_verification_ui.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "encoding/json" + "net/http" + + "github.com/ory/kratos/selfservice/flow/settings" + + "github.com/gofrs/uuid" + "github.com/tidwall/gjson" + + "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/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +var ( + _ registration.PostHookPostPersistExecutor = new(ShowVerificationUIHook) + _ login.PostHookExecutor = new(ShowVerificationUIHook) + _ settings.PostHookPostPersistExecutor = new(ShowVerificationUIHook) +) + +type ( + showVerificationUIDependencies interface { + httpx.WriterProvider + config.Provider + otelx.Provider + logrusx.Provider + } + + ShowVerfificationUIProvider interface { + HookShowVerificationUI() *ShowVerificationUIHook + } + + // ShowVerificationUIHook is a post registration hook that redirects browser clients to the verification UI. + ShowVerificationUIHook struct { + d showVerificationUIDependencies + } +) + +func NewShowVerificationUIHook(d showVerificationUIDependencies) *ShowVerificationUIHook { + return &ShowVerificationUIHook{d: d} +} + +// ExecutePostRegistrationPostPersistHook 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) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, r *http.Request, f *registration.Flow, _ *session.Session) error { + return e.execute(r, f) +} + +// 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 e.execute(r, f) +} + +func (e *ShowVerificationUIHook) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params settings.PostHookPostPersistExecutorParams) error { + return e.execute(r, params.Flow) +} + +type verificationUIFlow interface { + flow.InternalContexter + flow.FlowWithContinueWith +} + +type loginOrRegistrationFlow interface { + SetReturnToVerification(string) +} + +func (e *ShowVerificationUIHook) execute(r *http.Request, f verificationUIFlow) (err error) { + ctx, span := e.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.ShowVerificationUIHook.Do") + defer otelx.End(span, &err) + + var cw flow.ContinueWithVerificationUIFlow + + verificationFlow := gjson.GetBytes(f.GetInternalContext(), InternalContextRegistrationVerificationFlow).Raw + if verificationFlow == "" { + // TODO This is a fallback for flows that do not have the appropriate internalContext yet. + // Remove this once we have released this change. + for _, c := range f.ContinueWith() { + if item, ok := c.(*flow.ContinueWithVerificationUI); ok { + cw = item.Flow + } + } + } else { + if err := json.Unmarshal([]byte(verificationFlow), &cw); err != nil { + return err + } + } + + if cw.ID == uuid.Nil { + e.d.Logger().WithRequest(r).Warn("Ignoring hook `show_verification_ui` because no verification flow ID was found in the registration flow. Cannot show verification UI. This is likely a configuration issue or a bug.") + return nil + } + + vf := flow.NewContinueWithVerificationUI(cw.ID, cw.VerifiableAddress, cw.URL) + f.AddContinueWith(vf) + + if returnToVerification, ok := f.(loginOrRegistrationFlow); ok && x.IsBrowserRequest(r) { + verificationUI := e.d.Config().SelfServiceFlowVerificationUI(ctx) + returnToVerification.SetReturnToVerification(vf.AppendTo(verificationUI).String()) + } + + return nil +} diff --git a/selfservice/hook/show_verification_ui_test.go b/selfservice/hook/show_verification_ui_test.go new file mode 100644 index 0000000..fd216e4 --- /dev/null +++ b/selfservice/hook/show_verification_ui_test.go @@ -0,0 +1,227 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "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("flow=registration", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := pkg.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 := pkg.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 := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(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.ID, "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=no verification ui in continue with item returns 200 OK", func(t *testing.T) { + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(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("flow=login", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := pkg.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 := pkg.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 := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(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.ID, "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 := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(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) + }) + }) + + t.Run("internal_context=registration", func(t *testing.T) { + t.Run("case=verification flow from internal context returns redirect", func(t *testing.T) { + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vfID := uuid.Must(uuid.NewV4()) + + cw := flow.ContinueWithVerificationUIFlow{ + ID: vfID, + VerifiableAddress: "test@ory.sh", + } + + internalContext, err := json.Marshal(map[string]interface{}{ + hook.InternalContextRegistrationVerificationFlow: cw, + }) + require.NoError(t, err) + + rf := ®istration.Flow{} + rf.InternalContext = internalContext + + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vfID.String(), rf.ReturnToVerification) + }) + + t.Run("case=invalid json in internal context returns error", func(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + + internalContext, err := json.Marshal(map[string]interface{}{ + hook.InternalContextRegistrationVerificationFlow: "invalid json", + }) + require.NoError(t, err) + + rf := ®istration.Flow{} + rf.InternalContext = internalContext + + rec := httptest.NewRecorder() + err = h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil) + require.Error(t, err) + }) + }) + + t.Run("internal_context=login", func(t *testing.T) { + t.Run("case=verification flow from internal context returns redirect", func(t *testing.T) { + conf, reg := pkg.NewVeryFastRegistryWithoutDB(t) + conf.MustSet(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vfID := uuid.Must(uuid.NewV4()) + + cw := flow.ContinueWithVerificationUIFlow{ + ID: vfID, + VerifiableAddress: "test@ory.sh", + } + + internalContext, err := json.Marshal(map[string]interface{}{ + hook.InternalContextRegistrationVerificationFlow: cw, + }) + require.NoError(t, err) + + lf := &login.Flow{} + lf.InternalContext = internalContext + + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vfID.String(), lf.ReturnToVerification) + }) + + t.Run("case=invalid json in internal context returns error", func(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + + internalContext, err := json.Marshal(map[string]interface{}{ + hook.InternalContextRegistrationVerificationFlow: "invalid json", + }) + require.NoError(t, err) + + lf := &login.Flow{} + lf.InternalContext = internalContext + + rec := httptest.NewRecorder() + err = h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil) + require.Error(t, err) + }) + }) +} diff --git a/selfservice/hook/stub/bad_template.jsonnet b/selfservice/hook/stub/bad_template.jsonnet new file mode 100644 index 0000000..b1fe1b4 --- /dev/null +++ b/selfservice/hook/stub/bad_template.jsonnet @@ -0,0 +1,3 @@ +function(ctx) { + flow_id: ctx.foo, +} diff --git a/selfservice/hook/stub/cancel_template.jsonnet b/selfservice/hook/stub/cancel_template.jsonnet new file mode 100644 index 0000000..68b89aa --- /dev/null +++ b/selfservice/hook/stub/cancel_template.jsonnet @@ -0,0 +1 @@ +error "cancel" diff --git a/selfservice/hook/stub/code.schema.json b/selfservice/hook/stub/code.schema.json new file mode 100644 index 0000000..71219c0 --- /dev/null +++ b/selfservice/hook/stub/code.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://example.com/registration.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 + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/hook/stub/require_verified.schema.json b/selfservice/hook/stub/require_verified.schema.json new file mode 100644 index 0000000..0015695 --- /dev/null +++ b/selfservice/hook/stub/require_verified.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/registration.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": { + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/hook/stub/stub.schema.json b/selfservice/hook/stub/stub.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/hook/stub/stub.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/hook/stub/test_body.jsonnet b/selfservice/hook/stub/test_body.jsonnet new file mode 100644 index 0000000..117ecc5 --- /dev/null +++ b/selfservice/hook/stub/test_body.jsonnet @@ -0,0 +1,10 @@ +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, + cookies: ctx.request_cookies, + transient_payload: if std.objectHas(ctx.flow, "transient_payload") then ctx.flow.transient_payload, +}) diff --git a/selfservice/hook/stub/verify.schema.json b/selfservice/hook/stub/verify.schema.json new file mode 100644 index 0000000..f71a32a --- /dev/null +++ b/selfservice/hook/stub/verify.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/hook/stub/verify_email_and_phone.schema.json b/selfservice/hook/stub/verify_email_and_phone.schema.json new file mode 100644 index 0000000..20d3ce2 --- /dev/null +++ b/selfservice/hook/stub/verify_email_and_phone.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "verify_email_and_phone.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": { "password": { "identifier": true } }, + "verification": { "via": "email" } + } + }, + "phone": { + "type": "string", + "format": "no-validate", + "ory.sh/kratos": { + "verification": { "via": "sms" } + } + }, + "name": { "type": "string" } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/selfservice/hook/stub/verify_single_email.schema.json b/selfservice/hook/stub/verify_single_email.schema.json new file mode 100644 index 0000000..5f2f1fc --- /dev/null +++ b/selfservice/hook/stub/verify_single_email.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/verify-single-email.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": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + }, + "name": { + "type": "string" + } + }, + "required": ["email"] + } + } +} diff --git a/selfservice/hook/stub/verify_two_emails.schema.json b/selfservice/hook/stub/verify_two_emails.schema.json new file mode 100644 index 0000000..6b8b7ea --- /dev/null +++ b/selfservice/hook/stub/verify_two_emails.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "verify_two_emails.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Two Email Schema", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { "password": { "identifier": true } }, + "verification": { "via": "email" } + } + }, + "recovery_email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "verification": { "via": "email" } + } + }, + "name": { "type": "string" } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go new file mode 100644 index 0000000..4d8590f --- /dev/null +++ b/selfservice/hook/verification.go @@ -0,0 +1,220 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "net/http" + + "github.com/tidwall/sjson" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx/semconv" + + "github.com/gofrs/uuid" + + "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/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/otelx" +) + +var ( + _ registration.PostHookPostPersistExecutor = new(Verifier) + _ settings.PostHookPostPersistExecutor = new(Verifier) + _ login.PostHookExecutor = new(Verifier) +) + +type ( + verifierDependencies interface { + clock.Provider + config.Provider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + verification.StrategyProvider + verification.FlowPersistenceProvider + identity.PrivilegedPoolProvider + httpx.WriterProvider + otelx.Provider + } + Verifier struct { + r verifierDependencies + } +) + +func NewVerifier(r verifierDependencies) *Verifier { + return &Verifier{r: r} +} + +func (e *Verifier) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, f *registration.Flow, s *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.Verifier.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { + return e.do(w, r.WithContext(ctx), s.Identity, f, func(v *verification.Flow) { + v.OAuth2LoginChallenge = f.OAuth2LoginChallenge + v.SessionID = uuid.NullUUID{UUID: s.ID, Valid: true} + v.IdentityID = uuid.NullUUID{UUID: s.Identity.ID, Valid: true} + v.AMR = s.AMR + }) + }) +} + +func (e *Verifier) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params settings.PostHookPostPersistExecutorParams) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.Verifier.ExecuteSettingsPostPersistHook", func(ctx context.Context) error { + return e.do(w, r.WithContext(ctx), params.Updated, params.Flow, nil) + }) +} + +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.SetAttributes(attribute.String("skip_reason", "skipping verification hook because AAL is not 1")) + return nil + } + + return e.do(w, r.WithContext(ctx), s.Identity, f, func(vf *verification.Flow) { + vf.OAuth2LoginChallenge = f.OAuth2LoginChallenge + var sID uuid.UUID + if s != nil { + sID = s.ID + } + vf.SessionID = uuid.NullUUID{UUID: sID, Valid: sID != uuid.Nil} + var iID uuid.UUID + if s != nil && s.Identity != nil { + iID = s.Identity.ID + } + vf.IdentityID = uuid.NullUUID{UUID: iID, Valid: iID != uuid.Nil} + vf.AMR = s.AMR + }) +} + +const InternalContextRegistrationVerificationFlow = "registration_verification_flow_continue_with" + +func (e *Verifier) do( + w http.ResponseWriter, + r *http.Request, + i *identity.Identity, + f interface { + flow.FlowWithContinueWith + flow.InternalContexter + }, + flowCallback func(*verification.Flow), +) (err error) { + ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.do") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + // This is called after the identity has been created so we can safely assume that all addresses are available + // already. + + strategies, primaryStrategy, err := e.r.GetActiveVerificationStrategies(ctx) + if err != nil { + return err + } + + isBrowserFlow := f.GetType() == flow.TypeBrowser + isRegistrationOrLoginFlow := f.GetFlowName() == flow.RegistrationFlow || f.GetFlowName() == flow.LoginFlow + + for k := range i.VerifiableAddresses { + address := &i.VerifiableAddresses[k] + 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 + } + + if address.Value == "" { + continue + } + + var csrf string + + // TODO: this is pretty ugly, we should probably have a better way to handle CSRF tokens here. + if isBrowserFlow { + 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 { + // If it came from a settings flow, there already is a CSRF token, so we can just use that. + csrf = e.r.GenerateCSRFToken(r) + } + } + + verificationFlow, err := verification.NewPostHookFlow(e.r, + e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx), + csrf, r, strategies, f) + if err != nil { + return err + } + + if flowCallback != nil { + flowCallback(verificationFlow) + } + + verificationFlow.State = flow.StateEmailSent + if err := primaryStrategy.PopulateVerificationMethod(r, verificationFlow); err != nil { + return err + } + + // The code strategy's PopulateVerificationMethod sets the email "code sent" + // message unconditionally; override it with the message that matches the + // channel the code was sent over. The legacy link strategy sends a link, + // not a code, and sets no message — leave it untouched. + if primaryStrategy.NodeGroup() == node.CodeGroup { + verificationFlow.UI.Messages.Set(text.VerificationCodeSentMessage(address.Via)) + } + + verificationFlow.UI.Nodes.Append( + node.NewInputField(address.Via, address.Value, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + + if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil { + return err + } + + if err := primaryStrategy.SendVerificationCode(ctx, verificationFlow, i, address); err != nil { + return err + } + + address.Status = identity.VerifiableAddressStatusSent + if err := e.r.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status"); err != nil { + return err + } + + flowURL := "" + if verificationFlow.Type == flow.TypeBrowser { + flowURL = verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String() + } + + continueWith := flow.NewContinueWithVerificationUI(verificationFlow.ID, address.Value, flowURL) + internalContext, err := sjson.SetBytes(f.GetInternalContext(), InternalContextRegistrationVerificationFlow, continueWith.Flow) + if err != nil { + return err + } + f.SetInternalContext(internalContext) + + if e.r.Config().UseLegacyShowVerificationUI(ctx) { + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "legacy_continue_with_verification_ui")) + f.AddContinueWith(continueWith) + continue // Legacy behavior + } + + break // We only do this for the first address we find as we can't redirect to multiple flows at once. + } + return nil +} diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go new file mode 100644 index 0000000..20ae993 --- /dev/null +++ b/selfservice/hook/verification_test.go @@ -0,0 +1,280 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/ui/node" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "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/text" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func TestVerifier(t *testing.T) { + ctx := context.Background() + u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + + for _, tc := range []struct { + name string + execHook func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error + + originalFlow func() interface { + flow.InternalContexter + 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() interface { + flow.InternalContexter + flow.FlowWithContinueWith + } { + return &login.Flow{RequestURL: "http://foo.com/login", RequestedAAL: "aal1"} + }, + }, + { + 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() interface { + flow.InternalContexter + flow.FlowWithContinueWith + } { + return ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} + }, + }, + } { + t.Run("flow="+tc.name, func(t *testing.T) { + for _, enabled := range []bool{true, false} { + t.Run(fmt.Sprintf("legacy flag=%v", enabled), func(t *testing.T) { + t.Run("case=should send out emails for unverified addresses", func(t *testing.T) { + originalFlow := tc.originalFlow() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, enabled) + + 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, gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow).Array(), 1, "%s", originalFlow.GetInternalContext()) + + var verificationID uuid.UUID + if enabled { + 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) + verificationID = vf.(*flow.ContinueWithVerificationUI).Flow.ID + } else { + assert.Lenf(t, originalFlow.ContinueWith(), 0, "%#v", originalFlow.ContinueWith()) + verificationID = uuid.FromStringOrNil(gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow+".id").String()) + require.NotEqual(t, uuid.Nil, verificationID, "%s", originalFlow.GetInternalContext()) + } + + expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, verificationID) + require.NoError(t, err) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) + require.NotEmpty(t, expectedVerificationFlow.SessionID.UUID) + require.NotEmpty(t, expectedVerificationFlow.IdentityID.UUID) + require.NotNil(t, expectedVerificationFlow.UI.Nodes.Find("email")) + require.Len(t, expectedVerificationFlow.UI.Messages, 1) + require.Equal(t, text.InfoSelfServiceVerificationEmailWithCodeSent, expectedVerificationFlow.UI.Messages[0].ID) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.NoError(t, err) + if enabled { + require.Len(t, messages, 2) + } else { + require.Len(t, messages, 1) + } + }) + }) + } + + t.Run("case should skip already verified addresses", func(t *testing.T) { + t.Parallel() + originalFlow := tc.originalFlow() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/") + 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 = new(sqlxx.NullTime(time.Now())) + address.Status = identity.VerifiableAddressStatusCompleted + require.NoError(t, 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 := pkg.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=settings", func(t *testing.T) { + t.Parallel() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, true) + + 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.AddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "foo@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "bar@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "bar@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "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, settings.PostHookPostPersistExecutorParams{ + Flow: originalFlow, + Updated: i, + Previous: i, + Session: &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.AddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) + address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "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, settings.PostHookPostPersistExecutorParams{ + Flow: originalFlow, + Updated: i, + Previous: i, + Session: &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) { + t.Helper() + + require.Len(t, cs, len(addresses)) + + e := make([]string, len(cs)) + + for i, c := range cs { + require.IsType(t, &flow.ContinueWithVerificationUI{}, c) + fView := c.(*flow.ContinueWithVerificationUI).Flow + e[i] = fView.VerifiableAddress + } + + require.ElementsMatch(t, addresses, e) +} diff --git a/selfservice/hook/verify_new_address.go b/selfservice/hook/verify_new_address.go new file mode 100644 index 0000000..bc8c215 --- /dev/null +++ b/selfservice/hook/verify_new_address.go @@ -0,0 +1,253 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "net/http" + "slices" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "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/nosurfx" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +var _ settings.PostHookPrePersistExecutor = new(VerifyNewAddress) + +type ( + verifyNewAddressDependencies interface { + clock.Provider + config.Provider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + verification.StrategyProvider + verification.FlowPersistenceProvider + identity.PrivilegedPoolProvider + identity.PendingTraitsChangePersistenceProvider + settings.FlowPersistenceProvider + httpx.WriterProvider + otelx.Provider + session.ManagementProvider + } + + VerifyNewAddressProvider interface { + HookVerifyNewAddress() *VerifyNewAddress + } + + VerifyNewAddress struct { + r verifyNewAddressDependencies + } +) + +func NewVerifyNewAddress(r verifyNewAddressDependencies) *VerifyNewAddress { + return &VerifyNewAddress{r: r} +} + +func (e *VerifyNewAddress) ExecuteSettingsPrePersistHook( + w http.ResponseWriter, r *http.Request, + params settings.PostHookPrePersistExecutorParams, +) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.VerifyNewAddress.ExecuteSettingsPrePersistHook", func(ctx context.Context) error { + return e.execute(ctx, w, r, params.Flow, params.Identity, params.Session) + }) +} + +func (e *VerifyNewAddress) execute( + ctx context.Context, w http.ResponseWriter, r *http.Request, + f *settings.Flow, proposed *identity.Identity, s *session.Session, +) error { + original := s.Identity + if original == nil { + return nil + } + + // Find verifiable addresses that changed between original and proposed. + changed := findChangedAddresses(original.VerifiableAddresses, proposed.VerifiableAddresses) + if len(changed) == 0 { + return nil // No address changes — let the flow proceed normally. + } + + // At this point, we know the user is trying to change an address. We need to check that the session is currently privileged. + if !e.r.SessionManager().IsPrivileged(ctx, s) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + + // Only one address change can be verified at a time. Reject if multiple + // addresses changed to prevent unverified address changes from being applied. + if len(changed) > 1 { + f.UI.Messages.Clear() + f.UI.Messages.Add(text.NewErrorValidationSettingsTooManyAddressChanges()) + if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil { + return err + } + if x.IsJSONRequest(r) { + e.r.Writer().Write(w, r, f) + return errors.WithStack(settings.ErrHookAbortFlow) + } + http.Redirect(w, r, f.AppendTo(e.r.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther) + return errors.WithStack(settings.ErrHookAbortFlow) + } + + // Process the first changed address (we can only redirect to one verification flow). + addr := changed[0] + + // Reject if the new address already belongs to a different identity + existingAddr, err := e.r.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, addr.Via, addr.Value) + if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return err + } + if err == nil && existingAddr.IdentityID != original.ID { + f.UI.Messages.Clear() + f.UI.Messages.Add(text.NewErrorValidationDuplicateCredentials()) + if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil { + return err + } + if x.IsJSONRequest(r) { + e.r.Writer().WriteCode(w, r, http.StatusBadRequest, f) + return errors.WithStack(settings.ErrHookAbortFlow) + } + http.Redirect(w, r, f.AppendTo(e.r.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther) + return errors.WithStack(settings.ErrHookAbortFlow) + } + + // Delete any existing pending changes for this identity. + if err := e.r.PendingTraitsChangePersister().DeletePendingTraitsChangesByIdentity(ctx, original.ID); err != nil { + return err + } + + // Create the verification flow. + strategies, primaryStrategy, err := e.r.GetActiveVerificationStrategies(ctx) + if err != nil { + return err + } + + var csrf string + if f.Type == flow.TypeBrowser { + csrf = e.r.GenerateCSRFToken(r) + } + + verificationFlow, err := verification.NewPostHookFlow(e.r, + e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx), + csrf, r, strategies, f) + if err != nil { + return err + } + + verificationFlow.State = flow.StateEmailSent + if err := primaryStrategy.PopulateVerificationMethod(r, verificationFlow); err != nil { + return err + } + + // The code strategy's PopulateVerificationMethod sets the email "code sent" + // message unconditionally; override it with the message that matches the + // channel the code was sent over. The legacy link strategy sends a link, + // not a code, and sets no message — leave it untouched. + if primaryStrategy.NodeGroup() == node.CodeGroup { + verificationFlow.UI.Messages.Set(text.VerificationCodeSentMessage(addr.Via)) + } + + verificationFlow.UI.Nodes.Append( + node.NewInputField(addr.Via, addr.Value, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + + if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil { + return err + } + + // Create PendingTraitsChange record. + sessionID := s.ID + originFlowID := f.ID + ptc := &identity.PendingTraitsChange{ + IdentityID: original.ID, + IdentityRegion: original.Region, + SessionID: uuid.NullUUID{UUID: sessionID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true}, + NewAddressValue: addr.Value, + NewAddressVia: addr.Via, + OriginalTraitsHash: identity.HashTraits(json.RawMessage(original.Traits)), + ProposedTraits: json.RawMessage(proposed.Traits), + VerificationFlowID: verificationFlow.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + if err := e.r.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc); err != nil { + return err + } + + // Send verification code to the new address. + if err := primaryStrategy.SendVerificationCode(ctx, verificationFlow, proposed, ptc); err != nil { + return err + } + + // Store verification flow reference in InternalContext (for show_verification_ui). + flowURL := "" + if verificationFlow.Type == flow.TypeBrowser { + flowURL = verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String() + } + continueWith := flow.NewContinueWithVerificationUI(verificationFlow.ID, addr.Value, flowURL) + internalContext, err := sjson.SetBytes(f.GetInternalContext(), InternalContextRegistrationVerificationFlow, continueWith.Flow) + if err != nil { + return err + } + f.SetInternalContext(internalContext) + f.AddContinueWith(continueWith) + + // Persist the updated settings flow (with InternalContext/ContinueWith). + if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil { + return err + } + + // Write the HTTP response and abort the flow. + if x.IsJSONRequest(r) || verificationFlow.Type == flow.TypeAPI { + e.r.Writer().Write(w, r, f) + return errors.WithStack(settings.ErrHookAbortFlow) + } + + if flowURL != "" { + http.Redirect(w, r, flowURL, http.StatusSeeOther) + return errors.WithStack(settings.ErrHookAbortFlow) + } + + // This should not happen: we only end up here if, the flow is an API flow, but + + return errors.WithStack(settings.ErrHookAbortFlow) +} + +type changedAddress struct { + Value string + Via string +} + +// findChangedAddresses compares original and proposed verifiable addresses and +// returns addresses that are new or have a different value. +func findChangedAddresses(original, proposed []identity.VerifiableAddress) []changedAddress { + origByVia := make(map[string][]string, len(original)) + for _, a := range original { + origByVia[a.Via] = append(origByVia[a.Via], a.Value) + } + + var changed []changedAddress + for _, p := range proposed { + if !slices.Contains(origByVia[p.Via], p.Value) { + changed = append(changed, changedAddress{Value: p.Value, Via: p.Via}) + } + } + return changed +} diff --git a/selfservice/hook/verify_new_address_test.go b/selfservice/hook/verify_new_address_test.go new file mode 100644 index 0000000..47d2f85 --- /dev/null +++ b/selfservice/hook/verify_new_address_test.go @@ -0,0 +1,606 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "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/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/contextx" + "github.com/ory/x/randx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func TestVerifyNewAddress(t *testing.T) { + t.Parallel() + ctx := context.Background() + + setup := func(t *testing.T) (context.Context, *hook.VerifyNewAddress, *driver.RegistryDefault) { + _, reg := pkg.NewFastRegistryWithMocks(t) + ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_single_email.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + h := hook.NewVerifyNewAddress(reg) + return ctx, h, reg + } + + newSettingsFlow := func(t *testing.T, i identity.Identity) *settings.Flow { + t.Helper() + now := time.Now().UTC() + return &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: now.Add(time.Hour), + IssuedAt: now, + RequestURL: "http://foo.com/settings", + IdentityID: i.ID, + Identity: &i, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + UI: &container.Container{Method: "POST"}, + InternalContext: []byte("{}"), + } + } + + verifyAllAddresses := func(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, i *identity.Identity) *identity.Identity { + t.Helper() + for idx := range i.VerifiableAddresses { + i.VerifiableAddresses[idx].Verified = true + verifiedAt := sqlxx.NullTime(time.Now()) + i.VerifiableAddresses[idx].VerifiedAt = &verifiedAt + i.VerifiableAddresses[idx].Status = identity.VerifiableAddressStatusCompleted + require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &i.VerifiableAddresses[idx])) + } + var err error + i, err = reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault) + require.NoError(t, err) + return i + } + + createIdentity := func(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, email string, verified bool) *identity.Identity { + t.Helper() + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email":"` + email + `","name":"Test"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + if verified { + i = verifyAllAddresses(t, ctx, reg, i) + } + return i + } + + t.Run("case=no-op when address does not change", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + + original := createIdentity(t, ctx, reg, "old@example.com", true) + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"old@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "old@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ID: x.NewUUID(), Identity: original, AuthenticatedAt: time.Now()} + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.NoError(t, err, "hook should be a no-op when address did not change") + }) + + t.Run("case=aborts flow when address changes", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + + original := createIdentity(t, ctx, reg, "old@example.com", true) + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"new@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.Error(t, err) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + // Settings flow should have a ContinueWith entry pointing to the verification flow. + require.NotEmpty(t, f.ContinueWith(), "settings flow should have ContinueWith items") + cw := f.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, cw) + vfID := cw.(*flow.ContinueWithVerificationUI).Flow.ID + + // A PendingTraitsChange record should exist. + ptc, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID) + require.NoError(t, err) + assert.Equal(t, identity.PendingTraitsChangeStatusPending, ptc.Status) + assert.Equal(t, "new@example.com", ptc.NewAddressValue) + assert.Equal(t, original.ID, ptc.IdentityID) + assert.Equal(t, identity.HashTraits(json.RawMessage(original.Traits)), ptc.OriginalTraitsHash) + require.True(t, ptc.SessionID.Valid, "pending change should record the session id") + assert.Equal(t, sess.ID, ptc.SessionID.UUID) + require.True(t, ptc.OriginSettingsFlowID.Valid, "pending traits change should record the originating settings flow ID") + assert.Equal(t, f.ID, ptc.OriginSettingsFlowID.UUID, "originating settings flow ID should match the flow the hook ran in") + + // Verification flow should exist and be in StateEmailSent. + vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID) + require.NoError(t, err) + assert.Equal(t, flow.StateEmailSent, vf.State) + + // A verification code message should have been sent. + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.Equal(t, "new@example.com", messages[0].Recipient) + }) + + t.Run("case=previous pending changes are deleted on new submission", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + + original := createIdentity(t, ctx, reg, "old@example.com", true) + + // First change: old@example.com -> new1@example.com + f1 := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f1)) + + proposed1 := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"new1@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "new1@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f1, + Identity: proposed1, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow)) + + vfID1 := f1.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID + ptc1, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID1) + require.NoError(t, err) + assert.Equal(t, "new1@example.com", ptc1.NewAddressValue) + + // Drain courier messages from first change. + _, _ = reg.CourierPersister().NextMessages(ctx, 12) + + // Second change: old@example.com -> new2@example.com + f2 := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f2)) + + proposed2 := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"new2@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "new2@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + w2 := httptest.NewRecorder() + err = h.ExecuteSettingsPrePersistHook(w2, r, settings.PostHookPrePersistExecutorParams{ + Flow: f2, + Identity: proposed2, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow)) + + vfID2 := f2.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID + ptc2, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID2) + require.NoError(t, err) + assert.Equal(t, "new2@example.com", ptc2.NewAddressValue) + + // The first pending change should have been deleted. + _, err = reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID1) + require.Error(t, err, "first pending change should have been deleted") + }) + + t.Run("case=correctly detects change with two email addresses", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_two_emails.schema.json") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + // Verify both addresses. + i = verifyAllAddresses(t, ctx, reg, i) + require.Len(t, i.VerifiableAddresses, 2, "identity should have two verifiable addresses") + + // Change only the primary email; recovery email stays the same. + f := newSettingsFlow(t, *i) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: i.ID, + Traits: identity.Traits(`{"email":"new-primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "new-primary@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID}, + {Value: "recovery@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: i, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + // Should abort with exactly one changed address (not two, not zero). + require.Error(t, err) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + // Verify only one verification was triggered (for the changed address). + assert.Len(t, f.ContinueWith(), 1) + cw := f.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, cw) + vfID := cw.(*flow.ContinueWithVerificationUI).Flow.ID + + ptc, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID) + require.NoError(t, err) + assert.Equal(t, "new-primary@example.com", ptc.NewAddressValue, "pending change should be for the changed address only") + }) + + t.Run("case=no-op with two unchanged email addresses", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_two_emails.schema.json") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + i = verifyAllAddresses(t, ctx, reg, i) + + f := newSettingsFlow(t, *i) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: i.ID, + Traits: identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "primary@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID}, + {Value: "recovery@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID}, + }, + } + + sess := &session.Session{ID: x.NewUUID(), Identity: i, AuthenticatedAt: time.Now()} + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.NoError(t, err, "hook should be a no-op when no addresses changed") + assert.Empty(t, f.ContinueWith()) + }) + + t.Run("case=rejects change when new address is already owned by another identity", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + + _ = createIdentity(t, ctx, reg, "existing@example.com", true) + + original := createIdentity(t, ctx, reg, "other@example.com", true) + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"existing@example.com","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "existing@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + // Flow should carry the duplicate credentials error — no redirect to verification. + assert.Empty(t, f.ContinueWith(), "should not start a verification flow") + require.NotEmpty(t, f.UI.Messages) + assert.Contains(t, f.UI.Messages[0].Text, "exists already") + }) + + t.Run("case=rejects change when new phone number is already owned by another identity", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json") + + existing := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + existing.Traits = identity.Traits(`{"email":"phone-existing@example.com","phone":"+19999999999","name":"Existing"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, existing)) + verifyAllAddresses(t, ctx, reg, existing) + + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = identity.Traits(`{"email":"phone-original@example.com","phone":"+10000000000","name":"Original"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + original = verifyAllAddresses(t, ctx, reg, original) + + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + // Proposed: email unchanged, phone changed to the existing's number. + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"phone-original@example.com","phone":"+19999999999","name":"Original"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "phone-original@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + {Value: "+19999999999", Via: identity.AddressTypeSMS, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + // Flow should carry the duplicate credentials error — no redirect to verification. + assert.Empty(t, f.ContinueWith(), "should not start a verification flow") + require.NotEmpty(t, f.UI.Messages) + assert.Contains(t, f.UI.Messages[0].Text, "exists already") + }) + + t.Run("case=returns error when multiple addresses change at once", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email":"old@example.com","phone":"+12345678901","name":"Test"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + i = verifyAllAddresses(t, ctx, reg, i) + + f := newSettingsFlow(t, *i) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + proposed := &identity.Identity{ + ID: i.ID, + Traits: identity.Traits(`{"email":"new@example.com","phone":"+19876543210","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "new@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID}, + {Value: "+19876543210", Via: "sms", IdentityID: i.ID}, + }, + } + + sess := &session.Session{ID: x.NewUUID(), Identity: i, AuthenticatedAt: time.Now()} + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.Error(t, err) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + // The flow should contain the too-many-address-changes error message. + require.NotEmpty(t, f.UI.Messages) + }) + + t.Run("case=SMS verification flow shows the phone code-sent message, not the email one", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json") + // Configure an SMS courier channel so the verification code can be queued for a phone number. + ctx = contextx.WithConfigValue(ctx, config.ViperKeyCourierChannels, []map[string]any{ + {"id": "sms", "type": "http", "request_config": map[string]any{ + "url": "http://localhost:1234/sms", + "method": "POST", + "body": "base64://ZnVuY3Rpb24oY3R4KSBjdHg=", + }}, + }) + + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = identity.Traits(`{"email":"sms-msg@example.com","phone":"+10000000000","name":"Test"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + original = verifyAllAddresses(t, ctx, reg, original) + + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + // Proposed: email unchanged, phone changed to a new, unowned number. + const newPhone = "+19876543210" + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"sms-msg@example.com","phone":"` + newPhone + `","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: "sms-msg@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID}, + {Value: newPhone, Via: identity.AddressTypeSMS, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + require.NotEmpty(t, f.ContinueWith(), "settings flow should redirect to verification") + vfID := f.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID + vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID) + require.NoError(t, err) + + // An SMS verification must show only the phone "code has been sent" message, + // never the email wording. + require.Len(t, vf.UI.Messages, 1, "expected exactly one code-sent message") + assert.Equal(t, text.InfoSelfServiceVerificationPhoneWithCodeSent, vf.UI.Messages[0].ID) + }) + + t.Run("case=link strategy verification flow does not claim a code was sent", func(t *testing.T) { + t.Parallel() + ctx, h, reg := setup(t) + // Use the legacy link strategy as the primary verification method. It + // delivers a link, not a code, and sets no "code sent" message. + ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true) + ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceVerificationUse, "link") + + original := createIdentity(t, ctx, reg, "link-msg@example.com", true) + + f := newSettingsFlow(t, *original) + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f)) + + const newEmail = "link-msg-new@example.com" + proposed := &identity.Identity{ + ID: original.ID, + Traits: identity.Traits(`{"email":"` + newEmail + `","name":"Test"}`), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: newEmail, Via: identity.AddressTypeEmail, IdentityID: original.ID}, + }, + } + + sess := &session.Session{ + ID: x.NewUUID(), + Identity: original, + Token: randx.MustString(12, randx.AlphaLowerNum), + LogoutToken: randx.MustString(12, randx.AlphaLowerNum), + AuthenticatedAt: time.Now(), + } + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: proposed, + Session: sess, + }) + require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err) + + require.NotEmpty(t, f.ContinueWith(), "settings flow should redirect to verification") + vfID := f.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID + vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID) + require.NoError(t, err) + + // A link was sent, not a code — the flow must not carry a "code has + // been sent" message. + require.Empty(t, vf.UI.Messages, "link strategy flows must not claim a code was sent") + }) +} diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go new file mode 100644 index 0000000..d0eccdf --- /dev/null +++ b/selfservice/hook/web_hook.go @@ -0,0 +1,578 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/textproto" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/gofrs/uuid" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + "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/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/reqlog" +) + +var _ interface { + login.PreHookExecutor + login.PostHookExecutor + + registration.PostHookPostPersistExecutor + registration.PostHookPrePersistExecutor + registration.PreHookExecutor + + verification.PreHookExecutor + verification.PostHookExecutor + + recovery.PreHookExecutor + recovery.PostHookExecutor + + settings.PreHookExecutor + settings.PostHookPrePersistExecutor + settings.PostHookPostPersistExecutor +} = (*WebHook)(nil) + +var jsonnetCache, _ = ristretto.NewCache(&ristretto.Config[[]byte, []byte]{ + MaxCost: 100 << 20, // 100MB, + NumCounters: 1_000_000, // 1kB per snippet -> 100k snippets -> 1M counters + BufferItems: 64, +}) + +type ( + webHookDependencies interface { + logrusx.Provider + httpx.ClientProvider + otelx.Provider + jsonnetsecure.VMProvider + config.Provider + } + + templateContext struct { + Flow flow.Flow `json:"flow"` + RequestHeaders http.Header `json:"request_headers"` + RequestMethod string `json:"request_method"` + RequestURL string `json:"request_url"` + RequestCookies map[string]string `json:"request_cookies"` + Identity *identity.Identity `json:"identity,omitempty"` + Session *session.Session `json:"session,omitempty"` + } + + WebHook struct { + deps webHookDependencies + conf *request.Config + } + + detailedMessage struct { + ID int `json:"id"` + Text string `json:"text"` + Type string `json:"type"` + Context json.RawMessage `json:"context,omitempty"` + } + + errorMessage struct { + InstancePtr string `json:"instance_ptr"` + DetailedMessages []detailedMessage `json:"messages"` + } + + rawHookResponse struct { + Messages []errorMessage `json:"messages"` + } +) + +func cookies(req *http.Request) map[string]string { + cookies := make(map[string]string) + for _, c := range req.Cookies() { + if c.Name != "" { + cookies[c.Name] = c.Value + } + } + return cookies +} + +func NewWebHook(r webHookDependencies, c *request.Config) *WebHook { + return &WebHook{deps: r, conf: c} +} + +func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPreHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }) + }) +} + +func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPostHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + Session: session, + }) + }) +} + +func (e *WebHook) ExecuteVerificationPreHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteVerificationPreHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }) + }) +} + +func (e *WebHook) ExecutePostVerificationHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow, id *identity.Identity) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostVerificationHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }) + }) +} + +func (e *WebHook) ExecuteRecoveryPreHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRecoveryPreHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestCookies: cookies(req), + RequestURL: x.RequestURL(req).String(), + }) + }) +} + +func (e *WebHook) ExecutePostRecoveryHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow, session *session.Session) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRecoveryHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + }) + }) +} + +func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRegistrationPreHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }) + }) +} + +func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, id *identity.Identity) error { + if !e.conf.CanInterrupt && !e.conf.Response.Parse { + return nil + } + + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRegistrationPrePersistHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }) + }) +} + +func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error { + if e.conf.CanInterrupt || e.conf.Response.Parse { + return nil + } + + // We want to decouple the request from the hook execution, so that the hooks still execute even + // if the request is canceled. + ctx := context.WithoutCancel(req.Context()) + + return otelx.WithSpan(ctx, "selfservice.hook.WebHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + }) + }) +} + +func (e *WebHook) ExecuteSettingsPreHook(_ http.ResponseWriter, req *http.Request, params settings.PreHookExecutorParams) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPreHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: params.Flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Session: params.Session, + }) + }) +} + +func (e *WebHook) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, req *http.Request, params settings.PostHookPostPersistExecutorParams) error { + if e.conf.CanInterrupt || e.conf.Response.Parse { + return nil + } + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPostPersistHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: params.Flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: params.Updated, + Session: params.Session, + }) + }) +} + +func (e *WebHook) ExecuteSettingsPrePersistHook(_ http.ResponseWriter, req *http.Request, params settings.PostHookPrePersistExecutorParams) error { + if !e.conf.CanInterrupt && !e.conf.Response.Parse { + return nil + } + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPrePersistHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: params.Flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: params.Identity, + Session: params.Session, + }) + }) +} + +func (e *WebHook) execute(ctx context.Context, data *templateContext) error { + var ( + httpClient = e.deps.HTTPClient(ctx) + ignoreResponse = e.conf.Response.Ignore + canInterrupt = e.conf.CanInterrupt + parseResponse = e.conf.Response.Parse + emitEvent = e.conf.EmitAnalyticsEvent == nil || *e.conf.EmitAnalyticsEvent // default true + webhookID = e.conf.ID + // The trigger ID is a random ID. It can be used to correlate webhook requests across retries. + triggerID = x.NewUUID() + tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") + ) + if ignoreResponse && (parseResponse || canInterrupt) { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("A webhook is configured to ignore the response but also to parse the response. This is not possible.")) + } + + makeRequest := func() (finalErr error) { + if ignoreResponse { + // This means we want to run this closure asynchronously and not be + // canceled when the parent context is canceled. + // + // The webhook will still cancel after 30 seconds as that is the + // configured timeout for the HTTP client. + ctx = context.WithoutCancel(ctx) + } + ctx, span := tracer.Start(ctx, "selfservice.webhook") + defer otelx.End(span, &finalErr) + + if emitEvent { + InstrumentHTTPClientForEvents(ctx, httpClient, triggerID, webhookID) + } + + defer func(startTime time.Time) { + traceID, spanID := span.SpanContext().TraceID(), span.SpanContext().SpanID() + logger := e.deps.Logger().WithField("otel", map[string]string{ + "trace_id": traceID.String(), + "span_id": spanID.String(), + }).WithField("duration", time.Since(startTime)) + if finalErr != nil { + if emitEvent && !errors.Is(finalErr, context.Canceled) { + span.AddEvent(events.NewWebhookFailed(ctx, finalErr, triggerID, webhookID)) + } + if ignoreResponse { + logger.WithError(finalErr).Warning("Webhook request failed but the error was ignored because the configuration indicated that the upstream response should be ignored") + } else { + logger.WithError(finalErr).Error("Webhook request failed") + } + } else { + logger.Info("Webhook request succeeded") + if emitEvent { + span.AddEvent(events.NewWebhookSucceeded(ctx, triggerID, webhookID)) + } + } + }(time.Now()) + + builder, err := request.NewBuilder(e.conf, e.deps, request.WithCache(jsonnetCache)) + if err != nil { + return err + } + + span.SetAttributes( + attribute.String("webhook.jsonnet.template-uri", builder.Config.TemplateURI), + attribute.Bool("webhook.can_interrupt", canInterrupt), + attribute.Bool("webhook.response.ignore", ignoreResponse), + attribute.Bool("webhook.response.parse", parseResponse), + ) + + data.RequestHeaders = RemoveDisallowedHeaders(data.RequestHeaders, e.deps.Config().WebhookHeaderAllowlist(ctx)) + + req, err := builder.BuildRequest(ctx, data) + if errors.Is(err, request.ErrCancel) { + span.SetAttributes(attribute.Bool("webhook.jsonnet.canceled", true)) + return nil + } else if err != nil { + return err + } + + if data.Identity != nil { + span.SetAttributes( + attribute.String("webhook.identity.id", data.Identity.ID.String()), + attribute.String("webhook.identity.nid", data.Identity.NID.String()), + ) + } + + e.deps.Logger().WithRequest(req.Request).Info("Dispatching webhook") + + resp, err := httpClient.Do(req) + if err != nil { + if IsTimeoutError(err) { + return (&herodot.DefaultError{ + CodeField: http.StatusGatewayTimeout, + StatusField: http.StatusText(http.StatusGatewayTimeout), + GRPCCodeField: grpccodes.DeadlineExceeded, + ErrorField: err.Error(), + ReasonField: "A third-party upstream service could not be reached. Please try again later.", + }).WithWrap(errors.WithStack(err)) + } + return errors.WithStack(err) + } + // Close the original body, not the NopCloser below: closing it returns + // the connection to the pool and ends the otelhttp client span. + defer func(body io.ReadCloser) { _ = body.Close() }(resp.Body) + resp.Body = io.NopCloser(io.LimitReader(resp.Body, 5<<20)) // read at most 5 MB from the response + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + + if resp.StatusCode >= http.StatusBadRequest { + span.SetStatus(codes.Error, "HTTP status code >= 400") + if canInterrupt || parseResponse { + if err := parseWebhookResponse(resp, data.Identity); err != nil { + return err + } + } + 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("webhook failed with status code %v", resp.StatusCode), + } + } + + if parseResponse { + return parseWebhookResponse(resp, data.Identity) + } + return nil + } + + if !ignoreResponse { + t0 := time.Now() + err := makeRequest() + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + return err + } + go func() { + // we cannot handle the error as we are running async, and it is logged anyway + _ = makeRequest() + }() + return nil +} + +// RemoveDisallowedHeaders removes all headers from httpHeaders that are not in +// headerAllowlist. +func RemoveDisallowedHeaders(httpHeaders http.Header, headerAllowlist []string) http.Header { + res := make(http.Header, len(headerAllowlist)) + for _, allowed := range headerAllowlist { + allowed = textproto.CanonicalMIMEHeaderKey(allowed) + if h, ok := httpHeaders[allowed]; ok { + res[allowed] = h + } + } + return res +} + +func parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) { + if resp == nil { + return errors.Errorf("empty response provided from the webhook") + } + + if resp.StatusCode == http.StatusOK { + type localIdentity identity.Identity + var hookResponse struct { + Identity *localIdentity `json:"identity"` + } + // io.ReadAll is safe, because resp.Body is already a limited reader. + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "webhook response body could not be read") + } + if err = json.Unmarshal(body, &hookResponse); err != nil { + return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON") + } + + if hookResponse.Identity == nil { + return nil + } + + if len(hookResponse.Identity.Traits) > 0 { + id.Traits = hookResponse.Identity.Traits + } + + if len(hookResponse.Identity.SchemaID) > 0 { + id.SchemaID = hookResponse.Identity.SchemaID + } + + if len(hookResponse.Identity.State) > 0 { + id.State = hookResponse.Identity.State + } + + if len(hookResponse.Identity.VerifiableAddresses) > 0 { + id.VerifiableAddresses = hookResponse.Identity.VerifiableAddresses + } + + if len(hookResponse.Identity.RecoveryAddresses) > 0 { + id.RecoveryAddresses = hookResponse.Identity.RecoveryAddresses + } + + if len(hookResponse.Identity.MetadataPublic) > 0 { + id.MetadataPublic = hookResponse.Identity.MetadataPublic + } + + if len(hookResponse.Identity.MetadataAdmin) > 0 { + id.MetadataAdmin = hookResponse.Identity.MetadataAdmin + } + + if gjson.GetBytes(body, "identity.external_id").Exists() { + id.ExternalID = hookResponse.Identity.ExternalID + } + + return nil + } else if resp.StatusCode == http.StatusNoContent { + return nil + } else if resp.StatusCode >= http.StatusBadRequest { + var hookResponse rawHookResponse + if err := json.NewDecoder(resp.Body).Decode(&hookResponse); err != nil { + return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON") + } + + var validationErrs []*schema.ValidationError + for _, msg := range hookResponse.Messages { + messages := text.Messages{} + for _, detail := range msg.DetailedMessages { + var msgType text.UITextType + if detail.Type == "error" { + msgType = text.Error + } else { + msgType = text.Info + } + messages.Add(&text.Message{ + ID: text.ID(detail.ID), + Text: detail.Text, + Type: msgType, + Context: detail.Context, + }) + } + validationErrs = append(validationErrs, schema.NewHookValidationError(msg.InstancePtr, "a webhook target returned an error", messages)) + } + + if len(validationErrs) == 0 { + return errors.New("error while parsing webhook response: got no validation errors") + } + + return schema.NewValidationListError(validationErrs) + } + + return nil +} + +func IsTimeoutError(err error) bool { + var te interface{ Timeout() bool } + return errors.As(err, &te) && te.Timeout() || errors.Is(err, context.DeadlineExceeded) +} + +func InstrumentHTTPClientForEvents(ctx context.Context, httpClient *retryablehttp.Client, triggerID uuid.UUID, webhookID string) { + // TODO(@alnr): improve this implementation to redact sensitive data + var ( + attempt = 0 + requestID uuid.UUID + reqBody []byte + ) + httpClient.RequestLogHook = func(_ retryablehttp.Logger, req *http.Request, retryNumber int) { + attempt = retryNumber + 1 + requestID = x.NewUUID() + req.Header.Set("Ory-Webhook-Request-ID", requestID.String()) + req.Header.Set("Ory-Webhook-Trigger-ID", triggerID.String()) + // TODO(@alnr): redact sensitive data + // reqBody, _ = httputil.DumpRequestOut(req, true) + reqBody = []byte("") + } + httpClient.ResponseLogHook = func(_ retryablehttp.Logger, res *http.Response) { + // res.Body = io.NopCloser(io.LimitReader(res.Body, 5<<20)) // read at most 5 MB from the response + // resBody, _ := httputil.DumpResponse(res, true) + // resBody = resBody[:min(len(resBody), 2<<10)] // truncate response body to 2 kB for event + // TODO(@alnr): redact sensitive data + resBody := []byte("") + trace.SpanFromContext(ctx).AddEvent(events.NewWebhookDelivered(ctx, res.Request.URL, reqBody, res.StatusCode, resBody, attempt, requestID, triggerID, webhookID)) + } +} diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go new file mode 100644 index 0000000..f9aaf2c --- /dev/null +++ b/selfservice/hook/web_hook_integration_test.go @@ -0,0 +1,1516 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "crypto/tls" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strconv" + "sync" + "testing" + "time" + + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/hook" + "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/events" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/snapshotx" +) + +var transientPayload = json.RawMessage(`{ + "stuff": { + "name": "fubar", + "numbers": [42, 12345, 3.1415] + } +}`) + +type webHookDeps struct { + x.BasicRegistry + jsonnetsecure.VMProvider + config.Provider +} + +func newWebHookDeps(t *testing.T, logger *logrusx.Logger, reg *driver.RegistryDefault) *webHookDeps { + t.Helper() + return &webHookDeps{ + BasicRegistry: x.BasicRegistry{L: logger, C: reg.HTTPClient(t.Context()), T: otelx.NewNoop()}, + VMProvider: reg, + Provider: reg, + } +} + +func TestWebHooks(t *testing.T) { + logger := logrusx.New("kratos", "test") + + type WebHookRequest struct { + Body string + Headers http.Header + Method string + } + + webHookEndPoint := func(whr *WebHookRequest) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + whr.Body = string(body) + whr.Headers = r.Header + whr.Method = r.Method + } + } + + webHookHttpCodeEndPoint := func(code int) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(code) + } + } + + webHookHttpCodeWithBodyEndPoint := func(t *testing.T, code int, body []byte) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(code) + _, err := w.Write(body) + assert.NoError(t, err, "error while returning response from webHookHttpCodeWithBodyEndPoint") + } + } + + path := "/web_hook" + newServer := func(f http.HandlerFunc) *httptest.Server { + r := http.NewServeMux() + + r.HandleFunc("CONNECT "+path, f) + r.HandleFunc("DELETE "+path, f) + r.HandleFunc("GET "+path, f) + r.HandleFunc("OPTIONS "+path, f) + r.HandleFunc("PATCH "+path, f) + r.HandleFunc("POST "+path, f) + r.HandleFunc("PUT "+path, f) + r.HandleFunc("TRACE "+path, f) + + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + return ts + } + + bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string { + body := fmt.Sprintf(`{ + "flow_id": "%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(), req.Method, "http://www.ory.com/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 + } + + bodyWithFlowAndIdentityAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { + body := fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": "%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" + }, + "transient_payload": %s + }`, f.GetID(), s.Identity.ID, req.Method, "http://www.ory.com/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 + } + + bodyWithFlowAndIdentityAndSessionAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { + body := fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": "%s", + "session_id": "%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" + }, + "transient_payload": %s + }`, f.GetID(), s.Identity.ID, s.ID, req.Method, "http://www.ory.com/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 + } + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(t.Context(), config.ViperKeyWebhookHeaderAllowlist, []string{ + "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", + "Valid-Header", + }) + + for _, tc := range []struct { + uc string + callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error + expectedBody func(req *http.Request, f flow.Flow, s *session.Session) string + createFlow func() flow.Flow + }{ + { + uc: "Pre Login Hook", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow)) + }, + expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { + return bodyWithFlowOnly(req, f) + }, + }, + { + uc: "Post Login Hook", + 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 bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) + }, + }, + { + uc: "Pre Registration Hook", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow)) + }, + expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { + return bodyWithFlowOnly(req, f) + }, + }, + { + uc: "Post Registration Hook", + createFlow: func() flow.Flow { + return ®istration.Flow{ + ID: x.NewUUID(), + TransientPayload: transientPayload, + } + }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) + }, + }, + { + uc: "Post Recovery Hook", + 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 bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) + }, + }, + { + uc: "Post Verification Hook", + 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 bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) + }, + }, + { + uc: "Post Settings Hook", + 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, settings.PostHookPostPersistExecutorParams{ + Flow: f.(*settings.Flow), + Updated: s.Identity, + Previous: s.Identity, + Session: s, + }) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) + }, + }, + } { + tc := tc + t.Run("uc="+tc.uc, func(t *testing.T) { + t.Parallel() + for _, auth := range []struct { + uc string + authConfig request.AuthConfig + expectedHeader func(header http.Header) + }{ + { + uc: "no auth", + authConfig: request.AuthConfig{}, + expectedHeader: func(header http.Header) {}, + }, + { + uc: "api key in header", + authConfig: request.AuthConfig{Type: "api_key", Config: map[string]any{ + "name": "My-Key", + "value": "My-Key-Value", + "in": "header", + }}, + expectedHeader: func(header http.Header) { + header.Set("My-Key", "My-Key-Value") + }, + }, + { + uc: "api key in cookie", + authConfig: request.AuthConfig{Type: "api_key", Config: map[string]any{ + "name": "My-Key", + "value": "My-Key-Value", + "in": "cookie", + }}, + expectedHeader: func(header http.Header) { + header.Set("Cookie", "My-Key=My-Key-Value") + }, + }, + { + uc: "basic auth", + authConfig: request.AuthConfig{Type: "basic_auth", Config: map[string]any{ + "user": "My-User", + "password": "Super-Secret", + }}, + expectedHeader: func(header http.Header) { + header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("My-User:Super-Secret"))) + }, + }, + } { + t.Run("auth="+auth.uc, func(t *testing.T) { + for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE", "GARBAGE"} { + t.Run("method="+method, func(t *testing.T) { + f := tc.createFlow() + req := (&http.Request{ + Host: "www.ory.com", + Header: map[string][]string{ + "Some-Header": {"Some-Value"}, + "User-Agent": {"Foo-Bar-Browser"}, + "Invalid-Header": {"should be ignored"}, + "Valid-Header": {"should not be 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, + URL: &url.URL{Path: "/some_end_point"}, + }).WithContext(t.Context()) + cookie, err := req.Cookie("Some-Cookie-1") + require.NoError(t, err) + require.Equal(t, cookie.Name, "Some-Cookie-1") + require.Equal(t, cookie.Value, "Some-Cookie-Value") + cookie, err = req.Cookie("Some-Cookie-2") + require.NoError(t, err) + require.Equal(t, cookie.Name, "Some-Cookie-2") + require.Equal(t, cookie.Value, "Some-other-Cookie-Value") + cookie, err = req.Cookie("Some-Cookie-3") + require.NoError(t, err) + require.Equal(t, cookie.Name, "Some-Cookie-3") + require.Equal(t, cookie.Value, "Third-Cookie-Value") + + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + whr := &WebHookRequest{} + ts := newServer(webHookEndPoint(whr)) + + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: method, + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + Auth: auth.authConfig, + }) + + err = tc.callWebHook(wh, req, f, s) + if method == "GARBAGE" { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + + assert.Equal(t, method, whr.Method) + + expectedHeader := http.Header{} + expectedHeader.Set("Content-Type", "application/json") + auth.expectedHeader(expectedHeader) + for k, v := range expectedHeader { + vals := whr.Headers.Values(k) + assert.Equal(t, v, vals) + } + assert.NotZero(t, whr.Headers.Get("Ory-Webhook-Request-ID")) + assert.NotZero(t, whr.Headers.Get("Ory-Webhook-Trigger-ID")) + + if method != "TRACE" { + // According to the HTTP spec any request method, but TRACE is allowed to + // have a body. Even this is a really bad practice for some of them, like for + // GET + assert.Zero(t, gjson.Get(whr.Body, "headers.Invalid-Header")) + assert.NotZero(t, gjson.Get(whr.Body, "headers.Valid-Header")) + whr.Body, err = sjson.Delete(whr.Body, "headers.Valid-Header") + assert.NoError(t, err) + + assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body) + } else { + assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method) + } + }) + } + }) + } + }) + } + + webHookResponse := []byte( + `{ + "messages": [{ + "instance_ptr": "#/traits/username", + "messages": [{ + "id": 1234, + "text": "error message", + "type": "info" + }] + }] + }`, + ) + + webhookError := schema.NewValidationListError([]*schema.ValidationError{schema.NewHookValidationError("#/traits/username", "a webhook target returned an error", text.Messages{{ID: 1234, Type: "info", Text: "error message"}})}) + for _, tc := range []struct { + uc string + callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error + webHookResponse func() (int, []byte) + createFlow func() flow.Flow + expectedError error + }{ + { + uc: "Pre Login Hook - no block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Pre Login Hook - block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Login Hook - no block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + 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) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Login Hook - block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + 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) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Pre Registration Hook - no block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Pre Registration Hook - block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Registration Post Persist Hook - no block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Registration Post Persist Hook - block has no effect", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) + }, + // This would usually error, but post persist does not execute blocking web hooks, so we expect no error. + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: nil, + }, + { + uc: "Post Registration Pre Persist Hook - no block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPrePersistHook(nil, req, f.(*registration.Flow), s.Identity) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Registration Pre Persist Hook - block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPrePersistHook(nil, req, f.(*registration.Flow), s.Identity) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Recovery Hook - no block", + createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Recovery Hook - block", + createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Verification Hook - no block", + createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, + 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) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Verification Hook - block", + createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, + 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) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Settings Hook - no block", + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteSettingsPostPersistHook(nil, req, settings.PostHookPostPersistExecutorParams{ + Flow: f.(*settings.Flow), + Updated: s.Identity, + Previous: s.Identity, + Session: s, + }) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Post Settings Hook Pre Persist - block", + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteSettingsPrePersistHook(nil, req, settings.PostHookPrePersistExecutorParams{ + Flow: f.(*settings.Flow), + Identity: s.Identity, + Session: s, + }) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, + { + uc: "Post Settings Hook Post Persist - block has no effect", + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteSettingsPostPersistHook(nil, req, settings.PostHookPostPersistExecutorParams{ + Flow: f.(*settings.Flow), + Updated: s.Identity, + Previous: s.Identity, + Session: s, + }) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: nil, + }, + } { + tc := tc + t.Run("uc="+tc.uc, func(t *testing.T) { + t.Parallel() + for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"} { + t.Run("method="+method, func(t *testing.T) { + f := tc.createFlow() + req := &http.Request{ + Host: "www.ory.com", + Header: map[string][]string{ + "Some-Header": {"Some-Value"}, + "X-Forwarded-Proto": {"https"}, + "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, + URL: &url.URL{ + Path: "some_end_point", + }, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + code, res := tc.webHookResponse() + ts := newServer(webHookHttpCodeWithBodyEndPoint(t, code, res)) + + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: method, + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + CanInterrupt: true, + }) + + err := tc.callWebHook(wh, req, f, s) + if tc.expectedError == nil { + assert.NoError(t, err) + return + } + + var validationError *schema.ValidationListError + var expectedError *schema.ValidationListError + if assert.ErrorAs(t, err, &validationError) && assert.ErrorAs(t, tc.expectedError, &expectedError) { + assert.Equal(t, expectedError, validationError) + } + }) + } + }) + } + + t.Run("update identity fields", func(t *testing.T) { + t.Parallel() + run := func(t *testing.T, id identity.Identity, responseCode int, response []byte) *identity.WithCredentialsAndAdminMetadataInJSON { + f := ®istration.Flow{ID: x.NewUUID()} + req := &http.Request{ + Host: "www.ory.com", + Header: map[string][]string{}, + RequestURI: "/some_end_point", + Method: http.MethodPost, + URL: &url.URL{Path: "some_end_point"}, + } + ts := newServer(webHookHttpCodeWithBodyEndPoint(t, responseCode, response)) + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "POST", + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + Response: request.ResponseConfig{ + Parse: true, + }, + }) + in := &id + err := wh.ExecutePostRegistrationPrePersistHook(nil, req, f, in) + require.NoError(t, err) + result := identity.WithCredentialsAndAdminMetadataInJSON(*in) + return &result + } + + t.Run("case=update identity fields", func(t *testing.T) { + expected := identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: "password", + Identifiers: []string{"test"}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"}`), + }, + }, + ExternalID: "original-external-id", + SchemaID: "default", + SchemaURL: "file://stub/default.schema.json", + State: identity.StateActive, + Traits: []byte(`{"email":"some@example.org"}`), + VerifiableAddresses: []identity.VerifiableAddress{{ + Value: "some@example.org", + Verified: false, + Via: "email", + Status: identity.VerifiableAddressStatusPending, + }}, + RecoveryAddresses: []identity.RecoveryAddress{{ + Value: "some@example.org", + Via: "email", + }}, + MetadataPublic: []byte(`{"public":"data"}`), + MetadataAdmin: []byte(`{"admin":"data"}`), + } + + t.Run("case=body is empty", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity is present but empty", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated traits", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"}}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated state", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"state":"inactive"}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated public metadata", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_public":{"useful":"metadata"}}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated admin metadata", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_admin":{"useful":"admin metadata"}}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated verified addresses", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"verifiable_addresses":[{"value":"some@other-example.org","via":"email"}]}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated recovery addresses", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"recovery_addresses":[{"value":"some@other-example.org","via":"email"}]}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=identity has updated external_id", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"external_id":"updated-external-id"}}`)) + snapshotx.SnapshotT(t, &actual) + }) + + t.Run("case=unset external_id", func(t *testing.T) { + actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"external_id":""}}`)) + snapshotx.SnapshotT(t, &actual) + }) + }) + }) + + t.Run("cannot have parse and ignore both set", func(t *testing.T) { + t.Parallel() + ts := newServer(webHookHttpCodeEndPoint(200)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "GET", + URL: ts.URL + path, + TemplateURI: "./stub/test_body.jsonnet", + Response: request.ResponseConfig{ + Ignore: true, + Parse: true, + }, + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.Error(t, err) + }) + + for _, tc := range []struct { + uc string + parse bool + }{ + {uc: "Post Settings Hook - parse true", parse: true}, + {uc: "Post Settings Hook - parse false", parse: false}, + } { + t.Run("uc="+tc.uc, func(t *testing.T) { + t.Parallel() + ts := newServer(webHookHttpCodeWithBodyEndPoint(t, 200, []byte(`{"identity":{"traits":{"email":"some@other-example.org"}}}`))) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + + Method: http.MethodPost, + } + f := &settings.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "POST", + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + Response: request.ResponseConfig{ + Parse: tc.parse, + }, + }) + uuid := x.NewUUID() + in := &identity.Identity{ID: uuid} + s := &session.Session{ID: x.NewUUID(), Identity: in} + + postPersistErr := wh.ExecuteSettingsPostPersistHook(nil, req, settings.PostHookPostPersistExecutorParams{ + Flow: f, + Updated: in, + Previous: in, + Session: s, + }) + assert.NoError(t, postPersistErr) + assert.Equal(t, in, &identity.Identity{ID: uuid}) + + prePersistErr := wh.ExecuteSettingsPrePersistHook(nil, req, settings.PostHookPrePersistExecutorParams{ + Flow: f, + Identity: in, + Session: s, + }) + assert.NoError(t, prePersistErr) + if tc.parse == true { + assert.Equal(t, in, &identity.Identity{ID: uuid, Traits: identity.Traits(`{"email":"some@other-example.org"}`)}) + } else { + assert.Equal(t, in, &identity.Identity{ID: uuid}) + } + }) + } + + t.Run("must error when template is erroneous", func(t *testing.T) { + t.Parallel() + ts := newServer(webHookHttpCodeEndPoint(200)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "POST", + URL: ts.URL + path, + TemplateURI: "file://./stub/bad_template.jsonnet", + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.Error(t, err) + }) + + t.Run("must not error when template is erroneous and responses are ignored", func(t *testing.T) { + ts := newServer(webHookHttpCodeEndPoint(200)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "GET", + URL: ts.URL + path, + TemplateURI: "file://./stub/bad_template.jsonnet", + Response: request.ResponseConfig{ + Ignore: true, + }, + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.NoError(t, err) + }) + + t.Run("must not make request", func(t *testing.T) { + t.Parallel() + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "POST", + URL: "https://i-do-not-exist/", + TemplateURI: "file://./stub/cancel_template.jsonnet", + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.NoError(t, err) + }) + + boolToString := func(f bool) string { + if f { + return " not" + } else { + return "" + } + } + + t.Run("ignores the response and is async", func(t *testing.T) { + t.Parallel() + var wg sync.WaitGroup + wg.Add(1) + waitTime := time.Millisecond * 100 + ts := newServer(func(w http.ResponseWriter, _ *http.Request) { + defer wg.Done() + time.Sleep(waitTime) + w.WriteHeader(http.StatusBadRequest) + }) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "GET", + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + Response: request.ResponseConfig{ + Ignore: true, + }, + }) + + start := time.Now() + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.NoError(t, err) + assert.Less(t, time.Since(start), waitTime) + + wg.Wait() + }) + + t.Run("does not error on 500 request with retry", func(t *testing.T) { + t.Parallel() + // This test essentially ensures that we do not regress on the bug we had where 500 status code + // would cause a retry, but because the body was incorrectly set we ended up with a ContentLength + // error. + + var wg sync.WaitGroup + wg.Add(3) // HTTP client does 3 attempts + ts := newServer(func(w http.ResponseWriter, r *http.Request) { + defer wg.Done() + w.WriteHeader(500) + _, _ = w.Write([]byte(`{"error":"some error"}`)) + }) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "GET", + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + require.Error(t, err) + assert.NotContains(t, err.Error(), "ContentLength") + + wg.Wait() + }) + + for _, tc := range []struct { + code int + mustSuccess bool + }{ + {200, true}, + {299, true}, + {300, true}, + {399, true}, + {400, false}, + {499, false}, + {500, false}, + {599, false}, + } { + tc := tc + t.Run("Must"+boolToString(tc.mustSuccess)+" error when end point is returning "+strconv.Itoa(tc.code), func(t *testing.T) { + t.Parallel() + ts := newServer(webHookHttpCodeEndPoint(tc.code)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + Method: "POST", + URL: ts.URL + path, + TemplateURI: "file://./stub/test_body.jsonnet", + }) + + err := wh.ExecuteLoginPreHook(nil, req, f) + if tc.mustSuccess { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestDisallowPrivateIPRanges(t *testing.T) { + t.Parallel() + logger := logrusx.New("kratos", "test") + + mux := http.NewServeMux() + mux.HandleFunc("/exception", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { + t.Error("the request should not have reached the test server") + w.WriteHeader(http.StatusOK) + }) + ts := httptest.NewServer(mux) + t.Cleanup(ts.Close) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + f := &login.Flow{ID: x.NewUUID()} + + t.Run("not allowed to call url", func(t *testing.T) { + t.Parallel() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPNoPrivateIPRanges, true) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{ts.URL + "/exception"}) + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + URL: ts.URL, + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + }) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + assert.ErrorContains(t, err, "no route to host") + }) + + t.Run("allowed to call exempt url", func(t *testing.T) { + t.Parallel() + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPNoPrivateIPRanges, true) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{ts.URL + "/exception"}) + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + URL: ts.URL + "/exception", + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + }) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + require.NoError(t, err) + }) + + t.Run("not allowed to load from source", func(t *testing.T) { + t.Parallel() + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + f := &login.Flow{ID: x.NewUUID()} + conf, reg := pkg.NewFastRegistryWithMocks(t) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPNoPrivateIPRanges, true) + conf.MustSet(t.Context(), config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{ts.URL + "/exception"}) + whDeps := newWebHookDeps(t, logger, reg) + wh := hook.NewWebHook(whDeps, &request.Config{ + URL: ts.URL + "/exception", + Method: "GET", + TemplateURI: "http://192.168.178.0/test_body.jsonnet", + }) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + require.ErrorContains(t, err, "no route to host") + }) +} + +func TestAsyncWebhook(t *testing.T) { + t.Parallel() + _, reg := pkg.NewFastRegistryWithMocks(t) + logger := logrusx.New("kratos", "test") + logHook := new(test.Hook) + logger.Logger.Hooks.Add(logHook) + whDeps := newWebHookDeps(t, logger, reg) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + + incomingCtx, incomingCancel := context.WithCancel(context.Background()) + if deadline, ok := t.Deadline(); ok { + // cancel this context one second before test timeout for clean shutdown + var cleanup context.CancelFunc + incomingCtx, cleanup = context.WithDeadline(incomingCtx, deadline.Add(-time.Second)) + defer cleanup() + } + + req = req.WithContext(incomingCtx) + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + f := &login.Flow{ID: x.NewUUID()} + + handlerEntered, blockHandlerOnExit := make(chan struct{}), make(chan struct{}) + webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + close(handlerEntered) + <-blockHandlerOnExit + _, _ = w.Write([]byte("ok")) + })) + t.Cleanup(webhookReceiver.Close) + + wh := hook.NewWebHook(whDeps, &request.Config{ + URL: webhookReceiver.URL, + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + Response: request.ResponseConfig{ + Ignore: true, + }, + }) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + require.NoError(t, err) // execution returns immediately for async webhook + select { + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for webhook request to reach test handler") + case <-handlerEntered: + // ok + } + // at this point, a goroutine is in the middle of the call to our test handler and waiting for a response + incomingCancel() // simulate the incoming Kratos request having finished + close(blockHandlerOnExit) + timeout := time.After(1 * time.Second) + var found bool + for !found { + for _, entry := range logHook.AllEntries() { + if entry.Message == "Webhook request succeeded" { + found = true + break + } + } + + select { + case <-timeout: + t.Fatal("timed out waiting for successful webhook completion") + case <-time.After(50 * time.Millisecond): + // continue loop + } + } + require.True(t, found) +} + +func TestWebhookEvents(t *testing.T) { + t.Parallel() + _, reg := pkg.NewFastRegistryWithMocks(t) + logger := logrusx.New("kratos", "test") + whDeps := newWebHookDeps(t, logger, reg) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + + webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ok" { + w.WriteHeader(200) + _, _ = w.Write([]byte("ok")) + } else { + w.WriteHeader(500) + _, _ = w.Write([]byte("fail")) + } + })) + t.Cleanup(webhookReceiver.Close) + + getAttributes := func(attrs []attribute.KeyValue) (webhookID, triggerID, requestID string) { + for _, kv := range attrs { + switch semconv.AttributeKey(kv.Key) { + case events.AttributeKeyWebhookID: + webhookID = kv.Value.Emit() + case events.AttributeKeyWebhookTriggerID: + triggerID = kv.Value.Emit() + case events.AttributeKeyWebhookRequestID: + requestID = kv.Value.Emit() + } + } + return + } + + t.Run("success", func(t *testing.T) { + whID := x.NewUUID() + wh := hook.NewWebHook(whDeps, &request.Config{ + ID: whID.String(), + URL: webhookReceiver.URL + "/ok", + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + }) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(t.Context(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + + require.NoError(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.Name() == "selfservice.webhook" }) + require.GreaterOrEqual(t, i, 0) + + evs := ended[i].Events() + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookDelivered.String() }) + require.GreaterOrEqual(t, i, 0) + + actualWhID, deliveredTriggerID, deliveredRequestID := getAttributes(evs[i].Attributes) + require.Equal(t, whID.String(), actualWhID) + require.NotEmpty(t, deliveredTriggerID) + require.NotEmpty(t, deliveredRequestID) + assert.NotEqual(t, deliveredTriggerID, deliveredRequestID) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookSucceeded.String() }) + require.GreaterOrEqual(t, i, 0) + + actualWhID, succeededTriggerID, _ := getAttributes(evs[i].Attributes) + require.Equal(t, whID.String(), actualWhID) + require.NotEmpty(t, succeededTriggerID) + + assert.Equal(t, deliveredTriggerID, succeededTriggerID) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookFailed.String() }) + require.Equal(t, -1, i) + }) + + t.Run("failed", func(t *testing.T) { + whID := x.NewUUID() + wh := hook.NewWebHook(whDeps, &request.Config{ + ID: whID.String(), + URL: webhookReceiver.URL + "/fail", + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + }) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(t.Context(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.Name() == "selfservice.webhook" }) + require.GreaterOrEqual(t, i, 0) + + evs := ended[i].Events() + + var deliveredEvents []sdktrace.Event + deliveredTriggerIDs := map[string]struct{}{} + deliveredRequestIDs := map[string]struct{}{} + for _, ev := range evs { + if ev.Name == events.WebhookDelivered.String() { + deliveredEvents = append(deliveredEvents, ev) + actualWhID, triggerID, requestID := getAttributes(ev.Attributes) + require.Equal(t, whID.String(), actualWhID) + require.NotEmpty(t, triggerID) + require.NotEmpty(t, requestID) + deliveredTriggerIDs[triggerID] = struct{}{} + deliveredRequestIDs[requestID] = struct{}{} + } + } + + assert.Len(t, deliveredEvents, 3) + assert.Len(t, deliveredTriggerIDs, 1) + assert.Len(t, deliveredRequestIDs, 3) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == "WebhookFailed" }) + require.GreaterOrEqual(t, i, 0) + + actualWhID, failedTriggerID, _ := getAttributes(evs[i].Attributes) + require.Equal(t, whID.String(), actualWhID) + require.NotEmpty(t, failedTriggerID) + + assert.Contains(t, deliveredTriggerIDs, failedTriggerID) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == "WebhookSucceeded" }) + require.Equal(t, i, -1) + }) + + t.Run("event disabled", func(t *testing.T) { + wh := hook.NewWebHook(whDeps, &request.Config{ + URL: webhookReceiver.URL + "/fail", + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + EmitAnalyticsEvent: new(false), + }) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(t.Context(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { + return sp.Name() == "selfservice.webhook" + }) + require.GreaterOrEqual(t, i, 0) + + evs := ended[i].Events() + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookDelivered" + }) + require.Equal(t, -1, i) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookFailed" + }) + require.Equal(t, -1, i) + + i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookSucceeded" + }) + require.Equal(t, i, -1) + }) +} + +func TestWebhookHTTPClientSpan(t *testing.T) { + t.Parallel() + _, reg := pkg.NewFastRegistryWithMocks(t) + logger := logrusx.New("kratos", "test") + whDeps := newWebHookDeps(t, logger, reg) + + webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + t.Cleanup(webhookReceiver.Close) + + wh := hook.NewWebHook(whDeps, &request.Config{ + ID: x.NewUUID().String(), + URL: webhookReceiver.URL, + Method: "GET", + TemplateURI: "file://stub/test_body.jsonnet", + }) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(t.Context(), "parent") + defer span.End() + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.com", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + + require.NoError(t, wh.ExecuteLoginPreHook(nil, req.Clone(ctx), f)) + + ended := recorder.Ended() + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.Name() == "selfservice.webhook" }) + require.GreaterOrEqual(t, i, 0) + webhookSpan := ended[i] + + spanNames := make([]string, len(ended)) + for i, sp := range ended { + spanNames[i] = sp.Name() + } + + i = slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.SpanKind() == trace.SpanKindClient }) + require.GreaterOrEqual(t, i, 0, "expected the HTTP client span of the webhook request to be ended and recorded, got only %v", spanNames) + clientSpan := ended[i] + + assert.Equal(t, webhookSpan.SpanContext().TraceID(), clientSpan.SpanContext().TraceID(), "the HTTP client span must belong to the same trace as the selfservice.webhook span") + assert.Equal(t, webhookSpan.SpanContext().SpanID(), clientSpan.Parent().SpanID(), "the HTTP client span must be a direct child of the selfservice.webhook span") +} + +func TestRemoveDisallowedHeaders(t *testing.T) { + t.Parallel() + + t.Run("empty http headers", func(t *testing.T) { + headers := http.Header{} + allowList := []string{"Content-Type", "Host"} + newHeaders := hook.RemoveDisallowedHeaders(headers, allowList) + + require.Len(t, newHeaders, 0) + }) + t.Run("empty allow list", func(t *testing.T) { + headers := http.Header{"Accept": {"application/json"}, "Content-Type": {"text/html"}} + allowList := []string{} + newHeaders := hook.RemoveDisallowedHeaders(headers, allowList) + + require.Len(t, newHeaders, 0) + }) + t.Run("all forbidden", func(t *testing.T) { + headers := http.Header{"Accept": {"application/json"}, "Authorization": {"Bearer foo"}} + allowList := []string{"Content-Type", "Host"} + newHeaders := hook.RemoveDisallowedHeaders(headers, allowList) + + require.Len(t, newHeaders, 0) + }) + t.Run("general case", func(t *testing.T) { + headers := http.Header{"Accept": {"application/json"}, "Content-Type": {"text/html"}} + allowList := []string{"Content-Type", "Host"} + newHeaders := hook.RemoveDisallowedHeaders(headers, allowList) + + require.Len(t, newHeaders, 1) + h, present := newHeaders["Content-Type"] + require.True(t, present) + require.Equal(t, []string{"text/html"}, h) + }) +} diff --git a/selfservice/sessiontokenexchange/persistence.go b/selfservice/sessiontokenexchange/persistence.go new file mode 100644 index 0000000..ea8dbbb --- /dev/null +++ b/selfservice/sessiontokenexchange/persistence.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sessiontokenexchange + +import ( + "context" + "time" + + "github.com/gofrs/uuid" +) + +type Codes struct { + InitCode string + ReturnToCode string +} + +type Exchanger struct { + ID uuid.UUID `db:"id"` + NID uuid.UUID `db:"nid"` + FlowID uuid.UUID `db:"flow_id"` + SessionID uuid.NullUUID `db:"session_id"` + + InitCode string `db:"init_code"` + ReturnToCode string `db:"return_to_code"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `db:"updated_at"` +} + +func (Exchanger) TableName() string { return "session_token_exchanges" } + +type ( + Persister interface { + CreateSessionTokenExchanger(ctx context.Context, flowID uuid.UUID) (e *Exchanger, err error) + GetExchangerFromCode(ctx context.Context, initCode string, returnToCode string) (*Exchanger, error) + GetExchangerFromCodeAllowPending(ctx context.Context, initCode string, returnToCode string) (*Exchanger, error) + UpdateSessionOnExchanger(ctx context.Context, flowID uuid.UUID, sessionID uuid.UUID) error + CodeForFlow(ctx context.Context, flowID uuid.UUID) (codes *Codes, found bool, err error) + MoveToNewFlow(ctx context.Context, oldFlow, newFlow uuid.UUID) error + + DeleteExpiredExchangers(context.Context, time.Time, int) error + } + + PersistenceProvider interface { + SessionTokenExchangePersister() Persister + } +) diff --git a/selfservice/sessiontokenexchange/test/persistence.go b/selfservice/sessiontokenexchange/test/persistence.go new file mode 100644 index 0000000..cad4aff --- /dev/null +++ b/selfservice/sessiontokenexchange/test/persistence.go @@ -0,0 +1,207 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/x/randx" +) + +type testParams struct { + flowID, sessionID uuid.UUID + initCode, returnToCode string +} + +func newParams() testParams { + return testParams{ + flowID: uuid.Must(uuid.NewV4()), + sessionID: uuid.Must(uuid.NewV4()), + initCode: randx.MustString(64, randx.AlphaNum), + returnToCode: randx.MustString(64, randx.AlphaNum), + } +} +func (t *testParams) setCodes(e *sessiontokenexchange.Exchanger) { + t.initCode = e.InitCode + t.returnToCode = e.ReturnToCode +} + +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) + + t.Run("suite=create-update-get", func(t *testing.T) { + t.Parallel() + params := newParams() + + t.Run("step=create", func(t *testing.T) { + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + codes, ok, err := p.CodeForFlow(ctx, params.flowID) + assert.True(t, ok) + assert.NoError(t, err) + assert.Equal(t, params.initCode, codes.InitCode) + assert.Equal(t, params.returnToCode, codes.ReturnToCode) + }) + t.Run("step=update", func(t *testing.T) { + require.NoError(t, p.UpdateSessionOnExchanger(ctx, params.flowID, params.sessionID)) + }) + t.Run("step=get", func(t *testing.T) { + e, err := p.GetExchangerFromCode(ctx, params.initCode, params.returnToCode) + require.NoError(t, err) + + assert.Equal(t, params.sessionID, e.SessionID.UUID) + assert.Equal(t, nid, e.NID) + }) + }) + + t.Run("suite=CodeForFlow", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns false for non-existing flow", func(t *testing.T) { + t.Parallel() + _, ok, err := p.CodeForFlow(ctx, uuid.Must(uuid.NewV4())) + assert.False(t, ok) + assert.NoError(t, err) + }) + }) + + t.Run("suite=MoveToNewFlow", func(t *testing.T) { + t.Parallel() + + t.Run("case=move to new flow", func(t *testing.T) { + params := newParams() + other := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + require.NoError(t, p.MoveToNewFlow(ctx, params.flowID, other.flowID)) + require.NoError(t, p.UpdateSessionOnExchanger(ctx, other.flowID, params.sessionID)) + + e, err = p.GetExchangerFromCode(ctx, params.initCode, params.returnToCode) + require.NoError(t, err) + assert.Equal(t, params.sessionID, e.SessionID.UUID) + }) + }) + + t.Run("suite=GetExchangerFromCodeAllowPending", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns exchanger without session", func(t *testing.T) { + t.Parallel() + params := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + + e, err = p.GetExchangerFromCodeAllowPending(ctx, params.initCode, params.returnToCode) + require.NoError(t, err) + assert.Equal(t, params.flowID, e.FlowID) + assert.False(t, e.SessionID.Valid) + }) + + t.Run("case=returns exchanger with session", func(t *testing.T) { + t.Parallel() + params := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + require.NoError(t, p.UpdateSessionOnExchanger(ctx, params.flowID, params.sessionID)) + + e, err = p.GetExchangerFromCodeAllowPending(ctx, params.initCode, params.returnToCode) + require.NoError(t, err) + assert.Equal(t, params.flowID, e.FlowID) + assert.Equal(t, params.sessionID, e.SessionID.UUID) + }) + + t.Run("case=errors if code is invalid", func(t *testing.T) { + t.Parallel() + other := newParams() + + e, err := p.GetExchangerFromCodeAllowPending(ctx, other.initCode, other.returnToCode) + assert.Error(t, err) + assert.Nil(t, e) + }) + }) + + t.Run("suite=GetExchangerFromCode", func(t *testing.T) { + t.Parallel() + + t.Run("case=errors if session not found", func(t *testing.T) { + t.Parallel() + params := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + + e, err = p.GetExchangerFromCode(ctx, params.initCode, params.returnToCode) + + assert.Error(t, err) + assert.Nil(t, e) + }) + + t.Run("case=errors if code is invalid", func(t *testing.T) { + t.Parallel() + params := newParams() + other := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + + require.NoError(t, p.UpdateSessionOnExchanger(ctx, params.flowID, params.sessionID)) + e, err = p.GetExchangerFromCode(ctx, other.initCode, other.returnToCode) + + assert.Error(t, err) + assert.Nil(t, e) + }) + + t.Run("case=errors if code is empty", func(t *testing.T) { + t.Parallel() + params := newParams() + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + + require.NoError(t, p.UpdateSessionOnExchanger(ctx, params.flowID, params.sessionID)) + e, err = p.GetExchangerFromCode(ctx, "", "") + + assert.Error(t, err) + assert.Nil(t, e) + }) + + t.Run("case=errors if other network ID", func(t *testing.T) { + t.Parallel() + params := newParams() + otherNID := uuid.Must(uuid.NewV4()) + + e, err := p.CreateSessionTokenExchanger(ctx, params.flowID) + require.NoError(t, err) + params.setCodes(e) + + require.NoError(t, p.UpdateSessionOnExchanger(ctx, params.flowID, params.sessionID)) + e, err = p.WithNetworkID(otherNID).GetExchangerFromCode(ctx, params.initCode, params.returnToCode) + + assert.Error(t, err) + assert.Nil(t, e) + }) + }) + } +} diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json new file mode 100644 index 0000000..22286b0 --- /dev/null +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -0,0 +1,36 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "code": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "address": { + "type": "string" + }, + "resend": { + "type": "string", + "enum": [ + "code" + ] + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "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 new file mode 100644 index 0000000..f9842a5 --- /dev/null +++ b/selfservice/strategy/code/.schema/recovery.schema.json @@ -0,0 +1,41 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/recovery.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "code": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email_loose", + "maxLength": 320 + }, + "recovery_address": { + "type": "string" + }, + "recovery_select_address": { + "type": "string" + }, + "recovery_confirm_address": { + "type": "string" + }, + "screen": { + "type": "string" + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "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 new file mode 100644 index 0000000..0cd5282 --- /dev/null +++ b/selfservice/strategy/code/.schema/registration.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "csrf_token": { + "type": "string" + }, + "code": { + "type": "string" + }, + "resend": { + "type": "string", + "enum": [ + "code" + ] + }, + "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." + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/code/.schema/verification.schema.json b/selfservice/strategy/code/.schema/verification.schema.json new file mode 100644 index 0000000..14e390d --- /dev/null +++ b/selfservice/strategy/code/.schema/verification.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/verification.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code", + "link" + ] + }, + "code": { + "type": "string" + }, + "email": { + "type": "string", + "maxLength": 320 + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "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 new file mode 100644 index 0000000..2c06a21 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -0,0 +1,43 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_malformed_expiry_time.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_malformed_expiry_time.json new file mode 100644 index 0000000..e012008 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_malformed_expiry_time.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": 400, + "message": "The request was malformed or contained invalid parameters", + "reason": "Unable to parse \"expires_in\" as a duration string (e.g. \"1h\", \"1m30s\", \"500ms\"): not-a-valid-value", + "status": "Bad Request" + } +} 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 new file mode 100644 index 0000000..e9751ee --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": 400, + "message": "The request was malformed or contained invalid parameters", + "reason": "Value from \"expires_in\" must result to a future time: -1h0m0s", + "status": "Bad Request" + } +} diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_not_be_able_to_recover_an_account_that_does_not_exist.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_not_be_able_to_recover_an_account_that_does_not_exist.json new file mode 100644 index 0000000..6d8a900 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_not_be_able_to_recover_an_account_that_does_not_exist.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": 404, + "message": "Unable to locate the resource", + "reason": "could not find identity", + "status": "Not Found" + } +} 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 0000000..19765bd --- /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 0000000..8e9874d --- /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 0000000..19765bd --- /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 0000000..3c43006 --- /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,38 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "refresh-1fa-passwordless@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to refresh-1fa-passwordless@ory.sh", + "type": "info", + "context": { + "address": "refresh-1fa-passwordless@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=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 0000000..19765bd --- /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 0000000..66b84bf --- /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 0000000..19765bd --- /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 0000000..66b84bf --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..66b84bf --- /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 0000000..19765bd --- /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 0000000..8e9874d --- /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 0000000..19765bd --- /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 0000000..93389d2 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa_and_request_is_2fa.json @@ -0,0 +1,38 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "foo@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to foo@ory.sh", + "type": "info", + "context": { + "address": "foo@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-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 0000000..19765bd --- /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 0000000..0680486 --- /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 0000000..19765bd --- /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 0000000..d050a31 --- /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 0000000..19765bd --- /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 0000000..3747efb --- /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,38 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "fooaewdk@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to fooaewdk@ory.sh", + "type": "info", + "context": { + "address": "fooaewdk@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=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 0000000..19765bd --- /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_with_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login.json new file mode 100644 index 0000000..3e2a616 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_test_fast_browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_fast_browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_fast_browser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json new file mode 100644 index 0000000..50fcd0a --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_without_ccbrowser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_without_ccbrowser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_without_ccbrowser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json new file mode 100644 index 0000000..8f1cfee --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_with_ccbrowser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_with_ccbrowser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_with_ccbrowser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_without_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_without_fast_login.json new file mode 100644 index 0000000..0b814f2 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload_without_fast_login.json @@ -0,0 +1,94 @@ +{ + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "choose_method", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010004, + "text": "Please complete the second authentication challenge.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "fixed_mfa_test_browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_browser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "totp_code", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "totp" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "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-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 0000000..1d8d815 --- /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 0000000..1d8d815 --- /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 0000000..1d8d815 --- /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_with_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login.json new file mode 100644 index 0000000..e2d0da8 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login.json @@ -0,0 +1,114 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "api", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_test_fast_api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_fast_api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_fast_api@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json new file mode 100644 index 0000000..41b4920 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json @@ -0,0 +1,114 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "api", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_without_ccapi@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_without_ccapi@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_without_ccapi@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json new file mode 100644 index 0000000..66cf816 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json @@ -0,0 +1,114 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "api", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_with_ccapi@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_with_ccapi@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_with_ccapi@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_without_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_without_fast_login.json new file mode 100644 index 0000000..008b188 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload_without_fast_login.json @@ -0,0 +1,94 @@ +{ + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "choose_method", + "type": "api", + "ui": { + "messages": [ + { + "id": 1010004, + "text": "Please complete the second authentication challenge.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "fixed_mfa_test_api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_api@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "totp_code", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "totp" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "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-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 0000000..e3510d1 --- /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 0000000..e3510d1 --- /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 0000000..e3510d1 --- /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_with_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login.json new file mode 100644 index 0000000..42fcd16 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_test_fast_spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_fast_spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_fast_spa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json new file mode 100644 index 0000000..133815e --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=no_code_credential.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_without_ccspa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_without_ccspa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_without_ccspa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json new file mode 100644 index 0000000..d8fa3c8 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_with_fast_login_and_fallback_enabled-case=with_code_credential.json @@ -0,0 +1,127 @@ +{ + "active": "code", + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "sent_email", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010025, + "text": "A code was sent to your address. If you didn't receive it, please try again.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "fixed_mfa_fallback_with_ccspa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_fallback_with_ccspa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_fallback_with_ccspa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "hidden", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "one-time-code", + "disabled": false, + "name": "code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070013, + "text": "Login code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "resend", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } + ] + } +} diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_without_fast_login.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_without_fast_login.json new file mode 100644 index 0000000..2b77794 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload_without_fast_login.json @@ -0,0 +1,94 @@ +{ + "organization_id": null, + "refresh": false, + "requested_aal": "aal2", + "state": "choose_method", + "type": "browser", + "ui": { + "messages": [ + { + "id": 1010004, + "text": "Please complete the second authentication challenge.", + "type": "info" + } + ], + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "fixed_mfa_test_spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "fixed_mfa_test_spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_spa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "totp_code", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "totp" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "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-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 0000000..721c86a --- /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 0000000..721c86a --- /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 0000000..721c86a --- /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/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..b2ff094 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up 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/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..b2ff094 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up 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/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.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/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.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/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..f4dbe2c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.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": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.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/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..f4dbe2c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.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": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up code", + "type": "info" + } + } + } +] 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 new file mode 100644 index 0000000..195ca69 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..fd7bdfb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -0,0 +1,88 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "email", + "type": "submit", + "value": "test@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json new file mode 100644 index 0000000..058c2eb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address (Email, phone number, etc)", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json new file mode 100644 index 0000000..058c2eb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address (Email, phone number, etc)", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json new file mode 100644 index 0000000..058c2eb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address (Email, phone number, etc)", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json new file mode 100644 index 0000000..c9b414b --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -0,0 +1,35 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "email", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json new file mode 100644 index 0000000..c9b414b --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -0,0 +1,35 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "email", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json new file mode 100644 index 0000000..c9b414b --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -0,0 +1,35 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "email", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=api.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=browser.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=spa.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json new file mode 100644 index 0000000..b240a45 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "test-api@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "test-api@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json new file mode 100644 index 0000000..aa514a3 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "test-browser@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "test-browser@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json new file mode 100644 index 0000000..f6000f1 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Email-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "test-spa@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "test-spa@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=api.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=browser.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=spa.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json new file mode 100644 index 0000000..177ab9a --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "+491705550177", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550177", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json new file mode 100644 index 0000000..ab47f92 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "+491705550176", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550176", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json new file mode 100644 index 0000000..0e6df0c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_OneAddress_Phone-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -0,0 +1,120 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_confirm_address", + "type": "submit", + "value": "+491705550178", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550178", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1060007, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=api.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=browser.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=spa.json new file mode 100644 index 0000000..b922e4d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "recovery_address", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070016, + "text": "Recovery address", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json new file mode 100644 index 0000000..47f6209 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -0,0 +1,79 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "MEIbSbZvtBRV9TH7wdFTF5CGyhxaaM2zuLDSIELCIAI=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "+49****67", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "BgkSG7BOfSWi+9/IRUclTboO0eGGkgnBFQiBv/v2Jw4=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "te****@ory.sh", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550167", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json new file mode 100644 index 0000000..97b607d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -0,0 +1,79 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "52McLavwKA6+jlSuOBVOPvkoLn8r1fUhZDMj2eytcac=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "+49****66", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "9lRwDY12jB69oE1+tGQXV2XFJbgBtjjRwBjH+iHmHjA=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "te****@ory.sh", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550166", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json new file mode 100644 index 0000000..a60e619 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_V2_WithContinueWith_SeveralAddresses-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -0,0 +1,79 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "zx+NolDxaddHeLJ05eafzEcdjhJ0F8h5MAPtmbBSZkE=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "+49****68", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_select_address", + "type": "submit", + "value": "pti/PkfHLV3dkx+jIA+yDc8KCLmQ/K872SvKfFQ7C/c=", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070000, + "text": "te****@ory.sh", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "recovery_address", + "type": "hidden", + "value": "+491705550168", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] 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 new file mode 100644 index 0000000..195ca69 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..195ca69 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..195ca69 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..fd7bdfb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -0,0 +1,88 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "email", + "type": "submit", + "value": "test@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "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 new file mode 100644 index 0000000..fd7bdfb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -0,0 +1,88 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "email", + "type": "submit", + "value": "test@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "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 new file mode 100644 index 0000000..fd7bdfb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -0,0 +1,88 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "pattern": "[0-9]+", + "disabled": false, + "maxlength": 6, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070010, + "text": "Recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "email", + "type": "submit", + "value": "test@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "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 new file mode 100644 index 0000000..05fcb19 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070018, + "text": "Email or phone number", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "code" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] 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 new file mode 100644 index 0000000..8d3f030 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -0,0 +1,86 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "required": true, + "autocomplete": "one-time-code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070011, + "text": "Verification code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "email", + "type": "submit", + "value": "test@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070008, + "text": "Resend code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go new file mode 100644 index 0000000..aa2b747 --- /dev/null +++ b/selfservice/strategy/code/code_login.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/pkg/errors" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" + "github.com/ory/x/clock" + "github.com/ory/x/region" +) + +// swagger:ignore +type LoginCode struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeChannel `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_login_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (LoginCode) TableName(ctx context.Context) string { + return "identity_login_codes" +} + +func (c *LoginCode) Validate(clk clock.Clock) error { + if c == nil { + return errors.WithStack(ErrCodeNotFound()) + } + return validateOneTimeCode(c.ExpiresAt, c.UsedAt, clk) +} + +func (c *LoginCode) GetHMACCode() string { + return c.CodeHMAC +} + +func (c *LoginCode) GetID() uuid.UUID { + return c.ID +} + +// swagger:ignore +type CreateLoginCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeChannel + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID + + // IdentityID is the identity that this code is for + // required: true + IdentityID uuid.UUID + + // IdentityRegion optionally carries the already-known region of the + // identity the code is for, so persisters can use it without re-reading + // the identity row. + IdentityRegion region.Region `json:"-" db:"-" faker:"-"` + + // InsertExtraColumns appends fixed (key, value) columns to the INSERT + // statement (e.g. crdb_region). + InsertExtraColumns []identity.ExtraColumn `json:"-" db:"-" faker:"-"` +} diff --git a/selfservice/strategy/code/code_login_test.go b/selfservice/strategy/code/code_login_test.go new file mode 100644 index 0000000..8503262 --- /dev/null +++ b/selfservice/strategy/code/code_login_test.go @@ -0,0 +1,86 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestLoginCode(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + newCode := func(expiresIn time.Duration, f *login.Flow) *code.LoginCode { + return &code.LoginCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if code is expired", func(t *testing.T) { + f, err := login.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(-time.Hour, f) + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := login.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(time.Hour) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := login.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeAlreadyUsed()) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := login.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.LoginCode + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + }) +} diff --git a/selfservice/strategy/code/code_recovery.go b/selfservice/strategy/code/code_recovery.go new file mode 100644 index 0000000..63de19b --- /dev/null +++ b/selfservice/strategy/code/code_recovery.go @@ -0,0 +1,132 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/pkg/errors" + + "github.com/gofrs/uuid" + + "github.com/ory/herodot" + + "github.com/ory/kratos/identity" + "github.com/ory/x/clock" +) + +type RecoveryCodeType int + +const ( + RecoveryCodeTypeAdmin RecoveryCodeType = iota + 1 + RecoveryCodeTypeSelfService +) + +func ErrCodeNotFound() *herodot.DefaultError { + return herodot.ErrNotFound().WithReasonf("unknown code") +} + +func ErrCodeAlreadyUsed() *herodot.DefaultError { + return herodot.ErrBadRequest().WithReasonf("The code was already used. Please request another code.") +} + +func ErrCodeSubmittedTooOften() *herodot.DefaultError { + return herodot.ErrBadRequest().WithReasonf("The request was submitted too often. Please request another code.") +} + +// validateOneTimeCode reports whether a one-time code is still usable. Every +// one-time code type (login, registration, recovery, verification) shares this +// logic: an expired code is reported as not found, and an already-consumed code +// is reported as already used. +func validateOneTimeCode(expiresAt time.Time, usedAt sql.NullTime, clk clock.Clock) error { + if expiresAt.Before(clk.Now().UTC()) { + return errors.WithStack(ErrCodeNotFound()) + } + if usedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed()) + } + return nil +} + +type RecoveryCode struct { + // ID represents the code's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // CodeHMAC represents the HMACed value of the recovery code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // RecoveryAddress links this code to a recovery address. + // required: true + RecoveryAddress *identity.RecoveryAddress `json:"recovery_address" belongs_to:"identity_recovery_addresses" fk_id:"RecoveryAddressID"` + + // CodeType is the type of the code - either "admin" or "selfservice" + CodeType RecoveryCodeType `json:"-" faker:"-" db:"code_type"` + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the code was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + // RecoveryAddressID is a helper struct field for gobuffalo.pop. + RecoveryAddressID uuid.NullUUID `json:"-" faker:"-" db:"identity_recovery_address_id"` + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_recovery_flow_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (RecoveryCode) TableName(ctx context.Context) string { + return "identity_recovery_codes" +} + +func (f *RecoveryCode) Validate(clk clock.Clock) error { + if f == nil { + return errors.WithStack(ErrCodeNotFound()) + } + return validateOneTimeCode(f.ExpiresAt, f.UsedAt, clk) +} + +func (c *RecoveryCode) GetHMACCode() string { + return c.CodeHMAC +} + +func (c *RecoveryCode) GetID() uuid.UUID { + return c.ID +} + +type CreateRecoveryCodeParams struct { + // Code represents the recovery code + RawCode string + + // CodeType is the type of the code - either "admin" or "selfservice" + CodeType RecoveryCodeType + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // RecoveryAddressID is a helper struct field for gobuffalo.pop. + RecoveryAddress *identity.RecoveryAddress + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID + + IdentityID uuid.UUID +} diff --git a/selfservice/strategy/code/code_recovery_test.go b/selfservice/strategy/code/code_recovery_test.go new file mode 100644 index 0000000..754cdce --- /dev/null +++ b/selfservice/strategy/code/code_recovery_test.go @@ -0,0 +1,95 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "errors" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestRecoveryCode(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + newCode := func(expiresIn time.Duration, f *recovery.Flow) *code.RecoveryCode { + return &code.RecoveryCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns ErrCodeNotFound if code is expired", func(t *testing.T) { + f, err := recovery.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + t.Run("case=expired code does not return flow.ExpiredError", func(t *testing.T) { + f, err := recovery.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expired := new(flow.ExpiredError) + require.False(t, errors.As(c.Validate(reg.Clock()), &expired), "expired code should not return flow.ExpiredError") + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := recovery.NewFlow(reg, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := recovery.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeAlreadyUsed()) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := recovery.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RecoveryCode + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + }) +} + +func TestRecoveryCodeType(t *testing.T) { + assert.Equal(t, 1, int(code.RecoveryCodeTypeAdmin)) + assert.Equal(t, 2, int(code.RecoveryCodeTypeSelfService)) +} diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go new file mode 100644 index 0000000..5758e8b --- /dev/null +++ b/selfservice/strategy/code/code_registration.go @@ -0,0 +1,102 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/pkg/errors" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" + "github.com/ory/x/clock" +) + +// swagger:ignore +type RegistrationCode struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeChannel `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_registration_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (RegistrationCode) TableName(context.Context) string { + return "identity_registration_codes" +} + +func (c *RegistrationCode) Validate(clk clock.Clock) error { + if c == nil { + return errors.WithStack(ErrCodeNotFound()) + } + return validateOneTimeCode(c.ExpiresAt, c.UsedAt, clk) +} + +func (c *RegistrationCode) GetHMACCode() string { + return c.CodeHMAC +} + +func (c *RegistrationCode) GetID() uuid.UUID { + return c.ID +} + +// swagger:ignore +type CreateRegistrationCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeChannel + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID +} diff --git a/selfservice/strategy/code/code_registration_test.go b/selfservice/strategy/code/code_registration_test.go new file mode 100644 index 0000000..0e3b74e --- /dev/null +++ b/selfservice/strategy/code/code_registration_test.go @@ -0,0 +1,83 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestRegistrationCode(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + newCode := func(expiresIn time.Duration, f *registration.Flow) *code.RegistrationCode { + return &code.RegistrationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if code is expired", func(t *testing.T) { + f, err := registration.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(-time.Hour, f) + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := registration.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(time.Hour) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := registration.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeAlreadyUsed()) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := registration.NewFlow(reg, req, flow.TypeBrowser) + require.NoError(t, err) + f.ExpiresAt = f.IssuedAt.Add(-time.Hour) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RegistrationCode + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + }) +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go new file mode 100644 index 0000000..bbb0e9f --- /dev/null +++ b/selfservice/strategy/code/code_sender.go @@ -0,0 +1,539 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" +) + +type ( + senderDependencies interface { + courier.Provider + courier.ConfigProvider + + identity.PoolProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + logrusx.Provider + config.Provider + + RecoveryCodePersistenceProvider + VerificationCodePersistenceProvider + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider + + hydra.Provider + httpx.ClientProvider + } + SenderProvider interface { + CodeSender() *Sender + } + + Sender struct { + deps senderDependencies + } + Address struct { + To string + Via identity.CodeChannel + } +) + +func ErrUnknownAddress() *herodot.DefaultError { + return herodot.ErrNotFound().WithReason("recovery requested for unknown address") +} + +func NewSender(deps senderDependencies) *Sender { + return &Sender{deps: deps} +} + +func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identity, header http.Header, addresses ...Address) error { + s.deps.Logger(). + WithSensitiveField("address", addresses). + Debugf("Preparing %s code", f.GetFlowName()) + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + // If the flow was initiated by an OAuth2 login challenge, expose a scoped + // view of the OAuth2 client to the courier templates so that messages can + // be branded per client. + var oauth2LoginRequest *template.OAuth2LoginRequest + if p, ok := f.(flow.HydraLoginRequestProvider); ok { + oauth2LoginRequest = template.NewOAuth2LoginRequest(p.GetHydraLoginRequest()) + } + + // 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 + // address was used to verify the code. + // + // See also [this discussion](https://github.com/ory/kratos/pull/3456#discussion_r1307560988). + rawCode := GenerateCode() + + switch f.GetFlowName() { + case flow.RegistrationFlow: + code, err := s.deps. + RegistrationCodePersister(). + CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ + AddressType: address.Via, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + Address: address.To, + }) + if err != nil { + return err + } + model, err := x.StructToMap(id.Traits) + if err != nil { + return err + } + + s.deps.Logger(). + WithField("registration_flow_id", code.FlowID). + WithField("registration_code_id", code.ID). + WithSensitiveField("registration_code", rawCode). + Info("Sending out registration email with code.") + + var t courier.Template + switch address.Via { + case identity.ChannelTypeEmail: + t = email.NewRegistrationCodeValid(s.deps, &email.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + case identity.ChannelTypeSMS: + t = sms.NewRegistrationCodeValid(s.deps, &sms.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + RequestURLDomain: requestURLDomain(f.GetRequestURL()), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + } + + if err := s.send(ctx, string(address.Via), t); err != nil { + return errors.WithStack(err) + } + + case flow.LoginFlow: + code, err := s.deps. + LoginCodePersister(). + CreateLoginCode(ctx, &CreateLoginCodeParams{ + AddressType: address.Via, + Address: address.To, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + IdentityID: id.ID, + IdentityRegion: id.Region, + }) + if err != nil { + return err + } + + model, err := x.StructToMap(id) + if err != nil { + return err + } + s.deps.Logger(). + WithField("login_flow_id", code.FlowID). + WithField("login_code_id", code.ID). + WithSensitiveField("login_code", rawCode). + Info("Sending out login email with code.") + + var t courier.Template + switch address.Via { + case identity.ChannelTypeEmail: + t = email.NewLoginCodeValid(s.deps, &email.LoginCodeValidModel{ + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + case identity.ChannelTypeSMS: + t = sms.NewLoginCodeValid(s.deps, &sms.LoginCodeValidModel{ + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + RequestURLDomain: requestURLDomain(f.GetRequestURL()), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + } + + if err := s.send(ctx, string(address.Via), t); err != nil { + return errors.WithStack(err) + } + + default: + return errors.WithStack(errors.New("received unknown flow type")) + + } + } + return nil +} + +// SendRecoveryCode sends a recovery code to the specified address +// +// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is +// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the +// ErrUnknownAddress error. +func (s *Sender) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via, to string, requestHeader http.Header) error { + s.deps.Logger(). + WithField("via", via). + WithSensitiveField("address", to). + Debug("Preparing recovery code.") + + address, err := s.deps.IdentityPool().FindRecoveryAddressByValue(ctx, via, to) + if errors.Is(err, sqlcon.ErrNoRows()) { + notifyUnknownRecipients := s.deps.Config().SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx) + s.deps.Logger(). + WithField("via", via). + WithSensitiveField("address", to). + 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) + } + + // We only send a notification if the configuration allows it *and* the channel is email. + // That's because we pay per SMS sent (typically) so we want to avoid that, contrary to email. + shouldNotifyOfUnkownRecipient := notifyUnknownRecipients && via == identity.AddressTypeEmail + + if !shouldNotifyOfUnkownRecipient { + // do nothing + } else if err := s.send(ctx, via, email.NewRecoveryCodeInvalid(s.deps, &email.RecoveryCodeInvalidModel{ + To: to, + RequestURL: f.RequestURL, + TransientPayload: transientPayload, + })); err != nil { + return err + } + return errors.WithStack(ErrUnknownAddress()) + } else if err != nil { + // DB error + return err + } + + // Get the identity associated with the recovery address + i, err := s.deps.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault) + if err != nil { + return err + } + + rawCode := GenerateCode() + + var code *RecoveryCode + if code, err = s.deps. + RecoveryCodePersister(). + CreateRecoveryCode(ctx, &CreateRecoveryCodeParams{ + RawCode: rawCode, + CodeType: RecoveryCodeTypeSelfService, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + RecoveryAddress: address, + FlowID: f.ID, + IdentityID: i.ID, + }); err != nil { + return err + } + + return s.SendRecoveryCodeTo(ctx, i, rawCode, code, f, requestHeader) +} + +func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f *recovery.Flow, requestHeader http.Header) error { + s.deps.Logger(). + WithField("via", code.RecoveryAddress.Via). + WithField("identity_id", code.RecoveryAddress.IdentityID). + WithField("recovery_code_id", code.ID). + WithSensitiveField("address", code.RecoveryAddress.Value). + WithSensitiveField("recovery_code", codeString). + Info("Sending out recovery code.") + + model, err := x.StructToMap(i) + if err != nil { + return err + } + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + var t courier.Template + + switch code.RecoveryAddress.Via { + case identity.AddressTypeEmail: + t = email.NewRecoveryCodeValid(s.deps, &email.RecoveryCodeValidModel{ + To: code.RecoveryAddress.Value, + RecoveryCode: codeString, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + UserRequestHeaders: hook.RemoveDisallowedHeaders(requestHeader, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + case identity.AddressTypeSMS: + u, err := url.Parse(f.GetRequestURL()) + if err != nil { + return err + } + + t = sms.NewRecoveryCodeValid(s.deps, &sms.RecoveryCodeValidModel{ + To: code.RecoveryAddress.Value, + RecoveryCode: codeString, + Identity: model, + RequestURL: f.GetRequestURL(), + RequestURLDomain: u.Hostname(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + UserRequestHeaders: hook.RemoveDisallowedHeaders(requestHeader, s.deps.Config().WebhookHeaderAllowlist(ctx)), + }) + default: + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email or sms but got %s", code.RecoveryAddress.Via)) + } + + return s.send(ctx, code.RecoveryAddress.Via, t) +} + +// SendVerificationCode sends a verification code & link to the specified address +// +// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is +// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the +// ErrUnknownAddress error. +func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, via string, to string) error { + s.deps.Logger(). + WithField("via", via). + WithSensitiveField("address", to). + Debug("Preparing verification code.") + + address, err := s.deps.IdentityPool().FindVerifiableAddressByValue(ctx, via, to) + if errors.Is(err, sqlcon.ErrNoRows()) { + notifyUnknownRecipients := s.deps.Config().SelfServiceFlowVerificationNotifyUnknownRecipients(ctx) + s.deps.Logger(). + WithField("via", via). + WithField("strategy", "code"). + 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 || via != identity.AddressTypeEmail { + // Only send unknown-address notifications via email. SMS costs money per + // message, so we skip notification for unknown phone numbers. + } else if err := s.send(ctx, via, email.NewVerificationCodeInvalid(s.deps, &email.VerificationCodeInvalidModel{ + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + })); err != nil { + return err + } + return errors.WithStack(ErrUnknownAddress()) + + } else if err != nil { + return err + } + + rawCode := GenerateCode() + if _, err = s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + VerifiableAddress: address, + FlowID: f.ID, + }); err != nil { + return err + } + + // Get the identity associated with the recovery address + i, err := s.deps.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault) + if err != nil { + return err + } + + if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, address); err != nil { + return err + } + + address.Status = identity.VerifiableAddressStatusSent + return s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status") +} + +func (s *Sender) constructVerificationLink(ctx context.Context, f *verification.Flow, codeStr string) string { + return urlx.CopyWithQuery( + urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.deps.Config().SelfPublicURL(ctx)), verification.RouteSubmitFlow), + url.Values{ + "flow": {f.ID.String()}, + "code": {codeStr}, + }).String() +} + +func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flow, i *identity.Identity, codeString string, address identity.VerifiableAddressLike) error { + to, via := address.Address(), address.DeliveryVia() + s.deps.Logger(). + WithField("via", via). + WithField("identity_id", i.ID). + WithSensitiveField("email_address", to). + WithSensitiveField("verification_link_token", codeString). + Info("Sending out verification email with verification code.") + + model, err := x.StructToMap(i) + if err != nil { + return err + } + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + // If the flow was initiated by an OAuth2 login challenge, expose a scoped + // view of the OAuth2 client to the courier templates so that messages can + // be branded per client. + if f.OAuth2LoginChallenge != "" && f.HydraLoginRequest == nil { + hlr, err := s.deps.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge)) + if err != nil { + return errors.WithStack(err) + } + f.HydraLoginRequest = hlr + } + oauth2LoginRequest := template.NewOAuth2LoginRequest(f.HydraLoginRequest) + + var t courier.Template + + // TODO: this can likely be abstracted by making templates not specific to the channel they're using + switch via { + case identity.ChannelTypeEmail: + t = email.NewVerificationCodeValid(s.deps, &email.VerificationCodeValidModel{ + To: to, + VerificationURL: s.constructVerificationLink(ctx, f, codeString), + Identity: model, + VerificationCode: codeString, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + }) + case identity.ChannelTypeSMS: + t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{ + To: to, + VerificationCode: codeString, + Identity: model, + RequestURL: f.GetRequestURL(), + RequestURLDomain: requestURLDomain(f.GetRequestURL()), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: oauth2LoginRequest, + }) + default: + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email or sms but got %s", via)) + } + + return s.send(ctx, via, t) +} + +func (s *Sender) send(ctx context.Context, via string, t courier.Template) error { + switch f := stringsx.SwitchExact(via); { + case f.AddCase(identity.ChannelTypeEmail): + c, err := s.deps.Courier(ctx) + if err != nil { + return err + } + + t, ok := t.(courier.EmailTemplate) + if !ok { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email template but got %T", t)) + } + + _, err = c.QueueEmail(ctx, t) + return err + case f.AddCase(identity.ChannelTypeSMS): + c, err := s.deps.Courier(ctx) + if err != nil { + return err + } + + t, ok := t.(courier.SMSTemplate) + if !ok { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected sms template but got %T", t)) + } + + _, err = c.QueueSMS(ctx, t) + return err + default: + return f.ToUnknownCaseErr() + } +} + +// requestURLDomain returns the bare hostname of the flow's request URL. It is +// used to bind the SMS one-time code to its origin for the Web OTP API. The +// result is a best-effort hint: a malformed URL yields an empty string and must +// not block delivery of the code. +func requestURLDomain(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return "" + } + return u.Hostname() +} + +// hackyInferChannel infers the channel (email or sms) based on the address format. +// Once we support arbitrary custom channels, we need to refactor the recovery/verification +// flow to first look up the address, and let the user select the channel if multiple are available. +func hackyInferChannel(addr string) string { + // Inferring the address type like this is a bit hacky, and actually not really necessary. + // That's because `SendRecoveryCode` expects it, but not because it fundamentally is required. + if strings.ContainsRune(addr, '@') { + return identity.AddressTypeEmail + } + return identity.AddressTypeSMS +} diff --git a/selfservice/strategy/code/code_sender_test.go b/selfservice/strategy/code/code_sender_test.go new file mode 100644 index 0000000..d2fc928 --- /dev/null +++ b/selfservice/strategy/code/code_sender_test.go @@ -0,0 +1,433 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/x/configx" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +var b64 = func(str string) string { + return base64.StdEncoding.EncodeToString([]byte(str)) +} + +func TestSender(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/default.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeyPublicBaseURL: "https://www.ory.com/", + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients: true, + config.ViperKeySelfServiceVerificationNotifyUnknownRecipients: true, + }), + ) + + conf.MustSet(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{"user-agent", "X-CUSTOM-HEADER"}) + + u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + + // Fake phone numbers which are unallocated numbers that telephone companies cannot assign + // to customers in Germany under current legislation. + // This is to protect residents against the potential influx of phone calls + // that they may receive should their telephone numbers appear in a movie or film. + phoneNumberKnown := x.NormalizePhoneIdentifier("+49-160-555-5762") + phoneNumberUnknown := x.NormalizePhoneIdentifier("+49-155-555-4570") + i.Traits = identity.Traits(fmt.Sprintf(`{"email": "tracked@ory.sh", "phone": "%s"}`, phoneNumberKnown)) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + t.Run("method=SendRecoveryCode email", func(t *testing.T) { + recoveryCode := func(t *testing.T) { + t.Helper() + f, err := recovery.NewFlow(reg, time.Hour, "", u, recovery.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + header := http.Header{} + header.Add("user-agent", "user-agent header") + header.Add("X-CUSTOM-HEADER", "x-custom header 1") + header.Add("X-Custom-header", "x-custom header 2") + header.Add("some-other-header", "some-other-value") + require.NoError(t, reg.CodeSender().SendRecoveryCode(ctx, f, "email", "tracked@ory.sh", header)) + require.ErrorIs(t, reg.CodeSender().SendRecoveryCode(ctx, f, "email", "not-tracked@ory.sh", header), code.ErrUnknownAddress()) + } + + t.Run("case=with default templates", func(t *testing.T) { + recoveryCode(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Contains(t, messages[0].Subject, "Use code") + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Contains(t, messages[1].Subject, "Account access attempted") + + assert.NotRegexp(t, testhelpers.CodeRegex, messages[1].Body, "Expected message to not contain an 6 digit recovery code, but it did: ", messages[1].Body) + + headers := messages[0].RequestHeaders + assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value()) + assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value()) + assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty") + }) + + t.Run("case=with custom templates", func(t *testing.T) { + subject := "custom template recovery code" + body := "custom template recovery code body" + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeInvalidEmail, nil) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidEmail, nil) + }) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeInvalidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" invalid"), b64(body), b64(body))) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" valid"), b64(body+" {{ .RecoveryCode }}"), b64(body+" {{ .RecoveryCode }}"))) + recoveryCode(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Equal(t, messages[0].Subject, subject+" valid") + assert.Contains(t, messages[0].Body, body) + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Equal(t, messages[1].Subject, subject+" invalid") + assert.Equal(t, messages[1].Body, body) + + headers := messages[0].RequestHeaders + assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value()) + assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value()) + assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty") + }) + }) + + t.Run("method=SendRecoveryCode sms", func(t *testing.T) { + recoveryCode := func(t *testing.T) { + t.Helper() + f, err := recovery.NewFlow(reg, time.Hour, "", u, recovery.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + header := http.Header{} + header.Add("user-agent", "user-agent header") + header.Add("X-CUSTOM-HEADER", "x-custom header 1") + header.Add("X-Custom-header", "x-custom header 2") + header.Add("some-other-header", "some-other-value") + require.NoError(t, reg.CodeSender().SendRecoveryCode(ctx, f, "sms", phoneNumberKnown, header)) + require.ErrorIs(t, reg.CodeSender().SendRecoveryCode(ctx, f, "sms", phoneNumberUnknown, header), code.ErrUnknownAddress()) + } + + t.Run("case=with default templates", func(t *testing.T) { + recoveryCode(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + + assert.EqualValues(t, phoneNumberKnown, messages[0].Recipient) + assert.Contains(t, messages[0].Body, "Your recovery code is:") + assert.Contains(t, messages[0].Body, "@www.ory.com #") + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + }) + + t.Run("case=with custom templates", func(t *testing.T) { + body := "custom template recovery code body" + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidSMS, nil) + }) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidSMS, fmt.Sprintf(`{ "body": { "plaintext": "base64://%s"}}`, b64(body+" {{ .RecoveryCode }}"))) + recoveryCode(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + + assert.EqualValues(t, phoneNumberKnown, messages[0].Recipient) + assert.Contains(t, messages[0].Body, body) + headers := messages[0].RequestHeaders + assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value()) + assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value()) + assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty") + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + }) + }) + + t.Run("method=SendVerificationCode", func(t *testing.T) { + verificationFlow := func(t *testing.T) { + t.Helper() + + f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh")) + require.ErrorIs(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh"), code.ErrUnknownAddress()) + } + + t.Run("case=with default templates", func(t *testing.T) { + verificationFlow(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Contains(t, messages[0].Subject, "Use code") + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Contains(t, messages[1].Subject, "Someone tried to verify this email address") + + assert.NotRegexp(t, testhelpers.CodeRegex, messages[1].Body, "Expected message to not contain an 6 digit recovery code, but it did: ", messages[1].Body) + }) + + t.Run("case=with custom templates", func(t *testing.T) { + subject := "custom template verification code" + body := "custom template verification code body" + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeInvalidEmail, nil) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeValidEmail, nil) + }) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeInvalidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" invalid"), b64(body), b64(body))) + conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeValidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" valid"), b64(body+" {{ .VerificationCode }}"), b64(body+" {{ .VerificationCode }}"))) + verificationFlow(t) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Equal(t, messages[0].Subject, subject+" valid") + assert.Contains(t, messages[0].Body, body) + + assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body) + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Equal(t, messages[1].Subject, subject+" invalid") + assert.Equal(t, messages[1].Body, body) + }) + }) + + t.Run("case=should be able to disable invalid email dispatch", func(t *testing.T) { + for _, tc := range []struct { + flow string + send func(t *testing.T) + configKey string + }{ + { + flow: "recovery", + configKey: config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, + send: func(t *testing.T) { + s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("code") + require.NoError(t, err) + f, err := recovery.NewFlow(reg, time.Hour, "", u, s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + err = reg.CodeSender().SendRecoveryCode(ctx, f, "email", "not-tracked@ory.sh", nil) + require.ErrorIs(t, err, code.ErrUnknownAddress()) + }, + }, + { + flow: "verification", + configKey: config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, + send: func(t *testing.T) { + s, _, err := reg.VerificationStrategies(ctx).ActiveStrategies("code") + require.NoError(t, err) + f, err := verification.NewFlow(reg, time.Hour, "", u, s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + err = reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh") + require.ErrorIs(t, err, code.ErrUnknownAddress()) + }, + }, + } { + t.Run("strategy="+tc.flow, func(t *testing.T) { + conf.MustSet(ctx, tc.configKey, false) + + t.Cleanup(func() { + conf.MustSet(ctx, tc.configKey, true) + }) + + tc.send(t) + + messages, err := reg.CourierPersister().NextMessages(ctx, 0) + + require.ErrorIs(t, err, courier.ErrQueueEmpty) + require.Len(t, messages, 0) + }) + } + }) + + t.Run("method=SendCode with OAuth2 login request", func(t *testing.T) { + reg.WithCSRFTokenGenerator(nosurfx.FakeCSRFTokenGenerator) + // Drain messages queued by earlier subtests. + _, _ = reg.CourierPersister().NextMessages(ctx, 100) + + hlr := &hydraclientgo.OAuth2LoginRequest{ + Challenge: "test-challenge", + Client: hydraclientgo.OAuth2Client{ + ClientId: hydraclientgo.PtrString("test-client-id"), + ClientName: hydraclientgo.PtrString("Test Client"), + ClientUri: hydraclientgo.PtrString("https://client.example.com"), + LogoUri: hydraclientgo.PtrString("https://client.example.com/logo.png"), + Metadata: map[string]any{"brand": "acme"}, + }, + } + + expectScopedRequest := func(t *testing.T, templateData []byte) { + t.Helper() + assert.Equal(t, "test-challenge", gjson.GetBytes(templateData, "oauth2_login_request.challenge").String()) + assert.Equal(t, "test-client-id", gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String()) + assert.Equal(t, "Test Client", gjson.GetBytes(templateData, "oauth2_login_request.client.client_name").String()) + assert.Equal(t, "https://client.example.com", gjson.GetBytes(templateData, "oauth2_login_request.client.client_uri").String()) + assert.Equal(t, "https://client.example.com/logo.png", gjson.GetBytes(templateData, "oauth2_login_request.client.logo_uri").String()) + assert.Equal(t, "acme", gjson.GetBytes(templateData, "oauth2_login_request.client.metadata.brand").String()) + // Only the scoped fields may be exposed to messaging channels. + assert.ElementsMatch(t, []string{"challenge", "client"}, jsonKeys(templateData, "oauth2_login_request")) + assert.ElementsMatch(t, []string{"client_id", "client_name", "client_uri", "logo_uri", "metadata"}, jsonKeys(templateData, "oauth2_login_request.client")) + } + + t.Run("flow=login", func(t *testing.T) { + r := httptest.NewRequest("GET", "https://www.ory.com/self-service/login/browser", nil) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + f.HydraLoginRequest = hlr + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{}, + code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail}, + code.Address{To: phoneNumberKnown, Via: identity.ChannelTypeSMS}, + )) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + for _, m := range messages { + assert.EqualValues(t, template.TypeLoginCodeValid, m.TemplateType) + expectScopedRequest(t, m.TemplateData) + } + }) + + t.Run("flow=registration", func(t *testing.T) { + r := httptest.NewRequest("GET", "https://www.ory.com/self-service/registration/browser", nil) + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + f.HydraLoginRequest = hlr + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{}, + code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail}, + code.Address{To: phoneNumberKnown, Via: identity.ChannelTypeSMS}, + )) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + for _, m := range messages { + assert.EqualValues(t, template.TypeRegistrationCodeValid, m.TemplateType) + expectScopedRequest(t, m.TemplateData) + } + }) + + t.Run("flow=login without OAuth2 login request", func(t *testing.T) { + r := httptest.NewRequest("GET", "https://www.ory.com/self-service/login/browser", nil) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{}, + code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail}, + )) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.False(t, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request").Exists()) + }) + + t.Run("flow=verification", func(t *testing.T) { + reg.SetHydra(hydra.NewFake()) + f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser) + require.NoError(t, err) + f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeValidLoginChallenge) + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh")) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.EqualValues(t, template.TypeVerificationCodeValid, messages[0].TemplateType) + templateData := messages[0].TemplateData + assert.Equal(t, hydra.FakeValidLoginChallenge, gjson.GetBytes(templateData, "oauth2_login_request.challenge").String()) + assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String()) + assert.ElementsMatch(t, []string{"challenge", "client"}, jsonKeys(templateData, "oauth2_login_request")) + }) + + t.Run("flow=verification without OAuth2 login challenge", func(t *testing.T) { + f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh")) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.False(t, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request").Exists()) + }) + }) +} + +func jsonKeys(data []byte, path string) []string { + var keys []string + for key := range gjson.GetBytes(data, path).Map() { + keys = append(keys, key) + } + return keys +} diff --git a/selfservice/strategy/code/code_verification.go b/selfservice/strategy/code/code_verification.go new file mode 100644 index 0000000..b3f1966 --- /dev/null +++ b/selfservice/strategy/code/code_verification.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/x/clock" +) + +type VerificationCode struct { + // ID represents the code's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code_hmac"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // VerifiableAddress links this code to a verification address. + // required: true + VerifiableAddress *identity.VerifiableAddress `json:"verification_address" belongs_to:"identity_verifiable_addresses"` + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the code was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + // VerifiableAddressID is a helper struct field for gobuffalo.pop. + VerifiableAddressID uuid.NullUUID `json:"-" faker:"-" db:"identity_verifiable_address_id"` + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_verification_flow_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (VerificationCode) TableName(context.Context) string { + return "identity_verification_codes" +} + +// Validate validates the state of the verification code +// +// - If the code is expired, `flow.ExpiredError` is returned +// - If the code was already used `ErrCodeAlreadyUsed` is returnd +// - Otherwise, `nil` is returned +func (c *VerificationCode) Validate(clk clock.Clock) error { + if c == nil { + return errors.WithStack(ErrCodeNotFound()) + } + return validateOneTimeCode(c.ExpiresAt, c.UsedAt, clk) +} + +func (c *VerificationCode) GetHMACCode() string { + return c.CodeHMAC +} + +func (c *VerificationCode) GetID() uuid.UUID { + return c.ID +} + +type CreateVerificationCodeParams struct { + // Code represents the recovery code + RawCode string + + // ExpiresIn is the lifetime of the code + ExpiresIn time.Duration + + // VerifiableAddress is the address to be verified + VerifiableAddress identity.VerifiableAddressLike + + // FlowID is the id of the current verification flow + FlowID uuid.UUID +} diff --git a/selfservice/strategy/code/code_verification_test.go b/selfservice/strategy/code/code_verification_test.go new file mode 100644 index 0000000..cf9fcc9 --- /dev/null +++ b/selfservice/strategy/code/code_verification_test.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestVerificationCode(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + newCode := func(expiresIn time.Duration, f *verification.Flow) *code.VerificationCode { + return &code.VerificationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if code is expired", func(t *testing.T) { + f, err := verification.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := verification.NewFlow(reg, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := verification.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeAlreadyUsed()) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := verification.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate(reg.Clock())) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.VerificationCode + require.ErrorIs(t, c.Validate(reg.Clock()), code.ErrCodeNotFound()) + }) + }) +} diff --git a/selfservice/strategy/code/node.go b/selfservice/strategy/code/node.go new file mode 100644 index 0000000..637a55b --- /dev/null +++ b/selfservice/strategy/code/node.go @@ -0,0 +1,47 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "github.com/ory/kratos/identity" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func nodeSubmitRegistration() *node.Node { + return node.NewInputField("method", identity.CredentialsTypeCodeAuth, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterCode()) +} + +func nodeRegistrationResendNode() *node.Node { + return node.NewInputField("resend", identity.CredentialsTypeCodeAuth, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) +} + +func nodeRegistrationSelectCredentialsNode() *node.Node { + return node.NewInputField( + "screen", + "credential-selection", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack()) +} + +func nodeContinueButton() *node.Node { + return node.NewInputField("method", identity.CredentialsTypeCodeAuth, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue()) +} + +func nodeCodeInputFieldHidden() *node.Node { + return node.NewInputField("method", identity.CredentialsTypeCodeAuth, node.CodeGroup, node.InputAttributeTypeHidden) +} + +func nodeCodeInputField() *node.Node { + return node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, + node.WithInputAttributes(func(a *node.InputAttributes) { + // Hint the browser to autofill the one-time code from SMS or email, + // enabling the Web OTP API on supported platforms. + a.Autocomplete = node.InputAttributeAutocompleteOneTimeCode + })) +} diff --git a/selfservice/strategy/code/node_test.go b/selfservice/strategy/code/node_test.go new file mode 100644 index 0000000..e7295cf --- /dev/null +++ b/selfservice/strategy/code/node_test.go @@ -0,0 +1,21 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/ui/node" +) + +func TestNodeCodeInputFieldAutocomplete(t *testing.T) { + n := nodeCodeInputField() + + attr, ok := n.Attributes.(*node.InputAttributes) + require.True(t, ok, "expected input attributes") + assert.Equal(t, node.InputAttributeAutocompleteOneTimeCode, attr.Autocomplete) +} diff --git a/selfservice/strategy/code/persistence.go b/selfservice/strategy/code/persistence.go new file mode 100644 index 0000000..ea5aaff --- /dev/null +++ b/selfservice/strategy/code/persistence.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + + "github.com/gofrs/uuid" +) + +type ( + RecoveryCodePersister interface { + CreateRecoveryCode(ctx context.Context, dto *CreateRecoveryCodeParams) (*RecoveryCode, error) + UseRecoveryCode(ctx context.Context, fID uuid.UUID, code string) (*RecoveryCode, error) + DeleteRecoveryCodesOfFlow(ctx context.Context, fID uuid.UUID) error + } + + RecoveryCodePersistenceProvider interface { + RecoveryCodePersister() RecoveryCodePersister + } + + VerificationCodePersister interface { + CreateVerificationCode(context.Context, *CreateVerificationCodeParams) (*VerificationCode, error) + UseVerificationCode(context.Context, uuid.UUID, string) (*VerificationCode, error) + DeleteVerificationCodesOfFlow(context.Context, uuid.UUID) error + } + + VerificationCodePersistenceProvider interface { + VerificationCodePersister() VerificationCodePersister + } + + RegistrationCodePersistenceProvider interface { + RegistrationCodePersister() RegistrationCodePersister + } + + RegistrationCodePersister interface { + CreateRegistrationCode(context.Context, *CreateRegistrationCodeParams) (*RegistrationCode, error) + UseRegistrationCode(ctx context.Context, flowID uuid.UUID, code string, addresses ...string) (*RegistrationCode, error) + DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*RegistrationCode, error) + } + + LoginCodePersistenceProvider interface { + LoginCodePersister() LoginCodePersister + } + + LoginCodePersister interface { + CreateLoginCode(context.Context, *CreateLoginCodeParams) (*LoginCode, error) + UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, code string) (*LoginCode, error) + DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*LoginCode, error) + } +) diff --git a/selfservice/strategy/code/schema.go b/selfservice/strategy/code/schema.go new file mode 100644 index 0000000..d3ec2c6 --- /dev/null +++ b/selfservice/strategy/code/schema.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + _ "embed" +) + +//go:embed .schema/recovery.schema.json +var recoveryMethodSchema []byte + +//go:embed .schema/verification.schema.json +var verificationMethodSchema []byte + +//go:embed .schema/login.schema.json +var loginMethodSchema []byte + +//go:embed .schema/registration.schema.json +var registrationSchema []byte diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go new file mode 100644 index 0000000..443ff83 --- /dev/null +++ b/selfservice/strategy/code/strategy.go @@ -0,0 +1,597 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "encoding/json" + "net/http" + "sort" + "strings" + + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/x/nosurfx" + + "github.com/samber/lo" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/recovery" + "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/selfservice/sessiontokenexchange" + "github.com/ory/kratos/session" + "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/transaction" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/randx" + "github.com/ory/x/urlx" +) + +var ( + _ recovery.Strategy = (*Strategy)(nil) + _ verification.Strategy = (*Strategy)(nil) + _ login.Strategy = (*Strategy)(nil) + _ registration.Strategy = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) + _ x.Handler = (*Strategy)(nil) +) + +type ( + // FlowMethod contains the configuration for this selfservice strategy. + FlowMethod struct { + *container.Container + } + + dependencies interface { + clock.Provider + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + transaction.PersistenceProvider + + config.Provider + + session.HandlerProvider + session.ManagementProvider + settings.HandlerProvider + settings.FlowPersistenceProvider + settings.HookExecutorProvider + + identity.ValidationProvider + identity.ManagementProvider + identity.PoolProvider + identity.PrivilegedPoolProvider + identity.PendingTraitsChangePersistenceProvider + + courier.Provider + + errorx.ManagementProvider + + recovery.ErrorHandlerProvider + recovery.FlowPersistenceProvider + recovery.StrategyProvider + recovery.HookExecutorProvider + + verification.FlowPersistenceProvider + verification.StrategyProvider + verification.HookExecutorProvider + + login.StrategyProvider + login.FlowPersistenceProvider + + registration.StrategyProvider + registration.FlowPersistenceProvider + + RecoveryCodePersistenceProvider + VerificationCodePersistenceProvider + SenderProvider + + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider + + schema.IdentitySchemaProvider + session.PersistenceProvider + + sessiontokenexchange.PersistenceProvider + + continuity.ManagementProvider + + hydra.Provider + } + + Strategy struct{ deps dependencies } + + codeIdentifier struct { + Identifier string `json:"identifier"` + } +) + +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 0, nil + } + + // Get code credentials if they exist + creds, ok := cc[identity.CredentialsTypeCodeAuth] + if !ok { + return 0, nil + } + + // Check if the credentials config is valid JSON + if !gjson.Valid(string(creds.Config)) { + return 0, nil + } + + // Check for v0 format with address_type field + if gjson.GetBytes(creds.Config, "address_type").Exists() { + addressType := gjson.GetBytes(creds.Config, "address_type").String() + if addressType != "" { + return 1, nil + } + return 0, nil + } + + var conf identity.CredentialsCode + if err := json.Unmarshal(creds.Config, &conf); err != nil { + return 0, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to unmarshal credentials config: %s", err)) + } + + // If no addresses configured, return 0 + if len(conf.Addresses) == 0 { + return 0, nil + } + + // Count valid addresses configured for MFA + validAddresses := 0 + for _, addr := range conf.Addresses { + if addr.Address != "" { + validAddresses++ + } + } + + return validAddresses, nil +} + +func NewStrategy(deps dependencies) *Strategy { return &Strategy{deps: deps} } + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeCodeAuth +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.CodeGroup +} + +func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { + codeConfig := s.deps.Config().SelfServiceCodeStrategy(r.Context()) + switch f := f.(type) { + case *login.Flow: + if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 { + if !codeConfig.MFAEnabled { + // if the flow is requesting AAL2 but MFA is not enabled for the code strategy, we return nil so that + // other strategies can fulfil the request + return nil + } + } else if !codeConfig.PasswordlessEnabled { + // if the flow is a normal login flow but passwordless is not enabled for the code strategy, + // we return nil so that other strategies can fulfil the request + return nil + } + case *registration.Flow: + // for registration flows, we don't have AAL requirements, so we just check that passwordless is enabled. + if !codeConfig.PasswordlessEnabled { + return nil + } + } + + if string(f.GetState()) == "" { + f.SetState(flow.StateChooseMethod) + } + + f.GetUI().ResetMessages() + + switch f.GetState() { + case flow.StateChooseMethod: + if err := s.populateChooseMethodFlow(r, f); err != nil { + return err + } + case flow.StateEmailSent: + if err := s.populateEmailSentFlow(r.Context(), f); err != nil { + return err + } + case flow.StatePassedChallenge: + fallthrough + default: + return errors.WithStack(herodot.ErrBadRequest().WithReason("received an unexpected flow state")) + } + + // no matter the flow type or state we need to set the CSRF token + 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() + switch f := f.(type) { + case *verification.Flow: + label := text.NewInfoNodeInputEmail() + inputType := node.InputAttributeTypeEmail + if channels, err := s.deps.Config().CourierChannels(ctx); err == nil { + for _, ch := range channels { + if ch.ID == "sms" { + label = text.NewInfoNodeInputEmailOrPhone() + inputType = node.InputAttributeTypeText + break + } + } + } + f.GetUI().Nodes.Append( + node.NewInputField("email", nil, node.CodeGroup, inputType, node.WithRequiredInputAttribute). + WithMetaLabel(label), + ) + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue()), + ) + case *recovery.Flow: + f.GetUI().Nodes.Append( + node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue()), + ) + case *login.Flow: + ds, err := f.IdentitySchema.URL(ctx, s.deps.Config()) + if err != nil { + return err + } + if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 { + via := r.URL.Query().Get("via") + + // The full expansion is required: finding the code addresses reads the identity's + // credentials. + sess, err := s.deps.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + 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 + } + } + + addresses, err := s.FindCodeAddresses(ctx, sess, via) + if err != nil { + return err + } + 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 if f.Refresh && s.deps.Config().RefreshLoginChooseAddress(ctx) { + // On a refresh login the identity is already known from the active + // session. Asking for the identifier again makes the privileged + // login look like a fresh unified login (ory/kratos#4194). Instead, + // render one "Send code to
" button per available code address, + // exactly like the second-factor flow above. + // The full expansion is required: finding the code addresses reads the identity's + // credentials. + sess, err := s.deps.SessionManager().FetchFromRequest(ctx, r, session.ExpandEverything, identity.ExpandEverything) + if err != nil { + return err + } + + if len(sess.Identity.Credentials) == 0 { + if err := s.deps.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil { + return err + } + } + + addresses, err := s.FindCodeAddresses(ctx, sess, "") + if err != nil { + return err + } + + // If the identity has no code address, append nothing so another + // first-factor strategy (e.g. password) can fulfil the refresh. + 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 { + if f.Refresh { + // The refresh address picker is disabled, so we fall back to + // re-asking for the identifier. This is deprecated: the identity + // is already fixed by the active session (ory/kratos#4194). + trace.SpanFromContext(ctx).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "refresh_login_identifier_input")) + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(ctx, ds.String(), s.deps.Config().SecurityDisallowRefInIdentitySchemas(ctx)) + 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()), + ) + } + } + + return nil +} + +func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error { + // fresh ui node group + freshNodes := f.GetUI().Nodes + freshNodes.ClearTransientNodes() + var route string + var codeMetaLabel *text.Message + var message *text.Message + + var resendNode *node.Node + var backNode *node.Node + + switch f.GetFlowName() { + case flow.RecoveryFlow: + route = recovery.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRecoveryCode() + message = text.NewRecoveryEmailWithCodeSent() + + resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeResendOTP()) + + case flow.VerificationFlow: + route = verification.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelVerificationCode() + message = text.NewVerificationEmailWithCodeSent() + + case flow.LoginFlow: + route = login.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelLoginCode() + // On refresh and second factor flows the recipient address is bound to + // the authenticated identity, not typed by the user. Use a message that + // reflects that — the standard "address you provided" / "check the + // spelling" wording is inaccurate in those cases. + if lf, ok := f.(*login.Flow); ok && (lf.Refresh || lf.RequestedAAL == identity.AuthenticatorAssuranceLevel2) { + message = text.NewLoginCodeSentForAuthenticatedUser() + } else { + message = text.NewLoginCodeSent() + } + + // 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.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) + } + } + + resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) + + case flow.RegistrationFlow: + route = registration.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRegistrationCode() + message = text.NewRegistrationEmailWithCodeSent() + + // in the registration flow we need to preserve the trait fields that were submitted + // so we can retry the code flow with the same data + for _, n := range f.GetUI().Nodes { + if t, ok := n.Attributes.(*node.InputAttributes); ok && t.Type == node.InputAttributeTypeSubmit { + continue + } + + 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 + } + } + freshNodes = append(freshNodes, n) + } + } + + resendNode = nodeRegistrationResendNode() + + // 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 = nodeRegistrationSelectCredentialsNode() + } + default: + return errors.WithStack(herodot.ErrBadRequest().WithReason("received an unexpected flow type")) + } + + // Hidden field Required for the re-send code button + // !!important!!: this field must be appended before the code submit button since upsert will replace the first node with the same name + freshNodes.Upsert(nodeCodeInputFieldHidden()) + + // code input field + freshNodes.Upsert(nodeCodeInputField().WithMetaLabel(codeMetaLabel)) + + // code submit button + freshNodes.Append(nodeContinueButton()) + + if resendNode != nil { + freshNodes.Append(resendNode) + } + + if backNode != nil { + freshNodes.Append(backNode) + } + + f.GetUI().Nodes = freshNodes + + f.GetUI().Method = "POST" + f.GetUI().Action = flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), route), f.GetID()).String() + + f.GetUI().Messages.Set(message) + return nil +} + +// NewCodeUINodes creates a fresh UI for the code flow. +// this is used with the `recovery`, `verification`, `registration` and `login` flows. +func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data any) error { + if err := s.PopulateMethod(r, f); err != nil { + return err + } + + prefix := "" // The login flow does not process traits + if f.GetFlowName() == flow.RegistrationFlow { + // The registration form does however + prefix = "traits" + } + + cont, err := container.NewFromStruct("", node.CodeGroup, data, prefix) + if err != nil { + return err + } + + for _, n := range cont.Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + + return nil +} + +func (s *Strategy) FindCodeAddresses(ctx context.Context, sess *session.Session, via string) (result []Address, _ error) { + // 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 nil, err + } else if !found { + return nil, nil + } + + sort.SliceStable(addresses, func(i, j int) bool { + return addresses[i].To < addresses[j].To && addresses[i].Via < addresses[j].Via + }) + + return addresses, nil + } else { + value := gjson.GetBytes(sess.Identity.Traits, via).String() + if value == "" { + return nil, 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 nil, err + } else if !found { + return nil, nil + } + + address, found := lo.Find(addresses, func(item Address) bool { + return item.To == value + }) + if !found { + return nil, 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.")) + } + + return []Address{address}, nil + } +} + +func SetDefaultFlowState(f flow.Flow, resend string) { + // By Default the flow should be in the 'choose method' state. + if f.GetState() == "" { + f.SetState(flow.StateChooseMethod) + } + + if strings.EqualFold(resend, "code") { + f.SetState(flow.StateChooseMethod) + } +} + +const CodeLength = 6 + +func GenerateCode() string { + return randx.MustString(CodeLength, randx.Numeric) +} + +// MaskAddress masks an address by replacing the middle part with asterisks. +// +// If the address contains an @, the part before the @ is masked by taking the first 2 characters and adding 4 * +// (if the part before the @ is less than 2 characters the full value is used). +// Otherwise, the first 3 characters and last two characters are taken and 4 * are added in between. +// +// Examples: +// - foo@bar -> fo****@bar +// - foobar -> fo****ar +// - f@bar -> f@bar +// - fo@bar -> fo****@bar +// - +12345678910 -> +12****10 +func MaskAddress(input string) string { + if strings.Contains(input, "@") { + pre, post, found := strings.Cut(input, "@") + if !found || len(pre) < 2 { + return input + } + return pre[:2] + strings.Repeat("*", 4) + "@" + post + } + if len(input) < 6 { + return input + } + return input[:3] + strings.Repeat("*", 4) + input[len(input)-2:] +} diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go new file mode 100644 index 0000000..040466b --- /dev/null +++ b/selfservice/strategy/code/strategy_login.go @@ -0,0 +1,729 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "cmp" + "context" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/samber/lo" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "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/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/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +var ( + _ login.AAL1FormHydrator = new(Strategy) + _ login.AAL2FormHydrator = (*Strategy)(nil) + _ login.Strategy = (*Strategy)(nil) +) + +// Update Login flow using the code method +// +// swagger:model updateLoginFlowWithCodeMethod +type updateLoginFlowWithCodeMethod struct { + // Method should be set to "code" when logging in using the code strategy. + // + // required: true + Method string `json:"method" form:"method"` + + // CSRFToken is the anti-CSRF token + // + // required: true + CSRFToken string `json:"csrf_token" form:"csrf_token"` + + // Code is the 6 digits code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // Identifier is the code identifier + // The identifier requires that the user has already completed the registration or settings with code flow. + // 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) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + aal := identity.AuthenticatorAssuranceLevel1 + if s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled { + aal = identity.AuthenticatorAssuranceLevel2 + } + + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: aal, + } +} + +func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *updateLoginFlowWithCodeMethod, err error, hideIdentifier bool) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { + identifier := "" + if body != nil { + identifier = cmp.Or(body.Address, body.Identifier) + } + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + if hideIdentifier { + identifierNode := node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden) + identifierNode.Attributes.SetValue(identifier) + f.UI.GetNodes().Upsert(identifierNode) + } + + f.UI.Nodes.SetValueAttribute("identifier", identifier) + } + + return err +} + +// 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) (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) + 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, identity.ExpandDefault) + if err != nil { + 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, nil, true, nil + } else if err != nil { + return nil, nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + if len(cred.Identifiers) == 0 { + 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, cred, false, nil +} + +type decodedMethod struct { + Method string `json:"method" form:"method"` + Address string `json:"address" form:"address"` +} + +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) + } + + if err := decoderx.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 { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + } else if s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { + return nil, err + } + } else { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + if p, err := s.methodEnabledAndAllowedFromRequest(r, f); errors.Is(err, flow.ErrStrategyNotResponsible) { + // Address submit buttons post `address` with no `method`. Second-factor + // buttons require MFA. First-factor refresh buttons additionally require + // the refresh address picker feature flag and passwordless code login; + // the address resolves the identifier downstream (ory/kratos#4194). + codeConfig := s.deps.Config().SelfServiceCodeStrategy(ctx) + allowFirstFactorRefreshAddress := f.Refresh && + codeConfig.PasswordlessEnabled && + s.deps.Config().RefreshLoginChooseAddress(ctx) + if !codeConfig.MFAEnabled && !allowFirstFactorRefreshAddress { + span.SetAttributes(attribute.String("not_responsible_reason", "code method 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 + } + + var p updateLoginFlowWithCodeMethod + if err := decoderx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPKeepRequestBody(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.HandleLoginError(r, f, &p, err, false) + } + + 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, false) + } + + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) + + switch f.GetState() { + case flow.StateChooseMethod: + identifier := maybeNormalizeEmail( + cmp.Or(p.Identifier, p.Address), + ) + id, addresses, err := s.findIdentityForIdentifier(ctx, identifier, f.RequestedAAL, sess) + if err != nil { + return nil, s.HandleLoginError(r, f, &p, err, false) + } + // On a refresh login the identity is fixed by the active session. + // A submitted address that resolves to a different identity must not + // be able to trigger a code or switch the session (ory/kratos#4194). + if f.Refresh && sess.Identity != nil && id.ID != sess.Identity.ID { + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(schema.NewNoCodeAuthnCredentials()), false) + } + if err := s.loginSendCode(ctx, w, r, f, id, identifier, addresses, false); err != nil { + return nil, s.HandleLoginError(r, f, &p, err, false) + } + return nil, nil + case flow.StateEmailSent: + i, err := s.loginVerifyCode(ctx, f, &p, sess) + if err != nil { + return nil, s.HandleLoginError(r, f, &p, err, true) + } + return i, nil + case flow.StatePassedChallenge: + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(schema.NewNoLoginStrategyResponsible()), false) + } + + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unexpected flow state: %s", f.GetState())), false) +} + +// nonOrgFilter excludes strategies that support organization-based authentication. +// Used in FastLogin1FA to prevent org strategies from contributing to the +// competing-credential count that decides whether code login should defer. +var nonOrgFilter = []login.StrategyFilter{ + func(strategy login.Strategy) bool { + s, ok := strategy.(flow.OrganizationImplementor) + return !ok || !s.SupportsOrganizations() + }, +} + +func (s *Strategy) FastLogin1FA(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.FastLogin1FA") + defer otelx.End(span, &err) + + if f.RequestedAAL != identity.AuthenticatorAssuranceLevel1 || !s.deps.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled || f.GetState() != flow.StateChooseMethod { + return flow.ErrStrategyNotResponsible + } + + var p1 idfirst.UpdateLoginFlowWithIdentifierFirstMethod + if err := decoderx.Decode(r, &p1, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(idfirst.LoginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return err + } + identifier := p1.Identifier + id, addresses, err := s.findIdentityForIdentifier(ctx, identifier, f.RequestedAAL, sess) + if err != nil { + return err + } + + // On a refresh login the identity is fixed by the active session. Do not + // let a submitted identifier resolve a different identity and trigger a + // code to a foreign address (ory/kratos#4194). Returning "not responsible" + // keeps the response identical to the no-fast-login case (no enumeration). + if f.Refresh && sess.Identity != nil && id.ID != sess.Identity.ID { + return flow.ErrStrategyNotResponsible + } + + accountEnumerationMitigate := s.deps.Config().SecurityAccountEnumerationMitigate(r.Context()) + + if c, err := s.CountActiveFirstFactorCredentials(ctx, id.Credentials); err != nil { + return err + } else if c == 0 { + return flow.ErrStrategyNotResponsible + } else { + for _, strat := range s.deps.LoginStrategies(ctx, nonOrgFilter...) { + if strat, ok := strat.(identity.ActiveCredentialsCounter); !ok || strat.ID() == identity.CredentialsTypeCodeAuth { + continue + } else { + if cc, err := strat.CountActiveFirstFactorCredentials(ctx, id.Credentials); err != nil { + return err + } else if cc > 0 || accountEnumerationMitigate { + return flow.ErrStrategyNotResponsible + } + } + } + } + + if err := s.loginSendCode(ctx, w, r, f, id, identifier, addresses, f.RequestedAAL == "aal2"); err != nil { + return err + } + + return nil +} + +func (s *Strategy) FastLogin2FA(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.FastLogin2FA") + defer otelx.End(span, &err) + + if f.RequestedAAL != identity.AuthenticatorAssuranceLevel2 || !s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled || f.GetState() != flow.StateChooseMethod { + return flow.ErrStrategyNotResponsible + } + + addresses, err := s.FindCodeAddresses(ctx, sess, r.URL.Query().Get("via")) + if err != nil { + return err + } + if len(addresses) != 1 { + return flow.ErrStrategyNotResponsible + } + identifier := addresses[0].To + id, addresses, err := s.findIdentityForIdentifier(ctx, identifier, f.RequestedAAL, sess) + if err != nil { + return err + } + + fallbackEnabled := s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx) + + if c, err := s.CountActiveMultiFactorCredentials(ctx, sess.Identity.Credentials); err != nil { + return err + } else if c == 0 && !fallbackEnabled { + return flow.ErrStrategyNotResponsible + } else { + for _, strat := range s.deps.LoginStrategies(ctx, login.PrepareOrganizations(r, f, sess)...) { + if strat, ok := strat.(identity.ActiveCredentialsCounter); !ok || strat.ID() == identity.CredentialsTypeCodeAuth { + continue + } else { + if cc, err := strat.CountActiveMultiFactorCredentials(ctx, sess.Identity.Credentials); err != nil { + return err + } else if cc > 0 { + return flow.ErrStrategyNotResponsible + } + } + } + } + + if err := s.loginSendCode(ctx, w, r, f, id, identifier, addresses, true); err != nil { + return err + } + + return nil +} + +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(identifier) == 0 { + return nil, nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } + + var addresses []Address + + // Step 1: Get the identity + // The raw identifier is passed to findIdentityByIdentifier so that + // FindByCredentialsIdentifier can match both the normalized and raw forms + // via its IN(normalized, raw) query. This preserves backward compatibility + // with legacy database records that store non-normalized identifiers. + 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.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, x.GracefulNormalization(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 + } + } + 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: 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 { + 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) + } + + 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, id *identity.Identity, identifier string, addresses []Address, successResponse bool) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.Strategy.loginSendCode") + defer otelx.End(span, &err) + + if address, found := lo.Find(addresses, func(item Address) bool { + return item.To == x.GracefulNormalization(identifier) + }); found { + addresses = []Address{address} + } + + // Step 2: Delete any previous login codes for this flow ID + if err := s.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.GetID()); err != nil { + return errors.WithStack(err) + } + + // Fetch the OAuth2 login request before sending out the code so that + // courier templates can access the OAuth2 client that initiated the flow. + if f.OAuth2LoginChallenge != "" { + hlr, err := s.deps.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge)) + if err != nil { + return errors.WithStack(err) + } + f.HydraLoginRequest = hlr + } + + // 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, id, r.Header, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) + + if err := s.NewCodeUINodes(r, f, &codeIdentifier{Identifier: identifier}); err != nil { + return err + } + + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return err + } + + if x.IsJSONRequest(r) { + if successResponse { + s.deps.Writer().WriteCode(w, r, http.StatusOK, f) + } else { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the login flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +// If identifier is an email, we lower case it because on mobile phones the first letter sometimes is capitalized. +func maybeNormalizeEmail(input string) string { + if strings.Contains(input, "@") { + return strings.ToLower(input) + } + return input +} + +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) + + // we are in the second submission state of the flow + // we need to check the code and update the identity + if p.Code == "" { + return nil, errors.WithStack(schema.NewRequiredError("#/code", "code")) + } + + p.Identifier = maybeNormalizeEmail( + cmp.Or( + p.Address, + p.Identifier, // Older versions of Kratos required us to send the identifier here. + ), + ) + + var i *identity.Identity + if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 { + i = sess.Identity + } else { + i, _, err = s.findIdentityForIdentifier(ctx, p.Identifier, f.RequestedAAL, sess) + } + if err != nil { + return nil, err + } + // Defense in depth: a refresh login must complete against the session's + // own identity, never one resolved from a foreign submitted address. + if f.Refresh && sess.Identity != nil && i.ID != sess.Identity.ID { + return nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) + if err != nil { + if errors.Is(err, ErrCodeNotFound()) { + return nil, schema.NewLoginCodeInvalid() + } + return nil, errors.WithStack(err) + } + + i, err = s.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) + if err != nil { + return nil, errors.WithStack(err) + } + + // Step 2: The code was correct + f.Active = identity.CredentialsTypeCodeAuth + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the login flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, errors.WithStack(err) + } + + // Step 3: Verify the address + if err := s.verifyAddress(ctx, i, Address{ + To: loginCode.Address, + Via: loginCode.AddressType, + }, true); err != nil { + return nil, err + } + + verifiedAt := sqlxx.NullTime(time.Now().UTC()) + for idx := range i.VerifiableAddresses { + va := i.VerifiableAddresses[idx] + if !va.Verified && loginCode.Address == va.Value { + va.Verified = true + va.VerifiedAt = &verifiedAt + va.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &va, "verified", "verified_at", "status"); err != nil { + return nil, err + } + break + } + } + + return i, nil +} + +func (s *Strategy) verifyAddress(ctx context.Context, i *identity.Identity, verified Address, persistNow bool) error { + for idx := range i.VerifiableAddresses { + address := &i.VerifiableAddresses[idx] + if address.Verified { + continue + } + + if verified.To != address.Value || string(verified.Via) != address.Via { + continue + } + + address.Verified = true + address.VerifiedAt = new(sqlxx.NullTime(time.Now().UTC())) + address.Status = identity.VerifiableAddressStatusCompleted + if persistNow { + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); 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. + s.deps.Logger().WithError(err).Warnf("Could not update verifiable address for identity %s.", i.ID) + continue + } else if err != nil { + return err + } + } + i.VerifiableAddresses[idx] = *address + break + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow, _ *session.Session) 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 new file mode 100644 index 0000000..781d902 --- /dev/null +++ b/selfservice/strategy/code/strategy_login_test.go @@ -0,0 +1,1812 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/x/configx" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + oryClient "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/selfservice/strategy/totp" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/contextx" + "github.com/ory/x/ioutilx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" +) + +func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry, withoutCodeCredential bool, withTotp 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 + `"}]}`)} + } + if withTotp { + key, err := totp.NewKey(context.Background(), "foo", reg) + require.NoError(t, err) + credentials[identity.CredentialsTypeTOTP] = identity.Credentials{ + Type: identity.CredentialsTypeTOTP, + Identifiers: append([]string{email}, moreIdentifiers...), + Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`), + } + } + 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) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeCodeAuth, true)), + configx.WithValues(map[string]any{ + fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth): true, + }), + ) + conf.MustSet(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{"x-CUSTOM-header", "uSER-Agent"}) + + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + type state struct { + flowID string + identity *identity.Identity + client *http.Client + identityEmail string + testServer *httptest.Server + body string + } + + type ApiType string + + const ( + ApiTypeBrowser ApiType = "browser" + ApiTypeSPA ApiType = "spa" + ApiTypeNative ApiType = "api" + ) + + createLoginFlowWithIdentity := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, user *identity.Identity) *state { + t.Helper() + + var client *http.Client + if apiType == ApiTypeNative { + client = &http.Client{} + } else { + client = testhelpers.NewClientWithCookies(t) + } + + client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + + var clientInit *oryClient.LoginFlow + if apiType == ApiTypeNative { + clientInit = testhelpers.InitializeLoginFlowViaAPICtx(ctx, t, client, public, false) + } else { + clientInit = testhelpers.InitializeLoginFlowViaBrowserCtx(ctx, t, client, public, false, apiType == ApiTypeSPA, false, false) + } + + body, err := json.Marshal(clientInit) + require.NoError(t, err) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + if apiType == ApiTypeNative { + require.Emptyf(t, csrfToken, "csrf_token should be empty in native flows, but was found in: %s", body) + } else { + require.NotEmptyf(t, csrfToken, "could not find csrf_token in: %s", body) + } + + return &state{ + 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, false, 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() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(lf.Ui.Nodes) + // we need to remove resend here + // since it is not required for the first request + // subsequent requests might need it later + values.Del("resend") + values.Set("method", "code") + vals(&values) + + body, resp := testhelpers.LoginMakeRequestCtx(ctx, + t, + apiType == ApiTypeNative, + apiType == ApiTypeSPA, + lf, + s.client, + testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values), + testhelpers.WithHeader("uSER-Agent", "ory-kratos-test"), + testhelpers.WithHeader("x-CUSTOM-header", "value 1"), + testhelpers.WithHeader("x-custom-HEADER", "value 2"), + testhelpers.WithHeader("x-unknown-header", "should not be forwarded"), + ) + + if submitAssertion != nil { + submitAssertion(t, s, body, resp) + return s + } + s.body = body + + if mustHaveSession { + req, err := http.NewRequest("GET", s.testServer.URL+session.RouteWhoami, nil) + require.NoError(t, err) + req = req.WithContext(ctx) + + if apiType == ApiTypeNative { + req.Header.Set("Authorization", "Bearer "+gjson.Get(body, "session_token").String()) + } + + resp, err = s.client.Do(req) + require.NoError(t, err) + body = string(ioutilx.MustReadAll(resp.Body)) + require.EqualValuesf(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. + if apiType == ApiTypeBrowser { + require.EqualValuesf(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.EqualValuesf(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + } + + return s + } + + for _, tc := range []struct { + d string + apiType ApiType + }{ + { + d: "SPA client", + apiType: ApiTypeSPA, + }, + { + d: "Browser client", + apiType: ApiTypeBrowser, + }, + { + d: "Native client", + apiType: ApiTypeNative, + }, + } { + t.Run("test="+tc.d, func(t *testing.T) { + t.Run("case=email identifier should be case insensitive", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + // submit email + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", stringsx.ToUpperInitial(s.identityEmail)) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + }) + + 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) + + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with 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.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, "Use code") + assert.Contains(t, message.Body, "Login to your account with 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, []*identity.Identity{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, []*identity.Identity{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, []*identity.Identity{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 phone + 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) + headers := message.RequestHeaders + assert.Equal(t, []any{"ory-kratos-test"}, gjson.GetBytes(headers, "User-Agent").Value()) + assert.Equal(t, []any{"value 1", "value 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value()) + assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty") + + // 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=should be able to log in with code to sms with legacy non-normalized phone number", func(t *testing.T) { + // Test backward compatibility: login with a legacy identity whose credential + // identifier is stored in non-normalized form in the database (as it would be + // for records created before identifier normalization was introduced). + // The raw identifier reaches FindByCredentialsIdentifier, which queries + // IN(normalized, raw) — the raw parameter matches the legacy DB record. + nonNormalizedPhone := "+1 (415) 555-2671" + normalizedPhone := x.GracefulNormalization(nonNormalizedPhone) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, "phone_1": "%s"}`, nonNormalizedPhone)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{nonNormalizedPhone}, + Version: 0, + Config: sqlxx.JSONRawMessage(`{"address_type": "sms"}`), + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentities(ctx, []*identity.Identity{i})) + t.Cleanup(func() { + require.NoError(t, reg.PrivilegedIdentityPool().DeleteIdentity(ctx, i.ID)) + }) + + // CreateIdentities normalizes the credential identifier to E.164 format + // but preserves credential version. Revert the identifier via raw SQL + // to simulate a legacy database record. + require.NoError(t, reg.Persister().GetConnection(ctx).RawQuery( + "UPDATE identity_credential_identifiers SET identifier = ? WHERE identity_id = ? AND identifier = ?", + nonNormalizedPhone, i.ID, normalizedPhone, + ).Exec()) + + t.Run("login with non-normalized phone", func(t *testing.T) { + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + + // submit phone + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", nonNormalizedPhone) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, nonNormalizedPhone, "Your login code is:") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 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("login with normalized phone", func(t *testing.T) { + // Login with the normalized (E.164) phone number fails against a legacy + // record that only stores the non-normalized identifier. The lookup query + // searches for IN(normalizedInput, rawInput), but both resolve to the same + // E.164 value, so neither matches the non-normalized DB record. + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", normalizedPhone) + }, false, nil) + assert.Contains(t, s.body, "4000035", "Should not find the account because the normalized input does not match the legacy non-normalized DB record") + }) + }) + + t.Run("case=new identities automatically have login with code", func(t *testing.T) { + ctx := context.Background() + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) + + client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + + registrationFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, public, tc.apiType == ApiTypeNative, false, false) + + email := testhelpers.RandomEmail() + + values := testhelpers.SDKFormFieldsToURLValues(registrationFlow.Ui.Nodes) + values.Set("traits.email", email) + values.Set("method", "password") + values.Set("traits.tos", "1") + values.Set("password", x.NewUUID().String()) + + _, resp := testhelpers.RegistrationMakeRequest(t, tc.apiType == ApiTypeNative, tc.apiType == ApiTypeSPA, registrationFlow, client, testhelpers.EncodeFormAsJSON(t, tc.apiType == ApiTypeNative, values)) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + _, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeCodeAuth, email) + require.NoError(t, err, sqlcon.ErrNoRows()) + + s := createLoginFlow(ctx, t, public, tc.apiType, true) + + s.identityEmail = email + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + headers := message.RequestHeaders + assert.Equal(t, []any{"ory-kratos-test"}, gjson.GetBytes(headers, "User-Agent").Value()) + assert.Equal(t, []any{"value 1", "value 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value()) + assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty") + // submit code + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + + // assert that the identity contains a code credential + identity, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeCodeAuth, s.identityEmail) + require.NoError(t, err) + require.NotNil(t, cred) + assert.Equal(t, identity.ID, cred.IdentityID) + }) + + t.Run("case=old identities should be able to login with code", func(t *testing.T) { + // createLoginFlow uses the persister layer to create the identity + // we pass in `true` to not do automatic code credential creation + s := createLoginFlow(ctx, t, public, tc.apiType, true) + + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // submit code + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + + // assert that the identity contains a code credential + identity, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeCodeAuth, s.identityEmail) + require.NoError(t, err) + require.NotNil(t, cred) + assert.Equal(t, identity.ID, cred.IdentityID) + }) + + t.Run("case=should not be able to change submitted id on code submit", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", "not-"+s.identityEmail) + v.Set("code", loginCode) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + 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() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + }) + }) + + t.Run("case=should not be able to proceed to code entry when the account is unknown", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + // submit email + submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", testhelpers.RandomEmail()) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + 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() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + }) + }) + + t.Run("case=should not be able to use valid code after 5 attempts", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + for i := 0; i < 5; i++ { + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", "111111") + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The login code is invalid or has already been used") + }) + } + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + + t.Run("case=code should expire", func(t *testing.T) { + ctx := context.Background() + + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1ns") + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + // submit email + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The login code is invalid or has already been used. Please try again", "%s", body) + }) + }) + + t.Run("case=resend code should invalidate previous code", func(t *testing.T) { + ctx := context.Background() + + s := createLoginFlow(ctx, t, public, tc.apiType, false) + + 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // resend code + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("resend", "code") + v.Set("identifier", s.identityEmail) + }, false, nil) + + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Use code") + assert.Contains(t, message.Body, "Login to your account with the following code") + loginCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode2) + + assert.NotEqual(t, loginCode, loginCode2) + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, res *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.EqualValues(t, http.StatusOK, res.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The login code is invalid or has already been used. Please try again") + }) + + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode2) + v.Set("identifier", s.identityEmail) + }, true, nil) + }) + + t.Run("case=on login with un-verified address, should verify it", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.apiType, false, testhelpers.RandomEmail()) + + // we need to fetch only the first email + loginEmail := gjson.Get(s.identity.Traits.String(), "email_1").String() + require.NotEmpty(t, loginEmail) + + s.identityEmail = loginEmail + + var va *identity.VerifiableAddress + + for _, v := range s.identity.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.False(t, va.Verified) + + // submit email + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, loginEmail, "Use code") + require.Contains(t, message.Body, "Login to your account with the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + require.NotEmpty(t, loginCode) + + // Submit OTP + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, true, nil) + + id, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, s.identity.ID, identity.ExpandEverything) + require.NoError(t, err) + + va = nil + + for _, v := range id.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.True(t, va.Verified) + }) + + t.Run("suite=mfa", func(t *testing.T) { + ctx := context.Background() + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.mfa_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.mfa_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", false) + }) + + t.Run("case=should be able to get AAL2 session", func(t *testing.T) { + 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, false) + 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(ctx, t, reg, user) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"), testhelpers.ExpectActive("code")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, user) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"), testhelpers.ExpectActive("code")) + } + + 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, text.InfoSelfServiceLoginCodeSentForAuthenticatedUser, gjson.GetBytes(body, "ui.messages.0.id").Int(), "%s", body) + + s := &state{ + flowID: f.GetId(), + identity: user, + client: cl, + testServer: public, + identityEmail: gjson.Get(user.Traits.String(), "email").String(), + } + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Use code") + assert.Contains(t, message.Body, "Login to your account with 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, "Use code") + assert.Contains(t, message.Body, "Login to your account with 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 + switch tc.apiType { + case ApiTypeNative: + phone1 += 1 + case 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)) + + run := func(t *testing.T, identifierField string, identifier string) { + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, user) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, user) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2")) + } + + 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, text.InfoSelfServiceLoginMFA, gjson.GetBytes(body, "ui.messages.0.id").Int(), "%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), "Use code") + assert.Contains(t, message.Body, "Login to your account with 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) + + 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") + } + + t.Run("field=identifier-email", func(t *testing.T) { + run(t, "identifier", email1) + }) + + t.Run("field=address-email", func(t *testing.T) { + run(t, "address", email2) + }) + + 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, reg, false, true) + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), 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) + require.Len(t, gjson.GetBytes(body, "ui.nodes.#(group==totp)").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) + + s := &state{ + flowID: f.GetId(), + identity: identity, + client: cl, + testServer: public, + identityEmail: gjson.Get(identity.Traits.String(), "email").String(), + } + email := testhelpers.RandomEmail() + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", email) + }, false, nil) + + 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 with fast login", func(t *testing.T) { + fixedEmail := fmt.Sprintf("fixed_mfa_test_fast_%s@ory.sh", tc.apiType) + identity := createIdentity(ctx, t, reg, false, false, fixedEmail) + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } + + body, err := json.Marshal(f) + require.NoError(t, err) + + require.EqualValues(t, flow.StateEmailSent, gjson.GetBytes(body, "state").String(), "%s", body) + 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.InfoSelfServiceLoginCodeSentForAuthenticatedUser, "%s", body) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("ui.nodes.5.attributes.value", "id", "created_at", "expires_at", "updated_at", "issued_at", "request_url", "ui.action")) + }) + + t.Run("case=verify initial payload without fast login", func(t *testing.T) { + fixedEmail := fmt.Sprintf("fixed_mfa_test_%s@ory.sh", tc.apiType) + identity := createIdentity(ctx, t, reg, false, true, fixedEmail) + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1")) + } + + body, err := json.Marshal(f) + require.NoError(t, err) + + require.EqualValues(t, flow.StateChooseMethod, gjson.GetBytes(body, "state").String(), "%s", body) + require.Len(t, gjson.GetBytes(body, "ui.nodes.#(group==code)").Array(), 1, "%s", body) + require.Len(t, gjson.GetBytes(body, "ui.nodes.#(group==totp)").Array(), 1, "%s", body) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("ui.nodes.0.attributes.value", "id", "created_at", "expires_at", "updated_at", "issued_at", "request_url", "ui.action")) + }) + + t.Run("case=verify initial payload with fast login and fallback enabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) + t.Run("case=no code credential", func(t *testing.T) { + fixedEmail := fmt.Sprintf("fixed_mfa_fallback_without_cc%s@ory.sh", tc.apiType) + identity := createIdentity(ctx, t, reg, true, false, fixedEmail) + + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } + + body, err := json.Marshal(f) + require.NoError(t, err) + + require.EqualValues(t, flow.StateEmailSent, gjson.GetBytes(body, "state").String(), "%s", body) + 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, text.InfoSelfServiceLoginCodeSentForAuthenticatedUser, gjson.GetBytes(body, "ui.messages.0.id").Int(), "%s", body) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("ui.nodes.5.attributes.value", "id", "created_at", "expires_at", "updated_at", "issued_at", "request_url", "ui.action")) + }) + t.Run("case=with code credential", func(t *testing.T) { + fixedEmail := fmt.Sprintf("fixed_mfa_fallback_with_cc%s@ory.sh", tc.apiType) + identity := createIdentity(ctx, t, reg, false, false, fixedEmail) + + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + f = testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"), testhelpers.ExpectActive("code")) + } + + body, err := json.Marshal(f) + require.NoError(t, err) + + require.EqualValuesf(t, flow.StateEmailSent, gjson.GetBytes(body, "state").String(), "%s", body) + require.Lenf(t, gjson.GetBytes(body, "ui.nodes.#(group==code)").Array(), 1, "%s", body) + require.Lenf(t, gjson.GetBytes(body, "ui.messages").Array(), 1, "%s", body) + require.EqualValuesf(t, text.InfoSelfServiceLoginCodeSentForAuthenticatedUser, gjson.GetBytes(body, "ui.messages.0.id").Int(), "%s", body) + + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("ui.nodes.5.attributes.value", "id", "created_at", "expires_at", "updated_at", "issued_at", "request_url", "ui.action")) + }) + }) + + t.Run("case=using a non existing identity trait results in an error", func(t *testing.T) { + identity := createIdentity(ctx, t, reg, false, false) + var cl *http.Client + var res *http.Response + var err error + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=doesnt_exist") + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=doesnt_exist") + } + require.NoError(t, err) + + body := ioutilx.MustReadAll(res.Body) + if tc.apiType == ApiTypeNative { + body = []byte(gjson.GetBytes(body, "error").Raw) + } + require.Equal(t, "No value found for trait doesnt_exist in the current identity.", 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, reg, false, false) + var cl *http.Client + var res *http.Response + var err error + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, identity) + res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=email_1") + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, identity) + res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=email_1") + } + require.NoError(t, err) + + body := ioutilx.MustReadAll(res.Body) + 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) + }) + }) + }) + } + + t.Run("case=passwordless refresh sends code via address button without method", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + "mfa_enabled": false, + }) + // The refresh address picker is gated behind a feature flag. It must be + // set on the shared config; contextx overrides do not reach the server's + // request handlers. + conf.MustSet(ctx, config.ViperKeyRefreshLoginChooseAddress, true) + t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeyRefreshLoginChooseAddress, nil) }) + + email := "refresh-passwordless-" + testhelpers.RandomEmail() + user := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + user.Traits = identity.Traits(`{"email":"` + email + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, user)) + + cl := testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, user) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(ctx, t, cl, public, false, false, false, false, testhelpers.InitFlowWithRefresh()) + + body, err := json.Marshal(f) + require.NoError(t, err) + // The refresh screen offers a "Send code to " button, not an identifier input. + require.Len(t, gjson.GetBytes(body, `ui.nodes.#(attributes.name=="identifier")`).Array(), 0, "%s", body) + require.EqualValues(t, email, gjson.GetBytes(body, `ui.nodes.#(attributes.name=="address").attributes.value`).String(), "%s", body) + + s := &state{ + flowID: f.GetId(), + identity: user, + client: cl, + testServer: public, + identityEmail: email, + } + + // Submit only the address, exactly as clicking the button does (no method). + s = submitLogin(ctx, t, s, ApiTypeBrowser, func(v *url.Values) { + v.Del("method") + v.Set("address", email) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Use code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + submitLogin(ctx, t, s, ApiTypeBrowser, func(v *url.Values) { + v.Set("address", email) + v.Set("code", loginCode) + }, true, nil) + }) + + t.Run("case=passwordless refresh rejects another identity's address", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + "mfa_enabled": false, + }) + // The refresh address picker is gated behind a feature flag. It must be + // set on the shared config; contextx overrides do not reach the server's + // request handlers. + conf.MustSet(ctx, config.ViperKeyRefreshLoginChooseAddress, true) + t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeyRefreshLoginChooseAddress, nil) }) + + ownEmail := "refresh-own-" + testhelpers.RandomEmail() + own := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + own.Traits = identity.Traits(`{"email":"` + ownEmail + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, own)) + + otherEmail := "refresh-other-" + testhelpers.RandomEmail() + other := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + other.Traits = identity.Traits(`{"email":"` + otherEmail + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, other)) + + cl := testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, own) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(ctx, t, cl, public, false, false, false, false, testhelpers.InitFlowWithRefresh()) + + s := &state{ + flowID: f.GetId(), + identity: own, + client: cl, + testServer: public, + identityEmail: ownEmail, + } + + // Submitting another identity's address must be rejected during refresh. + s = submitLogin(ctx, t, s, ApiTypeBrowser, func(v *url.Values) { + v.Del("method") + v.Set("address", otherEmail) + }, false, nil) + + 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", s.body) + }) + + t.Run("case=passwordless refresh via identifier-first does not send code to foreign address", func(t *testing.T) { + // Enable identifier-first login and disable account-enumeration mitigation + // so the identifier-first fast-login path (FastLogin1FA) actually reaches + // loginSendCode — this is the path the guard protects. Passwordless code + // login is already enabled by the test registry. These settings must be + // set on the shared config (contextx config overrides do not reach the + // server's request handlers), so reset them after the subtest. + ctx := t.Context() + conf.MustSet(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginFlowStyle, nil) + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + // Both identities have only a code credential (created from the schema), + // so code is the single active first-factor credential and FastLogin1FA + // proceeds to send a code. + ownEmail := testhelpers.RandomEmail() + own := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + own.Traits = identity.Traits(`{"email":"` + ownEmail + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, own)) + + otherEmail := testhelpers.RandomEmail() + other := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + other.Traits = identity.Traits(`{"email":"` + otherEmail + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, other)) + + // Start a refresh flow as "own". + cl := testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(ctx, t, reg, own) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(ctx, t, cl, public, false, false, false, false, testhelpers.InitFlowWithRefresh()) + + s := &state{ + flowID: f.GetId(), + identity: own, + client: cl, + testServer: public, + identityEmail: ownEmail, + } + + // Submit via the identifier-first method with the foreign identity's email. + // FastLogin1FA must detect the identity mismatch and return ErrStrategyNotResponsible, + // which keeps the flow in choose_method without sending any code. + s = submitLogin(ctx, t, s, ApiTypeBrowser, func(v *url.Values) { + v.Del("address") + v.Set("method", "identifier_first") + v.Set("identifier", otherEmail) + }, false, nil) + + // The flow must remain in choose_method; no code-sent transition. + require.EqualValues(t, flow.StateChooseMethod, gjson.Get(s.body, "state").String(), "%s", s.body) + + // Assert no login code was dispatched to the foreign address. + messages, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{ + Recipient: otherEmail, + }, []keysetpagination.Option{}) + require.NoError(t, err) + assert.Empty(t, messages, "no courier message must be sent to the foreign address, got: %v", messages) + }) +} + +func TestFormHydration(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + ) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeCodeAuth) + require.NoError(t, err) + fhAAL1, ok := s.(login.AAL1FormHydrator) + require.True(t, ok) + fhAAL2, ok := s.(login.AAL2FormHydrator) + 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) { + t.Helper() + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + passwordlessEnabled := func(ctx context.Context) context.Context { + return contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + "mfa_enabled": false, + }) + } + + mfaEnabled := func(ctx context.Context) context.Context { + return contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": false, + "mfa_enabled": true, + }) + } + + toMFARequest := func(ctx context.Context, 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(ctx, t, 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.Context()), t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, fhAAL1.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.Context()), t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, fhAAL1.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) { + ctx := contextx.WithConfigValue(passwordlessEnabled(t.Context()), config.ViperKeyRefreshLoginChooseAddress, true) + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + f.Refresh = true + // A refresh login is always authenticated. Attach a session whose + // identity has a code address so the strategy can render the + // "Send code to
" buttons. + r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx, t, reg, []byte(`{"email":"refresh-1fa-passwordless@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader() + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + 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.Context()), t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + f.Refresh = true + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("case=address picker disabled falls back to the identifier input", func(t *testing.T) { + // With the refresh address picker feature flag off (the default), + // a 1fa refresh re-asks for the identifier instead of rendering the + // "Send code to
" buttons. + ctx := passwordlessEnabled(t.Context()) + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + f.Refresh = true + r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx, t, reg, []byte(`{"email":"refresh-1fa-fallback@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader() + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + + body, err := json.Marshal(f.UI) + require.NoError(t, err) + assert.Len(t, gjson.GetBytes(body, `nodes.#(attributes.name=="identifier")`).Array(), 1, "%s", body) + assert.Len(t, gjson.GetBytes(body, `nodes.#(attributes.name=="address")#`).Array(), 0, "%s", body) + }) + }) + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + t.Run("using via", func(t *testing.T) { + test := func(ctx context.Context, t *testing.T, email string) { + r, f := newFlow(ctx, t) + toMFARequest(ctx, 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, fhAAL2.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + } + + t.Run("case=code is used for 2fa", func(t *testing.T) { + test(mfaEnabled(t.Context()), t, "PopulateLoginMethodSecondFactor-code-mfa-via-2fa@ory.sh") + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + test(passwordlessEnabled(t.Context()), t, "PopulateLoginMethodSecondFactor-code-mfa-via-passwordless@ory.sh") + }) + }) + + t.Run("without via", func(t *testing.T) { + test := func(ctx context.Context, t *testing.T, traits string) { + r, f := newFlow(ctx, t) + toMFARequest(ctx, 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, fhAAL2.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + } + + t.Run("case=code is used for 2fa", func(t *testing.T) { + ctx := testhelpers.WithDefaultIdentitySchema(mfaEnabled(t.Context()), "file://./stub/code-mfa.identity.schema.json") + test(ctx, t, `{"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(t.Context()), "file://./stub/code-mfa.identity.schema.json") + test(ctx, t, `{"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) { + ctx := mfaEnabled(t.Context()) + r, f := newFlow(ctx, t) + toMFARequest(ctx, t, r, f, `{"email":"foo@ory.sh"}`) + require.NoError(t, fhAAL2.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login and request is 2fa", func(t *testing.T) { + ctx := passwordlessEnabled(t.Context()) + r, f := newFlow(ctx, t) + toMFARequest(ctx, t, r, f, `{"email":"fooaewasd@ory.sh"}`) + require.NoError(t, fhAAL2.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) { + ctx := mfaEnabled(t.Context()) + r, f := newFlow(ctx, t) + toMFARequest(ctx, t, r, f, `{"email":"fooaewdk@ory.sh"}`) + f.Refresh = true + require.NoError(t, fhAAL2.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) { + ctx := passwordlessEnabled(t.Context()) + r, f := newFlow(ctx, t) + toMFARequest(ctx, t, r, f, `{"email":"foojoajweids@ory.sh"}`) + f.Refresh = true + require.NoError(t, fhAAL2.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.Context()), t) + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled(t.Context()), t) + require.NoError(t, fhAAL1.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( + contextx.WithConfigValue(mfaEnabled(t.Context()), config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(passwordlessEnabled(t.Context()), config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.NoError(t, fhAAL1.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.Context()), t) + require.ErrorIs(t, fhAAL1.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.Context()), t) + require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + t.Run("case=identity has code method", func(t *testing.T) { + identifier := x.NewUUID().String() + "@ory.sh" + id := createIdentity(t.Context(), t, reg, false, false, identifier) + + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled(t.Context()), t) + require.ErrorIs(t, fhAAL1.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.Context()), t) + require.NoError(t, fhAAL1.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.Context()), t) + require.ErrorIs(t, fhAAL1.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.Context()), t) + require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} + +func TestCodeLoginWithLoginChallenge(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + ) + reg.SetHydra(hydra.NewFake()) + reg.WithCSRFTokenGenerator(nosurfx.FakeCSRFTokenGenerator) + s := code.NewStrategy(reg) + + loginChallenge := hydra.FakeValidLoginChallenge + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + t.Helper() + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + return r, f + } + + t.Run("case=fetches login challenge on code input state", func(t *testing.T) { + _, f := newFlow(t.Context(), t) + f.OAuth2LoginChallenge = sqlxx.NullString(loginChallenge) + i := createIdentity(t.Context(), t, reg, false, false) + email := gjson.Get(i.Traits.String(), "email").String() + + body := gjson.Parse(`{ + "method": "code", + "identifier": "` + email + `", + "csrf_token": "` + f.CSRFToken + `" + }`) + r := httptest.NewRequest("POST", "/self-service/login/browser", strings.NewReader(body.Raw)) + r.Header.Add("Content-Type", "application/json") + + _, err := s.Login(httptest.NewRecorder(), r, f, nil) + require.ErrorIs(t, err, flow.ErrCompletedByStrategy) + require.NotNil(t, f.HydraLoginRequest) + + messages, err := reg.CourierPersister().NextMessages(t.Context(), 10) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.Equal(t, loginChallenge, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.challenge").String()) + assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.client_id").String()) + assert.Equal(t, hydra.FakeClientName, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.client_name").String()) + assert.Equal(t, hydra.FakeClientLogoURI, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.logo_uri").String()) + }) + + t.Run("case=returns error if login challenge is invalid", func(t *testing.T) { + _, f := newFlow(t.Context(), t) + f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeInvalidLoginChallenge) + i := createIdentity(t.Context(), t, reg, false, false) + email := gjson.Get(i.Traits.String(), "email").String() + + body := gjson.Parse(`{ + "method": "code", + "identifier": "` + email + `", + "csrf_token": "` + f.CSRFToken + `" + }`) + r := httptest.NewRequest("POST", "/self-service/login/browser", strings.NewReader(body.Raw)) + r.Header.Add("Content-Type", "application/json") + + _, err := s.Login(httptest.NewRecorder(), r, f, nil) + require.ErrorIs(t, err, herodot.ErrBadRequest()) + require.Nil(t, f.HydraLoginRequest) + + // Pin that the invalid login challenge aborts before any one-time code is queued. + _, err = reg.CourierPersister().NextMessages(t.Context(), 1) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) +} diff --git a/selfservice/strategy/code/strategy_mfa.go b/selfservice/strategy/code/strategy_mfa.go new file mode 100644 index 0000000..ebbc7fb --- /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 0000000..5a97b88 --- /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 new file mode 100644 index 0000000..bd13214 --- /dev/null +++ b/selfservice/strategy/code/strategy_recovery.go @@ -0,0 +1,900 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "net/http" + "net/url" + "slices" + "strings" + "time" + + "github.com/ory/kratos/x/redir" + + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/sqlcon" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/x/decoderx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +func (s *Strategy) RecoveryStrategyID() string { + return string(recovery.RecoveryStrategyCode) +} + +func (s *Strategy) IsPrimary() bool { + return true +} + +// This builds the initial UI (first recovery screen). +func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) error { + switch f.State { + case flow.StateChooseMethod: + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + f.UI.GetNodes().Upsert( + node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + case flow.StateRecoveryAwaitingAddress: + // re-initialize the UI with a "clean" new state + f.UI = &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(), + Nodes: f.UI.Nodes, + } + f.UI.Nodes.ClearTransientNodes() + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + f.UI.GetNodes().Append( + node.NewInputField("recovery_address", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewRecoveryAskAnyRecoveryAddress()), + ) + default: + // Unreachable. + return errors.Errorf("unreachable state: %s", f.State) + } + + f.UI. + GetNodes(). + Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + return nil +} + +// Update Recovery Flow with Code Method +// +// swagger:model updateRecoveryFlowWithCodeMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRecoveryFlowWithCodeMethod struct { + // The email address of the account to recover + // + // If the email belongs to a valid account, a recovery email will be sent. + // + // If you want to notify the email address if the account does not exist, see + // the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/account-recovery-password-reset#attempted-recovery-notifications) + // + // If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. + // + // format: email + // required: false + Email string `json:"email" form:"email"` + + // Code from the recovery email + // + // If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + // + // required: false + Code string `json:"code" form:"code"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `form:"csrf_token" json:"csrf_token"` + + // Method is the method that should be used for this recovery flow + // + // Allowed values are `link` and `code`. + // + // 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"` + + // A recovery address that is registered for the user. + // It can be an email, a phone number (to receive the code via SMS), etc. + // Used in RecoveryV2. + RecoveryAddress string `json:"recovery_address" form:"recovery_address"` + + // If there are multiple addresses registered for the user, a choice is presented and this field + // stores the result of this choice. + // Addresses are 'masked' (never sent in full to the client and shown partially in the UI) since at this point in the recovery flow, + // the user has not yet proven that it knows the full address and we want to avoid + // information exfiltration. + // So for all intents and purposes, the value of this field should be treated as an opaque identifier. + // Used in RecoveryV2. + RecoverySelectAddress string `json:"recovery_select_address" form:"recovery_select_address"` + + // If there are multiple recovery addresses registered for the user, and the initially provided address + // is different from the address chosen when the choice (of masked addresses) is presented, then we need to make sure + // that the user actually knows the full address to avoid information exfiltration, so we ask for the full address. + // Used in RecoveryV2. + RecoveryConfirmAddress string `json:"recovery_confirm_address" form:"recovery_confirm_address"` + + // Set to "previous" to go back in the flow, meaningfully. + // Used in RecoveryV2. + Screen string `json:"screen" form:"screen"` +} + +func (s *Strategy) isCodeFlow(f *recovery.Flow) bool { + value, err := f.Active.Value() + if err != nil { + return false + } + return value == s.NodeGroup().String() +} + +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") + 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) + } + + body, err := s.decodeRecovery(r) + if err != nil { + return s.HandleRecoveryError(w, r, nil, body, err) + } + + f.TransientPayload = body.TransientPayload + + if f.DangerousSkipCSRFCheck { + s.deps.Logger(). + WithRequest(r). + Debugf("A recovery flow with `DangerousSkipCSRFCheck` set has been submitted, skipping anti-CSRF measures.") + } else if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, body.CSRFToken); err != nil { + // If a CSRF violation occurs the flow is most likely FUBAR, as the user either lost the CSRF token, or an attack occured. + // In this case, we just issue a new flow and "abandon" the old flow. + return s.retryRecoveryFlow(w, r, flow.TypeBrowser, RetryWithError(err)) + } + + sID := s.RecoveryStrategyID() + + f.UI.ResetMessages() + + // NOTE: This is implicitly looking at the state machine (for Recovery v1), by inspecting which fields are present, + // instead of inspecting the state explicitly. + // For Recovery v2 we inspect the state explicitly, a few lines below. + if !flow.IsStateRecoveryV2(f.State) { + // If the email is not present in the submission body, the user needs a new code via resend + if f.State != flow.StateChooseMethod && len(body.Email) == 0 { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, sID, s.deps); err != nil { + return s.HandleRecoveryError(w, r, nil, body, err) + } + return s.recoveryUseCode(w, r, body, f) + } + } + + if err := s.deps.SessionManager().SessionActiveForRequest(ctx, r); err == nil { + // User is already logged in + if x.IsJSONRequest(r) { + session.RespondWithJSONErrorOnAuthenticated(s.deps.Writer(), recovery.ErrAlreadyLoggedIn())(w, r) + } else { + session.RedirectOnAuthenticated(s.deps)(w, r) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + // Recovery V1 sets some magic fields in the UI and inspects them in the body, e.g. `method`. + // This is brittle and rendered unnecessary in Recovery V2 by properly inspecting the `state` (and the CSRF token). + if !flow.IsStateRecoveryV2(f.State) { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, body.Method, s.deps); err != nil { + return s.HandleRecoveryError(w, r, nil, body, err) + } + } + + recoveryFlow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow)) + if err != nil { + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) + } + + if err := recoveryFlow.Valid(s.deps.Clock()); err != nil { + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) + } + + if body.Screen == "previous" { + return s.recoveryV2HandleGoBack(r, f, body) + } + + switch recoveryFlow.State { + case flow.StateChooseMethod, + flow.StateEmailSent: + return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) + case flow.StatePassedChallenge: + // was already handled, do not allow retry + return s.retryRecoveryFlow(w, r, recoveryFlow.Type, RetryWithMessage(text.NewErrorValidationRecoveryRetrySuccess())) + + // Recovery V2. + case flow.StateRecoveryAwaitingAddress: + return s.recoveryV2HandleStateAwaitingAddress(r, recoveryFlow, body) + case flow.StateRecoveryAwaitingAddressChoice: + return s.recoveryV2HandleStateAwaitingAddressChoice(r, recoveryFlow, body) + case flow.StateRecoveryAwaitingAddressConfirm: + return s.recoveryV2HandleStateConfirmingAddress(r, recoveryFlow, body) + case flow.StateRecoveryAwaitingCode: + return s.recoveryV2HandleStateAwaitingCode(w, r, recoveryFlow, body) + + default: + return s.retryRecoveryFlow(w, r, recoveryFlow.Type, RetryWithMessage(text.NewErrorValidationRecoveryStateFailure())) + } +} + +func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity) error { + ctx := r.Context() + + f.UI.Messages.Clear() + f.State = flow.StatePassedChallenge + f.RecoveredIdentityID = uuid.NullUUID{ + UUID: id.ID, + Valid: true, + } + + if f.Type == flow.TypeBrowser { + f.SetCSRFToken(s.deps.CSRFHandler().RegenerateToken(w, r)) + } + + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(ctx, f); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + 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)) + } + + if err := s.deps.RecoveryExecutor().PostRecoveryHook(w, r, f, sess); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + switch f.Type { + case flow.TypeBrowser: + if err := s.deps.SessionManager().UpsertAndIssueCookie(ctx, w, r, sess); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + case flow.TypeAPI: + if err := s.deps.SessionPersister().UpsertSession(r.Context(), sess); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSetToken(sess.Token)) + } + + if f.ShouldSkipSettingsFlow() { + if err := redir.SecureContentNegotiationRedirection(w, r, f, f.RequestURL, s.deps.Writer(), s.deps.Config()); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + sf, err := s.deps.SettingsHandler().NewFlow(ctx, w, r, sess.Identity, sess, f.Type) + if err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + returnToURL := s.deps.Config().SelfServiceFlowRecoveryReturnTo(r.Context(), nil) + returnTo := "" + if returnToURL != nil { + returnTo = returnToURL.String() + } + sf.RequestURL, err = redir.TakeOverReturnToParameter(f.RequestURL, sf.RequestURL, returnTo) + if err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + config := s.deps.Config() + + sf.UI.Messages.Set(text.NewRecoverySuccessful(s.deps.Clock(), s.deps.Clock().Now().Add(config.SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)))) + if err := s.deps.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), sf); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + if s.deps.Config().UseContinueWithTransitions(ctx) { + redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String() + switch { + 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, redirectTo, http.StatusSeeOther) + } + } else { + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "no_continue_with_transition_recovery_issue_session")) + if x.IsJSONRequest(r) { + s.deps.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String())) + } else { + http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther) + } + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +// NOTE: This function handles two cases: +// - A code was submitted: try to use it +// - No code was submitted: delete all existing codes, re-generate a new one, send it. +// This corresponds to the user clicking on the 're-send code' button. +func (s *Strategy) recoveryUseCode(w http.ResponseWriter, r *http.Request, body *recoverySubmitPayload, f *recovery.Flow) error { + ctx := r.Context() + code, err := s.deps.RecoveryCodePersister().UseRecoveryCode(ctx, f.ID, body.Code) + if errors.Is(err, ErrCodeNotFound()) { + f.UI.Messages.Clear() + f.UI.Messages.Add(text.NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed()) + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(ctx, f); err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } else { + s.deps.Writer().Write(w, r, f) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } else if err != nil { + return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) + } + + // Important to expand everything here, as we need the data for recovery. + recovered, err := s.deps.IdentityPool().GetIdentity(ctx, code.IdentityID, identity.ExpandEverything) + if err != nil { + return s.HandleRecoveryError(w, r, f, nil, err) + } + + // mark address as verified only for a self-service flow + if code.CodeType == RecoveryCodeTypeSelfService { + if err := s.markRecoveryAddressVerified(w, r, f, recovered, code.RecoveryAddress); err != nil { + return s.HandleRecoveryError(w, r, f, body, err) + } + } + + return s.recoveryIssueSession(w, r, f, recovered) +} + +type retry struct { + err error + message *text.Message +} + +type RetryOption func(*retry) + +func RetryWithError(err error) RetryOption { + return func(r *retry) { + r.err = err + } +} + +func RetryWithMessage(msg *text.Message) RetryOption { + return func(r *retry) { + r.message = msg + } +} + +func (s *Strategy) retryRecoveryFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...RetryOption) error { + retryOptions := retry{} + + for _, o := range opts { + o(&retryOptions) + } + + logger := s.deps.Logger().WithRequest(r).WithError(retryOptions.err) + if retryOptions.message != nil { + logger = logger.WithField("message", retryOptions.message) + } + logger.Debug("A recovery flow is being retried because a validation error occurred.") + + ctx := r.Context() + config := s.deps.Config() + + f, err := recovery.NewFlow(s.deps, config.SelfServiceFlowRecoveryRequestLifespan(ctx), s.deps.CSRFHandler().RegenerateToken(w, r), r, recovery.Strategies{s}, ft) + if err != nil { + return err + } + + if retryOptions.message != nil { + f.UI.Messages.Add(retryOptions.message) + } + + if retryOptions.err != nil { + if expired := new(flow.ExpiredError); errors.As(retryOptions.err, &expired) { + f.UI.Messages.Add(text.NewErrorValidationRecoveryFlowExpired(s.deps.Clock(), expired.ExpiredAt)) + } else if err := f.UI.ParseError(node.CodeGroup, retryOptions.err); err != nil { + return err + } + } + if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f); err != nil { + return err + } + + if s.deps.Config().UseContinueWithTransitions(ctx) { + switch { + case x.IsJSONRequest(r): + rErr := new(herodot.DefaultError) + if !errors.As(retryOptions.err, &rErr) { + rErr = rErr.WithError(retryOptions.err.Error()) + } + s.deps.Writer().WriteError(w, r, flow.ErrorWithContinueWith(rErr, flow.NewContinueWithRecoveryUI(f))) + default: + http.Redirect(w, r, f.AppendTo(config.SelfServiceFlowRecoveryUI(ctx)).String(), http.StatusSeeOther) + } + } else { + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "no_continue_with_transition_recovery_retry_flow_handler")) + if x.IsJSONRequest(r) { + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(config.SelfPublicURL(ctx), + recovery.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, f.AppendTo(config.SelfServiceFlowRecoveryUI(ctx)).String(), http.StatusSeeOther) + } + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func AddressToHashBase64(address string) string { + hash := sha256.Sum256([]byte(address)) + return base64.StdEncoding.EncodeToString(hash[:]) +} + +func (s *Strategy) recoveryV2HandleStateAwaitingAddress(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + if f.State != flow.StateRecoveryAwaitingAddress { + return errors.Errorf("unreachable state: %s", f.State) + } + + if len(body.RecoveryAddress) == 0 { + return schema.NewRequiredError("#/recovery_address", "recovery_address") + } + + // Need to retrieve all possible recovery addresses and present a choice. + recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress) + // Real error. + if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return err + } + + // No rows returned. + if len(recoveryAddresses) == 0 { + // To avoid an attacker from using this case to probe for existing addresses, we pretend it exists. + // This is the same behavior as in Recovery V1. + recoveryAddresses = append(recoveryAddresses, body.RecoveryAddress) + } + + f.State = flow.StateRecoveryAwaitingAddressChoice + + if len(recoveryAddresses) == 1 && recoveryAddresses[0] == body.RecoveryAddress { + // Skip two states for convenience: + // - No need to present a choice with only one option + // - No need to ask for the full address if there is only one and it was just provided in full + + body.RecoveryConfirmAddress = body.RecoveryAddress + f.State = flow.StateRecoveryAwaitingAddressConfirm + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return err + } + return s.recoveryV2HandleStateConfirmingAddress(r, f, body) + } + + // re-initialize the UI with a "clean" new state + f.UI = &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(), + Nodes: f.UI.Nodes, + } + f.UI.Nodes.ClearTransientNodes() + + f.UI.SetCSRF(f.CSRFToken) + + f.State = flow.StateRecoveryAwaitingAddressChoice + f.UI.Messages.Set(text.NewRecoveryAskToChooseAddress()) + + slices.Sort(recoveryAddresses) + + for _, a := range recoveryAddresses { + // NOTE: Only send the masked value and the hash, to avoid information exfiltration. + // Why the hash? So that we can recognize later, when the user chooses the masked address in the list, + // that the chosen masked address is the `recovery_address` provided in the beginning, + // and then we do not ask again the user to provide it in full. + hashBase64 := AddressToHashBase64(a) + f.UI.GetNodes().Append(node.NewInputField("recovery_select_address", hashBase64, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(&text.Message{ + ID: text.InfoNodeLabel, + Text: MaskAddress(a), + Type: text.Info, + })) + } + + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden)) + // No back button here because there is no point for the user. + + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return err + } + + return nil +} + +func (s *Strategy) recoveryV2HandleStateAwaitingAddressChoice(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + if f.State != flow.StateRecoveryAwaitingAddressChoice { + return errors.Errorf("unreachable state: %s", f.State) + } + + if len(body.RecoverySelectAddress) == 0 { + return schema.NewRequiredError("#/recovery_select_address", "recovery_select_address") + } + + if len(body.RecoveryAddress) == 0 { + return schema.NewRequiredError("#/recovery_address", "recovery_address") + } + + // Is the chosen masked address the same as the address provided in full at the beginning? + // If yes, then do not ask it again in full. + // Technically we check `hash(recovery_address) == recovery_select_address` and + // `recovery_select_address` is `hash(recovery_address)`. + hashBase64 := AddressToHashBase64(body.RecoveryAddress) + + // Better safe than sorry, use constant time comparison. + if subtle.ConstantTimeCompare([]byte(hashBase64), []byte(body.RecoverySelectAddress)) == 1 { + // Skip a state: do not ask the user again to provide the full address. + body.RecoveryConfirmAddress = body.RecoveryAddress + f.State = flow.StateRecoveryAwaitingAddressConfirm + return s.recoveryV2HandleStateConfirmingAddress(r, f, body) + } + + // re-initialize the UI with a "clean" new state + f.UI = &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(), + Nodes: f.UI.Nodes, + } + f.UI.Nodes.ClearTransientNodes() + f.UI.SetCSRF(f.CSRFToken) + + f.State = flow.StateRecoveryAwaitingAddressConfirm + f.UI.Messages.Set(text.NewRecoveryAskForFullAddress()) + + // Retrieve the selected recovery address in plaintext to determine the input label and type. + var plaintextRecoveryAddress string + recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress) + if err == nil { + for _, a := range recoveryAddresses { + if subtle.ConstantTimeCompare([]byte(AddressToHashBase64(a)), []byte(body.RecoverySelectAddress)) == 1 { + plaintextRecoveryAddress = a + break + } + } + } + if plaintextRecoveryAddress == "" { + return herodot.ErrBadRequest(). + WithReason("The selected recovery address is not valid."). + WithDebug("The selected recovery address does not match any of the known recovery addresses.") + } + + var inputType node.UiNodeInputAttributeType + var label *text.Message + if strings.ContainsRune(plaintextRecoveryAddress, '@') { + inputType = node.InputAttributeTypeEmail + label = text.NewInfoNodeInputEmail() + } else { + inputType = node.InputAttributeTypeTel + label = text.NewInfoNodeInputPhoneNumber() + } + + f.UI.Nodes.Append(node.NewInputField("recovery_confirm_address", body.RecoveryConfirmAddress, node.CodeGroup, inputType, node.WithRequiredInputAttribute). + WithMetaLabel(label), + ) + f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden)) + + f.UI. + GetNodes(). + Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + + buttonScreen := node.NewInputField("screen", "previous", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewRecoveryBack()) + f.UI.GetNodes().Append(buttonScreen) + + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return err + } + + return nil +} + +func (s *Strategy) recoveryV2HandleStateConfirmingAddress(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + if f.State != flow.StateRecoveryAwaitingAddressConfirm { + return errors.Errorf("unreachable state: %s", f.State) + } + + if len(body.RecoveryConfirmAddress) == 0 { + return schema.NewRequiredError("#/recovery_confirm_address", "recovery_confirm_address") + } + + if err := s.deps.RecoveryCodePersister().DeleteRecoveryCodesOfFlow(r.Context(), f.ID); err != nil { + return err + } + + f.TransientPayload = body.TransientPayload + + // NOTE: We do not fetch the db address here. We only (try to) send the code to the user provided address. + // That way we avoid information exfiltration. + // `SendRecoveryCode` will anyway check by itself if the provided address is a known address or not. + if err := s.deps.CodeSender().SendRecoveryCode(r.Context(), f, hackyInferChannel(body.RecoveryConfirmAddress), body.RecoveryConfirmAddress, r.Header); err != nil { + if !errors.Is(err, ErrUnknownAddress()) { + return err + } + + // Continue execution + } + + // re-initialize the UI with a "clean" new state + f.UI = &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(), + Nodes: f.UI.Nodes, + } + f.UI.Nodes.ClearTransientNodes() + f.UI.SetCSRF(f.CSRFToken) + + f.State = flow.StateRecoveryAwaitingCode + + uiText := text.NewRecoveryCodeRecoverySelectAddressSent(MaskAddress(body.RecoveryConfirmAddress)) + + f.UI.Messages.Set(uiText) + 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 + a.Autocomplete = node.InputAttributeAutocompleteOneTimeCode + })). + WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), + ) + + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue()), + ) + + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + + // Required to make 'resend' work. + f.UI.Nodes.Append(node.NewInputField("recovery_confirm_address", body.RecoveryConfirmAddress, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden)) + + buttonScreen := node.NewInputField("screen", "previous", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewRecoveryBack()) + f.UI.GetNodes().Append(buttonScreen) + + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return err + } + + return nil +} + +func (s *Strategy) recoveryV2HandleStateAwaitingCode(w http.ResponseWriter, r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + if f.State != flow.StateRecoveryAwaitingCode { + return errors.Errorf("unreachable state: %s", f.State) + } + + if len(body.Code) == 0 { + // The 're-send' button was clicked. We handle it as if the user first arrived at the state `RecoveryV2StateAwaitingAddressConfirm`. + // That will invalidate all existing codes and send a new code. + f.State = flow.StateRecoveryAwaitingAddressConfirm + return s.recoveryV2HandleStateConfirmingAddress(r, f, body) + } else { + return s.recoveryUseCode(w, r, body, f) + } +} + +func (s *Strategy) recoveryV2HandleGoBack(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + // If no address choice needs to take place, just go to the first screen. + recoveryAddresses, _ := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress) + if len(recoveryAddresses) <= 1 { + f.State = flow.StateRecoveryAwaitingAddress + err := s.PopulateRecoveryMethod(r, f) + if err != nil { + return err + } + + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return err + } + + } + + switch f.State { + // Go back to the second screen (choose an address) by essentially going to the first screen + // and re-submitting the form (to arrive at the second screen). + // This contraption is necessary since the UI nodes are stored in the database and not generated on the fly. + // So simply redirecting to a previous screen (as in: 'web page') would do nothing, it would just show the same UI. + // This way we force the UI generation code to re-run and the new UI nodes to be stored to the database. + case flow.StateRecoveryAwaitingCode: + fallthrough + case flow.StateRecoveryAwaitingAddressConfirm: + // Reset some body fields since we are going to (almost) the beginning of the flow. + body.RecoveryConfirmAddress = "" + body.RecoverySelectAddress = "" + body.Screen = "" + + f.State = flow.StateRecoveryAwaitingAddress + + return s.recoveryV2HandleStateAwaitingAddress(r, f, body) + default: + // Should not trigger, but do something sensible: start from scratch. + return s.PopulateRecoveryMethod(r, f) + } +} + +// recoveryHandleFormSubmission handles the submission of an address for recovery +func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error { + if len(body.Email) == 0 { + return s.HandleRecoveryError(w, r, f, body, schema.NewRequiredError("#/email", "email")) + } + + ctx := r.Context() + config := s.deps.Config() + + if err := flow.EnsureCSRF(s.deps, r, f.Type, config.DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, body.CSRFToken); err != nil { + return s.HandleRecoveryError(w, r, f, body, err) + } + + if err := s.deps.RecoveryCodePersister().DeleteRecoveryCodesOfFlow(ctx, f.ID); err != nil { + return s.HandleRecoveryError(w, r, f, body, err) + } + + f.TransientPayload = body.TransientPayload + if err := s.deps.CodeSender().SendRecoveryCode(ctx, f, identity.AddressTypeEmail, body.Email, r.Header); err != nil { + if !errors.Is(err, ErrUnknownAddress()) { + return s.HandleRecoveryError(w, r, f, body, err) + } + // Continue execution + } + + // re-initialize the UI with a "clean" new state + f.UI = &container.Container{ + Method: "POST", + Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(), + Nodes: f.UI.Nodes, + } + f.UI.Nodes.ClearTransientNodes() + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent + f.UI.Messages.Set(text.NewRecoveryEmailWithCodeSent()) + 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 + a.Autocomplete = node.InputAttributeAutocompleteOneTimeCode + })). + WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), + ) + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + + f.UI. + GetNodes(). + Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return s.HandleRecoveryError(w, r, f, body, err) + } + + return nil +} + +func (s *Strategy) markRecoveryAddressVerified(w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity, recoveryAddress *identity.RecoveryAddress) error { + for k, v := range id.VerifiableAddresses { + if v.Value == recoveryAddress.Value { + id.VerifiableAddresses[k].Verified = true + id.VerifiableAddresses[k].VerifiedAt = new(sqlxx.NullTime(time.Now().UTC())) + id.VerifiableAddresses[k].Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &id.VerifiableAddresses[k], "verified", "verified_at", "status"); err != nil { + return s.HandleRecoveryError(w, r, f, nil, err) + } + } + } + + return nil +} + +func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, fl *recovery.Flow, body *recoverySubmitPayload, err error) error { + if fl != nil { + if flow.IsStateRecoveryV2(fl.State) { + // Unreachable: RecoveryV2 never uses this function. + return err + } + email := "" + if body != nil { + email = body.Email + } + + fl.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + fl.UI.GetNodes().Upsert( + node.NewInputField("email", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } + + return err +} + +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"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` + + // Used in RecoveryV2. + RecoveryAddress string `json:"recovery_address" form:"recovery_address"` + RecoverySelectAddress string `json:"recovery_select_address" form:"recovery_select_address"` + RecoveryConfirmAddress string `json:"recovery_confirm_address" form:"recovery_confirm_address"` + Screen string `json:"screen" form:"screen"` +} + +func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { + var body recoverySubmitPayload + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(recoveryMethodSchema) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := decoderx.Decode(r, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return nil, errors.WithStack(err) + } + + return &body, nil +} diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go new file mode 100644 index 0000000..806a741 --- /dev/null +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -0,0 +1,250 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" + "github.com/ory/kratos/x/redir" + "github.com/ory/pop/v6" + "github.com/ory/x/decoderx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" +) + +const ( + RouteAdminCreateRecoveryCode = "/recovery/code" +) + +func (s *Strategy) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + s.deps.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryCode) + public.POST(RouteAdminCreateRecoveryCode, redir.RedirectToAdminRoute(s.deps)) +} + +func (s *Strategy) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + wrappedCreateRecoveryCode := strategy.IsDisabled(s.deps, s.RecoveryStrategyID(), s.createRecoveryCodeForIdentity) + admin.POST(RouteAdminCreateRecoveryCode, wrappedCreateRecoveryCode) +} + +// Create Recovery Code for Identity Parameters +// +// swagger:parameters createRecoveryCodeForIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createRecoveryCodeForIdentity struct { + // in: body + Body createRecoveryCodeForIdentityBody +} + +// Create Recovery Code for Identity Request Body +// +// swagger:model createRecoveryCodeForIdentityBody +type createRecoveryCodeForIdentityBody struct { + // Identity to Recover + // + // The identity's ID you wish to recover. + // + // required: true + IdentityID uuid.UUID `json:"identity_id"` + + // 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`. + // + // + // pattern: ^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$ + // example: + // - 1h + // - 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 +// +// Used when an administrator creates a recovery code for an identity. +// +// swagger:model recoveryCodeForIdentity +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type recoveryCodeForIdentity struct { + // RecoveryLink with flow + // + // This link opens the recovery UI with an empty `code` field. + // + // required: true + // format: uri + RecoveryLink string `json:"recovery_link"` + + // RecoveryCode is the code that can be used to recover the account + // + // required: true + RecoveryCode string `json:"recovery_code"` + + // Expires At is the timestamp of when the recovery flow expires + // + // The timestamp when the recovery code expires. + ExpiresAt time.Time `json:"expires_at"` +} + +// swagger:route POST /admin/recovery/code identity 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. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 201: recoveryCodeForIdentity +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http.Request) { + var p createRecoveryCodeForIdentityBody + if err := decoderx.Decode(r, &p, decoderx.HTTPJSONDecoder()); err != nil { + s.deps.Writer().WriteError(w, r, err) + return + } + + ctx := r.Context() + config := s.deps.Config() + + expiresIn := config.SelfServiceCodeMethodLifespan(ctx) + if len(p.ExpiresIn) > 0 { + // If an expiration of the code was supplied use it instead of the default duration + var err error + expiresIn, err = time.ParseDuration(p.ExpiresIn) + if err != nil { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest(). + WithReasonf(`Unable to parse "expires_in" as a duration string (e.g. "1h", "1m30s", "500ms"): %s`, p.ExpiresIn))) + return + } + } + + 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(s.deps, expiresIn, s.deps.GenerateCSRFToken(r), r, recovery.Strategies{s}, flowType) + if err != nil { + s.deps.Writer().WriteError(w, r, err) + return + } + 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, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + a.Autocomplete = node.InputAttributeAutocompleteOneTimeCode + })). + WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), + ) + rawCode := GenerateCode() + + recoveryFlow.UI.Nodes. + Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + id, err := s.deps.IdentityPool().GetIdentity(ctx, p.IdentityID, identity.ExpandDefault) + if notFoundErr := sqlcon.ErrNoRows(); errors.As(err, ¬FoundErr) { + s.deps.Writer().WriteError(w, r, notFoundErr.WithReasonf("could not find identity")) + return + } else if err != nil { + s.deps.Writer().WriteError(w, r, err) + return + } + + if err := s.deps.TransactionalPersisterProvider().Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { + return err + } + + if _, err := s.deps.RecoveryCodePersister().CreateRecoveryCode(ctx, &CreateRecoveryCodeParams{ + RawCode: rawCode, + CodeType: RecoveryCodeTypeAdmin, + ExpiresIn: expiresIn, + FlowID: recoveryFlow.ID, + IdentityID: id.ID, + }); err != nil { + return err + } + + return nil + }); err != nil { + s.deps.Writer().WriteError(w, r, err) + return + } + + trace.SpanFromContext(r.Context()).AddEvent( + events.NewRecoveryInitiatedByAdmin(ctx, recoveryFlow.ID, id.ID, flowType.String(), "code"), + ) + + s.deps.Logger(). + WithField("identity_id", id.ID). + WithSensitiveField("recovery_code", rawCode). + Info("A recovery code has been created.") + + body := &recoveryCodeForIdentity{ + ExpiresAt: recoveryFlow.ExpiresAt.UTC(), + RecoveryLink: urlx.CopyWithQuery( + s.deps.Config().SelfServiceFlowRecoveryUI(ctx), + url.Values{ + "flow": {recoveryFlow.ID.String()}, + }).String(), + RecoveryCode: rawCode, + } + + s.deps.Writer().WriteCode(w, r, http.StatusCreated, body, herodot.UnescapedHTML) +} diff --git a/selfservice/strategy/code/strategy_recovery_admin_test.go b/selfservice/strategy/code/strategy_recovery_admin_test.go new file mode 100644 index 0000000..3691a96 --- /dev/null +++ b/selfservice/strategy/code/strategy_recovery_admin_test.go @@ -0,0 +1,254 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/pkg/errors" + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/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/x" + "github.com/ory/x/configx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/snapshotx" +) + +func TestAdminStrategy(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(defaultConfig), + configx.WithValue(config.ViperKeyUseContinueWithTransitions, true), + ) + + _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + publicTS, adminTS := testhelpers.NewKratosServer(t, reg) + adminSDK := testhelpers.NewSDKClient(adminTS) + + type createCodeParams = kratos.CreateRecoveryCodeForIdentityBody + createCode := func(params createCodeParams) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { + return adminSDK.IdentityAPI. + CreateRecoveryCodeForIdentity(context.Background()). + CreateRecoveryCodeForIdentityBody(params).Execute() + } + + t.Run("no panic on empty body #1384", func(t *testing.T) { + ctx := context.Background() + s, ps, err := reg.RecoveryStrategies(ctx).ActiveStrategies("code") + require.NoError(t, err) + require.Len(t, s, 1) + w := httptest.NewRecorder() + r := &http.Request{URL: new(url.URL)} + f, err := recovery.NewFlow(reg, time.Minute, "", r, s, flow.TypeBrowser) + require.NoError(t, err) + require.NotPanics(t, func() { + require.Error(t, ps.(*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(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(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: new("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(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: new("-1h")}) + require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) + snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) + }) + + submitRecoveryCode := func(t *testing.T, client *http.Client, link string, code string) []byte { + t.Helper() + 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 = client.PostForm(action, url.Values{ + "code": {code}, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + return ioutilx.MustReadAll(res.Body) + } + + assertEmailNotVerified := func(t *testing.T, email string) { + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, 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(createCodeParams{IdentityId: id.ID.String()}) + require.NoError(t, err) + + require.NotEmpty(t, code.RecoveryLink) + require.Contains(t, code.RecoveryLink, "flow=") + require.NotContains(t, code.RecoveryLink, "code=") + require.NotEmpty(t, code.RecoveryCode) + require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(t.Context())))) + + client := new(*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) + require.NoError(t, err) + 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) { + recoveryEmail := "recover.expired@ory.sh" + id := identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, recoveryEmail))} + + require.NoError(t, reg.IdentityManager().Create(context.Background(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String(), ExpiresIn: new("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(t.Context())))) + + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").Str, "The recovery flow expired", "%s", body) + + // The recovery address should not be verified if the flow was initiated by the admins + 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) { + recoveryEmail := "recoverme@ory.sh" + id := identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, recoveryEmail))} + + require.NoError(t, reg.IdentityManager().Create(context.Background(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + 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(t.Context())+time.Second))) + + 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.") + + // The recovery address should be verified if the flow was initiated by the admins + assertEmailNotVerified(t, recoveryEmail) + }) + + t.Run("description=should respect custom expires_in in response", func(t *testing.T) { + id := identity.Identity{Traits: identity.Traits(`{}`)} + require.NoError(t, reg.IdentityManager().Create(context.Background(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String(), ExpiresIn: new("24h")}) + require.NoError(t, err) + + // The response expires_at must reflect the requested 24h, not the default 1h lifespan. + require.WithinDuration(t, time.Now().Add(24*time.Hour), *code.ExpiresAt, time.Hour) + }) + + 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(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: new("1h")}) + require.NoError(t, err) + c2, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: new("1h")}) + require.NoError(t, err) + code2 := c2.RecoveryCode + require.NotEmpty(t, code2) + + 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.") + }) + + t.Run("case=form should not contain email field when creating recovery code", func(t *testing.T) { + email := testhelpers.RandomEmail() + i := createIdentityToRecover(t, reg, email) + + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: new("1h")}) + require.NoError(t, err) + + res, err := testhelpers.NewTestClient(t).Get(c1.RecoveryLink) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + 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: new(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_nonrfc_flow_test.go b/selfservice/strategy/code/strategy_recovery_nonrfc_flow_test.go new file mode 100644 index 0000000..7b24d35 --- /dev/null +++ b/selfservice/strategy/code/strategy_recovery_nonrfc_flow_test.go @@ -0,0 +1,68 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "maps" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/x/configx" +) + +// TestNonRFCRecoveryFullFlow drives account recovery end to end through the +// real router for an identity whose email is a schema-approved non-RFC carrier +// address, covering submission decode, address lookup, and courier queue. +func TestNonRFCRecoveryFullFlow(t *testing.T) { + t.Parallel() + ctx := context.Background() + + cfg := map[string]any{ + config.ViperKeySelfServiceRecoveryEnabled: true, + config.ViperKeySelfServiceRecoveryUse: "code", + } + maps.Copy(cfg, testhelpers.DefaultIdentitySchemaConfig("file://./stub/recovery.nonrfc.schema.json")) + maps.Copy(cfg, testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)) + maps.Copy(cfg, testhelpers.MethodEnableConfig(identity.CredentialsTypeCodeAuth, true)) + + conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(cfg)) + + _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + const email = "foo.@docomo.ne.jp" + + id := &identity.Identity{ + Traits: identity.Traits(`{"email":"` + email + `"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, id, identity.ManagerAllowWriteProtectedTraits)) + + client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper + + testhelpers.SubmitRecoveryForm(t, false, false, client, public, + func(v url.Values) { + v.Set("email", email) + v.Set("method", "code") + }, + http.StatusOK, + conf.SelfServiceFlowRecoveryUI(ctx).String(), + ) + + msg := testhelpers.CourierExpectMessage(ctx, t, reg, email, "recover access to your account") + require.Equal(t, email, msg.Recipient) +} diff --git a/selfservice/strategy/code/strategy_recovery_nonrfc_test.go b/selfservice/strategy/code/strategy_recovery_nonrfc_test.go new file mode 100644 index 0000000..02410fb --- /dev/null +++ b/selfservice/strategy/code/strategy_recovery_nonrfc_test.go @@ -0,0 +1,56 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/x/decoderx" +) + +// decodeRecoverySubmission drives the production decode path used by +// (*Strategy).decodeRecovery. +func decodeRecoverySubmission(t *testing.T, email string) error { + t.Helper() + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(recoveryMethodSchema) + require.NoError(t, err) + + form := url.Values{"email": {email}, "method": {"code"}} + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + var body struct { + Email string `json:"email"` + } + return decoderx.Decode(req, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +// TestCodeRecoverySubmissionAcceptsNonRFCEmail verifies the `email_loose` +// recovery submission schema accepts schema-approved non-RFC carrier addresses +// while still rejecting injection-unsafe input at decode. +func TestCodeRecoverySubmissionAcceptsNonRFCEmail(t *testing.T) { + t.Parallel() + + require.NoError(t, decodeRecoverySubmission(t, "user@example.org")) + require.NoError(t, decodeRecoverySubmission(t, "foo.@docomo.ne.jp")) + + for _, evil := range []string{"foo@ory.sh\r\nBcc: evil@x", "foo@ory.sh", "foo@ory.sh,evil@x"} { + t.Run("rejected="+evil, func(t *testing.T) { + require.Error(t, decodeRecoverySubmission(t, evil)) + }) + } +} diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go new file mode 100644 index 0000000..292c6f2 --- /dev/null +++ b/selfservice/strategy/code/strategy_registration.go @@ -0,0 +1,359 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/x/otelx" + + "github.com/pkg/errors" + + "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/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +var ( + _ registration.Strategy = new(Strategy) + _ registration.FormHydrator = new(Strategy) +) + +// Update Registration Flow with Code Method +// +// swagger:model updateRegistrationFlowWithCodeMethod +type updateRegistrationFlowWithCodeMethod struct { + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits" form:"traits"` + + // The OTP Code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // The CSRF Token + CSRFToken string `json:"csrf_token" form:"csrf_token"` + + // Method to use + // + // This field must be set to `code` when using the code method. + // + // required: true + Method string `json:"method" form:"method"` + + // Resend restarts the flow with a new 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 (p *updateRegistrationFlowWithCodeMethod) GetResend() string { + return p.Resend +} + +func (s *Strategy) HandleRegistrationError(ctx context.Context, r *http.Request, f *registration.Flow, body *updateRegistrationFlowWithCodeMethod, err error) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { + if body != nil { + action := f.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), registration.RouteSubmitFlow)).String() + for _, n := range container.NewFromJSON(action, node.DefaultGroup, body.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.SetCSRF(s.deps.GenerateCSRFToken(r)) + } + + return err +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled { + return nil + } + + f.GetUI().Nodes.Append(nodeSubmitRegistration()) + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled { + return nil + } + + f.GetUI().Nodes.RemoveMatching(nodeRegistrationResendNode()) + f.GetUI().Nodes.RemoveMatching(nodeRegistrationSelectCredentialsNode()) + f.GetUI().Nodes.RemoveMatching(nodeContinueButton()) + f.GetUI().Nodes.RemoveMatching(nodeCodeInputFieldHidden()) + f.GetUI().Nodes.RemoveMatching(nodeCodeInputField()) + + f.GetUI().Nodes.Append(nodeSubmitRegistration()) + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled { + return nil + } + + f.GetUI().Nodes.RemoveMatching(nodeSubmitRegistration()) + f.GetUI().Nodes.RemoveMatching(nodeRegistrationResendNode()) + f.GetUI().Nodes.RemoveMatching(nodeRegistrationSelectCredentialsNode()) + f.GetUI().Nodes.RemoveMatching(nodeContinueButton()) + f.GetUI().Nodes.RemoveMatching(nodeCodeInputFieldHidden()) + f.GetUI().Nodes.RemoveMatching(nodeCodeInputField()) + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + return nil +} + +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 + if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + return nil +} + +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, nil, errors.WithStack(schema.NewMissingIdentifierError()) + } else if len(strings.Join(cred.Identifiers, "")) == 0 { + return nil, nil, errors.WithStack(schema.NewMissingIdentifierError()) + } + + 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") + defer otelx.End(span, &err) + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { + return err + } + + ds, err := f.IdentitySchema.URL(ctx, s.deps.Config()) + if err != nil { + return err + } + + var p updateRegistrationFlowWithCodeMethod + if err := registration.DecodeBody(&p, r, registrationSchema, ds); err != nil { + 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) + } + + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) + + switch f.GetState() { + case flow.StateChooseMethod: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationSendEmail(ctx, w, r, f, &p, i)) + case flow.StateEmailSent: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationVerifyCode(ctx, f, &p, i)) + case flow.StatePassedChallenge: + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(schema.NewNoRegistrationStrategyResponsible())) + } + + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unexpected flow state: %s", f.GetState()))) +} + +func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.Strategy.registrationSendEmail") + defer otelx.End(span, &err) + + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } + + // Create the Registration code + + // Step 1: validate the identity's traits + _, conf, err := s.validateAndGetCredentialsFromTraits(ctx, i, p.Traits) + if err != nil { + return err + } + + // Step 2: Delete any previous registration codes for this flow ID + if err := s.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil { + return errors.WithStack(err) + } + + // Step 3: Get the identity email and send the code + var addresses []Address + for _, address := range conf.Addresses { + addresses = append(addresses, Address{To: address.Address, Via: address.Channel}) + } + + // Fetch the OAuth2 login request before sending out the code so that + // courier templates can access the OAuth2 client that initiated the flow. + if f.OAuth2LoginChallenge != "" { + hlr, err := s.deps.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge)) + if err != nil { + return errors.WithStack(err) + } + f.HydraLoginRequest = hlr + } + + // 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, r.Header, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) + + // Step 4: Generate the UI for the `code` input form + // re-initialize the UI with a "clean" new state + // this should also provide a "resend" button and an option to change the email address + if err := s.NewCodeUINodes(r, f, p.Traits); err != nil { + return errors.WithStack(err) + } + + f.Active = identity.CredentialsTypeCodeAuth + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + if x.IsJSONRequest(r) { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the registration flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.Strategy.registrationVerifyCode") + defer otelx.End(span, &err) + + if len(p.Code) == 0 { + return errors.WithStack(schema.NewRequiredError("#/code", "code")) + } + + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } + + // 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.validateAndGetCredentialsFromTraits(ctx, i, p.Traits) + if err != nil { + return err + } + + // Step 2: Check if the flow traits match the identity traits + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + ui := f.GetUI() + if ui == nil { + continue + } + + nodes := ui.GetNodes() + if nodes == nil { + continue + } + + node := nodes.Find(n.ID()) + if node == nil { + continue + } + + nodeAttrs := node.Attributes + if nodeAttrs == nil { + continue + } + + nAttrs := n.Attributes + if nAttrs == nil { + continue + } + + if nodeAttrs.GetValue() != nAttrs.GetValue() { + return errors.WithStack(schema.NewTraitsMismatch()) + } + } + + // Step 3: Attempt to use the code + registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...) + if err != nil { + if errors.Is(err, ErrCodeNotFound()) { + return errors.WithStack(schema.NewRegistrationCodeInvalid()) + } + return errors.WithStack(err) + } + + // Step 4: Verify the address + if err := s.verifyAddress(ctx, i, Address{ + To: registrationCode.Address, + Via: registrationCode.AddressType, + }, false); err != nil { + return err + } + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the registration flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go new file mode 100644 index 0000000..feb9069 --- /dev/null +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -0,0 +1,899 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/x/configx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + oryClient "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/pop/v6" + "github.com/ory/x/assertx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlcon" +) + +type state struct { + flowID string + client *http.Client + email string + testServer *httptest.Server + resultIdentity *identity.Identity + body string +} + +func TestRegistrationCodeStrategyDisabled(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeCodeAuth, false)), + configx.WithValues(map[string]any{ + fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth): false, + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + }), + ) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + client := testhelpers.NewClientWithCookies(t) + resp, err := client.Get(public.URL + registration.RouteInitBrowserFlow) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Falsef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + // attempt to still submit the code form even though it doesn't exist + + payload := strings.NewReader(url.Values{ + "csrf_token": {gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String()}, + "method": {"code"}, + "traits.email": {testhelpers.RandomEmail()}, + }.Encode()) + req, err := http.NewRequestWithContext(t.Context(), "POST", public.URL+registration.RouteSubmitFlow+"?flow="+gjson.GetBytes(body, "id").String(), payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "This endpoint was disabled by system administrator. Please check your url or contact the system administrator to enable it.", gjson.GetBytes(body, "error.reason").String()) +} + +func TestRegistrationCodeStrategy(t *testing.T) { + t.Parallel() + + type ApiType string + + const ( + ApiTypeBrowser ApiType = "browser" + ApiTypeSPA ApiType = "spa" + ApiTypeNative ApiType = "api" + ) + + setup := func(ctx context.Context, t *testing.T, cfgOpts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault, *httptest.Server) { + conf, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeCodeAuth, true)), + configx.WithValues(map[string]any{ + fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth): true, + config.ViperKeySelfServiceRegistrationAfter + ".code.hooks": []map[string]any{ + {"hook": "session"}, + }, + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + }), + }, cfgOpts...)...) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + return conf, reg, public + } + + createRegistrationFlowInternal := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, identitySchema string) *state { + t.Helper() + + var client *http.Client + + if apiType == ApiTypeNative { + client = &http.Client{} + } else { + client = testhelpers.NewClientWithCookies(t) + } + + client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + + var clientInit *oryClient.RegistrationFlow + if apiType == ApiTypeNative { + clientInit = testhelpers.InitializeRegistrationFlowViaAPICtx(ctx, t, client, public, testhelpers.InitFlowWithIdentitySchema(identitySchema)) + } else { + clientInit = testhelpers.InitializeRegistrationFlowViaBrowserCtx(ctx, t, client, public, apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithIdentitySchema(identitySchema)) + } + + body, err := json.Marshal(clientInit) + require.NoError(t, err) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + if apiType == ApiTypeNative { + require.Emptyf(t, csrfToken, "expected an empty value for csrf_token on native api flows but got %s", body) + } else { + require.NotEmpty(t, csrfToken) + } + + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email)").Exists(), "%s", body) + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + return &state{ + client: client, + flowID: clientInit.GetId(), + testServer: public, + } + } + + createRegistrationFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType) *state { + return createRegistrationFlowInternal(ctx, t, public, apiType, "") + } + + createRegistrationFlowWithIdentity := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, identitySchema string) *state { + return createRegistrationFlowInternal(ctx, t, public, apiType, identitySchema) + } + + type onSubmitAssertion func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) + + registerNewUser := func(ctx context.Context, t *testing.T, s *state, apiType ApiType, submitAssertion onSubmitAssertion) *state { + t.Helper() + + if s.email == "" { + s.email = testhelpers.RandomEmail() + } + + 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) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) + values.Set("traits.email", s.email) + values.Set("traits.tos", "1") + 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) + return s + } + t.Logf("%v", body) + + if apiType == ApiTypeBrowser { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + if apiType == ApiTypeNative { + assert.Emptyf(t, csrfToken, "expected an empty value for csrf_token on native api flows but got %s", body) + } else { + assert.NotEmptyf(t, csrfToken, "%s", body) + } + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + return s + } + + 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() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) + // the sdk to values always adds resend which isn't what we always need here. + // so we delete it here. + // the custom vals func can add it again if needed. + values.Del("resend") + values.Set("traits.email", s.email) + values.Set("traits.tos", "1") + 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, body) + + verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.AddressTypeEmail, s.email) + require.NoError(t, err) + require.Equal(t, strings.ToLower(s.email), verifiableAddress.Value) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, verifiableAddress.IdentityID) + require.NoError(t, err) + require.NotNil(t, id.ID) + + _, ok := id.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + + s.resultIdentity = id + return s + } + + t.Run("test=different flows on the same configurations", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg, public := setup(ctx, t) + + for _, tc := range []struct { + d string + apiType ApiType + }{ + { + d: "SPA client", + apiType: ApiTypeSPA, + }, + { + d: "Browser client", + apiType: ApiTypeBrowser, + }, + { + d: "Native client", + apiType: ApiTypeNative, + }, + } { + t.Run("flow="+tc.d, func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, nil) + + switch tc.apiType { + case 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) + case ApiTypeBrowser: + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + case 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) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.apiType) + sourceMail := testhelpers.RandomEmail() + state.email = strings.ToUpper(sourceMail) + assert.NotEqual(t, sourceMail, state.email) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, sourceMail, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, nil) + + creds, ok := state.resultIdentity.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + require.Len(t, creds.Identifiers, 1) + assert.Equal(t, sourceMail, creds.Identifiers[0]) + }) + + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() + + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + s = registerNewUser(ctx, t, s, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + if tc.apiType == ApiTypeNative { + require.Empty(t, csrfToken, "expected the csrf_token to be empty but got %s", body) + } else { + require.NotEmptyf(t, csrfToken, "expected the csrf_token to exist but got %s", body) + } + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) + + val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() + require.Equal(t, "code", val, body) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // resend code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("resend", "code") + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + if tc.apiType == ApiTypeNative { + assert.Emptyf(t, csrfToken, "expected an empty value for csrf_token on native api flows but got %s", body) + } else { + require.NotEmptyf(t, csrfToken, "expected to find the csrf_token but got %s", body) + } + require.Containsf(t, gjson.Get(body, "ui.messages").String(), "A code has been sent to the address(es) you provided", "%s", body) + }) + + // get the new code from email + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode2) + + require.NotEqual(t, registrationCode, registrationCode2) + + // try submit old code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + }) + + submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode2) + }, tc.apiType, nil) + }) + + t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s.email = "not-" + s.email // swap out email + // 3. Submit OTP + submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) + + t.Run("case=swapping out traits that aren't strings should not be possible on code submit", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + v.Set("traits.tos", "0") + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) + + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.apiType, nil) + + err := reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { + count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) + require.NoError(t, err) + require.Equal(t, 1, count) + return nil + }) + require.NoError(t, err) + + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + }) + } + }) + + t.Run("test=cases with different configs", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg, public := setup(ctx, t) + + for _, tc := range []struct { + d string + apiType ApiType + }{ + { + d: "SPA client", + apiType: ApiTypeSPA, + }, + { + d: "Browser client", + apiType: ApiTypeBrowser, + }, + { + d: "Native client", + apiType: ApiTypeNative, + }, + } { + 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/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") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + registerNewUser(ctx, t, s, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + // 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() + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(rf) + require.NoError(t, err) + require.Contains(t, gjson.GetBytes(body, "ui.messages").String(), "Could not find any login identifiers") + + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%v", body) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") + } + }) + }) + + t.Run("case=should have verifiable address even if after session hook is disabled", func(t *testing.T) { + // disable the after session hook + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]any{}) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]any{ + {"hook": "session"}, + }) + }) + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, nil) + }) + + t.Run("case=code should expire", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "10ns") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.apiType) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again", "%s", body) + }) + }) + }) + } + }) + + t.Run("test=multi-schema select", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg, public := setup(ctx, t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "code", URL: "file://./stub/code.identity.schema.json", SelfserviceSelectable: true}, + {ID: "no-code", URL: "file://stub/no-code.schema.json", SelfserviceSelectable: true}, + }, + config.ViperKeyDefaultIdentitySchemaID: "no-code", + })) + + for _, tc := range []struct { + d string + apiType ApiType + }{ + { + d: "SPA client", + apiType: ApiTypeSPA, + }, + { + d: "Browser client", + apiType: ApiTypeBrowser, + }, + { + d: "Native client", + apiType: ApiTypeNative, + }, + } { + t.Run("flow="+tc.d, func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlowWithIdentity(ctx, t, public, tc.apiType, "code") + state.email = testhelpers.RandomEmail() + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.apiType, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Use code") + assert.Contains(t, message.Body, "Complete your account registration with the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.apiType, nil) + + switch tc.apiType { + case 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) + case ApiTypeBrowser: + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + case ApiTypeNative: + assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body) + } + + identity, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeCodeAuth, state.email) + require.NoError(t, err, sqlcon.ErrNoRows()) + + assert.NotEmpty(t, identity.ID, "%s", identity.ID) + assert.Equal(t, state.email, gjson.Get(identity.Traits.String(), "email").String(), "%s", identity.Traits.String()) + assert.Equal(t, "code", identity.SchemaID, "%s", identity.SchemaID) + }) + + t.Run("case=registration should fail with invalid form data", func(t *testing.T) { + // 1. Initiate flow + s := createRegistrationFlowWithIdentity(t.Context(), t, public, tc.apiType, "code") + s.email = "invalidemail" + + // 2. Submit Identifier (email) + registerNewUser(t.Context(), t, s, tc.apiType, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.apiType == ApiTypeBrowser { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body) + require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body) + }) + }) + }) + } + }) +} + +func TestPopulateRegistrationMethod(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/code.identity.schema.json")), + configx.WithValue(fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true), + ) + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeCodeAuth) + require.NoError(t, err) + + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src")) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSON(t, snapshots[1], snapshots[3]) + }) + }) +} + +func TestCodeRegistrationWithLoginChallenge(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]any{ + "enabled": true, + "passwordless_enabled": true, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/code.identity.schema.json")), + ) + reg.SetHydra(hydra.NewFake()) + reg.WithCSRFTokenGenerator(nosurfx.FakeCSRFTokenGenerator) + s := code.NewStrategy(reg) + + loginChallenge := hydra.FakeValidLoginChallenge + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + t.Helper() + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, f)) + return r, f + } + + t.Run("case=fetches login challenge on code input state", func(t *testing.T) { + _, f := newFlow(t.Context(), t) + f.OAuth2LoginChallenge = sqlxx.NullString(loginChallenge) + i := identity.NewIdentity(f.IdentitySchema.ID(t.Context(), reg.Config())) + email := testhelpers.RandomEmail() + + body := gjson.Parse(`{ + "method": "code", + "traits.email": "` + email + `", + "csrf_token": "` + f.CSRFToken + `" + }`) + r := httptest.NewRequest("POST", "/self-service/registration/browser", strings.NewReader(body.Raw)) + r.Header.Add("Content-Type", "application/json") + + err := s.Register(httptest.NewRecorder(), r, f, i) + require.ErrorIs(t, err, flow.ErrCompletedByStrategy) + require.NotNil(t, f.HydraLoginRequest) + + messages, err := reg.CourierPersister().NextMessages(t.Context(), 10) + require.NoError(t, err) + require.Len(t, messages, 1) + assert.Equal(t, loginChallenge, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.challenge").String()) + assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.client_id").String()) + assert.Equal(t, hydra.FakeClientName, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.client_name").String()) + assert.Equal(t, hydra.FakeClientLogoURI, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request.client.logo_uri").String()) + }) + + t.Run("case=returns error if login challenge is invalid", func(t *testing.T) { + _, f := newFlow(t.Context(), t) + f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeInvalidLoginChallenge) + i := identity.NewIdentity(f.IdentitySchema.ID(t.Context(), reg.Config())) + + email := testhelpers.RandomEmail() + + body := gjson.Parse(`{ + "method": "code", + "traits.email": "` + email + `", + "csrf_token": "` + f.CSRFToken + `" + }`) + r := httptest.NewRequest("POST", "/self-service/registration/browser", strings.NewReader(body.Raw)) + r.Header.Add("Content-Type", "application/json") + + err := s.Register(httptest.NewRecorder(), r, f, i) + require.ErrorIs(t, err, herodot.ErrBadRequest()) + require.Nil(t, f.HydraLoginRequest) + + // Pin that the invalid login challenge aborts before any one-time code is queued. + _, err = reg.CourierPersister().NextMessages(t.Context(), 1) + require.ErrorIs(t, err, courier.ErrQueueEmpty) + }) +} diff --git a/selfservice/strategy/code/strategy_test.go b/selfservice/strategy/code/strategy_test.go new file mode 100644 index 0000000..5deec7a --- /dev/null +++ b/selfservice/strategy/code/strategy_test.go @@ -0,0 +1,235 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "fmt" + "maps" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/contextx" +) + +var defaultConfig = func() map[string]any { + cfg := map[string]any{ + config.ViperKeySelfServiceRecoveryEnabled: true, + config.ViperKeySelfServiceRecoveryUse: "code", + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceVerificationUse: "code", + } + maps.Copy(cfg, testhelpers.DefaultIdentitySchemaConfig("file://./stub/default.schema.json")) + maps.Copy(cfg, testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)) + maps.Copy(cfg, testhelpers.MethodEnableConfig(identity.CredentialsTypeCodeAuth, true)) + return cfg +}() + +func TestMaskAddress(t *testing.T) { + for _, tc := range []struct { + address string + expected string + }{ + { + address: "a", + expected: "a", + }, + { + address: "ab@cd", + expected: "ab****@cd", + }, + { + address: "fixed@ory.sh", + expected: "fi****@ory.sh", + }, + { + address: "f@ory.sh", + expected: "f@ory.sh", + }, + { + address: "+12345678910", + expected: "+12****10", + }, + { + address: "+123456", + expected: "+12****56", + }, + } { + t.Run("case="+tc.address, func(t *testing.T) { + assert.Equal(t, tc.expected, code.MaskAddress(tc.address)) + }) + } +} + +func TestCountActiveCredentials(t *testing.T) { + _, reg := pkg.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 := contextx.WithConfigValue(ctx, "selfservice.methods.code.passwordless_enabled", tc.passwordlessEnabled) + ctx = contextx.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: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{}, + mfaEnabled: true, + enabled: true, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"address_type":"email","used_at":{"Time":"0001-01-01T00:00:00Z","Valid":false}}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"addresses":[{"channel":"email","address":"test@ory.sh"}]}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"addresses":[]}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"addresses":[{"channel":"sms","address":"+1234567890"}]}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"addresses":[{"channel":"sms","address":"+1234567890"},{"channel":"email","address":"test@ory.sh"}]}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 2, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctx := contextx.WithConfigValue(ctx, "selfservice.methods.code.mfa_enabled", tc.mfaEnabled) + ctx = contextx.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 new file mode 100644 index 0000000..c575bfa --- /dev/null +++ b/selfservice/strategy/code/strategy_verification.go @@ -0,0 +1,492 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "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/x/decoderx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func (s *Strategy) VerificationStrategyID() string { + return string(verification.VerificationStrategyCode) +} + +// PopulateVerificationMethod set's the appropriate UI nodes on this flow +// +// If the flow's state is `sent_email`, the `code` input and the success notification is set +// Otherwise, the default email input is added. +// If the flow is a browser flow, the CSRF token is added to the UI. +func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) decodeVerification(r *http.Request) (*updateVerificationFlowWithCodeMethod, error) { + var body updateVerificationFlowWithCodeMethod + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(verificationMethodSchema) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := decoderx.Decode(r, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return nil, errors.WithStack(err) + } + + return &body, nil +} + +// handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation 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 + } + label := text.NewInfoNodeInputEmail() + inputType := node.InputAttributeTypeEmail + if channels, chErr := s.deps.Config().CourierChannels(r.Context()); chErr == nil { + for _, ch := range channels { + if ch.ID == "sms" { + label = text.NewInfoNodeInputEmailOrPhone() + inputType = node.InputAttributeTypeText + break + } + } + } + f.UI.GetNodes().Upsert( + node.NewInputField("email", email, node.CodeGroup, inputType, node.WithRequiredInputAttribute).WithMetaLabel(label), + ) + } + + return err +} + +// swagger:model updateVerificationFlowWithCodeMethod +type updateVerificationFlowWithCodeMethod struct { + // The email address or phone number to verify + // + // If the provided address belongs to a valid account, a verification email or SMS will be sent. + // + // If you want to notify the email address if the account does not exist, see + // the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation#attempted-verification-notifications) + // + // If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code. + // + // required: false + Email string `form:"email" json:"email"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `form:"csrf_token" json:"csrf_token"` + + // Method is the method that should be used for this verification flow + // + // Allowed values are `link` and `code`. + // + // required: true + Method verification.VerificationStrategy `json:"method"` + + // Code from the recovery email + // + // If you want to submit a code, use this field, but make sure to _not_ include the email field, as well. + // + // required: false + Code string `json:"code" form:"code"` + + // 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 +func (body *updateVerificationFlowWithCodeMethod) getMethod() verification.VerificationStrategy { + if body.Method != "" { + return body.Method + } + if body.Code != "" { + return verification.VerificationStrategyCode + } + + return "" +} + +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") + 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(r, nil, body, err) + } + + f.TransientPayload = body.TransientPayload + + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + if err := f.Valid(s.deps.Clock()); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + switch f.State { + case flow.StateChooseMethod: + fallthrough + case flow.StateEmailSent: + return s.verificationHandleFormSubmission(ctx, w, r, f, body) + case flow.StatePassedChallenge: + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) + default: + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) + } +} + +func (s *Strategy) handleLinkClick(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, code string) error { + // Pre-fill the code + if codeField := f.UI.Nodes.Find("code"); codeField != nil { + codeField.Attributes.SetValue(code) + } + + // In the verification flow, we can't enforce CSRF if the flow is opened from an email, so we initialize the CSRF + // token here, so all subsequent interactions are protected + csrfToken := s.deps.CSRFHandler().RegenerateToken(w, r) + f.UI.SetCSRF(csrfToken) + f.CSRFToken = csrfToken + + if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return err + } + + // we always redirect to the browser UI here to allow API flows to complete aswell + // TODO: In the future, we might want to redirect to a custom URI scheme here, to allow to open an app on the device of + // the user to handle the flow directly. + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) verificationHandleFormSubmission(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, body *updateVerificationFlowWithCodeMethod) error { + if len(body.Code) > 0 { + if r.Method == http.MethodGet { + // Special case: in the code strategy we send out links as well, that contain the code + return s.handleLinkClick(ctx, w, r, f, body.Code) + } + + // If not GET: try to use the submitted code + return s.verificationUseCode(ctx, 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(r, f, body, schema.NewRequiredError("#/email", "email")) + } + + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, body.CSRFToken); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + if err := s.deps.VerificationCodePersister().DeleteVerificationCodesOfFlow(ctx, f.ID); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + via := hackyInferChannel(body.Email) + + ptc, ptcErr := s.deps.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID) + if ptcErr != nil { + if !errors.Is(ptcErr, sqlcon.ErrNoRows()) { + return s.handleVerificationError(r, f, body, ptcErr) + } + if err := s.deps.CodeSender().SendVerificationCode(ctx, f, via, body.Email); err != nil { + if !errors.Is(err, ErrUnknownAddress()) { + return s.handleVerificationError(r, f, body, err) + } + // Continue execution + } + } else { + // Re-check whether the proposed address is still unclaimed before resending. + // Another identity may have claimed it between the failed code submission and this resend. + existingAddr, addrErr := s.deps.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, ptc.NewAddressVia, ptc.NewAddressValue) + if addrErr != nil && !errors.Is(addrErr, sqlcon.ErrNoRows()) { + return s.handleVerificationError(r, f, body, addrErr) + } + if addrErr == nil && existingAddr.IdentityID != ptc.IdentityID { + if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationDuplicateCredentials(), http.StatusBadRequest); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + proposedIdentity, err := s.deps.IdentityPool().GetIdentity(ctx, ptc.IdentityID, identity.ExpandDefault) + if err != nil { + return s.handleVerificationError(r, f, body, err) + } + if err := s.SendVerificationCode(ctx, f, proposedIdentity, ptc); err != nil { + return s.handleVerificationError(r, f, body, err) + } + } + + f.State = flow.StateEmailSent + + if err := s.PopulateVerificationMethod(r, f); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + // PopulateVerificationMethod sets the email "code sent" message unconditionally. + // Override it with the message that matches the channel the code was sent over. + f.UI.Messages.Set(text.VerificationCodeSentMessage(via)) + + if body.Email != "" { + f.UI.Nodes.Append( + node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()), + ) + } + + if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + return nil +} + +func (s *Strategy) updateVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, message *text.Message, statusCode int) error { + f.UI.Messages.Clear() + f.UI.Messages.Add(message) + + if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return err + } + + if x.IsBrowserRequest(r) { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + } else { + s.deps.Writer().WriteCode(w, r, statusCode, f) + } + return nil +} + +func (s *Strategy) verificationUseCode(ctx context.Context, w http.ResponseWriter, r *http.Request, codeString string, f *verification.Flow) error { + code, err := s.deps.VerificationCodePersister().UseVerificationCode(ctx, f.ID, codeString) + if errors.Is(err, ErrCodeNotFound()) { + if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } else if err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + + var identityID uuid.UUID + var addressVia string + var ptcIdentity *identity.Identity + + if code.VerifiableAddress == nil { + // Pending-change path: no persisted address — look up by verification flow ID. + ptc, ptcErr := s.deps.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID) + if ptcErr != nil { + if !errors.Is(ptcErr, sqlcon.ErrNoRows()) { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, ptcErr) + } + // PTC vanished between code issuance and use — show the standard + // "invalid or already used" message. + if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + // Security note: the verification flow is unauthenticated by design. The + // user clicks a link from email/SMS. The pending change is bound to the + // identity via IdentityID, but we cannot validate session ownership at + // completion time. The security model relies on: + // 1. The verification code is sent to the *new* address (attacker needs inbox access). + // 2. The verification flow has a TTL (enforced by the flow handler). + // 3. The one-time code is consumed on use (cannot be replayed). + // Creation of pending changes is gated by a privileged session check; + // ApplyPendingTraitsChange revalidates the session atomically with the apply. + updatedIdentity, applyErr := s.deps.SettingsHookExecutor().ApplyPendingTraitsChange(ctx, w, r, ptc) + if applyErr != nil { + if errors.Is(applyErr, identity.ErrConcurrentModification) || + errors.Is(applyErr, settings.ErrPendingTraitsChangeSessionInvalid) { + if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + if errors.Is(applyErr, sqlcon.ErrUniqueViolation()) { + if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationDuplicateCredentials(), http.StatusBadRequest); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, applyErr) + } + + ptcIdentity = updatedIdentity + addressVia = ptc.NewAddressVia + identityID = ptc.IdentityID + } else { + // Normal path: update the existing verifiable address directly. + address := code.VerifiableAddress + address.Verified = true + verifiedAt := sqlxx.NullTime(time.Now().UTC()) + address.VerifiedAt = &verifiedAt + address.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + + addressVia = address.Via + identityID = code.VerifiableAddress.IdentityID + } + + var i *identity.Identity + if ptcIdentity != nil { + i = ptcIdentity + } else { + i, err = s.deps.IdentityPool().GetIdentity(ctx, identityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + } + + // Remainder is shared between both paths. + returnTo := f.ContinueURL(ctx, s.deps.Config()) + + f.UI = &container.Container{ + Method: "GET", + Action: returnTo.String(), + } + + f.State = flow.StatePassedChallenge + f.SetCSRFToken(flow.GetCSRFToken(s.deps, w, r, f.Type)) + if addressVia == identity.AddressTypeSMS { + f.UI.Messages.Set(text.NewInfoSelfServiceVerificationPhoneSuccessful()) + } else { + f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) + } + f.UI.Nodes.Append(node.NewAnchorField("continue", returnTo.String(), node.CodeGroup, text.NewInfoNodeLabelContinue()). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + + return nil +} + +func (s *Strategy) retryVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error { + s.deps. + Logger(). + WithRequest(r). + WithField("message", message). + Debug("A verification flow is being retried because a validation error occurred.") + + f, err := verification.NewFlow(s.deps, + s.deps.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.deps.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) + if err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + f.UI.Messages.Add(message) + + if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + if x.IsJSONRequest(r) { + s.deps.Writer().WriteError(w, r, flow.NewFlowReplacedError(text.NewErrorSystemGeneric("An error occured, please use the new flow.")).WithFlow(f)) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) retryVerificationFlowWithError(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, verErr error) error { + s.deps. + Logger(). + WithRequest(r). + WithError(verErr). + Debug("A verification flow is being retried because an error occurred.") + + f, err := verification.NewFlow(s.deps, + s.deps.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.deps.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) + if err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + var toReturn error + + if expired := new(flow.ExpiredError); errors.As(verErr, &expired) { + f.UI.Messages.Add(text.NewErrorValidationVerificationFlowExpired(s.deps.Clock(), expired.ExpiredAt)) + toReturn = expired.WithFlow(f) + } else if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil { + return err + } + + if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + if x.IsJSONRequest(r) { + if toReturn == nil { + toReturn = flow.NewFlowReplacedError(text.NewErrorSystemGeneric("An error occured, please retry the flow.")). + WithFlow(f) + } + s.deps.Writer().WriteError(w, r, toReturn) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) SendVerificationCode(ctx context.Context, f *verification.Flow, i *identity.Identity, a identity.VerifiableAddressLike) (err error) { + rawCode := GenerateCode() + + if _, err = s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + VerifiableAddress: a, + FlowID: f.ID, + }); err != nil { + return err + } + + return s.deps.CodeSender().SendVerificationCodeTo(ctx, f, i, rawCode, a) +} diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go new file mode 100644 index 0000000..e8739a3 --- /dev/null +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -0,0 +1,1460 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/configx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func TestVerification(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(defaultConfig)) + + // Configure an SMS courier channel so verification supports phone numbers. + conf.MustSet(ctx, config.ViperKeyCourierChannels, []map[string]any{ + {"id": "sms", "type": "http", "request_config": map[string]any{ + "url": "http://localhost:1234/sms", + "method": "POST", + "body": "base64://ZnVuY3Rpb24oY3R4KSBjdHg=", + }}, + }) + + identityToVerify := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{"recoverme@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`), + }, + }, + } + + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() + + _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "returned", conf) + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + + require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, + identity.ManagerAllowWriteProtectedTraits)) + + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + if hc == nil { + hc = testhelpers.NewDebugClient(t) + if !isAPI { + hc = testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + } + } + + return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c, + testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) + } + + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) + } + + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, http.StatusOK) + } + + submitVerificationCode := func(t *testing.T, body string, c *http.Client, code string) (string, *http.Response) { + action := gjson.Get(body, "ui.action").String() + require.NotEmpty(t, action, "%v", string(body)) + csrfToken := extractCsrfToken([]byte(body)) + + res, err := c.PostForm(action, url.Values{ + "code": {code}, + "csrf_token": {csrfToken}, + }) + require.NoError(t, err) + + return string(ioutilx.MustReadAll(res.Body)), res + } + + t.Run("description=should set all the correct verification payloads after submission", func(t *testing.T) { + body := expectSuccess(t, nil, false, false, func(v url.Values) { + v.Set("email", "test@ory.sh") + }) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").String()), []string{"3.attributes.value"}) + }) + + t.Run("description=should set all the correct verification payloads", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"2.attributes.value"}) + assert.EqualValues(t, public.URL+verification.RouteSubmitFlow+"?flow="+rs.Id, rs.Ui.Action) + assert.Empty(t, rs.Ui.Messages) + + // The email input label should read "Email or phone number" since verification accepts both. + emailNode := rs.Ui.Nodes[0] + assert.EqualValues(t, int(text.InfoNodeLabelEmailOrPhone), emailNode.Meta.Label.Id) + assert.EqualValues(t, "Email or phone number", emailNode.Meta.Label.Text) + }) + + t.Run("description=should not execute submit without correct method set", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"not-link"}, "email": {verificationEmail}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) + + body := ioutilx.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + testhelpers.AssertFieldMessage(t, body, "method", `value must be one of "code", "link"`) + }) + + t.Run("description=should require an email to be sent", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, "Property email is missing.", + gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), + "%s", actual) + } + + values := func(v url.Values) { + v.Del("email") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, nil, true, false, values)) + }) + }) + + t.Run("description=should accept arbitrary addresses without validation error", func(t *testing.T) { + // The email field now accepts both email addresses and phone numbers. + // Malformed values are no longer rejected at the schema level — they are + // treated as unknown addresses (SMS channel for non-@ values, email for @ + // values). The flow still succeeds to prevent address enumeration. + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) + msgID := gjson.Get(actual, "ui.messages.0.id").Int() + assert.True(t, + msgID == int64(text.InfoSelfServiceVerificationEmailWithCodeSent) || + msgID == int64(text.InfoSelfServiceVerificationPhoneWithCodeSent), + "expected email or phone code-sent message, got %d in %s", msgID, actual) + } + + for _, addr := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { + values := func(v url.Values) { + v.Set("email", addr) + } + + t.Run("type=browser/value="+addr, func(t *testing.T) { + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("type=spa/value="+addr, func(t *testing.T) { + check(t, expectSuccess(t, nil, false, true, values)) + }) + + t.Run("type=api/value="+addr, func(t *testing.T) { + check(t, expectSuccess(t, nil, true, false, values)) + }) + } + }) + + t.Run("description=should accept a phone number for verification", func(t *testing.T) { + phoneNumber := "+12065551234" + phoneIdentity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, phoneNumber)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{phoneNumber}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`), + }, + }, + } + require.NoError(t, reg.IdentityManager().Create(context.Background(), phoneIdentity, + identity.ManagerAllowWriteProtectedTraits)) + + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) + assertx.EqualAsJSON(t, text.NewVerificationPhoneWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, phoneNumber, "verification code") + assert.EqualValues(t, "sms", message.Channel) + code := testhelpers.CourierExpectCodeInMessage(t, message, 1) + require.NotEmpty(t, code) + } + + values := func(v url.Values) { + v.Set("email", phoneNumber) + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectSuccess(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectSuccess(t, nil, true, false, values)) + }) + }) + + t.Run("description=should verify a phone number and show phone-specific success message", func(t *testing.T) { + phone := "+12065559999" + phoneIdentity := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, phone)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{phone}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`), + }, + }, + } + require.NoError(t, reg.IdentityManager().Create(context.Background(), phoneIdentity, + identity.ManagerAllowWriteProtectedTraits)) + + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", phone) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, phone, "verification code") + verificationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + body, _ = submitVerificationCode(t, body, testhelpers.NewClientWithCookies(t), verificationCode) + + assert.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String()) + assert.EqualValues(t, text.NewInfoSelfServiceVerificationPhoneSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String()) + assert.EqualValues(t, int(text.InfoSelfServiceVerificationPhoneSuccessful), gjson.Get(body, "ui.messages.0.id").Int()) + }) + + t.Run("description=should not send SMS for unknown phone number", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) + assertx.EqualAsJSON(t, text.NewVerificationPhoneWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + } + + t.Run("type=browser", func(t *testing.T) { + unknownPhone := "+10005551001" + check(t, expectSuccess(t, nil, false, false, func(v url.Values) { + v.Set("email", unknownPhone) + })) + }) + + t.Run("type=spa", func(t *testing.T) { + unknownPhone := "+10005551002" + check(t, expectSuccess(t, nil, false, true, func(v url.Values) { + v.Set("email", unknownPhone) + })) + }) + + t.Run("type=api", func(t *testing.T) { + unknownPhone := "+10005551003" + check(t, expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", unknownPhone) + })) + }) + }) + + t.Run("description=should not send SMS for unknown phone number even with notify_unknown_recipients", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) + }) + + unknownPhone := "+10005559999" + + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", unknownPhone) + }) + + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(body, "active").String(), "%s", body) + assertx.EqualAsJSON(t, text.NewVerificationPhoneWithCodeSent(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) + + // Verify no SMS was enqueued for the unknown phone number. + messages, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{ + Recipient: unknownPhone, + }, []keysetpagination.Option{}) + require.NoError(t, err) + assert.Empty(t, messages, "no SMS should be sent to an unknown phone number even with notify_unknown_recipients enabled") + }) + + t.Run("description=should try to verify an email that does not exist", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) + }) + + var email string + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) + assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") + assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") + } + + values := func(v url.Values) { + v.Set("email", email) + } + + t.Run("type=browser", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, true, false, values)) + }) + }) + + t.Run("description=clicking link should prefill code", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + f := testhelpers.SubmitVerificationForm(t, false, false, c, public, func(v url.Values) { + v.Set("email", verificationEmail) + }, 200, "") + fID := gjson.Get(f, "id").String() + res, err := c.Get(public.URL + verification.RouteSubmitFlow + "?flow=" + fID + "&code=12312312") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()+"?flow=") + + body := ioutilx.MustReadAll(res.Body) + + assert.Equal(t, "12312312", gjson.GetBytes(body, "ui.nodes.#(attributes.name==code).attributes.value").String(), "%v", string(body)) + }) + + t.Run("description=should not be able to use an invalid code", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + f := testhelpers.SubmitVerificationForm(t, false, false, c, public, func(v url.Values) { + v.Set("email", verificationEmail) + }, 200, "") + + body, res := submitVerificationCode(t, f, c, "12312312") + assert.Equal(t, http.StatusOK, res.StatusCode) + + testhelpers.AssertMessage(t, body, "The verification code is invalid or has already been used. Please try again.") + }) + + t.Run("description=should not be able to submit email in expired flow", func(t *testing.T) { + // Drive expiry with a deterministic clock instead of a real sleep, so the + // "expired N minutes ago" message no longer depends on wall-clock timing. + mockClock := clock.NewMock(time.Now()) + reg.SetClock(mockClock) + t.Cleanup(func() { reg.SetClock(clock.New()) }) + + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute) + + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + // 1-minute lifespan + 2 minutes => deterministically "expired 2.00 minutes ago". + mockClock.Add(3 * time.Minute) + + res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"code"}, "email": {verificationEmail}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotContains(t, res.Request.URL.String(), "flow="+rs.Id) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) + body := ioutilx.MustReadAll(res.Body) + assert.Equalf(t, "The verification flow expired 2.00 minutes ago, please try again.", gjson.GetBytes(body, "ui.messages.0.text").Str, "%s", body) + }) + + t.Run("description=should not be able to submit code in expired flow", func(t *testing.T) { + // Deterministic clock: the courier still runs on the real clock, only flow + // expiry is driven by the mock, so the assertion no longer races the load. + mockClock := clock.NewMock(time.Now()) + reg.SetClock(mockClock) + t.Cleanup(func() { reg.SetClock(clock.New()) }) + + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute) + + c := testhelpers.NewClientWithCookies(t) + body := expectSuccess(t, c, false, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + assert.Contains(t, message.Body, "Verify your account with the following code") + + code := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + // 1-minute lifespan + 2 minutes => deterministically "expired 2.00 minutes ago". + mockClock.Add(3 * time.Minute) + + f, _ := submitVerificationCode(t, body, c, code) + + assert.Equalf(t, "The verification flow expired 2.00 minutes ago, please try again.", gjson.Get(f, "ui.messages.0.text").Str, "%s", body) + }) + + 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) + assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + assert.Contains(t, message.Body, "Verify your account with the following code") + + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + assert.Contains(t, verificationLink, public.URL+verification.RouteSubmitFlow) + assert.Contains(t, verificationLink, "code=") + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(verificationLink) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + f := ioutilx.MustReadAll(res.Body) + + code := gjson.GetBytes(f, "ui.nodes.#(attributes.name==code).attributes.value").String() + require.NotEmpty(t, code) + + body, res := submitVerificationCode(t, string(f), cl, code) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String()) + assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String()) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), identityToVerify.ID) + require.NoError(t, err) + require.Len(t, id.VerifiableAddresses, 1) + + address := id.VerifiableAddresses[0] + assert.EqualValues(t, verificationEmail, address.Value) + assert.True(t, address.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, address.Status) + assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) + } + + values := func(v url.Values) { + v.Set("email", verificationEmail) + } + + 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() + }) + }) + + t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { + values := func(v url.Values) { + v.Set("email", verificationEmail) + } + + expectSuccess(t, nil, false, false, values) + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + code := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(verificationLink) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 1) + assert.Contains(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))[0].Name, nosurfx.CSRFTokenName) + + actualBody, _ := submitVerificationCode(t, body, cl, code) + assert.EqualValues(t, "passed_challenge", gjson.Get(actualBody, "state").String()) + }) + + newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *code.VerificationCode, string) { + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), verification.Strategies{code.NewStrategy(reg)}, fType) + require.NoError(t, err) + f.State = flow.StateEmailSent + u, err := url.Parse(f.RequestURL) + require.NoError(t, err) + f.OAuth2LoginChallenge = sqlxx.NullString(u.Query().Get("login_challenge")) + f.IdentityID = uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + f.SessionID = uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) + email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) + identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) + require.NoError(t, reg.IdentityManager().Update(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) + + params := &code.CreateVerificationCodeParams{ + RawCode: "12312312", + ExpiresIn: time.Hour, + VerifiableAddress: &identityToVerify.VerifiableAddresses[0], + FlowID: f.ID, + } + verificationCode, err := reg.VerificationCodePersister().CreateVerificationCode(context.Background(), params) + require.NoError(t, err) + return f, verificationCode, params.RawCode + } + + newValidBrowserFlow := func(t *testing.T, requestURL string) (*verification.Flow, *code.VerificationCode, string) { + return newValidFlow(t, flow.TypeBrowser, requestURL) + } + + t.Run("case=contains link to return_to", func(t *testing.T) { + returnToURL := public.URL + "/after-verification" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL}) + client := testhelpers.NewTestClient(t) + + f, _, rawCode := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{"return_to": {returnToURL}}.Encode()) + + action := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + + res, err := client.PostForm(action, url.Values{ + "code": {rawCode}, + "csrf_token": {nosurfx.FakeCSRFToken}, + }) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + assert.Equal(t, returnToURL, gjson.GetBytes(body, "ui.nodes.#(attributes.id==continue).attributes.href").String()) + }) + + t.Run("case=should respond with replaced error if successful code is submitted again via api", func(t *testing.T) { + _ = expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + assert.Contains(t, message.Body, "Verify your account with the following code") + + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + assert.Contains(t, verificationLink, public.URL+verification.RouteSubmitFlow) + assert.Contains(t, verificationLink, "code=") + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(verificationLink) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + original := ioutilx.MustReadAll(res.Body) + + code := gjson.GetBytes(original, "ui.nodes.#(attributes.name==code).attributes.value").String() + require.NotEmpty(t, code) + action := gjson.GetBytes(original, "ui.action").String() + require.NotEmpty(t, action) + + c := testhelpers.NewDebugClient(t) + res, err = c.Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{"code": "%v"}`, code))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + f1 := ioutilx.MustReadAll(res.Body) + + assert.EqualValues(t, "passed_challenge", gjson.GetBytes(f1, "state").String()) + + res, err = c.Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{"code": "%v"}`, code))) + require.NoError(t, err) + assert.Equal(t, http.StatusGone, res.StatusCode) + + f2 := ioutilx.MustReadAll(res.Body) + assert.Equal(t, text.ErrIDSelfServiceFlowReplaced, gjson.GetBytes(f2, "error.id").String()) + }) + + resendVerificationCode := func(t *testing.T, client *http.Client, flow string, flowType ClientType, statusCode int) string { + action := gjson.Get(flow, "ui.action").String() + assert.NotEmpty(t, action) + + email := gjson.Get(flow, "ui.nodes.#(attributes.name==email).attributes.value").String() + + values := withCSRFToken(t, flowType, flow, url.Values{ + "method": {"code"}, + "email": {email}, + }) + + contentType := "application/json" + if flowType == RecoveryClientTypeBrowser { + contentType = "application/x-www-form-urlencoded" + } + + res, err := client.Post(action, contentType, bytes.NewBufferString(values)) + require.NoError(t, err) + assert.Equal(t, statusCode, res.StatusCode) + + return string(ioutilx.MustReadAll(res.Body)) + } + + t.Run("case=should be able to resend code", func(t *testing.T) { + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + _ = testhelpers.CourierExpectCodeInMessage(t, message, 1) + + c := testhelpers.NewClientWithCookies(t) + body = resendVerificationCode(t, c, body, RecoveryClientTypeBrowser, http.StatusOK) + + assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) + assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) + + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + verificationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + submitVerificationCode(t, body, c, verificationCode) + }) + + t.Run("case=should not be able to use first code after resending code", func(t *testing.T) { + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + firstCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + c := testhelpers.NewClientWithCookies(t) + body = resendVerificationCode(t, c, body, RecoveryClientTypeBrowser, http.StatusOK) + + assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) + assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) + + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + secondCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + body, res := submitVerificationCode(t, body, c, firstCode) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.AssertMessage(t, body, "The verification code is invalid or has already been used. Please try again.") + + // For good measure, check that the second code still works! + + body, res = submitVerificationCode(t, body, c, secondCode) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.AssertMessage(t, body, "You successfully verified your email address.") + }) + + t.Run("description=should not be able to use an invalid code more than 5 times", func(t *testing.T) { + email := strings.ToLower(testhelpers.RandomEmail()) + createIdentityToRecover(t, reg, email) + c := testhelpers.NewClientWithCookies(t) + + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + initialFlowId := gjson.Get(body, "id") + + for submitTry := 0; submitTry < 5; submitTry++ { + xcbody, _ := submitVerificationCode(t, body, c, "12312312") + require.Equal(t, initialFlowId.String(), gjson.Get(xcbody, "id").String()) + + testhelpers.AssertMessage(t, []byte(xcbody), "The verification code is invalid or has already been used. Please try again.") + } + + // submit an invalid code for the 6th time + body, _ = submitVerificationCode(t, body, c, "12312312") + + require.Len(t, gjson.Get(body, "ui.messages").Array(), 1) + assert.Equal(t, "The request was submitted too often. Please request another code.", gjson.Get(body, "ui.messages.0.text").String()) + + // check that a new flow has been created + assert.NotEqual(t, gjson.Get(body, "id"), initialFlowId) + + assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==email)").Exists()) + }) + + t.Run("description=should be able to verify already verified email address", func(t *testing.T) { + email := strings.ToLower(testhelpers.RandomEmail()) + createIdentityToRecover(t, reg, email) + c := testhelpers.NewClientWithCookies(t) + + body := expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + code := testhelpers.CourierExpectCodeInMessage(t, message, 1) + + body, res := submitVerificationCode(t, body, c, code) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.AssertMessage(t, body, "You successfully verified your email address.") + + body = expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Use code") + code = testhelpers.CourierExpectCodeInMessage(t, message, 1) + + body, res = submitVerificationCode(t, body, c, code) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.AssertMessage(t, body, "You successfully verified your email address.") + }) + + t.Run("case=respects return_to URI parameter", func(t *testing.T) { + returnToURL := public.URL + "/after-verification" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL}) + + for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + flow, _, rawCode := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{"return_to": {returnToURL}}.Encode()) + + body := fmt.Sprintf( + `{"csrf_token":"%s","code":"%s"}`, flow.CSRFToken, rawCode, + ) + + res, err := client.Post(public.URL+verification.RouteSubmitFlow+"?"+url.Values{"flow": {flow.ID.String()}}.Encode(), "application/json", bytes.NewBuffer([]byte(body))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody) + assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody) + assert.Equal(t, returnToURL, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody) + }) + } + }) + + t.Run("case=contains default return to url", func(t *testing.T) { + globalReturnTo := public.URL + "/global" + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo) + + for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + flow, _, rawCode := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow) + + body := fmt.Sprintf( + `{"csrf_token":"%s","code":"%s"}`, flow.CSRFToken, rawCode, + ) + + res, err := client.Post(public.URL+verification.RouteSubmitFlow+"?"+url.Values{"flow": {flow.ID.String()}}.Encode(), "application/json", bytes.NewBuffer([]byte(body))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + t.Logf("%v", responseBody) + + assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody) + assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody) + assert.Equal(t, globalReturnTo, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody) + }) + } + }) + + t.Run("case=doesn't continue with OAuth2 flow if code is invalid", func(t *testing.T) { + returnToURL := public.URL + "/after-verification" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL}) + + client := testhelpers.NewClientWithCookies(t) + flow, _, _ := newValidFlow(t, flow.TypeBrowser, + public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{ + "return_to": {returnToURL}, + "login_challenge": {"any_valid_challenge"}, + }.Encode()) + + body := fmt.Sprintf( + `{"csrf_token":"%s","code":"%s"}`, flow.CSRFToken, "2475", + ) + + res, err := client.Post( + public.URL+verification.RouteSubmitFlow+"?"+url.Values{"flow": {flow.ID.String()}}.Encode(), + "application/json", + bytes.NewBuffer([]byte(body)), + ) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, responseBody.Get("state").String(), "sent_email", "%v", responseBody) + assert.Len(t, responseBody.Get("ui.messages").Array(), 1, "%v", responseBody) + assert.Equal(t, "The verification code is invalid or has already been used. Please try again.", responseBody.Get("ui.messages.0.text").String(), "%v", responseBody) + }) + + t.Run("description=should apply pending traits change when code is redeemed", func(t *testing.T) { + // Create an identity with original traits. + pendingID := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"original-code@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{"original-code@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + } + require.NoError(t, reg.IdentityManager().Create(ctx, pendingID, identity.ManagerAllowWriteProtectedTraits)) + + // The apply step now requires a live session that created the change. + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, pendingID, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Create a verification flow in StateEmailSent. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfApply := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: pendingID.ID, + Identity: pendingID, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfApply)) + + // Create a PendingTraitsChange record linked to this flow. + sessID := sess.ID + originFlowIDApply := sfApply.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: pendingID.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDApply, Valid: true}, + NewAddressValue: "changed-code@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(pendingID.Traits)), + ProposedTraits: json.RawMessage(`{"email":"changed-code@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification code with a nil address ID (pending-change signal). + rawCode := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: time.Hour, + VerifiableAddress: ptc, // PendingTraitsChange implements VerifiableAddressLike; ToPersistable() returns nil address + FlowID: f.ID, + }) + require.NoError(t, err) + + // Submit the code via HTTP POST. + cl := testhelpers.NewClientWithCookies(t) + action := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := cl.PostForm(action, url.Values{ + "code": {rawCode}, + "csrf_token": {nosurfx.FakeCSRFToken}, + }) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String(), "%s", body) + assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + + // Verify the identity traits were updated. + updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, pendingID.ID) + require.NoError(t, err) + assert.EqualValues(t, "changed-code@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String()) + + // Verify the new address is marked as verified. + var foundAddress *identity.VerifiableAddress + for idx := range updated.VerifiableAddresses { + if updated.VerifiableAddresses[idx].Value == "changed-code@ory.sh" { + foundAddress = &updated.VerifiableAddresses[idx] + break + } + } + require.NotNil(t, foundAddress, "new address should exist on identity") + assert.True(t, foundAddress.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, foundAddress.Status) + }) + + t.Run("description=fires settings post-persist webhook after pending traits change is applied", func(t *testing.T) { + // Set up a recording webhook target. + var receivedBody []byte + receivedCh := make(chan struct{}, 1) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedBody, _ = io.ReadAll(r.Body) + select { + case receivedCh <- struct{}{}: + default: + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(ts.Close) + + // Register webhook as a settings-profile post-persist hook. + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{ + { + "hook": "web_hook", + "config": map[string]any{ + "url": ts.URL, + "method": "POST", + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + }, + }) + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{}) + }) + + // Create an identity + active session. + ident := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"posthook-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Create verification flow + PTC linked to the session. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfHook := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfHook)) + + sessID := sess.ID + originFlowIDHook := sfHook.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDHook, Valid: true}, + NewAddressValue: "posthook-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"posthook-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + rawCode := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: time.Hour, + VerifiableAddress: ptc, + FlowID: f.ID, + }) + require.NoError(t, err) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.PostForm(public.URL+verification.RouteSubmitFlow+"?flow="+f.ID.String(), + url.Values{"code": {rawCode}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + select { + case <-receivedCh: + case <-time.After(5 * time.Second): + t.Fatal("settings post-persist webhook was not invoked after pending traits change applied") + } + assert.Contains(t, string(receivedBody), "posthook-new@ory.sh") + }) + + t.Run("description=post-persist webhook error after pending traits change is surfaced", func(t *testing.T) { + // Webhook target that always returns 500. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"boom"}`)) + })) + t.Cleanup(ts.Close) + + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{ + { + "hook": "web_hook", + "config": map[string]any{ + "url": ts.URL, + "method": "POST", + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + }, + }) + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{}) + }) + + ident := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"posthook-err-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfErr := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfErr)) + + sessID := sess.ID + originFlowIDErr := sfErr.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDErr, Valid: true}, + NewAddressValue: "posthook-err-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"posthook-err-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + rawCode := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: time.Hour, + VerifiableAddress: ptc, + FlowID: f.ID, + }) + require.NoError(t, err) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.PostForm(public.URL+verification.RouteSubmitFlow+"?flow="+f.ID.String(), + url.Values{"code": {rawCode}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + // Apply already committed before the webhook fired: the new traits and + // the completed PTC status persist even though the hook failed. + updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID) + require.NoError(t, err) + assert.EqualValues(t, "posthook-err-new@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String(), + "traits are committed before the post-persist hook runs") + }) + + t.Run("description=should reject pending traits change on concurrent modification", func(t *testing.T) { + // Create an identity with original traits. + concurrentID := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"original2-code@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{"original2-code@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + } + require.NoError(t, reg.IdentityManager().Create(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits)) + + // A live session is required so we reach the traits-hash check rather + // than bailing at the SessionID-nil guard first. + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, concurrentID, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Compute traits hash from the original traits. + originalHash := identity.HashTraits(json.RawMessage(concurrentID.Traits)) + + // Simulate a concurrent modification: update the identity's traits directly. + concurrentID.Traits = identity.Traits(`{"email":"concurrent-code@ory.sh"}`) + require.NoError(t, reg.IdentityManager().Update(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits)) + + // Create a verification flow in StateEmailSent. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfConc := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: concurrentID.ID, + Identity: concurrentID, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfConc)) + + // Create a PendingTraitsChange with the OLD traits hash (before concurrent modification). + sessID := sess.ID + originFlowIDConc := sfConc.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: concurrentID.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDConc, Valid: true}, + NewAddressValue: "changed2-code@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: originalHash, + ProposedTraits: json.RawMessage(`{"email":"changed2-code@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification code with nil address ID. + rawCode := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: time.Hour, + VerifiableAddress: ptc, + FlowID: f.ID, + }) + require.NoError(t, err) + + // Submit the code via HTTP POST — should be rejected due to concurrent modification. + cl := testhelpers.NewClientWithCookies(t) + action := public.URL + verification.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := cl.PostForm(action, url.Values{ + "code": {rawCode}, + "csrf_token": {nosurfx.FakeCSRFToken}, + }) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + // Should show the "code invalid" error (concurrent modification detected). + assert.Contains(t, body, "The verification code is invalid or has already been used") + + // Identity traits should NOT have been updated. + unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, concurrentID.ID) + require.NoError(t, err) + assert.EqualValues(t, "concurrent-code@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String(), + "traits should remain at the concurrent value, not the proposed value") + }) + + t.Run("description=should reject pending traits change when session is revoked", func(t *testing.T) { + ident := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"revoke-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfRevokeCode := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfRevokeCode)) + + sessID := sess.ID + originFlowIDRevokeCode := sfRevokeCode.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDRevokeCode, Valid: true}, + NewAddressValue: "revoke-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"revoke-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + rawCode := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCode, + ExpiresIn: time.Hour, + VerifiableAddress: ptc, + FlowID: f.ID, + }) + require.NoError(t, err) + + // Revoke the session AFTER the PTC is created — simulates logout between flow start and verification. + require.NoError(t, reg.SessionPersister().RevokeSessionById(ctx, sess.ID)) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.PostForm(public.URL+verification.RouteSubmitFlow+"?flow="+f.ID.String(), + url.Values{"code": {rawCode}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + assert.Contains(t, body, "The verification code is invalid or has already been used") + + // Traits should NOT be updated. + unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID) + require.NoError(t, err) + assert.EqualValues(t, "revoke-original@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String()) + }) + + t.Run("description=should show duplicate credentials error when two identities race to claim the same address", func(t *testing.T) { + // Both identities race past the pre-check (which is a non-atomic read) and both receive OTP codes. + // When the second one tries to apply the change, it hits a unique constraint violation. + identA := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"race-a@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + require.NoError(t, reg.IdentityManager().Create(ctx, identA, identity.ManagerAllowWriteProtectedTraits)) + + identB := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"race-b@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + require.NoError(t, reg.IdentityManager().Create(ctx, identB, identity.ManagerAllowWriteProtectedTraits)) + + sessA, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, identA, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sessA)) + + sessB, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, identB, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sessB)) + + newAddr := "race-claimed@ory.sh" + + // Two separate verification flows — one per identity. + fA, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + fA.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, fA)) + + fB, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + fB.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, fB)) + + sfA := &settings.Flow{ + ID: x.NewUUID(), ExpiresAt: time.Now().UTC().Add(time.Hour), IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", IdentityID: identA.ID, Identity: identA, + Type: flow.TypeBrowser, State: flow.StateShowForm, InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfA)) + + sfB := &settings.Flow{ + ID: x.NewUUID(), ExpiresAt: time.Now().UTC().Add(time.Hour), IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", IdentityID: identB.ID, Identity: identB, + Type: flow.TypeBrowser, State: flow.StateShowForm, InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfB)) + + ptcA := &identity.PendingTraitsChange{ + ID: x.NewUUID(), IdentityID: identA.ID, + SessionID: uuid.NullUUID{UUID: sessA.ID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: sfA.ID, Valid: true}, + NewAddressValue: newAddr, NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(identA.Traits)), + ProposedTraits: json.RawMessage(`{"email":"` + newAddr + `"}`), + VerificationFlowID: fA.ID, Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptcA)) + + ptcB := &identity.PendingTraitsChange{ + ID: x.NewUUID(), IdentityID: identB.ID, + SessionID: uuid.NullUUID{UUID: sessB.ID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: sfB.ID, Valid: true}, + NewAddressValue: newAddr, NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(identB.Traits)), + ProposedTraits: json.RawMessage(`{"email":"` + newAddr + `"}`), + VerificationFlowID: fB.ID, Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptcB)) + + rawCodeA := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCodeA, ExpiresIn: time.Hour, VerifiableAddress: ptcA, FlowID: fA.ID, + }) + require.NoError(t, err) + + rawCodeB := code.GenerateCode() + _, err = reg.VerificationCodePersister().CreateVerificationCode(ctx, &code.CreateVerificationCodeParams{ + RawCode: rawCodeB, ExpiresIn: time.Hour, VerifiableAddress: ptcB, FlowID: fB.ID, + }) + require.NoError(t, err) + + // Identity A verifies first — succeeds, address is now owned by A. + clA := testhelpers.NewClientWithCookies(t) + resA, err := clA.PostForm(public.URL+verification.RouteSubmitFlow+"?flow="+fA.ID.String(), + url.Values{"code": {rawCodeA}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + bodyA := string(ioutilx.MustReadAll(resA.Body)) + require.NoError(t, resA.Body.Close()) + require.Equal(t, http.StatusOK, resA.StatusCode) + assert.EqualValues(t, "passed_challenge", gjson.Get(bodyA, "state").String(), "identity A should pass: %s", bodyA) + + // Identity B verifies second — address already claimed, should get duplicate credentials error. + clB := testhelpers.NewClientWithCookies(t) + resB, err := clB.PostForm(public.URL+verification.RouteSubmitFlow+"?flow="+fB.ID.String(), + url.Values{"code": {rawCodeB}, "csrf_token": {nosurfx.FakeCSRFToken}}) + require.NoError(t, err) + bodyB := string(ioutilx.MustReadAll(resB.Body)) + require.NoError(t, resB.Body.Close()) + + require.Equal(t, http.StatusOK, resB.StatusCode) + assert.EqualValues(t, "sent_email", gjson.Get(bodyB, "state").String(), "identity B should stay on verification: %s", bodyB) + assert.Contains(t, bodyB, "An account with the same identifier (email, phone, username, ...) exists already.", "identity B should see duplicate credentials error: %s", bodyB) + + // Identity B tries to resend — the resend path re-checks uniqueness and shows the same error again. + // Pre-seed the CSRF cookie so EnsureCSRF passes for the resend path. + csrfURL, csrfCookies := nosurfx.WithFakeCSRFCookie(t, reg, public.URL) + clB.Jar.SetCookies(csrfURL, csrfCookies) + resendBody := url.Values{"email": {newAddr}, "csrf_token": {nosurfx.FakeCSRFToken}, "method": {"code"}}.Encode() + resendReq, err := http.NewRequest(http.MethodPost, + public.URL+verification.RouteSubmitFlow+"?flow="+fB.ID.String(), + strings.NewReader(resendBody)) + require.NoError(t, err) + resendReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resendReq.Header.Set("Accept", "application/json") + resResend, err := clB.Do(resendReq) + require.NoError(t, err) + bodyResend := string(ioutilx.MustReadAll(resResend.Body)) + require.NoError(t, resResend.Body.Close()) + + require.Equal(t, http.StatusBadRequest, resResend.StatusCode, "resend should return 400 duplicate credentials: %s", bodyResend) + assert.EqualValues(t, fB.ID.String(), gjson.Get(bodyResend, "id").String(), "resend should stay on the same flow: %s", bodyResend) + assert.Contains(t, bodyResend, "An account with the same identifier (email, phone, username, ...) exists already.", "resend should repeat the duplicate credentials error: %s", bodyResend) + + // Identity B's traits must be unchanged. + unchangedB, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identB.ID) + require.NoError(t, err) + assert.EqualValues(t, "race-b@ory.sh", gjson.GetBytes([]byte(unchangedB.Traits), "email").String()) + }) +} 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 0000000..9ccdaa4 --- /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 new file mode 100644 index 0000000..ecbeb33 --- /dev/null +++ b/selfservice/strategy/code/stub/code.identity.schema.json @@ -0,0 +1,86 @@ +{ + "$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", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + }, + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + }, + "email_0": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email_1": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "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": [] + } + } +} diff --git a/selfservice/strategy/code/stub/default.schema.json b/selfservice/strategy/code/stub/default.schema.json new file mode 100644 index 0000000..3d8a8ce --- /dev/null +++ b/selfservice/strategy/code/stub/default.schema.json @@ -0,0 +1,54 @@ +{ + "$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 + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "sms" + } + }, + "verification": { + "via": "sms" + }, + "recovery": { + "via": "sms" + } + } + } + } + } + } +} 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 0000000..3d2dee0 --- /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 0000000..8dc9232 --- /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/stub/recovery.nonrfc.schema.json b/selfservice/strategy/code/stub/recovery.nonrfc.schema.json new file mode 100644 index 0000000..005327b --- /dev/null +++ b/selfservice/strategy/code/stub/recovery.nonrfc.schema.json @@ -0,0 +1,35 @@ +{ + "$id": "https://example.com/person.nonrfc.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "no-validate", + "pattern": "^[a-zA-Z0-9!#$%&'*+\\=?^_`{|}~.\\-]+@docomo\\.ne\\.jp$", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go new file mode 100644 index 0000000..4a6313d --- /dev/null +++ b/selfservice/strategy/code/test/persistence.go @@ -0,0 +1,230 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/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/x" + "github.com/ory/x/contextx" + "github.com/ory/x/randx" +) + +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) + + ctx := contextx.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) { + var f recovery.Flow + require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod + + require.NoError(t, p.CreateRecoveryFlow(ctx, &f)) + + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + + address := &identity.RecoveryAddress{Value: email, Via: identity.AddressTypeEmail, IdentityID: i.ID} + i.RecoveryAddresses = append(i.RecoveryAddresses, *address) + + require.NoError(t, p.CreateIdentity(ctx, &i)) + + return &code.CreateRecoveryCodeParams{ + RawCode: randx.MustString(8, randx.Numeric), + FlowID: f.ID, + RecoveryAddress: &i.RecoveryAddresses[0], + ExpiresIn: time.Minute, + IdentityID: i.ID, + }, &f, &i.RecoveryAddresses[0] + } + + t.Run("case=should error when the recovery token does not exist", func(t *testing.T) { + _, err := p.UseRecoveryCode(ctx, x.NewUUID(), "i-do-not-exist") + require.Error(t, err) + }) + + t.Run("case=should create a new recovery code", func(t *testing.T) { + dto, f, a := newRecoveryCodeDTO(t, "foo-user@ory.sh") + rCode, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + assert.Equal(t, f.ID, rCode.FlowID) + assert.Equal(t, dto.IdentityID, rCode.IdentityID) + require.True(t, rCode.RecoveryAddressID.Valid) + assert.Equal(t, a.ID, rCode.RecoveryAddressID.UUID) + assert.Equal(t, a.ID, rCode.RecoveryAddress.ID) + }) + + t.Run("case=should create a recovery code and use it", func(t *testing.T) { + dto, f, _ := newRecoveryCodeDTO(t, "other-user@ory.sh") + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + t.Run("not work on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.UseRecoveryCode(ctx, f.ID, dto.RawCode) + require.ErrorIs(t, err, code.ErrCodeNotFound()) + }) + + actual, err := p.UseRecoveryCode(ctx, f.ID, dto.RawCode) + require.NoError(t, err) + assert.Equal(t, nid, actual.NID) + assert.Equal(t, dto.IdentityID, actual.IdentityID) + assert.NotEqual(t, dto.RawCode, actual.CodeHMAC) + assert.EqualValues(t, f.ID, actual.FlowID) + + _, err = p.UseRecoveryCode(ctx, f.ID, dto.RawCode) + require.ErrorIs(t, err, code.ErrCodeAlreadyUsed()) + }) + + t.Run("case=concurrent use of the same code succeeds at most once", func(t *testing.T) { + // HackerOne #3692149 / #3695625: the consume UPDATE must be + // atomic. On READ COMMITTED backends two concurrent + // submissions of the same valid code must not both succeed. + dto, f, _ := newRecoveryCodeDTO(t, "concurrent-code@ory.sh") + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + const concurrency = 8 + var success int32 + var start sync.WaitGroup + var done sync.WaitGroup + start.Add(1) + for range concurrency { + done.Add(1) + go func() { + defer done.Done() + start.Wait() + if _, err := p.UseRecoveryCode(ctx, f.ID, dto.RawCode); err == nil { + atomic.AddInt32(&success, 1) + } + }() + } + start.Done() + done.Wait() + + require.EqualValues(t, 1, atomic.LoadInt32(&success), "exactly one concurrent consumer may use the code") + }) + + t.Run("case=should not be able to use expired codes", func(t *testing.T) { + dto, f, _ := newRecoveryCodeDTO(t, "expired-code@ory.sh") + dto.ExpiresIn = -time.Hour + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + _, err = p.UseRecoveryCode(ctx, f.ID, dto.RawCode) + require.ErrorIs(t, err, code.ErrCodeNotFound()) + }) + + t.Run("case=should increment flow submit count and fail after too many tries (default limit)", func(t *testing.T) { + dto, f, _ := newRecoveryCodeDTO(t, "submit-count-default-limit@ory.sh") + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + var tooOften, wrongCode int32 + var wg sync.WaitGroup + for range 50 { + wg.Add(1) + go func() { + defer wg.Done() + _, err := p.UseRecoveryCode(ctx, f.ID, "i-do-not-exist") + if !assert.Error(t, err) { + return + } + if errors.Is(err, code.ErrCodeSubmittedTooOften()) { + atomic.AddInt32(&tooOften, 1) + } else { + atomic.AddInt32(&wrongCode, 1) + } + }() + } + wg.Wait() + + require.EqualValues(t, 50, wrongCode+tooOften, "all 50 attempts made") + require.LessOrEqual(t, wrongCode, int32(5), "max. 5 attempts have gone past the duplication check") + + // Submit again, just to be sure + _, err = p.UseRecoveryCode(ctx, f.ID, "i-do-not-exist") + require.ErrorIs(t, err, code.ErrCodeSubmittedTooOften()) + }) + + t.Run("case=should increment flow submit count and fail after too many tries (custom limit)", func(t *testing.T) { + limit := 2 + ctx := contextx.WithConfigValue(ctx, config.ViperKeyCodeMaxSubmissions, limit) + + dto, f, _ := newRecoveryCodeDTO(t, "submit-count-custom-limit@ory.sh") + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + + var tooOften, wrongCode int32 + var wg sync.WaitGroup + for range 50 { + wg.Add(1) + go func() { + defer wg.Done() + _, err := p.UseRecoveryCode(ctx, f.ID, "i-do-not-exist") + if !assert.Error(t, err) { + return + } + if errors.Is(err, code.ErrCodeSubmittedTooOften()) { + atomic.AddInt32(&tooOften, 1) + } else { + atomic.AddInt32(&wrongCode, 1) + } + }() + } + wg.Wait() + + require.EqualValues(t, 50, wrongCode+tooOften, "all 50 attempts made") + require.LessOrEqual(t, wrongCode, int32(limit), "max. %d attempts have gone past the duplication check", limit) + + // Submit again, just to be sure + _, err = p.UseRecoveryCode(ctx, f.ID, "i-do-not-exist") + require.ErrorIs(t, err, code.ErrCodeSubmittedTooOften()) + }) + + t.Run("case=should delete codes of flow", func(t *testing.T) { + dto, f, _ := newRecoveryCodeDTO(t, testhelpers.RandomEmail()) + for i := 0; i < 10; i++ { + dto.RawCode = string(randx.MustString(8, randx.Numeric)) + _, err := p.CreateRecoveryCode(ctx, dto) + require.NoError(t, err) + } + + count, err := p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ?", f.ID).Count(&code.RecoveryCode{}) + require.NoError(t, err) + require.Equal(t, 10, count) + + err = p.DeleteRecoveryCodesOfFlow(ctx, f.ID) + require.NoError(t, err) + + // Count again, should be 0 + count, err = p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ?", f.ID).Count(&code.RecoveryCode{}) + require.NoError(t, err) + require.Equal(t, 0, count) + }) + }) + } +} diff --git a/selfservice/strategy/deviceauthn/attestation.go b/selfservice/strategy/deviceauthn/attestation.go new file mode 100644 index 0000000..3bd0bb7 --- /dev/null +++ b/selfservice/strategy/deviceauthn/attestation.go @@ -0,0 +1,110 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package deviceauthn + +import ( + "encoding/asn1" +) + +// Attestation holds the platform-specific attestation parsed at enrollment. +// Exactly one of `android` or `ios` is set, matching the key's device_type. +// +// swagger:model deviceAuthnAttestation +type Attestation struct { + Android *AndroidKeyDescription `json:"android,omitempty"` + IOS *IOSAttestation `json:"ios,omitempty"` +} + +// AndroidKeyDescription matches the ASN.1 sequence defined by Android. +// It is stored in the leaf certificate's x509 extension. +// +// Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +// +// swagger:model deviceAuthnAndroidKeyDescription +type AndroidKeyDescription struct { + AttestationVersion int `json:"attestation_version,omitempty"` + AttestationSecurityLevel asn1.Enumerated `json:"attestation_security_level,omitempty"` + KeymasterVersion int `json:"keymaster_version,omitempty"` + KeymasterSecurityLevel asn1.Enumerated `json:"keymaster_security_level,omitempty"` + // AttestationChallenge is verified at enrollment time and not re-used + // later, so we don't persist it. + AttestationChallenge []byte `json:"-"` + // UniqueID is a hardware-tied identifier; we don't persist it for + // privacy reasons. + UniqueID []byte `json:"-"` + SoftwareEnforced AndroidAuthorizationList `json:"software_enforced"` + TeeEnforced AndroidAuthorizationList `json:"tee_enforced"` +} + +// AndroidAuthorizationList maps the ASN.1 tags to the fields defined in KeyMint 4. +// Schema: https://source.android.com/docs/security/features/keystore/attestation#schema +// +// swagger:model deviceAuthnAndroidAuthorizationList +type AndroidAuthorizationList struct { + Purpose []int `asn1:"tag:1,explicit,set,optional" json:"purpose,omitempty"` + Algorithm int `asn1:"tag:2,explicit,optional" json:"algorithm,omitempty"` + KeySize int `asn1:"tag:3,explicit,optional" json:"key_size,omitempty"` + Digest []int `asn1:"tag:5,explicit,set,optional" json:"digest,omitempty"` + EcCurve int `asn1:"tag:10,explicit,optional" json:"ec_curve,omitempty"` + ActiveDateTime int `asn1:"tag:400,explicit,optional" json:"active_date_time,omitempty"` + OriginationExpireDateTime int `asn1:"tag:401,explicit,optional" json:"origination_expire_date_time,omitempty"` + UsageExpireDateTime int `asn1:"tag:402,explicit,optional" json:"usage_expire_date_time,omitempty"` + NoAuthRequired asn1.RawValue `asn1:"tag:503,explicit,optional" json:"-"` + UserAuthType int `asn1:"tag:504,explicit,optional" json:"user_auth_type,omitempty"` + AuthTimeout int `asn1:"tag:505,explicit,optional" json:"auth_timeout,omitempty"` + AllowWhileOnBody asn1.RawValue `asn1:"tag:506,explicit,optional" json:"-"` + TrustedUserPresenceRequired asn1.RawValue `asn1:"tag:507,explicit,optional" json:"-"` + TrustedConfirmationRequired asn1.RawValue `asn1:"tag:508,explicit,optional" json:"-"` + UnlockedDeviceRequired asn1.RawValue `asn1:"tag:509,explicit,optional" json:"-"` + ApplicationID []byte `asn1:"tag:601,explicit,optional" json:"application_id,omitempty"` + CreationDateTime int `asn1:"tag:701,explicit,optional" json:"creation_date_time,omitempty"` + Origin int `asn1:"tag:702,explicit,optional" json:"origin,omitempty"` + RootOfTrust AndroidRootOfTrust `asn1:"tag:704,explicit,optional" json:"root_of_trust"` + OsVersion int `asn1:"tag:705,explicit,optional" json:"os_version,omitempty"` + OsPatchLevel int `asn1:"tag:706,explicit,optional" json:"os_patch_level,omitempty"` + AttestationChallenge []byte `asn1:"tag:708,explicit,optional" json:"-"` + AttestationApplicationID []byte `asn1:"tag:709,explicit,optional" json:"attestation_application_id,omitempty"` + AttestationIDBrand []byte `asn1:"tag:710,explicit,optional" json:"attestation_id_brand,omitempty"` + AttestationIDDevice []byte `asn1:"tag:711,explicit,optional" json:"attestation_id_device,omitempty"` + AttestationIDProduct []byte `asn1:"tag:712,explicit,optional" json:"attestation_id_product,omitempty"` + AttestationIDSerial []byte `asn1:"tag:713,explicit,optional" json:"attestation_id_serial,omitempty"` + AttestationIDImei []byte `asn1:"tag:714,explicit,optional" json:"attestation_id_imei,omitempty"` + AttestationIDMeid []byte `asn1:"tag:715,explicit,optional" json:"attestation_id_meid,omitempty"` + AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional" json:"attestation_id_manufacturer,omitempty"` + AttestationIDModel []byte `asn1:"tag:717,explicit,optional" json:"attestation_id_model,omitempty"` + VendorPatchLevel int `asn1:"tag:718,explicit,optional" json:"vendor_patch_level,omitempty"` + BootPatchLevel int `asn1:"tag:719,explicit,optional" json:"boot_patch_level,omitempty"` + DeviceUniqueAttestation asn1.RawValue `asn1:"tag:720,explicit,optional" json:"-"` +} + +// swagger:model deviceAuthnAndroidRootOfTrust +type AndroidRootOfTrust struct { + VerifiedBootKey []byte `asn1:"optional" json:"verified_boot_key,omitempty"` + DeviceLocked bool `json:"device_locked,omitempty"` + // Actually it is `AndroidVerifiedBootState` but due to ASN1 quirks we define it so. + VerifiedBootState asn1.Enumerated `json:"verified_boot_state,omitempty"` + VerifiedBootHash []byte `asn1:"optional" json:"verified_boot_hash,omitempty"` +} + +// IOSAttStmt is the attestation statement of an Apple App Attest attestation. +// Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +// +// swagger:model deviceAuthnIOSAttStmt +type IOSAttStmt struct { + // Receipt is several KB of proprietary opaque data; not persisted. + Receipt []byte `cbor:"receipt" json:"-"` + // X5c is the leaf-first certificate chain encoded as base64(DER), per + // the JOSE convention defined in RFC 7515 §4.1.6. + X5c [][]byte `cbor:"x5c" json:"x5c,omitempty"` +} + +// IOSAttestation is an Apple App Attest attestation. +// Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation . +// +// swagger:model deviceAuthnIOSAttestation +type IOSAttestation struct { + Fmt string `cbor:"fmt" json:"fmt,omitempty"` + AttStmt IOSAttStmt `cbor:"attStmt" json:"att_stmt"` + AuthData []byte `cbor:"authData" json:"auth_data,omitempty"` +} diff --git a/selfservice/strategy/deviceauthn/attestation_test.go b/selfservice/strategy/deviceauthn/attestation_test.go new file mode 100644 index 0000000..625287c --- /dev/null +++ b/selfservice/strategy/deviceauthn/attestation_test.go @@ -0,0 +1,44 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package deviceauthn_test + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/selfservice/strategy/deviceauthn" +) + +func TestAndroidKeyDescriptionJSON_dropsChallengeAndUniqueID(t *testing.T) { + desc := deviceauthn.AndroidKeyDescription{ + AttestationVersion: 3, + AttestationSecurityLevel: 1, // TEE-backed. + AttestationChallenge: []byte("nonce"), + UniqueID: []byte("hardware-id"), + TeeEnforced: deviceauthn.AndroidAuthorizationList{ + AttestationIDManufacturer: []byte("Google"), + }, + } + + out, err := json.Marshal(desc) + require.NoError(t, err) + + res := gjson.ParseBytes(out) + assert.Equal(t, int64(3), res.Get("attestation_version").Int()) + assert.Equal(t, base64.StdEncoding.EncodeToString([]byte("Google")), res.Get("tee_enforced.attestation_id_manufacturer").String()) + assert.False(t, res.Get("attestation_challenge").Exists(), "challenge is verified at enrollment, not serialized") + assert.False(t, res.Get("unique_id").Exists(), "unique id is privacy-sensitive, not serialized") +} + +func TestKeyAttestationOmitemptyWhenNil(t *testing.T) { + k := deviceauthn.Key{ClientKeyID: "abc"} + out, err := json.Marshal(k) + require.NoError(t, err) + assert.False(t, gjson.ParseBytes(out).Get("attestation").Exists()) +} diff --git a/selfservice/strategy/deviceauthn/key.go b/selfservice/strategy/deviceauthn/key.go new file mode 100644 index 0000000..b9eb1da --- /dev/null +++ b/selfservice/strategy/deviceauthn/key.go @@ -0,0 +1,198 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package deviceauthn + +import ( + "time" +) + +// The valid values (Android, iOS) are pinned as an enum via the OpenAPI patch +// in .schema/openapi/patches/schema.yaml. (Detached from the doc comment to +// stay out of the generated API description.) + +// DeviceType is the platform the key was enrolled on. +// +// "Android" and "iOS" use different attestation and signature encodings. +type DeviceType string + +const ( + DeviceTypeAndroid DeviceType = "Android" + DeviceTypeIOS DeviceType = "iOS" +) + +// The valid values (initial, confirmed, locked) are pinned as an enum via the +// OpenAPI patch in .schema/openapi/patches/schema.yaml. (Detached from the doc +// comment to stay out of the generated API description.) + +// KeyState is the lifecycle state of an enrolled key. +// +// Only a "confirmed" key can be used to authenticate. A "locked" key was +// disabled after too many wrong-PIN attempts; rotating its pin_secret unlocks +// it. +type KeyState string + +const ( + KeyStateInitial KeyState = "initial" + KeyStateConfirmed KeyState = "confirmed" + // KeyStateLocked marks a PIN key disabled after too many wrong-PIN attempts. + KeyStateLocked KeyState = "locked" +) + +// The schema name stays UpperCamelCase on purpose: it is part of the published +// DeviceAuthn schema family (DeviceAuthnKey, DeviceType, KeyState, PINConfig), +// which all use UpperCamelCase. Renaming it to the lowerCamelCase the core +// naming convention prefers would be a breaking change to the public SDK and +// would split the family's casing, so the deliberate exception is kept. +// The valid values (pin, platform, none) are pinned as an enum via the OpenAPI +// patch in .schema/openapi/patches/schema.yaml. (Detached from the doc comment +// to stay out of the generated API description.) + +// UserVerification records how the key's holder is verified at use time. +// +// "pin" means the holder proves knowledge of a PIN via pin_proof at login; +// "platform" means the device gates key use behind platform biometrics or an +// equivalent lock screen; "none" marks a possession-only key, usable as a +// second factor but never as the sole first factor. The empty value marks a +// legacy key from before user verification existed; such keys are rejected at +// login and must be re-enrolled. +type UserVerification string + +const ( + UserVerificationPIN UserVerification = "pin" + UserVerificationPlatform UserVerification = "platform" + // UserVerificationNone marks a possession-only key: usable as a second + // factor (step-up) but never as a first factor, which login enforces. + // Unlike JWT's alg=none the value strictly reduces what the key may do — + // it bypasses no verification. It covers keys deliberately enrolled + // without PIN or platform verification. + UserVerificationNone UserVerification = "none" +) + +// PINConfig holds only scalar fields, so it is safe to shallow copy; the pointer +// on Key.PIN encodes optionality (nil means no PIN), not copy safety. +// +// The schema name stays UpperCamelCase: it belongs to the published DeviceAuthn +// schema family (DeviceAuthnKey, DeviceType, KeyState, UserVerification), and +// renaming it would break the public SDK. +// (Detached from the doc comment to stay out of the generated API description.) + +// PINConfig is the per-key PIN state. +// +// The pin_secret field holds only the at-rest ciphertext; the plaintext +// exists transiently in server memory during verification and is invalidated +// once the key locks. +type PINConfig struct { + // pin_secret holds ciphertext of the kratos cipher (hex) and must never be + // returned to or logged by clients; the OpenAPI patch in + // .schema/openapi/patches/schema.yaml marks it write-only. + // (Detached from the doc comment to stay out of the API description.) + + // The at-rest ciphertext of the pin_secret. It never leaves the server and + // is cleared once the key locks. + PINSecret string `json:"pin_secret"` + // A negative stored value must be a decode error instead of silently + // extending the attempt budget, hence uint. (Detached from the doc comment + // to stay out of the generated API description.) + + // The number of consecutive wrong-PIN attempts so far; the key locks when + // it reaches the configured maximum (pin_max_attempts, default 5). + FailedAttempts uint `json:"failed_attempts"` + // When the pin_secret was first issued. + CreatedAt time.Time `json:"created_at"` + + // When the pin_secret was last rotated. Omitted if the secret was never + // rotated. + RotatedAt time.Time `json:"rotated_at,omitzero"` +} + +// Key is safe to shallow copy as long as the shared reference fields (PublicKey, +// PIN, Attestation, RelaxedAttestationExpiresAt) are treated as read-only: +// in-place mutation goes through a *Key into the backing slice, and the merge +// copies PIN rather than aliasing it. Kept out of the doc comment so it stays +// out of the generated API description. + +// An enrolled device authentication (DeviceAuthn) key. +// +// Represents a hardware-backed signing key enrolled from a mobile device. +// The private key resides inside the device and never exists on the server. +// +// To list the identity's enrolled keys, fetch a settings flow: each key's +// remove button (a `ui.nodes` entry named `deviceauthn_remove` in group +// `deviceauthn`) carries the key, with its PIN state redacted, in the node +// label's `context`. +// +// swagger:model DeviceAuthnKey +type Key struct { + // We intentionally avoid storing the cryptographic algorithm a la JWT/TLS to + // avoid security issues and algorithm negotiation; a future v2 may use + // ML-DSA (post-quantum) once Android/iOS support it. (Detached from the doc + // comment to stay out of the generated API description.) + + // The cryptography version of the key. Version 1 uses ECDSA with SHA-256 + // on an elliptic curve (P-224, P-256, P-384, or P-521); further versions + // are reserved for future signature suites. + Version int `json:"version"` + + // A human-readable name for the device, helping the user tell this key + // apart from others. + DeviceName string `json:"device_name"` + // The device's public key (an elliptic-curve key on P-224, P-256, P-384, + // or P-521 in version 1) in PKIX, ASN.1 DER (SubjectPublicKeyInfo) form, + // base64-encoded. Signatures are verified against this key. + // + // swagger:strfmt byte + PublicKey []byte `json:"public_key,omitempty"` + + // The key's stable id, unique per identity. Submit it as the + // `client_key_id` when logging in with the key, deleting it, or rotating + // its pin_secret. + // + // The device can also compute the id without reading it back from the + // server: it is the lowercase-hex SHA-256 of `public_key` (the key's + // PKIX, ASN.1 DER encoding). Keys enrolled before the server derived the + // id keep their original client-chosen value, so prefer reading this + // field over recomputing it for older keys. + ClientKeyID string `json:"client_key_id"` + // When the key was enrolled. Only used for troubleshooting and UI. + CreatedAt time.Time `json:"created_at"` + + // The platform the key was enrolled on, "Android" or "iOS". The two + // platforms use different attestation and signature encodings. + DeviceType DeviceType `json:"device_type"` + + // Auto-confirmation vs. an admin confirmation call (for out-of-band KYC, + // e.g. a physical letter with a code) is not configurable yet; keys are + // auto-confirmed at creation time. (Detached from the doc comment to stay + // out of the generated API description.) + + // The lifecycle state of the key. Only a "confirmed" key can be used to + // authenticate; a "locked" key was disabled after too many wrong-PIN + // attempts. + State KeyState `json:"state"` + + // How the key's holder is verified at use time: "pin", "platform", or + // "none". Empty for legacy keys enrolled before user verification existed; + // such keys are rejected at login and must be re-enrolled. + UserVerification UserVerification `json:"user_verification,omitempty"` + // The per-key PIN state. Absent unless the key is PIN-protected. + PIN *PINConfig `json:"pin,omitempty"` + + // The platform-specific attestation captured at enrollment. Useful for + // telling keys apart in the UI (e.g. OS version, manufacturer) and for + // forensics. + Attestation *Attestation `json:"attestation,omitempty"` + + // Set only when the key's attestation chain was accepted under relaxed + // rules (software roots, expired certificates, software security level) + // rather than strict hardware attestation. Such keys are refused at login + // after this time, or immediately once relaxed attestation is turned off. + // Absent for hardware-attested keys that pass strict validation. + RelaxedAttestationExpiresAt *time.Time `json:"relaxed_attestation_expires_at,omitempty"` +} + +// CredentialsDeviceAuthnConfig is the JSON shape stored in +// `identity_credentials.config` for the `deviceauthn` credential type. +type CredentialsDeviceAuthnConfig struct { + Credentials []Key `json:"credentials"` +} diff --git a/selfservice/strategy/deviceauthn/key_test.go b/selfservice/strategy/deviceauthn/key_test.go new file mode 100644 index 0000000..d60756b --- /dev/null +++ b/selfservice/strategy/deviceauthn/key_test.go @@ -0,0 +1,44 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package deviceauthn_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/selfservice/strategy/deviceauthn" +) + +func TestKeyVersion2RoundTrip(t *testing.T) { + t.Parallel() + k := deviceauthn.Key{ + Version: 2, + ClientKeyID: "abc123", + State: deviceauthn.KeyStateConfirmed, + UserVerification: deviceauthn.UserVerificationPIN, + PIN: &deviceauthn.PINConfig{ + PINSecret: "ciphertext-hex", + FailedAttempts: 2, + }, + } + b, err := json.Marshal(k) + require.NoError(t, err) + + var got deviceauthn.Key + require.NoError(t, json.Unmarshal(b, &got)) + assert.Equal(t, k, got) +} + +func TestKeyVersion1ParsesAsLegacy(t *testing.T) { + t.Parallel() + legacy := []byte(`{"version":1,"client_key_id":"old","state":"confirmed"}`) + var got deviceauthn.Key + require.NoError(t, json.Unmarshal(legacy, &got)) + assert.Equal(t, 1, got.Version) + assert.Nil(t, got.PIN) + assert.Equal(t, deviceauthn.UserVerification(""), got.UserVerification) +} diff --git a/selfservice/strategy/handler.go b/selfservice/strategy/handler.go new file mode 100644 index 0000000..59748f6 --- /dev/null +++ b/selfservice/strategy/handler.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package strategy + +import ( + "net/http" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httpx" +) + +const EndpointDisabledMessage = "This endpoint was disabled by system administrator. Please check your url or contact the system administrator to enable it." + +type disabledChecker interface { + config.Provider + httpx.WriterProvider +} + +func disabledWriter(c disabledChecker, enabled bool, wrap http.HandlerFunc, w http.ResponseWriter, r *http.Request) { + if !enabled { + c.Writer().WriteError(w, r, herodot.ErrNotFound().WithReason(EndpointDisabledMessage)) + return + } + wrap(w, r) +} + +func IsDisabled(c disabledChecker, strategy string, wrap http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + disabledWriter(c, c.Config().SelfServiceStrategy(r.Context(), strategy).Enabled, wrap, w, r) + } +} diff --git a/selfservice/strategy/idfirst/.schema/login.schema.json b/selfservice/strategy/idfirst/.schema/login.schema.json new file mode 100644 index 0000000..9e8be40 --- /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-case=Multi-Schema-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 0000000..4517b39 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1,55 @@ +[ + { + "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, + "autocomplete": "username webauthn", + "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=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json new file mode 100644 index 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..4517b39 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1,55 @@ +[ + { + "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, + "autocomplete": "username webauthn", + "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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..dc1ad6c --- /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 0000000..447ab23 --- /dev/null +++ b/selfservice/strategy/idfirst/strategy.go @@ -0,0 +1,51 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst + +import ( + "context" + + "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/nosurfx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + otelx.Provider + + config.Provider + + identity.PrivilegedPoolProvider + login.StrategyProvider + login.FlowPersistenceProvider +} + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeIdentifierFirst +} + +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 0000000..d031bd8 --- /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.AAL1FormHydrator = 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, sess *session.Session) (_ *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.idfirst.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 := decoderx.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) + } + + expand := identity.ExpandCredentials + if s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + expand = identity.ExpandNothing + } + // 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, + expand, + ) + 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) + } + + f.UI.ResetMessages() + f.UI.Nodes.SetValueAttribute("identifier", p.Identifier) + + // Add identity hint + opts := []login.FormHydratorModifier{login.WithIdentityHint(identityHint), login.WithIdentifier(p.Identifier)} + + didPopulate := false + for _, ls := range s.d.LoginStrategies(ctx, login.PrepareOrganizations(r, f, sess)...) { + populator, ok := ls.(login.AAL1FormHydrator) + 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 the method supports FastLogin, we can attempt a fast path. + if strategy, ok := ls.(login.FastLoginStrategy); ok { + err = strategy.FastLogin1FA(w, r, f, sess) + if err == nil { + return nil, flow.ErrCompletedByStrategy + } else if !errors.Is(err, flow.ErrStrategyNotResponsible) { + return nil, err + } + } + } + } + + // 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.Autocomplete = node.InputAttributeAutocompleteUsernameWebauthn + + 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, _ *session.Session) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(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 := f.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + + f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Autocomplete = node.InputAttributeAutocompleteUsernameWebauthn + a.Required = true + })).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 +} diff --git a/selfservice/strategy/idfirst/strategy_login_test.go b/selfservice/strategy/idfirst/strategy_login_test.go new file mode 100644 index 0000000..a9e3a4d --- /dev/null +++ b/selfservice/strategy/idfirst/strategy_login_test.go @@ -0,0 +1,719 @@ +// 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/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/urlx" +) + +func TestCompleteLogin(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + // We enable the password method to test the identifier first strategy + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + config.ViperKeySelfServiceLoginFlowStyle: "identifier_first", + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/default.schema.json")), + ) + + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + // 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.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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(t.Context())) + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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(t.Context())) + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + update, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), 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, 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.LoginMakeRequestCtx(t.Context(), t, 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.LoginMakeRequestCtx(t.Context(), t, 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.LoginMakeRequestCtx(t.Context(), t, 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) { + lifespan := 100 * time.Millisecond + conf.MustSet(t.Context(), config.ViperKeySelfServiceLoginRequestLifespan, lifespan) + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceLoginRequestLifespan, time.Hour) + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + // Freeze the clock so flow expiry is deterministic and the "expired N minutes ago" reason does not depend on + // the request round-trip. Each subtest creates a flow, then advances the clock past its lifespan instead of + // sleeping, so the handler and the expected error read the same instant. + mock := clock.NewMock(time.Now().UTC()) + reg.SetClock(mock) + t.Cleanup(func() { reg.SetClock(clock.New()) }) + + values := url.Values{ + "csrf_token": {nosurfx.FakeCSRFToken}, + "identifier": {"identifier"}, + "method": {"identifier_first"}, + } + + t.Run("type=api", func(t *testing.T) { + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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(reg.Clock(), expiresAt), 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) + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false, false) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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) + expiresAt := mock.Now().Add(lifespan) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, true, false, false) + mock.Set(expiresAt.Add(time.Minute)) + + actual, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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(reg.Clock(), expiresAt), 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(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), 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) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + 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, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), + 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) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + 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, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), + 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.InitializeLoginFlowViaAPICtx(t.Context(), 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.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer func() { _ = 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(t.Context()).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(t.Context(), 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(t.Context(), config.ViperKeyWebAuthnPasswordless, true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455") + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.display_name", "Ory Corp") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.id", "localhost") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.origins", []string{"http://localhost:4455"}) + + t.Cleanup(func() { + conf.MustSet(t.Context(), "selfservice.methods.password", nil) + conf.MustSet(t.Context(), "selfservice.methods.oidc", nil) + conf.MustSet(t.Context(), "selfservice.methods.passkey", nil) + conf.MustSet(t.Context(), "selfservice.methods.webauthn", nil) + conf.MustSet(t.Context(), "selfservice.methods.code", nil) + }) + + t.Run("account enumeration mitigation enabled", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, true) + + t.Cleanup(func() { + conf.MustSet(t.Context(), 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(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, false) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + check := func(t *testing.T, body string) { + t.Logf("aaxx %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.Equal(t, "google", gjson.Get(body, "ui.nodes.#(attributes.name==provider).attributes.value").String(), "google oidc button is not hidden") + + 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) + } + + 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(t.Context(), reg, t, identifier, true, false) + + 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.email").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.email").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.email").String(), "%s", body) + st := gjson.Get(body, "session_token").String() + assert.NotEmpty(t, st, "%s", body) + }) + }) + + t.Run("with code method", func(t *testing.T) { + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeCodeAuth.String(), true) + conf.MustSet(t.Context(), fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + t.Cleanup(func() { + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeCodeAuth.String(), false) + conf.MustSet(t.Context(), fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), nil) + }) + + browserClient := testhelpers.NewClientWithCookies(t) + + t.Run("only code method enabled", func(t *testing.T) { + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), false) + t.Cleanup(func() { + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + }) + + email := testhelpers.RandomEmail() + createIdentity(t.Context(), reg, t, email, false, true) + firstStepValues := func(v url.Values) { + v.Set("identifier", email) + v.Set("method", "identifier_first") + } + + t.Run("account enumeration mitigation off", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, false) + + firstStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstStepValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + b := []byte(firstStep) + + // Expect email sent (fast login) + require.EqualValues(t, flow.StateEmailSent, gjson.GetBytes(b, "state").String(), "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==code)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.messages").Array(), 1, "%s", b) + require.EqualValues(t, text.InfoSelfServiceLoginCodeSent, gjson.GetBytes(b, "ui.messages.0.id").Int(), "%s", b) + }) + + t.Run("account enumeration mitigation on", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, false) + }) + + firstStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstStepValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + b := []byte(firstStep) + + // Expect email sent (fast login) + require.EqualValues(t, flow.StateEmailSent, gjson.GetBytes(b, "state").String(), "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==code)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.messages").Array(), 1, "%s", b) + require.EqualValues(t, text.InfoSelfServiceLoginCodeSent, gjson.GetBytes(b, "ui.messages.0.id").Int(), "%s", b) + }) + }) + t.Run("all methods enabled", func(t *testing.T) { + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeCodeAuth.String(), true) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeWebAuthn.String(), true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455") + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.display_name", "Ory Corp") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.id", "localhost") + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.origins", []string{"http://localhost:4455"}) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true) + conf.MustSet(t.Context(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{ + { + ID: "google", + Provider: "google", + Label: "Google", + ClientID: "a", + ClientSecret: "b", + Mapper: "file://", + }, + }}) + + t.Run("all methods configured", func(t *testing.T) { + email := testhelpers.RandomEmail() + createIdentityWithAllMethods(t.Context(), reg, t, email) + firstStepValues := func(v url.Values) { + v.Set("identifier", email) + v.Set("method", "identifier_first") + } + + firstStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstStepValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + b := []byte(firstStep) + + // Expect method selection + require.EqualValues(t, flow.StateChooseMethod, gjson.GetBytes(b, "state").String(), "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==password)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==code)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==webauthn)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==passkey)").Array(), 1, "%s", b) + require.False(t, gjson.GetBytes(b, "ui.messages").Exists()) + }) + t.Run("only code method configured", func(t *testing.T) { + email := testhelpers.RandomEmail() + createIdentity(t.Context(), reg, t, email, false, true) + firstStepValues := func(v url.Values) { + v.Set("identifier", email) + v.Set("method", "identifier_first") + } + t.Run("account enumeration mitigation off", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, false) + + firstStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstStepValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + b := []byte(firstStep) + + // Expect email sent (fast login) + require.EqualValues(t, flow.StateEmailSent, gjson.GetBytes(b, "state").String(), "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==code)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.messages").Array(), 1, "%s", b) + require.EqualValues(t, text.InfoSelfServiceLoginCodeSent, gjson.GetBytes(b, "ui.messages.0.id").Int(), "%s", b) + }) + t.Run("account enumeration mitigation on", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySecurityAccountEnumerationMitigate, false) + }) + + firstStep := testhelpers.SubmitLoginFormCtx(t.Context(), t, false, browserClient, publicTS, firstStepValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + b := []byte(firstStep) + + // Expect method selection + require.EqualValues(t, flow.StateChooseMethod, gjson.GetBytes(b, "state").String(), "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==password)").Array(), 1, "%s", b) + require.Len(t, gjson.GetBytes(b, "ui.nodes.#(group==code)").Array(), 1, "%s", b) + require.False(t, gjson.GetBytes(b, "ui.messages").Exists()) + }) + }) + }) + }) +} + +func TestFormHydration(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeySelfServiceLoginFlowStyle: "identifier_first", + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: "file://stub/default.schema.json", SelfserviceSelectable: true}, + {ID: "not-default", URL: "file://stub/doesnotexist.schema.json", SelfserviceSelectable: true}, + }, + })) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsType(node.IdentifierFirstGroup)) + require.NoError(t, err) + fh, ok := s.(login.AAL1FormHydrator) + 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) { + t.Helper() + query := "" + + r := httptest.NewRequest("GET", "/self-service/login/browser"+query, nil) + r = r.WithContext(t.Context()) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + r, f := newFlow(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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(t.Context(), 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(t.Context(), 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 0000000..af10740 --- /dev/null +++ b/selfservice/strategy/idfirst/strategy_test.go @@ -0,0 +1,124 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/ui/node" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" +) + +func TestCompletedAuthenticationMethod(t *testing.T) { + _, reg := pkg.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 := pkg.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, email string, withPassword bool, withCode bool) *identity.Identity { + iId := x.NewUUID() + id := &identity.Identity{ + ID: iId, + Traits: identity.Traits(fmt.Sprintf(`{ "email": "%s" }`, email)), + Credentials: map[identity.CredentialsType]identity.Credentials{}, + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: email, + Verified: true, + Status: identity.VerifiableAddressStatusCompleted, + }, + }, + } + if withPassword { + p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte("password")) + id.Credentials[identity.CredentialsTypePassword] = identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + } + } + if withCode { + id.Credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{ + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"` + email + `"}]}`), + } + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), id)) + return id +} + +func createIdentityWithAllMethods(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, email string) *identity.Identity { + id := x.NewUUID() + pwd, _ := reg.Hasher(ctx).Generate(context.Background(), []byte("password")) + newIdentity := &identity.Identity{ + ID: id, + Traits: identity.Traits(fmt.Sprintf(`{ "email": "%s" }`, email)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(pwd) + `"}`), + }, + identity.CredentialsTypeCodeAuth: { + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"` + email + `"}]}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"some" : "secret"}`), + }, + identity.CredentialsTypeSAML: { + Type: identity.CredentialsTypeSAML, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"saml" : "secret"}`), + }, + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"some" : "secret", "user_handle": "rVIFaWRcTTuQLkXFmQWpgA=="}`), + }, + identity.CredentialsTypePasskey: { + Type: identity.CredentialsTypePasskey, + Identifiers: []string{email}, + Config: []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: email, + Verified: true, + Status: identity.VerifiableAddressStatusCompleted, + }, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), newIdentity)) + return newIdentity +} diff --git a/selfservice/strategy/idfirst/stub/default.schema.json b/selfservice/strategy/idfirst/stub/default.schema.json new file mode 100644 index 0000000..f13da2b --- /dev/null +++ b/selfservice/strategy/idfirst/stub/default.schema.json @@ -0,0 +1,33 @@ +{ + "$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 + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "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 0000000..8cd70ef --- /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/email.schema.json b/selfservice/strategy/link/.schema/email.schema.json new file mode 100644 index 0000000..ef66d13 --- /dev/null +++ b/selfservice/strategy/link/.schema/email.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "email" + ], + "properties": { + "csrf_token": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "minLength": 1 + } + } +} diff --git a/selfservice/strategy/link/.schema/recovery.schema.json b/selfservice/strategy/link/.schema/recovery.schema.json new file mode 100644 index 0000000..e17a587 --- /dev/null +++ b/selfservice/strategy/link/.schema/recovery.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "token": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email_loose", + "maxLength": 320 + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "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 new file mode 100644 index 0000000..e17a587 --- /dev/null +++ b/selfservice/strategy/link/.schema/verification.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "token": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email_loose", + "maxLength": 320 + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "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 new file mode 100644 index 0000000..5ac9946 --- /dev/null +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "link", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "link" + }, + "group": "link", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..1a8d048 --- /dev/null +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.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": "link", + "attributes": { + "name": "email", + "type": "email", + "value": "test@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "link", + "attributes": { + "name": "method", + "type": "submit", + "value": "link", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "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 new file mode 100644 index 0000000..5ac9946 --- /dev/null +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "link", + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "link" + }, + "group": "link", + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..1a8d048 --- /dev/null +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.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": "link", + "attributes": { + "name": "email", + "type": "email", + "value": "test@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070007, + "text": "Email", + "type": "info" + } + } + }, + { + "type": "input", + "group": "link", + "attributes": { + "name": "method", + "type": "submit", + "value": "link", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/link/persistence.go b/selfservice/strategy/link/persistence.go new file mode 100644 index 0000000..f8f73bb --- /dev/null +++ b/selfservice/strategy/link/persistence.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + + "github.com/gofrs/uuid" +) + +type ( + RecoveryTokenPersister interface { + CreateRecoveryToken(ctx context.Context, token *RecoveryToken) error + UseRecoveryToken(ctx context.Context, fID uuid.UUID, token string) (*RecoveryToken, error) + DeleteRecoveryToken(ctx context.Context, token string) error + } + + RecoveryTokenPersistenceProvider interface { + RecoveryTokenPersister() RecoveryTokenPersister + } + + VerificationTokenPersister interface { + CreateVerificationToken(ctx context.Context, token *VerificationToken) error + UseVerificationToken(ctx context.Context, fID uuid.UUID, token string) (*VerificationToken, error) + DeleteVerificationToken(ctx context.Context, token string) error + } + + VerificationTokenPersistenceProvider interface { + VerificationTokenPersister() VerificationTokenPersister + } +) diff --git a/selfservice/strategy/link/schema.go b/selfservice/strategy/link/schema.go new file mode 100644 index 0000000..5ecda28 --- /dev/null +++ b/selfservice/strategy/link/schema.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + _ "embed" +) + +//go:embed .schema/recovery.schema.json +var recoveryMethodSchema []byte + +//go:embed .schema/verification.schema.json +var verificationMethodSchema []byte diff --git a/selfservice/strategy/link/sender.go b/selfservice/strategy/link/sender.go new file mode 100644 index 0000000..5178fca --- /dev/null +++ b/selfservice/strategy/link/sender.go @@ -0,0 +1,279 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "net/url" + + "github.com/pkg/errors" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/urlx" +) + +type ( + senderDependencies interface { + courier.Provider + courier.ConfigProvider + + identity.PoolProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + logrusx.Provider + config.Provider + + VerificationTokenPersistenceProvider + RecoveryTokenPersistenceProvider + + hydra.Provider + httpx.ClientProvider + } + SenderProvider interface { + LinkSender() *Sender + } + + Sender struct { + r senderDependencies + } +) + +var ErrUnknownAddress = errors.New("verification requested for unknown address") + +func NewSender(r senderDependencies) *Sender { + return &Sender{r: r} +} + +// SendRecoveryLink sends a recovery link to the specified address +// +// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is +// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the +// ErrUnknownAddress error. +func (s *Sender) SendRecoveryLink(ctx context.Context, f *recovery.Flow, via, to string) error { + s.r.Logger(). + WithField("via", via). + WithSensitiveField("address", to). + Debug("Preparing recovery link.") + + address, err := s.r.IdentityPool().FindRecoveryAddressByValue(ctx, via, to) + if errors.Is(err, sqlcon.ErrNoRows()) { + notifyUnknownRecipients := s.r.Config().SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx) + s.r.Logger(). + WithField("via", via). + WithField("strategy", "link"). + 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(), + TransientPayload: transientPayload, + })); err != nil { + return err + } + return errors.WithStack(ErrUnknownAddress) + } else if err != nil { + // DB error + return err + } + + // Get the identity associated with the recovery address + i, err := s.r.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault) + if err != nil { + return err + } + + token := NewSelfServiceRecoveryToken(address, f, s.r.Config().SelfServiceLinkMethodLifespan(ctx)) + if err := s.r.RecoveryTokenPersister().CreateRecoveryToken(ctx, token); err != nil { + return err + } + + if err := s.SendRecoveryTokenTo(ctx, f, i, address, token); err != nil { + return err + } + + return nil +} + +// SendVerificationLink sends a verification link to the specified address +// +// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is +// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the +// ErrUnknownAddress error. +func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, via, to string) error { + s.r.Logger(). + WithField("via", via). + WithSensitiveField("address", to). + Debug("Preparing verification link.") + + address, err := s.r.IdentityPool().FindVerifiableAddressByValue(ctx, via, to) + if errors.Is(err, sqlcon.ErrNoRows()) { + notifyUnknownRecipients := s.r.Config().SelfServiceFlowVerificationNotifyUnknownRecipients(ctx) + s.r.Logger(). + WithField("via", via). + WithField("strategy", "link"). + 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(), + TransientPayload: transientPayload, + })); err != nil { + return err + } + return errors.WithStack(ErrUnknownAddress) + } else if err != nil { + // DB error + return err + } + + // Get the identity associated with the verification address + i, err := s.r.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault) + if err != nil { + return err + } + + token := NewSelfServiceVerificationToken(address, f, s.r.Config().SelfServiceLinkMethodLifespan(ctx)) + if err := s.r.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil { + return err + } + + if err := s.SendVerificationTokenTo(ctx, f, i, address, token); err != nil { + return err + } + + address.Status = identity.VerifiableAddressStatusSent + return s.r.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status") +} + +func (s *Sender) SendRecoveryTokenTo(ctx context.Context, f *recovery.Flow, i *identity.Identity, address *identity.RecoveryAddress, token *RecoveryToken) error { + s.r.Logger(). + WithField("via", address.Via). + WithField("identity_id", address.IdentityID). + WithField("recovery_link_id", token.ID). + WithSensitiveField("email_address", address.Value). + WithSensitiveField("recovery_link_token", token.Token). + Info("Sending out recovery email with recovery link.") + + model, err := x.StructToMap(i) + if err != nil { + return err + } + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + recoveryUrl := urlx.CopyWithQuery( + urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.r.Config().SelfPublicURL(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: recoveryUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.r.Config().SelfServiceLinkMethodLifespan(ctx).Minutes()), + })) +} + +func (s *Sender) SendVerificationTokenTo(ctx context.Context, f *verification.Flow, i *identity.Identity, sendable identity.VerifiableAddressLike, token *VerificationToken) error { + address, via := sendable.Address(), sendable.DeliveryVia() + s.r.Logger(). + WithField("via", via). + WithField("identity_id", i.ID). + WithField("verification_link_id", token.ID). + WithSensitiveField("email_address", address). + WithSensitiveField("verification_link_token", token.Token). + Info("Sending out verification email with verification link.") + + model, err := x.StructToMap(i) + if err != nil { + return err + } + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + // If the flow was initiated by an OAuth2 login challenge, expose a scoped + // view of the OAuth2 client to the courier templates so that messages can + // be branded per client. + if f.OAuth2LoginChallenge != "" && f.HydraLoginRequest == nil { + hlr, err := s.r.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge)) + if err != nil { + return errors.WithStack(err) + } + f.HydraLoginRequest = hlr + } + + verificationUrl := urlx.CopyWithQuery( + urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.r.Config().SelfPublicURL(ctx)), verification.RouteSubmitFlow), + url.Values{ + "flow": {f.ID.String()}, + "token": {token.Token}, + }).String() + + if err := s.send(ctx, via, email.NewVerificationValid(s.r, + &email.VerificationValidModel{ + To: address, + VerificationURL: verificationUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + ExpiresInMinutes: int(s.r.Config().SelfServiceLinkMethodLifespan(ctx).Minutes()), + OAuth2LoginRequest: template.NewOAuth2LoginRequest(f.HydraLoginRequest), + })); err != nil { + return err + } + return nil +} + +func (s *Sender) send(ctx context.Context, via string, t courier.EmailTemplate) error { + switch via { + case identity.AddressTypeEmail: + c, err := s.r.Courier(ctx) + if err != nil { + return err + } + _, err = c.QueueEmail(ctx, t) + return err + default: + return errors.Errorf("received unexpected via type: %s", via) + } +} diff --git a/selfservice/strategy/link/sender_test.go b/selfservice/strategy/link/sender_test.go new file mode 100644 index 0000000..7d7ac76 --- /dev/null +++ b/selfservice/strategy/link/sender_test.go @@ -0,0 +1,253 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/x" + "github.com/ory/x/contextx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func TestManager(t *testing.T) { + t.Parallel() + + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + initViper(t, conf) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json") + ctx = contextx.WithConfigValues(ctx, map[string]any{ + config.ViperKeyPublicBaseURL: "https://www.ory.com/", + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients: true, + config.ViperKeySelfServiceVerificationNotifyUnknownRecipients: true, + }) + + u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"email": "tracked@ory.sh"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + for _, tc := range []struct { + d string + setupContext func(ctx context.Context) context.Context + recoveryURL string + verificationURL string + }{{ + d: "without BaseURL", + setupContext: func(ctx context.Context) context.Context { return ctx }, + recoveryURL: "https://www.ory.com/self-service/recovery?flow=", + verificationURL: "https://www.ory.com/self-service/verification?flow=", + }, { + d: "with BaseURL", + setupContext: func(ctx context.Context) context.Context { + return x.WithBaseURL(ctx, urlx.ParseOrPanic("https://proxy.example.com/some/subpath/")) + }, + recoveryURL: "https://proxy.example.com/some/subpath/self-service/recovery?flow=", + verificationURL: "https://proxy.example.com/some/subpath/self-service/verification?flow=", + }} { + ctx := tc.setupContext(ctx) + + t.Run("case="+tc.d, func(t *testing.T) { + t.Run("method=SendRecoveryLink", func(t *testing.T) { + s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") + require.NoError(t, err) + f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + require.NoError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "tracked@ory.sh")) + require.EqualError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Contains(t, messages[0].Subject, "Recover access to your account") + assert.Contains(t, messages[0].Body, tc.recoveryURL) + assert.Contains(t, messages[0].Body, "token=") + assert.Contains(t, messages[0].Body, "flow=") + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Contains(t, messages[1].Subject, "Account access attempted") + assert.NotContains(t, messages[1].Body, f.RequestURL+"self-service/recovery?flow=") + assert.NotContains(t, messages[1].Body, "token=") + assert.NotContains(t, messages[1].Body, "flow=") + }) + + t.Run("method=SendRecoveryLink via HTTP", func(t *testing.T) { + type requestBody struct { + Recipient string + RecoveryURL string `json:"recovery_url"` + To string + TemplateType string + Subject string + } + messages := make(chan *requestBody, 2) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var message requestBody + require.NoError(t, json.NewDecoder(r.Body).Decode(&message)) + messages <- &message + })) + t.Cleanup(srv.Close) + + ctx = contextx.WithConfigValues(ctx, map[string]any{ + config.ViperKeyCourierDeliveryStrategy: "http", + config.ViperKeyCourierHTTPRequestConfig + ".url": srv.URL, + }) + + s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") + require.NoError(t, err) + f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + require.NoError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "tracked@ory.sh")) + require.EqualError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) + + cour, err := reg.Courier(ctx) + require.NoError(t, err) + + require.NoError(t, cour.DispatchQueue(ctx)) + close(messages) + + require.Len(t, messages, 2) + + msg := <-messages + assert.EqualValues(t, "tracked@ory.sh", msg.To) + assert.Contains(t, msg.Subject, "Recover access to your account") + assert.Contains(t, msg.RecoveryURL, tc.recoveryURL) + + msg = <-messages + assert.EqualValues(t, "not-tracked@ory.sh", msg.To) + assert.Contains(t, msg.Subject, "Account access attempted") + assert.NotContains(t, msg.RecoveryURL, tc.recoveryURL) + }) + + t.Run("method=SendVerificationLink", func(t *testing.T) { + strategies, _, err := reg.GetActiveVerificationStrategies(ctx) + require.NoError(t, err) + + f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), strategies, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + require.NoError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "tracked@ory.sh")) + require.EqualError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) + assert.Contains(t, messages[0].Subject, "Please verify") + assert.Contains(t, messages[0].Body, tc.verificationURL) + assert.Contains(t, messages[0].Body, "token=") + assert.Contains(t, messages[0].Body, "flow=") + + assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) + assert.Contains(t, messages[1].Subject, "tried to verify") + assert.NotContains(t, messages[1].Body, tc.verificationURL) + address, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.AddressTypeEmail, "tracked@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address.Status) + }) + }) + } + + t.Run("method=SendVerificationLink with OAuth2 login challenge", func(t *testing.T) { + reg.SetHydra(hydra.NewFake()) + strategies, _, err := reg.GetActiveVerificationStrategies(ctx) + require.NoError(t, err) + + f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), strategies, flow.TypeBrowser) + require.NoError(t, err) + f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeValidLoginChallenge) + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + require.NoError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "tracked@ory.sh")) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + templateData := messages[0].TemplateData + assert.Equal(t, hydra.FakeValidLoginChallenge, gjson.GetBytes(templateData, "oauth2_login_request.challenge").String()) + assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String()) + }) + + t.Run("case=should be able to disable invalid email dispatch", func(t *testing.T) { + for _, tc := range []struct { + flow string + send func(t *testing.T) + configKey string + }{ + { + flow: "recovery", + configKey: config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, + send: func(t *testing.T) { + s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") + require.NoError(t, err) + f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) + + err = reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh") + require.ErrorIs(t, err, link.ErrUnknownAddress) + }, + }, + { + flow: "verification", + configKey: config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, + send: func(t *testing.T) { + s, _, err := reg.VerificationStrategies(ctx).ActiveStrategies("link") + require.NoError(t, err) + f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + err = reg.LinkSender().SendVerificationLink(ctx, f, "email", "not-tracked@ory.sh") + require.ErrorIs(t, err, link.ErrUnknownAddress) + }, + }, + } { + t.Run("strategy="+tc.flow, func(t *testing.T) { + ctx = contextx.WithConfigValue(ctx, tc.configKey, false) + + tc.send(t) + + messages, err := reg.CourierPersister().NextMessages(ctx, 0) + + require.ErrorIs(t, err, courier.ErrQueueEmpty) + require.Len(t, messages, 0) + }) + } + }) +} diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go new file mode 100644 index 0000000..6566401 --- /dev/null +++ b/selfservice/strategy/link/strategy.go @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "github.com/ory/kratos/courier" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/transaction" + "github.com/ory/x/clock" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +var ( + _ recovery.Strategy = (*Strategy)(nil) + _ verification.Strategy = (*Strategy)(nil) + _ x.Handler = (*Strategy)(nil) +) + +type ( + // FlowMethod contains the configuration for this selfservice strategy. + FlowMethod struct { + *container.Container + } + + dependencies interface { + clock.Provider + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + transaction.PersistenceProvider + + config.Provider + + session.HandlerProvider + session.ManagementProvider + session.PersistenceProvider + settings.HandlerProvider + settings.FlowPersistenceProvider + settings.HookExecutorProvider + + identity.ValidationProvider + identity.ManagementProvider + identity.PoolProvider + identity.PrivilegedPoolProvider + identity.PendingTraitsChangePersistenceProvider + + courier.Provider + + errorx.ManagementProvider + + recovery.ErrorHandlerProvider + recovery.FlowPersistenceProvider + recovery.StrategyProvider + recovery.HookExecutorProvider + + verification.ErrorHandlerProvider + verification.FlowPersistenceProvider + verification.StrategyProvider + verification.HookExecutorProvider + verification.HandlerProvider + + RecoveryTokenPersistenceProvider + VerificationTokenPersistenceProvider + SenderProvider + + schema.IdentitySchemaProvider + } + + Strategy struct{ d dependencies } +) + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.LinkGroup } diff --git a/selfservice/strategy/link/strategy_nonrfc_test.go b/selfservice/strategy/link/strategy_nonrfc_test.go new file mode 100644 index 0000000..4af8664 --- /dev/null +++ b/selfservice/strategy/link/strategy_nonrfc_test.go @@ -0,0 +1,68 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/x/decoderx" +) + +// decodeSubmission drives the production decode path used by the link +// strategy's decodeRecovery / decodeVerification. +func decodeSubmission(t *testing.T, schema []byte, email string) error { + t.Helper() + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(schema) + require.NoError(t, err) + + form := url.Values{"email": {email}, "method": {"link"}} + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + var body struct { + Email string `json:"email"` + } + return decoderx.Decode(req, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +// TestLinkRecoverySubmissionAcceptsNonRFCEmail and +// TestLinkVerificationSubmissionAcceptsNonRFCEmail verify the `email_loose` +// submission schemas accept schema-approved non-RFC carrier addresses while +// still rejecting injection-unsafe input at decode. +func TestLinkRecoverySubmissionAcceptsNonRFCEmail(t *testing.T) { + t.Parallel() + + require.NoError(t, decodeSubmission(t, recoveryMethodSchema, "user@example.org")) + require.NoError(t, decodeSubmission(t, recoveryMethodSchema, "foo.@docomo.ne.jp")) + for _, evil := range []string{"foo@ory.sh\r\nBcc: evil@x", "foo@ory.sh", "foo@ory.sh,evil@x"} { + t.Run("rejected="+evil, func(t *testing.T) { + require.Error(t, decodeSubmission(t, recoveryMethodSchema, evil)) + }) + } +} + +func TestLinkVerificationSubmissionAcceptsNonRFCEmail(t *testing.T) { + t.Parallel() + + require.NoError(t, decodeSubmission(t, verificationMethodSchema, "user@example.org")) + require.NoError(t, decodeSubmission(t, verificationMethodSchema, "foo.@docomo.ne.jp")) + for _, evil := range []string{"foo@ory.sh\r\nBcc: evil@x", "foo@ory.sh", "foo@ory.sh,evil@x"} { + t.Run("rejected="+evil, func(t *testing.T) { + require.Error(t, decodeSubmission(t, verificationMethodSchema, evil)) + }) + } +} diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go new file mode 100644 index 0000000..3e6b3a6 --- /dev/null +++ b/selfservice/strategy/link/strategy_recovery.go @@ -0,0 +1,560 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "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/recovery" + "github.com/ory/kratos/selfservice/strategy" + "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/events" + "github.com/ory/kratos/x/redir" + "github.com/ory/pop/v6" + "github.com/ory/x/decoderx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +const RouteAdminCreateRecoveryLink = "/recovery/link" + +func (s *Strategy) RecoveryStrategyID() string { + return string(recovery.RecoveryStrategyLink) +} + +func (s *Strategy) IsPrimary() bool { + return true +} + +func (s *Strategy) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + s.d.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryLink) + public.POST(RouteAdminCreateRecoveryLink, redir.RedirectToAdminRoute(s.d)) +} + +func (s *Strategy) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + wrappedCreateRecoveryLink := strategy.IsDisabled(s.d, s.RecoveryStrategyID(), s.createRecoveryLinkForIdentity) + admin.POST(RouteAdminCreateRecoveryLink, wrappedCreateRecoveryLink) +} + +func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.GetNodes().Upsert( + // 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.NewInfoNodeLabelContinue())) + + return nil +} + +// Create Recovery Link for Identity Parameters +// +// swagger:parameters createRecoveryLinkForIdentity +type _ struct { + // in: body + Body createRecoveryLinkForIdentityBody + // in: query + ReturnTo string `json:"return_to"` +} + +// Create Recovery Link for Identity Request Body +// +// swagger:model createRecoveryLinkForIdentityBody +type createRecoveryLinkForIdentityBody struct { + // Identity to Recover + // + // The identity's ID you wish to recover. + // + // required: true + IdentityID uuid.UUID `json:"identity_id"` + + // Link Expires In + // + // The recovery link will expire after that amount of time has passed. Defaults to the configuration value of + // `selfservice.methods.code.config.lifespan`. + // + // + // pattern: ^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$ + // example: + // - 1h + // - 1m + // - 1s + ExpiresIn string `json:"expires_in"` +} + +// Identity Recovery Link +// +// Used when an administrator creates a recovery link for an identity. +// +// swagger:model recoveryLinkForIdentity +type recoveryLinkForIdentity struct { + // Recovery Link + // + // This link can be used to recover the account. + // + // required: true + // format: uri + RecoveryLink string `json:"recovery_link"` + + // Recovery Link Expires At + // + // The timestamp when the recovery link expires. + ExpiresAt time.Time `json:"expires_at"` +} + +// swagger:route POST /admin/recovery/link identity 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. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: recoveryLinkForIdentity +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (s *Strategy) createRecoveryLinkForIdentity(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var p createRecoveryLinkForIdentityBody + if err := decoderx.Decode(r, &p, decoderx.HTTPJSONDecoder()); err != nil { + s.d.Writer().WriteError(w, r, err) + return + } + + expiresIn := s.d.Config().SelfServiceLinkMethodLifespan(ctx) + if len(p.ExpiresIn) > 0 { + var err error + expiresIn, err = time.ParseDuration(p.ExpiresIn) + if err != nil { + s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to parse "expires_in" as a duration string (e.g. "1h", "1m30s", "500ms"): %s`, p.ExpiresIn))) + return + } + } + + if expiresIn <= 0 { + s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Value from "expires_in" must be result to a future time: %s`, p.ExpiresIn))) + return + } + + req, err := recovery.NewFlow(s.d, expiresIn, s.d.GenerateCSRFToken(r), r, recovery.Strategies{s}, flow.TypeBrowser) + if err != nil { + s.d.Writer().WriteError(w, r, err) + return + } + + id, err := s.d.IdentityPool().GetIdentity(ctx, p.IdentityID, identity.ExpandDefault) + if errors.Is(err, sqlcon.ErrNoRows()) { + s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("The requested identity id does not exist.").WithWrap(err))) + return + } else if err != nil { + s.d.Writer().WriteError(w, r, err) + return + } + + token := NewAdminRecoveryToken(id.ID, req.ID, expiresIn) + if err := s.d.TransactionalPersisterProvider().Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := s.d.RecoveryFlowPersister().CreateRecoveryFlow(ctx, req); err != nil { + return err + } + + return s.d.RecoveryTokenPersister().CreateRecoveryToken(ctx, token) + }); err != nil { + s.d.Writer().WriteError(w, r, err) + return + } + + trace.SpanFromContext(ctx).AddEvent( + events.NewRecoveryInitiatedByAdmin(ctx, req.ID, id.ID, req.Type.String(), "link"), + ) + + s.d.Logger(). + WithField("identity_id", id.ID). + WithSensitiveField("recovery_link_token", token). + Info("A recovery link has been created.") + + s.d.Writer().Write(w, r, &recoveryLinkForIdentity{ + ExpiresAt: req.ExpiresAt.UTC(), + RecoveryLink: urlx.CopyWithQuery( + urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), recovery.RouteSubmitFlow), + url.Values{ + "token": {token.Token}, + "flow": {req.ID.String()}, + }).String(), + }, + herodot.UnescapedHTML) +} + +// Update Recovery Flow with Link Method +// +// swagger:model updateRecoveryFlowWithLinkMethod +type _ struct { + // Email to Recover + // + // Needs to be set when initiating the flow. If the email is a registered + // recovery email, a recovery link will be sent. If the email is not known, + // an email with details on what happened will be sent instead. + // + // format: email + // required: true + Email string `json:"email" form:"email"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `form:"csrf_token" json:"csrf_token"` + + // Method is the method that should be used for this recovery flow + // + // Allowed values are `link` and `code` + // + // 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") + span.SetAttributes(attribute.String("selfservice_flows_recovery_use", s.d.Config().SelfServiceFlowRecoveryUse(ctx))) + defer otelx.End(span, &err) + + body, err := s.decodeRecovery(r) + if err != nil { + return s.HandleRecoveryError(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(r, nil, body, err) + } + + return s.recoveryUseToken(ctx, w, r, f.ID, body) + } + + if err := s.d.SessionManager().SessionActiveForRequest(r.Context(), r); err == nil { + if x.IsJSONRequest(r) { + session.RespondWithJSONErrorOnAuthenticated(s.d.Writer(), recovery.ErrAlreadyLoggedIn())(w, r) + } else { + session.RedirectOnAuthenticated(s.d)(w, r) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { + return s.HandleRecoveryError(r, nil, body, err) + } + + req, err := s.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), x.ParseUUID(body.Flow)) + if err != nil { + return s.HandleRecoveryError(r, req, body, err) + } + + if err := req.Valid(s.d.Clock()); err != nil { + return s.HandleRecoveryError(r, req, body, err) + } + + switch req.State { + case flow.StateChooseMethod, + flow.StateEmailSent: + return s.recoveryHandleFormSubmission(r, req) + case flow.StatePassedChallenge: + // was already handled, do not allow retry + return s.retryRecoveryFlowWithMessage(w, r, req.Type, text.NewErrorValidationRecoveryRetrySuccess()) + default: + return s.retryRecoveryFlowWithMessage(w, r, req.Type, text.NewErrorValidationRecoveryStateFailure()) + } +} + +func (s *Strategy) recoveryIssueSession(ctx context.Context, w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity) error { + f.UI.Messages.Clear() + f.State = flow.StatePassedChallenge + f.SetCSRFToken(s.d.CSRFHandler().RegenerateToken(w, r)) + f.RecoveredIdentityID = uuid.NullUUID{ + UUID: id.ID, + Valid: true, + } + if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + 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) + } + + // Force load. + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandEverything); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + if err := s.d.RecoveryExecutor().PostRecoveryHook(w, r, f, sess); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + if err := s.d.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, sess); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + sf, err := s.d.SettingsHandler().NewFlow(ctx, w, r, sess.Identity, sess, flow.TypeBrowser) + if err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + returnToURL := s.d.Config().SelfServiceFlowRecoveryReturnTo(r.Context(), nil) + returnTo := "" + if returnToURL != nil { + returnTo = returnToURL.String() + } + + sf.RequestURL, err = redir.TakeOverReturnToParameter(f.RequestURL, sf.RequestURL, returnTo) + if err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + sf.UI.Messages.Set(text.NewRecoverySuccessful(s.d.Clock(), s.d.Clock().Now().Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())))) + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), sf); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + http.Redirect(w, r, sf.AppendTo(s.d.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther) + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) recoveryUseToken(ctx context.Context, w http.ResponseWriter, r *http.Request, fID uuid.UUID, body *recoverySubmitPayload) error { + token, err := s.d.RecoveryTokenPersister().UseRecoveryToken(r.Context(), fID, body.Token) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + return s.retryRecoveryFlowWithMessage(w, r, flow.TypeBrowser, text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed()) + } + + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + var f *recovery.Flow + if !token.FlowID.Valid { + f, err = recovery.NewFlow(s.d, token.ExpiresAt.Sub(s.d.Clock().Now()), s.d.GenerateCSRFToken(r), r, recovery.Strategies{s}, flow.TypeBrowser) + if err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + if err := s.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), f); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + } else { + f, err = s.d.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), token.FlowID.UUID) + if err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + } + + if err := token.Valid(s.d.Clock()); err != nil { + return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) + } + + // Important to expand everything here, as we need the data for recovery. + recovered, err := s.d.IdentityPool().GetIdentity(r.Context(), token.IdentityID, identity.ExpandEverything) + if err != nil { + return s.HandleRecoveryError(r, f, nil, err) + } + + // mark address as verified only for a self-service flow + if token.TokenType == RecoveryTokenTypeSelfService { + if err := s.markRecoveryAddressVerified(r, f, recovered, token.RecoveryAddress); err != nil { + return s.HandleRecoveryError(r, f, body, err) + } + } + + return s.recoveryIssueSession(ctx, w, r, f, recovered) +} + +func (s *Strategy) retryRecoveryFlowWithMessage(w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error { + s.d.Logger().WithRequest(r).WithField("message", message).Debug("A recovery flow is being retried because a validation error occurred.") + + req, err := recovery.NewFlow(s.d, s.d.Config().SelfServiceFlowRecoveryRequestLifespan(r.Context()), s.d.CSRFHandler().RegenerateToken(w, r), r, recovery.Strategies{s}, ft) + if err != nil { + return err + } + + req.UI.Messages.Add(message) + if err := s.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), req); err != nil { + return err + } + + if ft == flow.TypeBrowser { + http.Redirect(w, r, req.AppendTo(s.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + recovery.RouteGetFlow), url.Values{"id": {req.ID.String()}}).String(), http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) retryRecoveryFlowWithError(w http.ResponseWriter, r *http.Request, ft flow.Type, recErr error) error { + s.d.Logger().WithRequest(r).WithError(recErr).Debug("A recovery flow is being retried because a validation error occurred.") + + req, err := recovery.NewFlow(s.d, s.d.Config().SelfServiceFlowRecoveryRequestLifespan(r.Context()), s.d.CSRFHandler().RegenerateToken(w, r), r, recovery.Strategies{s}, ft) + if err != nil { + return err + } + + if expired := new(flow.ExpiredError); errors.As(recErr, &expired) { + return s.retryRecoveryFlowWithMessage(w, r, ft, text.NewErrorValidationRecoveryFlowExpired(s.d.Clock(), expired.ExpiredAt)) + } else { + if err := req.UI.ParseError(node.LinkGroup, recErr); err != nil { + return err + } + } + + if err := s.d.RecoveryFlowPersister().CreateRecoveryFlow(r.Context(), req); err != nil { + return err + } + + if ft == flow.TypeBrowser { + http.Redirect(w, r, req.AppendTo(s.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } else { + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + recovery.RouteGetFlow), url.Values{"id": {req.ID.String()}}).String(), http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) recoveryHandleFormSubmission(r *http.Request, f *recovery.Flow) error { + body, err := s.decodeRecovery(r) + if err != nil { + return s.HandleRecoveryError(r, f, body, err) + } + + if len(body.Email) == 0 { + return s.HandleRecoveryError(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.HandleRecoveryError(r, f, body, err) + } + + if err := s.d.LinkSender().SendRecoveryLink(r.Context(), f, identity.AddressTypeEmail, body.Email); err != nil { + if !errors.Is(err, ErrUnknownAddress) { + return s.HandleRecoveryError(r, f, body, err) + } + // Continue execution + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + 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()), + ) + + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent + f.UI.Messages.Set(text.NewRecoveryEmailSent()) + if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { + return s.HandleRecoveryError(r, f, body, err) + } + + return nil +} + +func (s *Strategy) markRecoveryAddressVerified(r *http.Request, f *recovery.Flow, id *identity.Identity, recoveryAddress *identity.RecoveryAddress) error { + for k, v := range id.VerifiableAddresses { + if v.Value == recoveryAddress.Value { + id.VerifiableAddresses[k].Verified = true + id.VerifiableAddresses[k].VerifiedAt = new(sqlxx.NullTime(time.Now().UTC())) + id.VerifiableAddresses[k].Status = identity.VerifiableAddressStatusCompleted + if err := s.d.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &id.VerifiableAddresses[k], "verified", "verified_at", "status"); err != nil { + return s.HandleRecoveryError(r, f, nil, err) + } + } + } + + return nil +} + +func (s *Strategy) HandleRecoveryError(r *http.Request, req *recovery.Flow, body *recoverySubmitPayload, err error) error { + if req != nil { + email := "" + if body != nil { + email = body.Email + } + + req.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + req.UI.GetNodes().Upsert( + // v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email} + node.NewInputField("email", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } + + return err +} + +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"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} + +func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { + var body recoverySubmitPayload + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(recoveryMethodSchema) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := decoderx.Decode(r, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return nil, errors.WithStack(err) + } + + return &body, nil +} diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go new file mode 100644 index 0000000..b15bc4e --- /dev/null +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -0,0 +1,924 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/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/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/clock" + "github.com/ory/x/contextx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func init() { + corpx.RegisterFakes() +} + +func createIdentityToRecover(t *testing.T, reg *driver.RegistryDefault, email string) *identity.Identity { + id := &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + require.NoError(t, reg.IdentityManager().Create(t.Context(), id, identity.ManagerAllowWriteProtectedTraits)) + + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, email) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + return id +} + +func TestAdminStrategy(t *testing.T) { + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + initViper(t, conf) + + _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + publicTS, adminTS := testhelpers.NewKratosServer(t, reg) + adminSDK := testhelpers.NewSDKClient(adminTS) + + checkLink := func(t *testing.T, l *kratos.RecoveryLinkForIdentity, isBefore time.Time) { + require.Contains(t, l.RecoveryLink, publicTS.URL+recovery.RouteSubmitFlow) + rl := urlx.ParseOrPanic(l.RecoveryLink) + assert.NotEmpty(t, rl.Query().Get("token")) + assert.NotEmpty(t, rl.Query().Get("flow")) + require.True(t, (*l.ExpiresAt).Before(isBefore)) + } + + t.Run("no panic on empty body #1384", func(t *testing.T) { + s, ps, err := reg.RecoveryStrategies(t.Context()).ActiveStrategies("link") + require.NoError(t, err) + + r := &http.Request{URL: new(url.URL)} + f, err := recovery.NewFlow(reg, time.Minute, "", r, s, flow.TypeBrowser) + require.NoError(t, err) + + require.IsType(t, &link.Strategy{}, ps) + expectedErr := errors.New("test") + err = ps.(*link.Strategy).HandleRecoveryError(r, f, nil, expectedErr) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) { + _, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(t.Context()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: x.NewUUID().String(), + }).Execute() + require.IsTypef(t, err, new(kratos.GenericOpenAPIError), "%T", err) + assert.EqualError(t, err, "400 Bad Request") + }) + + t.Run("description=should create a valid recovery link without email", func(t *testing.T) { + id := identity.Identity{Traits: identity.Traits(`{}`)} + + require.NoError(t, reg.IdentityManager().Create(t.Context(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(t.Context()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + ExpiresIn: new("100ms"), + }).Execute() + require.NoError(t, err) + + time.Sleep(time.Millisecond * 100) + checkLink(t, rl, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx))) + res, err := publicTS.Client().Get(rl.RecoveryLink) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, res.StatusCode) + + // We end up here because the link is expired. + assert.Contains(t, res.Request.URL.Path, "/recover", rl.RecoveryLink) + }) + + t.Run("description=should create a valid recovery link and set the expiry time and not be able to recover the account", func(t *testing.T) { + recoveryEmail := "recover.expired@ory.sh" + id := identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, recoveryEmail))} + + require.NoError(t, reg.IdentityManager().Create(t.Context(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(t.Context()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + ExpiresIn: new("100ms"), + }).Execute() + require.NoError(t, err) + + time.Sleep(time.Millisecond * 100) + checkLink(t, rl, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx))) + res, err := publicTS.Client().Get(rl.RecoveryLink) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, res.StatusCode) + + // We end up here because the link is expired. + assert.Contains(t, res.Request.URL.Path, "/recover", rl.RecoveryLink) + + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + 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 a valid recovery link and set the expiry time as well and recover the account", func(t *testing.T) { + recoveryEmail := "recoverme@ory.sh" + id := identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, recoveryEmail))} + + require.NoError(t, reg.IdentityManager().Create(t.Context(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(t.Context()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + }).Execute() + require.NoError(t, err) + + checkLink(t, rl, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second)) + res, err := publicTS.Client().Get(rl.RecoveryLink) + require.NoError(t, err) + + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowSettingsUI(ctx).String()) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.LogJSON(t, rl) + + f, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow"))) + require.NoError(t, err, "%s", res.Request.URL.String()) + + require.Len(t, f.UI.Messages, 1) + assert.Equal(t, "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.", f.UI.Messages[0].Text) + + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + }) + + t.Run("case=should not be able to use code from different flow", func(t *testing.T) { + email := strings.ToLower(testhelpers.RandomEmail()) + id := createIdentityToRecover(t, reg, email) + + rl1, _, err := adminSDK.IdentityAPI. + CreateRecoveryLinkForIdentity(t.Context()). + CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + }). + Execute() + require.NoError(t, err) + + checkLink(t, rl1, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second)) + + rl2, _, err := adminSDK.IdentityAPI. + CreateRecoveryLinkForIdentity(t.Context()). + CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + }). + Execute() + require.NoError(t, err) + + checkLink(t, rl2, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second)) + + recoveryUrl1, err := url.Parse(rl1.RecoveryLink) + require.NoError(t, err) + + recoveryUrl2, err := url.Parse(rl2.RecoveryLink) + require.NoError(t, err) + + token1 := recoveryUrl1.Query().Get("token") + require.NotEmpty(t, token1) + token2 := recoveryUrl2.Query().Get("token") + require.NotEmpty(t, token2) + require.NotEqual(t, token1, token2) + + values := recoveryUrl1.Query() + + values.Set("token", token2) + + recoveryUrl1.RawQuery = values.Encode() + + action := recoveryUrl1.String() + // Submit the modified link with token from rl2 and flow from rl1 + res, err := publicTS.Client().Get(action) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + action = gjson.GetBytes(body, "ui.action").String() + require.NotEmpty(t, action) + assert.Equal(t, "The recovery token is invalid or has already been used. Please retry the flow.", gjson.GetBytes(body, "ui.messages.0.text").String()) + }) +} + +func TestRecovery(t *testing.T) { + ctx := t.Context() + conf, reg := pkg.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) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, publicRouter, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + if hc == nil { + hc = testhelpers.NewDebugClient(t) + if !isAPI { + hc = testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + } + } + + return testhelpers.SubmitRecoveryForm(t, isAPI, isSPA, hc, public, values, c, + testhelpers.ExpectURL(isAPI || isSPA, public.URL+recovery.RouteSubmitFlow, conf.SelfServiceFlowRecoveryUI(ctx).String())) + } + + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) + } + + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, http.StatusOK) + } + + t.Run("description=should set all the correct recovery payloads after submission", func(t *testing.T) { + body := expectSuccess(t, nil, false, false, func(v url.Values) { + v.Set("email", "test@ory.sh") + }) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").String()), []string{"0.attributes.value"}) + }) + + t.Run("description=should set all the correct recovery payloads", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetRecoveryFlow(t, c, public) + + testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value"}) + assert.EqualValues(t, public.URL+recovery.RouteSubmitFlow+"?flow="+rs.Id, rs.Ui.Action) + assert.Empty(t, rs.Ui.Messages) + }) + + t.Run("description=should require an email to be sent", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, "Property email is missing.", + gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), + "%s", actual) + } + + values := func(v url.Values) { + v.Del("email") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, nil, true, false, values)) + }) + }) + + t.Run("description=should require a valid email to be sent", func(t *testing.T) { + check := func(t *testing.T, actual string, value string) { + assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, "Enter a valid email address", + gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), + "%s", actual) + } + for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { + values := func(v url.Values) { + v.Set("email", email) + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, false, values), email) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, true, values), email) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, nil, true, false, values), email) + }) + } + }) + + t.Run("description=should try to submit the form while authenticated", func(t *testing.T) { + run := func(t *testing.T, flow string) { + isAPI := flow == "api" + isSPA := flow == "spa" + hc := testhelpers.NewDebugClient(t) + if !isAPI { + hc = testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + } + + var f *kratos.RecoveryFlow + if isAPI { + f = testhelpers.InitializeRecoveryFlowViaAPI(t, hc, public) + } else { + f = testhelpers.InitializeRecoveryFlowViaBrowser(t, hc, isSPA, public, nil) + } + + v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v.Set("email", "some-email@example.org") + v.Set("method", "link") + + authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(ctx, t, reg) + if isAPI { + req := httptest.NewRequest("GET", "/sessions/whoami", nil).WithContext(contextx.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(ctx, t, reg, s) + } + + body, res := testhelpers.RecoveryMakeRequest(t, isAPI || isSPA, f, authClient, testhelpers.EncodeFormAsJSON(t, isAPI || isSPA, v)) + + if isAPI || isSPA { + require.EqualValuesf(t, http.StatusBadRequest, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), recovery.RouteSubmitFlow, "%+v\n\t%s", res.Request, body) + assertx.EqualAsJSONExcept(t, recovery.ErrAlreadyLoggedIn(), json.RawMessage(gjson.Get(body, "error").Raw), nil) + } else { + assert.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%+v\n\t%s", res.Request, body) + } + } + + for _, f := range []string{"browser", "spa", "api"} { + t.Run("type="+f, func(t *testing.T) { + run(t, f) + }) + } + }) + + t.Run("description=should try to recover an email that does not exist", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, false) + }) + var email string + check := func(t *testing.T, actual string) { + assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) + assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Account access attempted") + assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") + } + + values := func(v url.Values) { + v.Set("email", email) + } + + t.Run("type=browser", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, true, false, values)) + }) + }) + + t.Run("description=should not be able to recover an inactive account", func(t *testing.T) { + for _, name := range []string{"browser", "spa", "api"} { + t.Run("case="+name, func(t *testing.T) { + recoveryEmail := "recovery_" + name + "@ory.sh" + id := createIdentityToRecover(t, reg, recoveryEmail) + expectSuccess(t, nil, name == "api", name == "spa", func(v url.Values) { + v.Set("email", recoveryEmail) + }) + + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account"), 1) + cl := testhelpers.NewClientWithCookies(t) + + // Deactivate the identity + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery("UPDATE identities SET state=? WHERE id = ?", identity.StateInactive, id.ID).Exec()) + + res, err := cl.Get(recoveryLink) + require.NoError(t, err) + + body := ioutilx.MustReadAll(res.Body) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()) + assert.Equal(t, text.NewErrorValidationIdentityDisabled().Text, gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", spew.Sdump(body)) + }) + } + }) + + t.Run("description=should recover an account", func(t *testing.T) { + check := func(t *testing.T, recoverySubmissionResponse, recoveryEmail, returnTo string) { + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + + assert.EqualValues(t, node.LinkGroup, gjson.Get(recoverySubmissionResponse, "active").String(), "%s", recoverySubmissionResponse) + assert.EqualValues(t, recoveryEmail, gjson.Get(recoverySubmissionResponse, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", recoverySubmissionResponse) + require.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse) + assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + assert.Contains(t, message.Body, "Recover access to your account by clicking the following link") + + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + assert.Contains(t, recoveryLink, public.URL+recovery.RouteSubmitFlow) + assert.Contains(t, recoveryLink, "token=") + + cl := testhelpers.NewClientWithCookies(t) + + res, err := cl.Get(recoveryLink) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowSettingsUI(ctx).String()) + + body := ioutilx.MustReadAll(res.Body) + assert.Equal(t, text.NewRecoverySuccessful(clock.New(), time.Now().Add(time.Hour)).Text, + gjson.GetBytes(body, "ui.messages.0.text").String()) + assert.Equal(t, returnTo, gjson.GetBytes(body, "return_to").String()) + + addr, err = reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + assert.NoError(t, err) + assert.True(t, addr.Verified) + assert.NotEqual(t, sqlxx.NullTime{}, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusCompleted, addr.Status) + + res, err = cl.Get(public.URL + session.RouteWhoami) + require.NoError(t, err) + body = x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Equal(t, "link_recovery", gjson.GetBytes(body, "authentication_methods.0.method").String(), "%s", body) + assert.Equal(t, "aal1", gjson.GetBytes(body, "authenticator_assurance_level").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + testhelpers.NewRecoveryAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) { + defer wg.Done() + assert.EqualValues(t, "recoverme1@ory.sh", gjson.GetBytes(msg, "identity.verifiable_addresses.0.value").String(), string(msg)) + 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)) + }) + + email := "recoverme1@ory.sh" + createIdentityToRecover(t, reg, email) + check(t, expectSuccess(t, nil, false, false, func(v url.Values) { + v.Set("email", email) + }), email, "") + + wg.Wait() + }) + + t.Run("description=should return browser to return url", func(t *testing.T) { + returnTo := public.URL + "/return-to" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + for _, tc := range []struct { + desc string + returnTo string + f func(t *testing.T, client *http.Client) *kratos.RecoveryFlow + }{ + { + desc: "should use return_to from recovery flow", + returnTo: returnTo, + f: func(t *testing.T, client *http.Client) *kratos.RecoveryFlow { + return testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, url.Values{"return_to": []string{returnTo}}) + }, + }, + { + desc: "should use return_to from config", + returnTo: returnTo, + f: func(t *testing.T, client *http.Client) *kratos.RecoveryFlow { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, returnTo) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "") + }) + return testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) + }, + }, + { + desc: "no return to", + returnTo: "", + f: func(t *testing.T, client *http.Client) *kratos.RecoveryFlow { + return testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + email := testhelpers.RandomEmail() + createIdentityToRecover(t, reg, email) + + hc := testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + + f := tc.f(t, hc) + + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + formPayload := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + formPayload.Set("email", email) + + b, res := testhelpers.RecoveryMakeRequest(t, false, f, hc, testhelpers.EncodeFormAsJSON(t, false, formPayload)) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", b) + expectedURL := testhelpers.ExpectURL(false, public.URL+recovery.RouteSubmitFlow, conf.SelfServiceFlowRecoveryUI(ctx).String()) + assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) + + check(t, b, email, tc.returnTo) + }) + } + }) + + t.Run("type=spa", func(t *testing.T) { + email := "recoverme3@ory.sh" + createIdentityToRecover(t, reg, email) + check(t, expectSuccess(t, nil, true, true, func(v url.Values) { + v.Set("email", email) + }), email, "") + }) + + t.Run("type=api", func(t *testing.T) { + email := "recoverme4@ory.sh" + createIdentityToRecover(t, reg, email) + check(t, expectSuccess(t, nil, true, false, func(v url.Values) { + v.Set("email", email) + }), email, "") + }) + }) + + t.Run("description=should recover an account and set the csrf cookies", func(t *testing.T) { + check := func(t *testing.T, actual, recoveryEmail string, cl *http.Client, do func(*http.Client, *http.Request) (*http.Response, error)) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + res, err := do(cl, testhelpers.NewTestHTTPRequest(t, "GET", recoveryLink, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusSeeOther, res.StatusCode) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 2) + cookies := spew.Sdump(cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))) + assert.Contains(t, cookies, nosurfx.CSRFTokenName) + assert.Contains(t, cookies, "ory_kratos_session") + returnTo, err := res.Location() + require.NoError(t, err) + assert.Contains(t, returnTo.String(), conf.SelfServiceFlowSettingsUI(ctx).String(), "we end up at the settings screen") + + rl := urlx.ParseOrPanic(recoveryLink) + actualRes, err := cl.Get(public.URL + recovery.RouteGetFlow + "?id=" + rl.Query().Get("flow")) + require.NoError(t, err) + body := x.MustReadAll(actualRes.Body) + require.NoError(t, actualRes.Body.Close()) + assert.Equal(t, http.StatusOK, actualRes.StatusCode, "%s", body) + assert.Equal(t, string(flow.StatePassedChallenge), gjson.GetBytes(body, "state").String(), "%s", body) + } + + email := x.NewUUID().String() + "@ory.sh" + id := createIdentityToRecover(t, reg, email) + + t.Run("case=unauthenticated", func(t *testing.T) { + values := func(v url.Values) { + v.Set("email", email) + } + check(t, expectSuccess(t, nil, false, false, values), email, testhelpers.NewClientWithCookies(t), (*http.Client).Do) + }) + + t.Run("case=already logged into another account", func(t *testing.T) { + values := func(v url.Values) { + v.Set("email", email) + } + + check(t, expectSuccess(t, nil, false, false, values), email, testhelpers.NewClientWithCookies(t), func(cl *http.Client, req *http.Request) (*http.Response, error) { + _, res := testhelpers.MockMakeAuthenticatedRequestWithClient(t, reg, conf, publicRouter, req, cl) + return res, nil + }) + }) + + t.Run("case=already logged into the account", func(t *testing.T) { + values := func(v url.Values) { + v.Set("email", email) + } + + cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, 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, req, cl, id) + return res, nil + }) + }) + }) + + t.Run("description=should recover and invalidate all other sessions if hook is set", func(t *testing.T) { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), []config.SelfServiceHook{{Name: "revoke_active_sessions"}}) + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + recoveryEmail := strings.ToLower(testhelpers.RandomEmail()) + email := recoveryEmail + id := createIdentityToRecover(t, reg, email) + + req := httptest.NewRequest("GET", "/sessions/whoami", nil) + sess, err := testhelpers.NewActiveSession(req, reg, id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), sess)) + + actualSession, err := reg.SessionPersister().GetSession(t.Context(), sess.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actualSession.IsActive()) + + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + cl := testhelpers.NewClientWithCookies(t) + cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + res, err := cl.Get(recoveryLink) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusSeeOther, res.StatusCode) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 2) + cookies := spew.Sdump(cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))) + assert.Contains(t, cookies, "ory_kratos_session") + + actualSession, err := reg.SessionPersister().GetSession(t.Context(), sess.ID, session.ExpandNothing) + require.NoError(t, err) + assert.False(t, actualSession.IsActive()) + } + + values := func(v url.Values) { + v.Set("email", recoveryEmail) + } + + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("description=should not be able to use an invalid link", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRecoveryFlowViaBrowser(t, c, false, public, nil) + res, err := c.Get(f.Ui.Action + "&token=i-do-not-exist") + require.NoError(t, err) + 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(t.Context()).Id(res.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + + require.Len(t, rs.Ui.Messages, 1) + assert.Equal(t, "The recovery token is invalid or has already been used. Please retry the flow.", rs.Ui.Messages[0].Text) + }) + + t.Run("description=should not be able to use an outdated link", func(t *testing.T) { + recoveryEmail := "recoverme5@ory.sh" + createIdentityToRecover(t, reg, recoveryEmail) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Millisecond*200) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Minute) + }) + + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetRecoveryFlow(t, c, public) + + time.Sleep(time.Millisecond * 201) + + res, err := c.PostForm(rs.Ui.Action, url.Values{"email": {recoveryEmail}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotContains(t, res.Request.URL.String(), "flow="+rs.Id) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()) + + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + 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 not be able to use an outdated flow", func(t *testing.T) { + recoveryEmail := "recoverme6@ory.sh" + createIdentityToRecover(t, reg, recoveryEmail) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Millisecond*200) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Minute) + }) + + c := testhelpers.NewClientWithCookies(t) + body := expectSuccess(t, c, false, false, func(v url.Values) { + v.Set("email", recoveryEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + assert.Contains(t, message.Body, "Recover access to your account by clicking the following link") + + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + time.Sleep(time.Millisecond * 201) + + res, err := c.Get(recoveryLink) + require.NoError(t, err) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + 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(t.Context()).Id(res.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + + require.Len(t, rs.Ui.Messages, 1) + assert.Contains(t, rs.Ui.Messages[0].Text, "The recovery flow expired") + + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(t.Context(), identity.AddressTypeEmail, recoveryEmail) + 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 recover if post recovery hook is successful", func(t *testing.T) { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), nil) + }) + + recoveryEmail := testhelpers.RandomEmail() + createIdentityToRecover(t, reg, recoveryEmail) + + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + cl := testhelpers.NewClientWithCookies(t) + cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + res, err := cl.Get(recoveryLink) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusSeeOther, res.StatusCode) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 2) + cookies := spew.Sdump(cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))) + assert.Contains(t, cookies, "ory_kratos_session") + } + + values := func(v url.Values) { + v.Set("email", recoveryEmail) + } + + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("description=should not be able to recover if post recovery hook fails", func(t *testing.T) { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostRecoveryHook": "err"}`)}}) + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), nil) + }) + + recoveryEmail := testhelpers.RandomEmail() + createIdentityToRecover(t, reg, recoveryEmail) + + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + cl := testhelpers.NewClientWithCookies(t) + cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + res, err := cl.Get(recoveryLink) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusSeeOther, res.StatusCode) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 1) + cookies := spew.Sdump(cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))) + assert.NotContains(t, cookies, "ory_kratos_session") + } + + values := func(v url.Values) { + v.Set("email", recoveryEmail) + } + + check(t, expectSuccess(t, nil, false, false, values)) + }) +} + +func TestDisabledEndpoint(t *testing.T) { + ctx := t.Context() + conf, reg := pkg.NewFastRegistryWithMocks(t) + initViper(t, conf) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyLink)+".enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyCode)+".enabled", false) + + publicTS, adminTS := testhelpers.NewKratosServer(t, reg) + adminSDK := testhelpers.NewSDKClient(adminTS) + _ = testhelpers.NewErrorTestServer(t, reg) + + t.Run("role=admin", func(t *testing.T) { + t.Run("description=can not create recovery link when link method is disabled", func(t *testing.T) { + id := identity.Identity{Traits: identity.Traits(`{"email":"recovery-endpoint-disabled@ory.sh"}`)} + + require.NoError(t, reg.IdentityManager().Create(t.Context(), + &id, identity.ManagerAllowWriteProtectedTraits)) + + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(t.Context()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + IdentityId: id.ID.String(), + }).Execute() + assert.Nil(t, rl) + require.IsType(t, new(kratos.GenericOpenAPIError), err, "%s", err) + + br, _ := err.(*kratos.GenericOpenAPIError) + assert.Contains(t, string(br.Body()), "This endpoint was disabled by system administrator", "%s", br.Body()) + }) + }) + + t.Run("role=public", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + + t.Run("description=can not recover an account by get request when link method is disabled", func(t *testing.T) { + f := testhelpers.PersistNewRecoveryFlow(t, nil, conf, reg) + u := publicTS.URL + recovery.RouteSubmitFlow + "?flow=" + f.ID.String() + "&token=endpoint-disabled" + res, err := c.Get(u) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + + t.Run("description=can not recover an account by post request when link method is disabled", func(t *testing.T) { + f := testhelpers.PersistNewRecoveryFlow(t, nil, conf, reg) + u := publicTS.URL + recovery.RouteSubmitFlow + "?flow=" + f.ID.String() + res, err := c.PostForm(u, url.Values{"email": {"email@ory.sh"}, "method": {"link"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + }) +} diff --git a/selfservice/strategy/link/strategy_test.go b/selfservice/strategy/link/strategy_test.go new file mode 100644 index 0000000..a58cd18 --- /dev/null +++ b/selfservice/strategy/link/strategy_test.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ory/kratos/pkg/testhelpers" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" +) + +func initViper(t *testing.T, c *config.Config) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`OK`)) + })) + t.Cleanup(ts.Close) + ctx := context.Background() + testhelpers.SetDefaultIdentitySchema(c, "file://./stub/default.schema.json") + c.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL) + c.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{ts.URL}) + c.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+identity.CredentialsTypePassword.String()+".enabled", true) + c.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyLink)+".enabled", true) + c.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "link") + c.MustSet(ctx, config.ViperKeySelfServiceRecoveryEnabled, true) + c.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + c.MustSet(ctx, config.ViperKeySelfServiceVerificationUse, "link") +} diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go new file mode 100644 index 0000000..69c4971 --- /dev/null +++ b/selfservice/strategy/link/strategy_verification.go @@ -0,0 +1,368 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "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/x/decoderx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func (s *Strategy) VerificationStrategyID() string { + return string(verification.VerificationStrategyLink) +} + +func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.GetNodes().Upsert( + // 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.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"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} + +func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPayload, error) { + var body verificationSubmitPayload + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(verificationMethodSchema) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := decoderx.Decode(r, &body, compiler, + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return nil, errors.WithStack(err) + } + + return &body, nil +} + +// handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation 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", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } + + return err +} + +// Update Verification Flow with Link Method +// +// swagger:model updateVerificationFlowWithLinkMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateVerificationFlowWithLinkMethod struct { + // Email to Verify + // + // Needs to be set when initiating the flow. If the email is a registered + // verification email, a verification link will be sent. If the email is not known, + // a email with details on what happened will be sent instead. + // + // format: email + // required: true + Email string `json:"email"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `form:"csrf_token" json:"csrf_token"` + + // Method is the method that should be used for this verification flow + // + // Allowed values are `link` and `code` + // + // 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") + span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.d.Config().SelfServiceFlowVerificationUse(ctx))) + defer otelx.End(span, &err) + + body, err := s.decodeVerification(r) + if err != nil { + return s.handleVerificationError(r, nil, body, err) + } + f.TransientPayload = body.TransientPayload + + if len(body.Token) > 0 { + 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(ctx, w, r, body, f) + } + + 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(s.d.Clock()); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + switch f.State { + case flow.StateChooseMethod, flow.StateEmailSent: + return s.verificationHandleFormSubmission(ctx, r, f) + case flow.StatePassedChallenge: + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) + default: + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) + } +} + +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(r, f, body, err) + } + + if len(body.Email) == 0 { + return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email")) + } + + 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(ctx, f, identity.AddressTypeEmail, body.Email); err != nil { + if !errors.Is(err, ErrUnknownAddress) { + return s.handleVerificationError(r, f, body, err) + } + // Continue execution + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + 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()), + ) + + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent + f.UI.Messages.Set(text.NewVerificationEmailSent()) + if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, body, err) + } + + return nil +} + +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(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) + } + + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + if err := token.Valid(s.d.Clock()); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + var identityID uuid.UUID + var addressVia string + var ptcIdentity *identity.Identity + + if token.VerifiableAddress == nil { + ptc, ptcErr := s.d.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID) + if ptcErr != nil { + if errors.Is(ptcErr, sqlcon.ErrNoRows()) { + return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) + } + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, ptcErr) + } + + updatedIdentity, applyErr := s.d.SettingsHookExecutor().ApplyPendingTraitsChange(ctx, w, r, ptc) + if applyErr != nil { + if errors.Is(applyErr, identity.ErrConcurrentModification) || + errors.Is(applyErr, settings.ErrPendingTraitsChangeSessionInvalid) { + f.UI.Messages.Clear() + f.UI.Messages.Add(text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) + if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) + } + if x.IsBrowserRequest(r) { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) + } else { + s.d.Writer().Write(w, r, f) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + return s.retryVerificationFlowWithError(ctx, w, r, f.Type, applyErr) + } + + ptcIdentity = updatedIdentity + addressVia = ptc.NewAddressVia + identityID = ptc.IdentityID + } else { + // Normal path: update the existing verifiable address directly. + address := token.VerifiableAddress + address.Verified = true + verifiedAt := sqlxx.NullTime(time.Now().UTC()) + address.VerifiedAt = &verifiedAt + address.Status = identity.VerifiableAddressStatusCompleted + if err := s.d.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + addressVia = address.Via + identityID = address.IdentityID + } + + var i *identity.Identity + if ptcIdentity != nil { + i = ptcIdentity + } else { + i, err = s.d.IdentityPool().GetIdentity(ctx, identityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + } + + // Remainder is shared between both paths. + returnTo := f.ContinueURL(ctx, s.d.Config()) + + f.UI = &container.Container{ + Method: "GET", + Action: returnTo.String(), + } + + f.State = flow.StatePassedChallenge + // See https://github.com/ory/kratos/issues/1547 + f.SetCSRFToken(flow.GetCSRFToken(s.d, w, r, f.Type)) + if addressVia == identity.AddressTypeSMS { + f.UI.Messages.Set(text.NewInfoSelfServiceVerificationPhoneSuccessful()) + } else { + f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) + } + f.UI.Nodes.Append(node.NewAnchorField("continue", returnTo.String(), node.LinkGroup, text.NewInfoNodeLabelContinue()). + WithMetaLabel(text.NewInfoNodeLabelContinue())) + + if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { + 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, f.Type, err) + } + + return nil +} + +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, + s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) + if err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + f.UI.Messages.Add(message) + 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(ctx)).String(), http.StatusSeeOther) + } else { + 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(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, + s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) + if err != nil { + return s.handleVerificationError(r, f, nil, err) + } + + if expired := new(flow.ExpiredError); errors.As(verErr, &expired) { + return s.retryVerificationFlowWithMessage(ctx, w, r, ft, text.NewErrorValidationVerificationFlowExpired(s.d.Clock(), expired.ExpiredAt)) + } else { + if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil { + return 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(ctx)).String(), http.StatusSeeOther) + } else { + 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) SendVerificationCode(ctx context.Context, f *verification.Flow, i *identity.Identity, addressLike identity.VerifiableAddressLike) error { + token := NewSelfServiceVerificationToken(addressLike, f, s.d.Config().SelfServiceLinkMethodLifespan(ctx)) + + if err := s.d.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil { + return err + } + + return s.d.LinkSender().SendVerificationTokenTo(ctx, f, i, addressLike, token) +} diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go new file mode 100644 index 0000000..6cbc27d --- /dev/null +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -0,0 +1,932 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/x/urlx" + + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/ui/node" + + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" +) + +func TestVerification(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + initViper(t, conf) + + identityToVerify := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + } + + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() + + _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRedirTS(t, "returned", conf) + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + + require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, + identity.ManagerAllowWriteProtectedTraits)) + + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + if hc == nil { + hc = testhelpers.NewDebugClient(t) + if !isAPI { + hc = testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper + } + } + + return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c, + testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) + } + + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) + } + + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, http.StatusOK) + } + + t.Run("description=should set all the correct verification payloads after submission", func(t *testing.T) { + body := expectSuccess(t, nil, false, false, func(v url.Values) { + v.Set("email", "test@ory.sh") + }) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").String()), []string{"0.attributes.value"}) + }) + + t.Run("description=should set all the correct verification payloads", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value"}) + assert.EqualValues(t, public.URL+verification.RouteSubmitFlow+"?flow="+rs.Id, rs.Ui.Action) + assert.Empty(t, rs.Ui.Messages) + }) + + t.Run("description=should not execute submit without correct method set", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"not-link"}, "email": {verificationEmail}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) + + body := ioutilx.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Equal(t, "Could not find a strategy to verify your account with. Did you fill out the form correctly?", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("description=should require an email to be sent", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, "Property email is missing.", + gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), + "%s", actual) + } + + values := func(v url.Values) { + v.Del("email") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, nil, true, false, values)) + }) + }) + + t.Run("description=should require a valid email to be sent", func(t *testing.T) { + check := func(t *testing.T, actual string, value string) { + assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, "Enter a valid email address", + gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), + "%s", actual) + } + + for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { + values := func(v url.Values) { + v.Set("email", email) + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, false, values), email) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, nil, false, true, values), email) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, nil, true, false, values), email) + }) + } + }) + + t.Run("description=should try to verify an email that does not exist", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) + }) + var email string + check := func(t *testing.T, actual string) { + assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) + assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) + assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") + assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") + } + + values := func(v url.Values) { + v.Set("email", email) + } + + t.Run("type=browser", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, false, values)) + }) + + t.Run("type=spa", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + email = x.NewUUID().String() + "@ory.sh" + check(t, expectSuccess(t, nil, true, false, values)) + }) + }) + + t.Run("description=should not be able to use an invalid link", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeVerificationFlowViaBrowser(t, c, false, public) + res, err := c.Get(public.URL + verification.RouteSubmitFlow + "?flow=" + f.Id + "&token=i-do-not-exist") + require.NoError(t, err) + 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() + require.NoError(t, err) + + require.Len(t, sr.Ui.Messages, 1) + assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", sr.Ui.Messages[0].Text) + }) + + t.Run("description=should not be able to request link with an outdated flow", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Millisecond*200) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute) + }) + + c := testhelpers.NewClientWithCookies(t) + rs := testhelpers.GetVerificationFlow(t, c, public) + + time.Sleep(time.Millisecond * 201) + + res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"link"}, "email": {verificationEmail}}) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotContains(t, res.Request.URL.String(), "flow="+rs.Id) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) + }) + + t.Run("description=should not be able to use link with an outdated flow", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Millisecond*200) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute) + }) + + c := testhelpers.NewClientWithCookies(t) + body := expectSuccess(t, c, false, false, func(v url.Values) { + v.Set("email", verificationEmail) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") + assert.Contains(t, message.Body, "Verify your account by opening the following link") + + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + time.Sleep(time.Millisecond * 201) + + // Clear cookies as link might be opened in another browser + c = testhelpers.NewClientWithCookies(t) + res, err := c.Get(verificationLink) + require.NoError(t, err) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + 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() + require.NoError(t, err) + + require.Len(t, sr.Ui.Messages, 1) + assert.Contains(t, sr.Ui.Messages[0].Text, "The verification flow expired") + }) + + 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) + assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") + assert.Contains(t, message.Body, "Verify your account by opening the following link") + + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + assert.Contains(t, verificationLink, public.URL+verification.RouteSubmitFlow) + assert.Contains(t, verificationLink, "token=") + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(verificationLink) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) + body := string(ioutilx.MustReadAll(res.Body)) + assert.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String()) + assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String()) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), identityToVerify.ID) + require.NoError(t, err) + require.Len(t, id.VerifiableAddresses, 1) + + address := id.VerifiableAddresses[0] + assert.EqualValues(t, verificationEmail, address.Value) + assert.True(t, address.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, address.Status) + assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) + } + + values := func(v url.Values) { + v.Set("email", verificationEmail) + } + + 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() + }) + }) + + t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") + verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(verificationLink) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 1) + assert.Contains(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))[0].Name, nosurfx.CSRFTokenName) + + actualRes, err := cl.Get(public.URL + verification.RouteGetFlow + "?id=" + gjson.Get(body, "id").String()) + require.NoError(t, err) + actualBody := string(ioutilx.MustReadAll(actualRes.Body)) + require.NoError(t, actualRes.Body.Close()) + assert.Equal(t, http.StatusOK, actualRes.StatusCode) + + assertx.EqualAsJSON(t, body, actualBody) + assert.EqualValues(t, "passed_challenge", gjson.Get(actualBody, "state").String()) + } + + values := func(v url.Values) { + v.Set("email", verificationEmail) + } + + check(t, expectSuccess(t, nil, false, false, values)) + }) + + newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *link.VerificationToken) { + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), nil, fType) + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) + email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) + identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) + require.NoError(t, reg.IdentityManager().Update(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) + + token := link.NewSelfServiceVerificationToken(&identityToVerify.VerifiableAddresses[0], f, time.Hour) + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(context.Background(), token)) + return f, token + } + + newValidBrowserFlow := func(t *testing.T, requestURL string) (*verification.Flow, *link.VerificationToken) { + return newValidFlow(t, flow.TypeBrowser, requestURL) + } + + t.Run("case=respects return_to URI parameter", func(t *testing.T) { + returnToURL := public.URL + "/after-verification" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL}) + + for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + flow, token := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{"return_to": {returnToURL}}.Encode()) + + res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {flow.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody) + assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody) + assert.Equal(t, returnToURL, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody) + }) + } + }) + + t.Run("case=contains default return to url", func(t *testing.T) { + globalReturnTo := public.URL + "/global" + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo) + + for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + flow, token := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow) + + res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {flow.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody) + assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody) + assert.Equal(t, globalReturnTo, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody) + }) + } + }) + + t.Run("case=should not be able to use code from different flow", func(t *testing.T) { + f1, _ := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) + + _, t2 := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) + + formValues := url.Values{ + "flow": {f1.ID.String()}, + "token": {t2.Token}, + } + submitUrl := public.URL + verification.RouteSubmitFlow + "?" + formValues.Encode() + + res, err := public.Client().Get(submitUrl) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", gjson.GetBytes(body, "ui.messages.0.text").String()) + }) + + t.Run("description=should apply pending traits change when token is redeemed", func(t *testing.T) { + // Sibling subtests "fires settings post-persist webhook..." and + // "post-persist webhook error..." mutate the shared + // selfservice.flows.settings.after.profile.hooks config via + // conf.MustSet and clean up on subtest exit. Running this subtest + // in parallel races against that cleanup and against the shared + // SQLite database (SQLITE_BUSY), so it stays sequential. + // Create an identity with original traits. + pendingID := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{"original@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + } + require.NoError(t, reg.IdentityManager().Create(ctx, pendingID, identity.ManagerAllowWriteProtectedTraits)) + + // The apply step now requires a live session that created the change. + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, pendingID, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Create a verification flow in StateEmailSent. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sf := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: pendingID.ID, + Identity: pendingID, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sf)) + + // Create a PendingTraitsChange record linked to this flow. + sessID := sess.ID + originFlowID := sf.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: pendingID.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true}, + NewAddressValue: "changed@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(pendingID.Traits)), + ProposedTraits: json.RawMessage(`{"email":"changed@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification token with a nil address ID (pending-change signal). + // The address is only used to construct the token; nil it before persisting + // so pop does not try to create a non-existent association row. + token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour) + token.VerifiableAddress = nil + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token)) + + // Redeem the token via HTTP GET. + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String(), "%s", body) + assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + + // Verify the identity traits were updated. + updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, pendingID.ID) + require.NoError(t, err) + assert.EqualValues(t, "changed@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String()) + + // Verify the new address is marked as verified. + var foundAddress *identity.VerifiableAddress + for idx := range updated.VerifiableAddresses { + if updated.VerifiableAddresses[idx].Value == "changed@ory.sh" { + foundAddress = &updated.VerifiableAddresses[idx] + break + } + } + require.NotNil(t, foundAddress, "new address should exist on identity") + assert.True(t, foundAddress.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, foundAddress.Status) + }) + + t.Run("description=fires settings post-persist webhook after pending traits change is applied", func(t *testing.T) { + // Set up a recording webhook target. + var receivedBody []byte + receivedCh := make(chan struct{}, 1) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedBody, _ = io.ReadAll(r.Body) + select { + case receivedCh <- struct{}{}: + default: + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(ts.Close) + + // Register webhook as a settings-profile post-persist hook. + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{ + { + "hook": "web_hook", + "config": map[string]any{ + "url": ts.URL, + "method": "POST", + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + }, + }) + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{}) + }) + + // Create an identity + active session. + ident := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"posthook-link-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Create verification flow + PTC linked to the session. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfPosthook := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfPosthook)) + + sessID := sess.ID + originFlowIDPosthook := sfPosthook.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDPosthook, Valid: true}, + NewAddressValue: "posthook-link-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"posthook-link-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification token with a nil address ID (pending-change signal). + token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour) + token.VerifiableAddress = nil + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token)) + + // Redeem the token via HTTP GET (the link-based verification path). + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + select { + case <-receivedCh: + case <-time.After(5 * time.Second): + t.Fatal("settings post-persist webhook was not invoked after pending traits change applied") + } + assert.Contains(t, string(receivedBody), "posthook-link-new@ory.sh") + }) + + t.Run("description=post-persist webhook error after pending traits change is surfaced", func(t *testing.T) { + // Webhook target that always returns 500. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"boom"}`)) + })) + t.Cleanup(ts.Close) + + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{ + { + "hook": "web_hook", + "config": map[string]any{ + "url": ts.URL, + "method": "POST", + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + }, + }) + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{}) + }) + + ident := &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"email":"posthook-link-err-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(), + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfErr := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfErr)) + + sessID := sess.ID + originFlowIDErr := sfErr.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDErr, Valid: true}, + NewAddressValue: "posthook-link-err-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"posthook-link-err-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification token with a nil address ID (pending-change signal). + token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour) + token.VerifiableAddress = nil + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token)) + + // Redeem the token via HTTP GET (the link-based verification path). + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + // Apply already committed before the webhook fired: the new traits and + // the verified address persist even though the hook failed. + updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID) + require.NoError(t, err) + assert.EqualValues(t, "posthook-link-err-new@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String(), + "traits are committed before the post-persist hook runs") + + var foundAddress *identity.VerifiableAddress + for idx := range updated.VerifiableAddresses { + if updated.VerifiableAddresses[idx].Value == "posthook-link-err-new@ory.sh" { + foundAddress = &updated.VerifiableAddresses[idx] + break + } + } + require.NotNil(t, foundAddress, "new address should exist on identity") + assert.True(t, foundAddress.Verified, "new address should be verified even though the webhook failed") + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, foundAddress.Status) + }) + + t.Run("description=should reject pending traits change on concurrent modification", func(t *testing.T) { + // See the note on "should apply pending traits change when token + // is redeemed" — this subtest shares config and SQLite state with + // the webhook-mutating siblings above and must stay sequential. + // Create an identity with original traits. + concurrentID := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"original2@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{"original2@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + } + require.NoError(t, reg.IdentityManager().Create(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits)) + + // A live session is required so we reach the traits-hash check rather + // than bailing at the SessionID-nil guard first. + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, concurrentID, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // Compute traits hash from the original traits. + originalHash := identity.HashTraits(json.RawMessage(concurrentID.Traits)) + + // Simulate a concurrent modification: update the identity's traits directly. + concurrentID.Traits = identity.Traits(`{"email":"concurrent@ory.sh"}`) + require.NoError(t, reg.IdentityManager().Update(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits)) + + // Create a verification flow in StateEmailSent. + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfConcurrent := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: concurrentID.ID, + Identity: concurrentID, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfConcurrent)) + + // Create a PendingTraitsChange with the OLD traits hash (before concurrent modification). + sessID := sess.ID + originFlowIDConcurrent := sfConcurrent.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: concurrentID.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDConcurrent, Valid: true}, + NewAddressValue: "changed2@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: originalHash, + ProposedTraits: json.RawMessage(`{"email":"changed2@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + // Create a verification token with a nil address ID. + token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour) + token.VerifiableAddress = nil + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token)) + + // Redeem via GET (the link-based path). + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + // Should show the "token invalid" error (concurrent modification detected). + assert.Contains(t, body, "The verification token is invalid or has already been used") + + // Identity traits should NOT have been updated. + unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, concurrentID.ID) + require.NoError(t, err) + assert.EqualValues(t, "concurrent@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String(), + "traits should remain at the concurrent value, not the proposed value") + }) + + t.Run("description=should reject pending traits change when session is revoked", func(t *testing.T) { + ident := &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"revoke-link-original@ory.sh"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits)) + + sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, + time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, + httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser) + + require.NoError(t, err) + f.State = flow.StateEmailSent + require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) + + // Create a settings flow that acts as the origin for the PTC. + sfRevoke := &settings.Flow{ + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + IssuedAt: time.Now().UTC(), + RequestURL: public.URL + "/settings", + IdentityID: ident.ID, + Identity: ident, + Type: flow.TypeBrowser, + State: flow.StateShowForm, + InternalContext: []byte("{}"), + } + require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfRevoke)) + + sessID := sess.ID + originFlowIDRevoke := sfRevoke.ID + ptc := &identity.PendingTraitsChange{ + ID: x.NewUUID(), + IdentityID: ident.ID, + SessionID: uuid.NullUUID{UUID: sessID, Valid: true}, + OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDRevoke, Valid: true}, + NewAddressValue: "revoke-link-new@ory.sh", + NewAddressVia: string(identity.AddressTypeEmail), + OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)), + ProposedTraits: json.RawMessage(`{"email":"revoke-link-new@ory.sh"}`), + VerificationFlowID: f.ID, + Status: identity.PendingTraitsChangeStatusPending, + } + require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc)) + + token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour) + token.VerifiableAddress = nil + require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token)) + + // Revoke the session AFTER the PTC is created — simulates logout between flow start and verification. + require.NoError(t, reg.SessionPersister().RevokeSessionById(ctx, sess.ID)) + + cl := testhelpers.NewClientWithCookies(t) + res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode()) + require.NoError(t, err) + body := string(ioutilx.MustReadAll(res.Body)) + require.NoError(t, res.Body.Close()) + + assert.Contains(t, body, "The verification token is invalid or has already been used") + + // Traits should NOT be updated. + unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID) + require.NoError(t, err) + assert.EqualValues(t, "revoke-link-original@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String()) + }) + + t.Run("case=doesn't continue with OAuth2 flow if code is invalid", func(t *testing.T) { + globalReturnTo := public.URL + "/global" + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo) + + client := testhelpers.NewClientWithCookies(t) + flow, _ := newValidFlow(t, flow.TypeBrowser, public.URL+verification.RouteInitBrowserFlow) + + res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{ + "flow": {flow.ID.String()}, + "token": {"invalid token"}, + }.Encode()) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, "choose_method", responseBody.Get("state").String(), "%v", responseBody) + assert.Len(t, responseBody.Get("ui.messages").Array(), 1, "%v", responseBody) + assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", responseBody.Get("ui.messages.0.text").String(), "%v", responseBody) + }) +} diff --git a/selfservice/strategy/link/stub/default.schema.json b/selfservice/strategy/link/stub/default.schema.json new file mode 100644 index 0000000..8dc9232 --- /dev/null +++ b/selfservice/strategy/link/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/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go new file mode 100644 index 0000000..566c838 --- /dev/null +++ b/selfservice/strategy/link/test/persistence.go @@ -0,0 +1,248 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/contextx" + "github.com/ory/x/sqlcon" +) + +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) + + ctx := contextx.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) { + var req recovery.Flow + require.NoError(t, faker.FakeData(&req)) + req.State = flow.StateChooseMethod + require.NoError(t, p.CreateRecoveryFlow(ctx, &req)) + + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + + address := &identity.RecoveryAddress{Value: email, Via: identity.AddressTypeEmail, IdentityID: i.ID} + i.RecoveryAddresses = append(i.RecoveryAddresses, *address) + + require.NoError(t, p.CreateIdentity(ctx, &i)) + + return &link.RecoveryToken{ + Token: x.NewUUID().String(), + FlowID: uuid.NullUUID{UUID: req.ID, Valid: true}, + RecoveryAddress: &i.RecoveryAddresses[0], + ExpiresAt: time.Now(), + IssuedAt: time.Now(), + IdentityID: i.ID, + TokenType: link.RecoveryTokenTypeAdmin, + }, &req + } + + t.Run("case=should error when the recovery token does not exist", func(t *testing.T) { + _, err := p.UseRecoveryToken(ctx, x.NewUUID(), "i-do-not-exist") + require.Error(t, err) + }) + + t.Run("case=should create a new recovery token", func(t *testing.T) { + token, _ := newRecoveryToken(t, "foo-user@ory.sh") + require.NoError(t, p.CreateRecoveryToken(ctx, token)) + }) + + t.Run("case=should error when token is used with different flow id", func(t *testing.T) { + token, _ := newRecoveryToken(t, "foo-user1@ory.sh") + require.NoError(t, p.CreateRecoveryToken(ctx, token)) + _, err := p.UseRecoveryToken(ctx, x.NewUUID(), token.Token) + require.Error(t, err) + }) + + t.Run("case=should create a recovery token and use it", func(t *testing.T) { + expected, f := newRecoveryToken(t, "other-user@ory.sh") + require.NoError(t, p.CreateRecoveryToken(ctx, expected)) + + t.Run("not work on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.UseRecoveryToken(ctx, f.ID, expected.Token) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + actual, err := p.UseRecoveryToken(ctx, f.ID, expected.Token) + require.NoError(t, err) + assert.Equal(t, nid, actual.NID) + assert.Equal(t, expected.IdentityID, actual.IdentityID) + assert.NotEqual(t, expected.Token, actual.Token) + assert.EqualValues(t, expected.FlowID, actual.FlowID) + + t.Run("double spend", func(t *testing.T) { + _, err = p.UseRecoveryToken(ctx, f.ID, expected.Token) + require.Error(t, err) + }) + }) + + t.Run("case=concurrent use of the same token succeeds at most once", func(t *testing.T) { + // HackerOne #3692149 / #3695625: the consume UPDATE must be + // atomic. On READ COMMITTED backends two concurrent submissions + // of the same valid token must not both succeed. + expected, f := newRecoveryToken(t, "concurrent-recovery@ory.sh") + require.NoError(t, p.CreateRecoveryToken(ctx, expected)) + + const concurrency = 8 + var success int32 + var start sync.WaitGroup + var done sync.WaitGroup + start.Add(1) + for range concurrency { + done.Add(1) + go func() { + defer done.Done() + start.Wait() + if _, err := p.UseRecoveryToken(ctx, f.ID, expected.Token); err == nil { + atomic.AddInt32(&success, 1) + } + }() + } + start.Done() + done.Wait() + + require.EqualValues(t, 1, atomic.LoadInt32(&success), "exactly one concurrent consumer may use the token") + }) + + t.Run("case=update to identity should not invalidate token", func(t *testing.T) { + expected, f := newRecoveryToken(t, "some-user@ory.sh") + + require.NoError(t, p.CreateRecoveryToken(ctx, expected)) + id, err := p.GetIdentity(ctx, expected.IdentityID, identity.ExpandDefault) + require.NoError(t, err) + require.NoError(t, p.UpdateIdentity(ctx, id)) + + actual, err := p.UseRecoveryToken(ctx, f.ID, expected.Token) + require.NoError(t, err) + assert.Equal(t, nid, actual.NID) + assert.Equal(t, expected.IdentityID, actual.IdentityID) + assert.NotEqual(t, expected.Token, actual.Token) + assert.EqualValues(t, expected.FlowID, actual.FlowID) + + t.Run("double spend", func(t *testing.T) { + _, err = p.UseRecoveryToken(ctx, f.ID, expected.Token) + require.Error(t, err) + }) + }) + }) + + t.Run("token=verification", func(t *testing.T) { + newVerificationToken := func(t *testing.T, email string) (*verification.Flow, *link.VerificationToken) { + var f verification.Flow + require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod + require.NoError(t, p.CreateVerificationFlow(ctx, &f)) + + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + + address := &identity.VerifiableAddress{Value: email, Via: identity.AddressTypeEmail} + i.VerifiableAddresses = append(i.VerifiableAddresses, *address) + + require.NoError(t, p.CreateIdentity(ctx, &i)) + return &f, &link.VerificationToken{ + Token: x.NewUUID().String(), + FlowID: f.ID, + VerifiableAddress: &i.VerifiableAddresses[0], + ExpiresAt: time.Now(), + IssuedAt: time.Now(), + } + } + + t.Run("case=should error when the verification token does not exist", func(t *testing.T) { + _, err := p.UseVerificationToken(ctx, x.NewUUID(), "i-do-not-exist") + require.Error(t, err) + }) + + t.Run("case=should error when the verification token does exist but the flow does not", func(t *testing.T) { + _, token := newVerificationToken(t, x.NewUUID().String()+"@ory.sh") + require.NoError(t, p.CreateVerificationToken(ctx, token)) + _, err := p.UseVerificationToken(ctx, x.NewUUID(), token.Token) + require.Error(t, err) + }) + + t.Run("case=should create a new verification token", func(t *testing.T) { + _, token := newVerificationToken(t, "foo-user@ory.sh") + require.NoError(t, p.CreateVerificationToken(ctx, token)) + }) + + t.Run("case=should create a verification token and use it", func(t *testing.T) { + f, expected := newVerificationToken(t, "other-user@ory.sh") + require.NoError(t, p.CreateVerificationToken(ctx, expected)) + + t.Run("not work on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.UseVerificationToken(ctx, f.ID, expected.Token) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + actual, err := p.UseVerificationToken(ctx, f.ID, expected.Token) + require.NoError(t, err) + assertx.EqualAsJSONExcept(t, expected.VerifiableAddress, actual.VerifiableAddress, []string{"created_at", "updated_at"}) + assert.Equal(t, nid, actual.NID) + assert.Equal(t, expected.VerifiableAddress.IdentityID, actual.VerifiableAddress.IdentityID) + assert.NotEqual(t, expected.Token, actual.Token) + assert.EqualValues(t, expected.FlowID, actual.FlowID) + + _, err = p.UseVerificationToken(ctx, f.ID, expected.Token) + require.Error(t, err) + }) + + t.Run("case=concurrent use of the same token succeeds at most once", func(t *testing.T) { + // HackerOne #3692149 / #3695625: the consume UPDATE must be + // atomic. On READ COMMITTED backends two concurrent submissions + // of the same valid token must not both succeed. + f, expected := newVerificationToken(t, "concurrent-verification@ory.sh") + require.NoError(t, p.CreateVerificationToken(ctx, expected)) + + const concurrency = 8 + var success int32 + var start sync.WaitGroup + var done sync.WaitGroup + start.Add(1) + for range concurrency { + done.Add(1) + go func() { + defer done.Done() + start.Wait() + if _, err := p.UseVerificationToken(ctx, f.ID, expected.Token); err == nil { + atomic.AddInt32(&success, 1) + } + }() + } + start.Done() + done.Wait() + + require.EqualValues(t, 1, atomic.LoadInt32(&success), "exactly one concurrent consumer may use the token") + }) + }) + } +} diff --git a/selfservice/strategy/link/token_recovery.go b/selfservice/strategy/link/token_recovery.go new file mode 100644 index 0000000..84f426c --- /dev/null +++ b/selfservice/strategy/link/token_recovery.go @@ -0,0 +1,110 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "time" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/gofrs/uuid" + errors "github.com/pkg/errors" + + "github.com/ory/x/clock" + "github.com/ory/x/randx" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/x" +) + +type RecoveryTokenType int + +const ( + RecoveryTokenTypeAdmin RecoveryTokenType = iota + 1 + RecoveryTokenTypeSelfService +) + +type RecoveryToken struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Token represents the recovery token. It can not be longer than 64 chars! + Token string `json:"-" db:"token"` + + // RecoveryAddress links this token to a recovery address. + // required: true + RecoveryAddress *identity.RecoveryAddress `json:"recovery_address" belongs_to:"identity_recovery_addresses" fk_id:"RecoveryAddressID"` + + TokenType RecoveryTokenType `json:"-" faker:"-" db:"token_type"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + // RecoveryAddressID is a helper struct field for gobuffalo.pop. + RecoveryAddressID *uuid.UUID `json:"-" faker:"-" db:"identity_recovery_address_id"` + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.NullUUID `json:"-" faker:"-" db:"selfservice_recovery_flow_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (RecoveryToken) TableName(ctx context.Context) string { + return "identity_recovery_tokens" +} + +func NewSelfServiceRecoveryToken(address *identity.RecoveryAddress, f *recovery.Flow, expiresIn time.Duration) *RecoveryToken { + now := time.Now().UTC() + var identityID = uuid.UUID{} + var recoveryAddressID = uuid.UUID{} + if address != nil { + identityID = address.IdentityID + recoveryAddressID = address.ID + } + return &RecoveryToken{ + ID: x.NewUUID(), + Token: randx.MustString(32, randx.AlphaNum), + RecoveryAddress: address, + ExpiresAt: now.Add(expiresIn), + IssuedAt: now, + IdentityID: identityID, + FlowID: uuid.NullUUID{UUID: f.ID, Valid: true}, + RecoveryAddressID: &recoveryAddressID, + TokenType: RecoveryTokenTypeSelfService, + } +} + +func NewAdminRecoveryToken(identityID uuid.UUID, fID uuid.UUID, expiresIn time.Duration) *RecoveryToken { + now := time.Now().UTC() + return &RecoveryToken{ + ID: x.NewUUID(), + Token: randx.MustString(32, randx.AlphaNum), + ExpiresAt: now.Add(expiresIn), + IssuedAt: now, + IdentityID: identityID, + FlowID: uuid.NullUUID{UUID: fID, Valid: true}, + TokenType: RecoveryTokenTypeAdmin, + } +} + +func (f *RecoveryToken) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} diff --git a/selfservice/strategy/link/token_recovery_test.go b/selfservice/strategy/link/token_recovery_test.go new file mode 100644 index 0000000..4c4aff5 --- /dev/null +++ b/selfservice/strategy/link/token_recovery_test.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "net/http" + "testing" + "time" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/link" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/clock" + "github.com/ory/x/stringslice" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/recovery" +) + +func TestRecoveryToken(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("func=NewSelfServiceRecoveryToken", func(t *testing.T) { + t.Run("case=creates unique tokens", func(t *testing.T) { + f, err := recovery.NewFlow(reg, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + tokens := make([]string, 10) + for k := range tokens { + tokens[k] = link.NewSelfServiceRecoveryToken(nil, f, time.Hour).Token + } + + assert.Len(t, stringslice.Unique(tokens), len(tokens)) + }) + }) + t.Run("method=Valid", func(t *testing.T) { + t.Run("case=is invalid when the flow is expired", func(t *testing.T) { + f, err := recovery.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + token := link.NewSelfServiceRecoveryToken(nil, f, -time.Hour) + require.Error(t, token.Valid(clock.New())) + assert.EqualError(t, token.Valid(clock.New()), f.Valid(clock.New()).Error()) + }) + }) +} + +func TestRecoveryTokenType(t *testing.T) { + assert.Equal(t, 1, int(link.RecoveryTokenTypeAdmin)) + assert.Equal(t, 2, int(link.RecoveryTokenTypeSelfService)) +} diff --git a/selfservice/strategy/link/token_verification.go b/selfservice/strategy/link/token_verification.go new file mode 100644 index 0000000..1cd189b --- /dev/null +++ b/selfservice/strategy/link/token_verification.go @@ -0,0 +1,86 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/x" + "github.com/ory/x/clock" + "github.com/ory/x/randx" +) + +type VerificationToken struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Token represents the verification token. It can not be longer than 64 chars! + Token string `json:"-" db:"token"` + + // VerifiableAddress links this token to a verification address. + // required: true + VerifiableAddress *identity.VerifiableAddress `json:"verification_address" belongs_to:"identity_verifiable_addresses" fk_id:"identity_verifiable_address_id"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + // VerifiableAddressID is a helper struct field for gobuffalo.pop. + VerifiableAddressID uuid.NullUUID `json:"-" faker:"-" db:"identity_verifiable_address_id"` + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_verification_flow_id"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (VerificationToken) TableName(ctx context.Context) string { + return "identity_verification_tokens" +} + +func NewSelfServiceVerificationToken(address identity.VerifiableAddressLike, f *verification.Flow, expiresIn time.Duration) *VerificationToken { + now := time.Now().UTC() + + token := &VerificationToken{ + ID: x.NewUUID(), + Token: randx.MustString(32, randx.AlphaNum), + ExpiresAt: now.Add(expiresIn), + IssuedAt: now, + FlowID: f.ID, + } + if address != nil { + if va, isPersistable := address.ToPersistable(); isPersistable { + token.VerifiableAddress = va + token.VerifiableAddressID = uuid.NullUUID{ + UUID: va.ID, + Valid: true, + } + } + } + return token +} + +func (f *VerificationToken) Valid(c clock.Clock) error { + if f.ExpiresAt.Before(c.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt)) + } + return nil +} diff --git a/selfservice/strategy/link/token_verification_test.go b/selfservice/strategy/link/token_verification_test.go new file mode 100644 index 0000000..741fde2 --- /dev/null +++ b/selfservice/strategy/link/token_verification_test.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package link_test + +import ( + "net/http" + "testing" + "time" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/link" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/clock" + "github.com/ory/x/stringslice" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" +) + +func TestVerificationToken(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} + t.Run("func=NewSelfServiceVerificationToken", func(t *testing.T) { + t.Run("case=creates unique tokens", func(t *testing.T) { + f, err := verification.NewFlow(reg, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + tokens := make([]string, 10) + for k := range tokens { + tokens[k] = link.NewSelfServiceVerificationToken(nil, f, time.Hour).Token + } + + assert.Len(t, stringslice.Unique(tokens), len(tokens)) + }) + }) + t.Run("method=Valid", func(t *testing.T) { + t.Run("case=is invalid when the flow is expired", func(t *testing.T) { + f, err := verification.NewFlow(reg, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + token := link.NewSelfServiceVerificationToken(nil, f, -time.Hour) + require.Error(t, token.Valid(clock.New())) + assert.EqualError(t, token.Valid(clock.New()), f.Valid(clock.New()).Error()) + }) + }) +} diff --git a/selfservice/strategy/lookup/.schema/login.schema.json b/selfservice/strategy/lookup/.schema/login.schema.json new file mode 100644 index 0000000..7637578 --- /dev/null +++ b/selfservice/strategy/lookup/.schema/login.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/lookup/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "lookup_secret", + "method" + ], + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "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 new file mode 100644 index 0000000..d20cdb7 --- /dev/null +++ b/selfservice/strategy/lookup/.schema/settings.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/lookup/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "lookup_secret_disable": { + "type": "boolean" + }, + "lookup_secret_reveal": { + "type": "boolean" + }, + "lookup_secret_regenerate": { + "type": "boolean" + }, + "lookup_secret_confirm": { + "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json b/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json new file mode 100644 index 0000000..4de9e6d --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json @@ -0,0 +1,54 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1010007, + "text": "Backup recovery code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "lookup_secret" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1010010, + "text": "Use backup recovery code", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=api.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=api.json new file mode 100644 index 0000000..8ea9c29 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=api.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_regenerate", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=browser.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=browser.json new file mode 100644 index 0000000..8ea9c29 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=browser.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_regenerate", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=spa.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=spa.json new file mode 100644 index 0000000..8ea9c29 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=button_for_regeneration_is_displayed_when_identity_has_no_recovery_codes_yet-case=spa.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_regenerate", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=api.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=api.json new file mode 100644 index 0000000..698b38f --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=api.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=browser.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=browser.json new file mode 100644 index 0000000..698b38f --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=browser.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=spa.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=spa.json new file mode 100644 index 0000000..698b38f --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button-case=spa.json @@ -0,0 +1,34 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=api.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=api.json new file mode 100644 index 0000000..9e2c868 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=api.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_disable", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=browser.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=browser.json new file mode 100644 index 0000000..9e2c868 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=browser.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_disable", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=spa.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=spa.json new file mode 100644 index 0000000..9e2c868 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=hide_recovery_codes_behind_reveal_button_and_show_disable_button-case=spa.json @@ -0,0 +1,53 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_reveal", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050007, + "text": "Reveal backup recovery codes", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "lookup_secret_disable", + "node_type": "input", + "type": "submit", + "value": "true" + }, + "group": "lookup_secret", + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=should_pass_without_csrf_if_API_flow.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=should_pass_without_csrf_if_API_flow.json new file mode 100644 index 0000000..8b4fe73 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-case=should_pass_without_csrf_if_API_flow.json @@ -0,0 +1,178 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "text", + "group": "lookup_secret", + "attributes": { + "text": { + "id": 1050015, + "text": "key-0, used, key-2, key-3, used, key-5, key-6, used, key-8, key-9, used, key-11", + "type": "info", + "context": { + "secrets": [ + { + "id": 1050009, + "text": "key-0", + "type": "info", + "context": { + "secret": "key-0" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:39 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:39Z", + "used_at_unix": 1629199959 + } + }, + { + "id": 1050009, + "text": "key-2", + "type": "info", + "context": { + "secret": "key-2" + } + }, + { + "id": 1050009, + "text": "key-3", + "type": "info", + "context": { + "secret": "key-3" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:42 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:42Z", + "used_at_unix": 1629199962 + } + }, + { + "id": 1050009, + "text": "key-5", + "type": "info", + "context": { + "secret": "key-5" + } + }, + { + "id": 1050009, + "text": "key-6", + "type": "info", + "context": { + "secret": "key-6" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:45 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:45Z", + "used_at_unix": 1629199965 + } + }, + { + "id": 1050009, + "text": "key-8", + "type": "info", + "context": { + "secret": "key-8" + } + }, + { + "id": 1050009, + "text": "key-9", + "type": "info", + "context": { + "secret": "key-9" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:48 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:48Z", + "used_at_unix": 1629199968 + } + }, + { + "id": 1050009, + "text": "key-11", + "type": "info", + "context": { + "secret": "key-11" + } + } + ] + } + }, + "id": "lookup_secret_codes", + "node_type": "text" + }, + "messages": [], + "meta": { + "label": { + "id": 1050010, + "text": "These are your back up recovery codes. Please keep them in a safe place!", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_disable", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_regenerate", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=api.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=api.json new file mode 100644 index 0000000..8b4fe73 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=api.json @@ -0,0 +1,178 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "text", + "group": "lookup_secret", + "attributes": { + "text": { + "id": 1050015, + "text": "key-0, used, key-2, key-3, used, key-5, key-6, used, key-8, key-9, used, key-11", + "type": "info", + "context": { + "secrets": [ + { + "id": 1050009, + "text": "key-0", + "type": "info", + "context": { + "secret": "key-0" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:39 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:39Z", + "used_at_unix": 1629199959 + } + }, + { + "id": 1050009, + "text": "key-2", + "type": "info", + "context": { + "secret": "key-2" + } + }, + { + "id": 1050009, + "text": "key-3", + "type": "info", + "context": { + "secret": "key-3" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:42 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:42Z", + "used_at_unix": 1629199962 + } + }, + { + "id": 1050009, + "text": "key-5", + "type": "info", + "context": { + "secret": "key-5" + } + }, + { + "id": 1050009, + "text": "key-6", + "type": "info", + "context": { + "secret": "key-6" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:45 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:45Z", + "used_at_unix": 1629199965 + } + }, + { + "id": 1050009, + "text": "key-8", + "type": "info", + "context": { + "secret": "key-8" + } + }, + { + "id": 1050009, + "text": "key-9", + "type": "info", + "context": { + "secret": "key-9" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:48 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:48Z", + "used_at_unix": 1629199968 + } + }, + { + "id": 1050009, + "text": "key-11", + "type": "info", + "context": { + "secret": "key-11" + } + } + ] + } + }, + "id": "lookup_secret_codes", + "node_type": "text" + }, + "messages": [], + "meta": { + "label": { + "id": 1050010, + "text": "These are your back up recovery codes. Please keep them in a safe place!", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_disable", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_regenerate", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=browser.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=browser.json new file mode 100644 index 0000000..8b4fe73 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=browser.json @@ -0,0 +1,178 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "text", + "group": "lookup_secret", + "attributes": { + "text": { + "id": 1050015, + "text": "key-0, used, key-2, key-3, used, key-5, key-6, used, key-8, key-9, used, key-11", + "type": "info", + "context": { + "secrets": [ + { + "id": 1050009, + "text": "key-0", + "type": "info", + "context": { + "secret": "key-0" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:39 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:39Z", + "used_at_unix": 1629199959 + } + }, + { + "id": 1050009, + "text": "key-2", + "type": "info", + "context": { + "secret": "key-2" + } + }, + { + "id": 1050009, + "text": "key-3", + "type": "info", + "context": { + "secret": "key-3" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:42 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:42Z", + "used_at_unix": 1629199962 + } + }, + { + "id": 1050009, + "text": "key-5", + "type": "info", + "context": { + "secret": "key-5" + } + }, + { + "id": 1050009, + "text": "key-6", + "type": "info", + "context": { + "secret": "key-6" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:45 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:45Z", + "used_at_unix": 1629199965 + } + }, + { + "id": 1050009, + "text": "key-8", + "type": "info", + "context": { + "secret": "key-8" + } + }, + { + "id": 1050009, + "text": "key-9", + "type": "info", + "context": { + "secret": "key-9" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:48 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:48Z", + "used_at_unix": 1629199968 + } + }, + { + "id": 1050009, + "text": "key-11", + "type": "info", + "context": { + "secret": "key-11" + } + } + ] + } + }, + "id": "lookup_secret_codes", + "node_type": "text" + }, + "messages": [], + "meta": { + "label": { + "id": 1050010, + "text": "These are your back up recovery codes. Please keep them in a safe place!", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_disable", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_regenerate", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=spa.json b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=spa.json new file mode 100644 index 0000000..8b4fe73 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteSettings-type=regenerate_but_no_confirmation-type=spa.json @@ -0,0 +1,178 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "text", + "group": "lookup_secret", + "attributes": { + "text": { + "id": 1050015, + "text": "key-0, used, key-2, key-3, used, key-5, key-6, used, key-8, key-9, used, key-11", + "type": "info", + "context": { + "secrets": [ + { + "id": 1050009, + "text": "key-0", + "type": "info", + "context": { + "secret": "key-0" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:39 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:39Z", + "used_at_unix": 1629199959 + } + }, + { + "id": 1050009, + "text": "key-2", + "type": "info", + "context": { + "secret": "key-2" + } + }, + { + "id": 1050009, + "text": "key-3", + "type": "info", + "context": { + "secret": "key-3" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:42 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:42Z", + "used_at_unix": 1629199962 + } + }, + { + "id": 1050009, + "text": "key-5", + "type": "info", + "context": { + "secret": "key-5" + } + }, + { + "id": 1050009, + "text": "key-6", + "type": "info", + "context": { + "secret": "key-6" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:45 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:45Z", + "used_at_unix": 1629199965 + } + }, + { + "id": 1050009, + "text": "key-8", + "type": "info", + "context": { + "secret": "key-8" + } + }, + { + "id": 1050009, + "text": "key-9", + "type": "info", + "context": { + "secret": "key-9" + } + }, + { + "id": 1050014, + "text": "Secret was used at 2021-08-17 11:32:48 +0000 UTC", + "type": "info", + "context": { + "used_at": "2021-08-17T11:32:48Z", + "used_at_unix": 1629199968 + } + }, + { + "id": 1050009, + "text": "key-11", + "type": "info", + "context": { + "secret": "key-11" + } + } + ] + } + }, + "id": "lookup_secret_codes", + "node_type": "text" + }, + "messages": [], + "meta": { + "label": { + "id": 1050010, + "text": "These are your back up recovery codes. Please keep them in a safe place!", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_disable", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050016, + "text": "Disable this method", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "lookup_secret_regenerate", + "type": "submit", + "value": "true", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050008, + "text": "Generate new backup recovery codes", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 0000000..3baf2e5 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.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": "lookup_secret", + "attributes": { + "name": "lookup_secret", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010007, + "text": "Backup recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "method", + "type": "submit", + "value": "lookup_secret", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010010, + "text": "Use backup recovery code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json new file mode 100644 index 0000000..3baf2e5 --- /dev/null +++ b/selfservice/strategy/lookup/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.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": "lookup_secret", + "attributes": { + "name": "lookup_secret", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010007, + "text": "Backup recovery code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "lookup_secret", + "attributes": { + "name": "method", + "type": "submit", + "value": "lookup_secret", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010010, + "text": "Use backup recovery code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/lookup/login.go b/selfservice/strategy/lookup/login.go new file mode 100644 index 0000000..a2ae9cc --- /dev/null +++ b/selfservice/strategy/lookup/login.go @@ -0,0 +1,171 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "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/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/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { + // This strategy can only solve AAL2 + if requestedAAL != identity.AuthenticatorAssuranceLevel2 { + return nil + } + + // We have done proper validation before so this should never error. + // Only the identity ID is read below; the identity is re-fetched with its confidential + // fields anyway, so skip expanding anything here. + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing) + if err != nil { + return err + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), sess.IdentityID) + if err != nil { + return err + } + + _, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no lookup codes + return nil + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField(node.LookupCodeEnter, "", node.LookupGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoLoginLookupLabel())) + sr.UI.GetNodes().Append(node.NewInputField("method", s.ID(), node.LookupGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginLookup())) + + return nil +} + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error { + if f != nil { + f.UI.Nodes.ResetNodes(node.LookupCodeEnter) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +// Update Login Flow with Lookup Secret Method +// +// swagger:model updateLoginFlowWithLookupSecretMethod +type updateLoginFlowWithLookupSecretMethod struct { + // Method should be set to "lookup_secret" when logging in using the lookup_secret strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // The lookup secret. + // + // required: true + Code string `json:"lookup_secret"` +} + +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 + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { + return nil, err + } + + var p updateLoginFlowWithLookupSecretMethod + if err := decoderx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); 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) + } + + 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 { + return nil, s.handleLoginError(r, f, err) + } + + var o identity.CredentialsLookupConfig + if err := json.Unmarshal(c.Config, &o); err != nil { + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("The lookup secrets could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) + } + + var found bool + for k, rc := range o.RecoveryCodes { + if rc.Code == p.Code { + if time.Time(rc.UsedAt).IsZero() { + o.RecoveryCodes[k].UsedAt = sqlxx.NullTime(time.Now().UTC().Round(time.Second)) + found = true + } else { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewLookupAlreadyUsed()), i.ID)) + } + } + } + + if !found { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewErrorValidationLookupInvalid()), i.ID)) + } + + // 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, s.handleLoginError(r, f, x.WrapWithIdentityIDError(err, i.ID)) + } + + encoded, err := json.Marshal(&o) + if err != nil { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("Unable to encode updated lookup secrets.").WithDebug(err.Error())), i.ID)) + } + + c.Config = encoded + toUpdate.SetCredentials(s.ID(), *c) + + // 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, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("Unable to update identity.").WithDebug(err.Error())), i.ID)) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("Could not update flow.").WithDebug(err.Error())), i.ID)) + } + + return i, nil +} diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go new file mode 100644 index 0000000..dd6c85f --- /dev/null +++ b/selfservice/strategy/lookup/login_test.go @@ -0,0 +1,444 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup_test + +import ( + "bytes" + "context" + _ "embed" + "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/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/snapshotx" +) + +var lookupCodeGJSONQuery = "ui.nodes.#(attributes.name==" + identity.CredentialsTypeLookup.String() + ")" + +func TestCompleteLogin(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeLookup, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + t.Run("case=lookup payload is set when identity has lookup", func(t *testing.T) { + id, _ := createIdentity(t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) + }) + + t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) { + id := createIdentityWithoutLookup(t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + assertx.EqualAsJSON(t, nil, f.Ui.Nodes) + }) + + t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) { + id := createIdentityWithoutLookup(t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + assertx.EqualAsJSON(t, nil, f.Ui.Nodes) + }) + + doAPIFlowWithClient := func(t *testing.T, v func(url.Values), id *identity.Identity, apiClient *http.Client, forced bool) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, forced, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", identity.CredentialsTypeLookup.String()) + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.LoginMakeRequest(t, true, false, f, apiClient, payload) + } + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + return doAPIFlowWithClient(t, v, id, apiClient, false) + } + + doBrowserFlowWithClient := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity, browserClient *http.Client, forced bool) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, forced, spa, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", identity.CredentialsTypeLookup.String()) + v(values) + return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) + } + + doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id) + return doBrowserFlowWithClient(t, spa, v, id, browserClient, false) + } + + checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) { + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts") + } else { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + } + } + + t.Run("case=should fail if code is invalid", func(t *testing.T) { + for _, tc := range []struct { + d string + code string + message string + key string + }{ + {d: "empty", key: lookupCodeGJSONQuery + ".messages.0.text", message: "Property lookup_secret is missing."}, + {d: "invalid", key: "ui.messages.0.text", code: "invalid", message: text.NewErrorValidationLookupInvalid().Text}, + {d: "already-used", key: "ui.messages.0.text", code: "key-1", message: text.NewErrorValidationLookupAlreadyUsed().Text}, + } { + t.Run(fmt.Sprintf("code=%s", tc.d), func(t *testing.T) { + id, _ := createIdentity(t, reg) + payload := func(v url.Values) { + v.Set(node.LookupCodeEnter, tc.code) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, tc.message, gjson.Get(body, tc.key).String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id) + check(t, false, body, res) + }) + }) + } + }) + + t.Run("case=should fail if lookup was not set up for identity", func(t *testing.T) { + id := createIdentityWithoutLookup(t, reg) + + payload := func(v url.Values) { + v.Set(node.LookupCodeEnter, "1111111") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationNoLookup().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id) + check(t, false, body, res) + }) + }) + + t.Run("case=should pass when code is supplied correctly", func(t *testing.T) { + id, _ := createIdentity(t, reg) + payload := func(code string) func(v url.Values) { + return func(v url.Values) { + v.Set(node.LookupCodeEnter, code) + } + } + + startAt := time.Now() + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response, usedKey string, expectedAuths int) { + prefix := "session." + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), redirTS.URL+"/return-ts") + prefix = "" + } else { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + } + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String()) + require.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), expectedAuths) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, prefix+"authentication_methods.0.method").String()) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time().After(startAt)) + assert.EqualValues(t, identity.CredentialsTypeLookup, gjson.Get(body, prefix+"authentication_methods.1.method").String()) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(startAt)) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time())) + if expectedAuths == 3 { + assert.EqualValues(t, identity.CredentialsTypeLookup, gjson.Get(body, prefix+"authentication_methods.2.method").String()) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.2.completed_at").Time().After(startAt)) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.2.completed_at").Time().After(gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time())) + assert.False(t, gjson.Get(body, prefix+"authenticated_at").Time().Before(gjson.Get(body, prefix+"authentication_methods.2.completed_at").Time())) + } else { + assert.False(t, gjson.Get(body, prefix+"authenticated_at").Time().Before(gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time())) + } + + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), uuid.FromStringOrNil(gjson.Get(body, prefix+"identity.id").String())) + require.NoError(t, err) + creds, ok := actual.GetCredentials(identity.CredentialsTypeLookup) + require.True(t, ok) + + var conf identity.CredentialsLookupConfig + require.NoError(t, json.Unmarshal(creds.Config, &conf)) + + var found bool + for _, rc := range conf.RecoveryCodes { + if rc.Code == usedKey { + found = true + require.False(t, time.Time(rc.UsedAt).IsZero()) + } + } + + require.True(t, found) + } + + t.Run("type=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, 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(ctx, t, 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(ctx, t, 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) + }) + }) + + // Regression coverage for https://github.com/ory/kratos/issues/4561. + // Lookup-secret has the same shape of bug as TOTP: the AAL2 login + // flow looks the credential up via FindByCredentialsIdentifier keyed + // by the identity ID, so the import path must persist a matching + // row in identity_credential_identifiers. Both the create and + // update entry points are covered. + t.Run("case=imported lookup-secret via create identity should succeed at AAL2 login", func(t *testing.T) { + id, codes := createIdentityWithImportedLookupViaCreate(t.Context(), t, reg) + + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + assert.Equal(t, []string{actual.ID.String()}, actual.Credentials[identity.CredentialsTypeLookup].Identifiers) + + payload := func(v url.Values) { + v.Set(node.LookupCodeEnter, codes[0].Code) + } + + startAt := time.Now() + check := func(t *testing.T, body string) { + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, "session.authenticator_assurance_level").String()) + require.Len(t, gjson.Get(body, "session.authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, "session.authentication_methods.0.method").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypeLookup, gjson.Get(body, "session.authentication_methods.1.method").String(), "%s", body) + assert.True(t, gjson.Get(body, "session.authentication_methods.1.completed_at").Time().After(startAt), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + body, res := doAPIFlowWithClient(t, payload, id, apiClient, false) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, body) + }) + }) + + t.Run("case=imported lookup-secret via update identity should succeed at AAL2 login", func(t *testing.T) { + id, codes := createIdentityWithImportedLookupViaUpdate(t.Context(), t, reg) + + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + assert.Equal(t, []string{actual.ID.String()}, actual.Credentials[identity.CredentialsTypeLookup].Identifiers) + + payload := func(v url.Values) { + v.Set(node.LookupCodeEnter, codes[0].Code) + } + + startAt := time.Now() + check := func(t *testing.T, body string) { + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, "session.authenticator_assurance_level").String()) + require.Len(t, gjson.Get(body, "session.authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, "session.authentication_methods.0.method").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypeLookup, gjson.Get(body, "session.authentication_methods.1.method").String(), "%s", body) + assert.True(t, gjson.Get(body, "session.authentication_methods.1.completed_at").Time().After(startAt), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + body, res := doAPIFlowWithClient(t, payload, id, apiClient, false) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, body) + }) + }) + + t.Run("case=should fail because lookup can not handle AAL1", func(t *testing.T) { + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPICtx(ctx, t, apiClient, publicTS, false) + + update, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), update)) + + req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"lookup"}`)) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + res, err := testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(body, "ui.messages.0.text").String()) + }) + + t.Run("case=should pass without csrf if API flow", func(t *testing.T) { + id, _ := createIdentity(t, reg) + body, res := doAPIFlow(t, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupCodeEnter, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationLookupInvalid().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) { + id, _ := createIdentity(t, reg) + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupCodeEnter, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "reason").String(), body) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupCodeEnter, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + }) + }) +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeLookup)+".enabled", true) + + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeLookup) + require.NoError(t, err) + fh, ok := s.(login.AAL2FormHydrator) + 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(reg, 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) { + id, _ := createIdentity(t, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + r, f := newFlow(ctx, t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + id, _ := createIdentity(t, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + r, f := newFlow(ctx, t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/lookup/nodes.go b/selfservice/strategy/lookup/nodes.go new file mode 100644 index 0000000..563288c --- /dev/null +++ b/selfservice/strategy/lookup/nodes.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func NewRevealLookupNode() *node.Node { + return node.NewInputField(node.LookupReveal, "true", node.LookupGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceSettingsRevealLookup()) +} + +func NewRegenerateLookupNode() *node.Node { + return node.NewInputField( + node.LookupRegenerate, "true", node.LookupGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceSettingsRegenerateLookup()) +} + +func NewDisableLookupNode() *node.Node { + return node.NewInputField(node.LookupDisable, "true", node.LookupGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceSettingsDisableLookup()) +} + +func NewConfirmLookupNode() *node.Node { + return node.NewInputField(node.LookupConfirm, "true", node.LookupGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceSettingsLookupConfirm()) +} diff --git a/selfservice/strategy/lookup/schema.go b/selfservice/strategy/lookup/schema.go new file mode 100644 index 0000000..77a20ae --- /dev/null +++ b/selfservice/strategy/lookup/schema.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte + +//go:embed .schema/settings.schema.json +var settingsSchema []byte diff --git a/selfservice/strategy/lookup/settings.go b/selfservice/strategy/lookup/settings.go new file mode 100644 index 0000000..9170db9 --- /dev/null +++ b/selfservice/strategy/lookup/settings.go @@ -0,0 +1,382 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup + +import ( + "context" + "encoding/json" + "net/http" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/x/randx" + + "github.com/ory/herodot" + "github.com/ory/kratos/ui/node" + + "github.com/ory/kratos/session" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "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/x" + "github.com/ory/x/decoderx" +) + +func (s *Strategy) SettingsStrategyID() string { + return identity.CredentialsTypeLookup.String() +} + +const ( + internalContextKeyRevealed = "revealed" + InternalContextKeyRegenerated = "regenerated" +) + +const numCodes = 12 + +var allSettingsNodes = []string{ + node.LookupRegenerate, + node.LookupReveal, + node.LookupRegenerate, + node.LookupDisable, + node.LookupCodes, + node.LookupConfirm, +} + +// Update Settings Flow with Lookup Method +// +// swagger:model updateSettingsFlowWithLookupMethod +type updateSettingsFlowWithLookupMethod struct { + // If set to true will reveal the lookup secrets + RevealLookup bool `json:"lookup_secret_reveal"` + + // If set to true will regenerate the lookup secrets + RegenerateLookup bool `json:"lookup_secret_regenerate"` + + // If set to true will save the regenerated lookup secrets + ConfirmLookup bool `json:"lookup_secret_confirm"` + + // Disables this method if true. + DisableLookup bool `json:"lookup_secret_disable"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to "lookup" when trying to add, update, or remove a lookup pairing. + // + // 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" form:"transient_payload"` +} + +func (p *updateSettingsFlowWithLookupMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithLookupMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.lookup.Strategy.Settings") + defer otelx.End(span, &err) + + 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(ctx, r, ctxUpdate, p) + } else if err != nil { + 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) + } + + if p.RegenerateLookup || p.RevealLookup || p.ConfirmLookup || p.DisableLookup { + // 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(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(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) + } + + return ctxUpdate, 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.Decode(r, dest, compiler, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +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(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 !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + } else { + return errors.New("ended up in unexpected state") + } + + if p.ConfirmLookup { + return s.continueSettingsFlowConfirm(ctx, ctxUpdate) + } else if p.RevealLookup { + if err := s.continueSettingsFlowReveal(ctx, ctxUpdate); err != nil { + return err + } + return flow.ErrStrategyAsksToReturnToUI + } else if p.DisableLookup { + return s.continueSettingsFlowDisable(ctx, ctxUpdate) + } else if p.RegenerateLookup { + if err := s.continueSettingsFlowRegenerate(ctx, ctxUpdate); err != nil { + return err + } + // regen + return flow.ErrStrategyAsksToReturnToUI + } + + return errors.New("ended up in unexpected state") +} + +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 + } + + i.DeleteCredentialsType(s.ID()) + + for _, n := range allSettingsNodes { + ctxUpdate.Flow.UI.Nodes.Remove(n) + } + + ctxUpdate.Flow.UI.Nodes.Upsert(NewRegenerateLookupNode()) + ctxUpdate.Flow.InternalContext, err = sjson.SetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), internalContextKeyRevealed), false) + if err != nil { + return err + } + ctxUpdate.Flow.InternalContext, err = sjson.SetRawBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyRegenerated), []byte("{}")) + if err != nil { + return err + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) continueSettingsFlowReveal(ctx context.Context, ctxUpdate *settings.UpdateContext) error { + hasLookup, err := s.identityHasLookup(ctx, ctxUpdate.Session.Identity) + if err != nil { + return err + } + + if !hasLookup { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Can not reveal lookup codes because you have none.")) + } + + _, cred, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), ctxUpdate.Session.IdentityID.String()) + if err != nil { + return err + } + + var creds identity.CredentialsLookupConfig + if err := json.Unmarshal(cred.Config, &creds); err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode lookup codes from JSON.").WithDebug(err.Error())) + } + + for _, n := range allSettingsNodes { + ctxUpdate.Flow.UI.Nodes.Remove(n) + } + + ctxUpdate.Flow.UI.Nodes.Upsert(creds.ToNode()) + ctxUpdate.Flow.UI.Nodes.Upsert(NewDisableLookupNode()) + ctxUpdate.Flow.UI.Nodes.Upsert(NewRegenerateLookupNode()) + + ctxUpdate.Flow.InternalContext, err = sjson.SetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), internalContextKeyRevealed), true) + if err != nil { + return err + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { + return err + } + + return nil +} + +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)} + } + + for _, n := range allSettingsNodes { + ctxUpdate.Flow.UI.Nodes.Remove(n) + } + + ctxUpdate.Flow.UI.Nodes.Upsert((&identity.CredentialsLookupConfig{RecoveryCodes: codes}).ToNode()) + ctxUpdate.Flow.UI.Nodes.Upsert(NewConfirmLookupNode()) + + var err error + ctxUpdate.Flow.InternalContext, err = sjson.SetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyRegenerated), codes) + if err != nil { + return err + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { + return err + } + + return nil +} + +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.")) + } + + rc := make([]identity.RecoveryCode, len(codes)) + for k := range rc { + rc[k] = identity.RecoveryCode{Code: codes[k].Get("code").String()} + } + + co, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: rc}) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode totp options to JSON: %s", err)) + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return err + } + + // We do not really need the identifier, so we add the identity's ID + c := &identity.Credentials{Type: s.ID(), Identifiers: []string{i.ID.String()}, Config: co} + c.Config = co + i.SetCredentials(s.ID(), *c) + + // Remove the TOTP URL from the internal context now that it is set! + ctxUpdate.Flow.InternalContext, err = sjson.DeleteBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyRegenerated)) + if err != nil { + return err + } + + 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(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel2, + }); err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) identityHasLookup(ctx context.Context, id *identity.Identity) (bool, error) { + if len(id.Credentials) == 0 { + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, id, identity.ExpandCredentials); err != nil { + return false, err + } + } + + count, err := s.CountActiveMultiFactorCredentials(ctx, id.Credentials) + if err != nil { + return false, err + } + + return count > 0, nil +} + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.lookup.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + hasLookup, err := s.identityHasLookup(ctx, id) + if err != nil { + return err + } + + if hasLookup { + f.UI.Nodes.Upsert(NewRevealLookupNode()) + f.UI.Nodes.Upsert(NewDisableLookupNode()) + } else { + f.UI.Nodes.Upsert(NewRegenerateLookupNode()) + } + + return nil +} + +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 { + key := settings.ContinuityKey(s.SettingsStrategyID()) + cookieStore := continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(r.Context())) + opts := settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity()) + if _, err := s.d.ContinuityManager().Pause(r.Context(), w, r, key, cookieStore, opts...); 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/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go new file mode 100644 index 0000000..fdf9394 --- /dev/null +++ b/selfservice/strategy/lookup/settings_test.go @@ -0,0 +1,625 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + "github.com/ory/x/snapshotx" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/lookup" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func createIdentityWithoutLookup(t *testing.T, reg driver.Registry) *identity.Identity { + id, _ := createIdentity(t, reg) + delete(id.Credentials, identity.CredentialsTypeLookup) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) + return id +} + +func createIdentity(t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) { + codes := make([]identity.RecoveryCode, 12) + for k := range codes { + var usedAt sqlxx.NullTime + if k%3 == 1 { + usedAt = sqlxx.NullTime(time.Unix(int64(1629199958+k), 0)) + } + codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("key-%d", k), UsedAt: usedAt} + } + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := reg.Hasher(context.Background()).Generate(context.Background(), []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: identifier, + Verified: false, + CreatedAt: time.Now(), + }, + }, + } + + rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes}) + require.NoError(t, err) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + identity.CredentialsTypeLookup: { + Type: identity.CredentialsTypeLookup, + Identifiers: []string{i.ID.String()}, + Config: rc, + }, + } + + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), i)) + return i, codes +} + +// createIdentityWithImportedLookupViaCreate mimics the admin identity import +// path (`POST /admin/identities` with `credentials.lookup_secret`): it +// builds an Identity with both password and lookup-secret credentials +// populated upfront, leaves `Identifiers` empty on the lookup-secret +// credential exactly like `Handler.importLookupSecretCredentials` does, +// and persists it via `Manager.Create`. The persister is expected to +// default the lookup-secret identifier to the persisted identity ID. See +// https://github.com/ory/kratos/issues/4561. +func createIdentityWithImportedLookupViaCreate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) { + codes := make([]identity.RecoveryCode, 4) + for k := range codes { + codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("imported-key-%d", k)} + } + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes}) + require.NoError(t, err) + + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: identifier, Verified: false, CreatedAt: time.Now()}, + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + identity.CredentialsTypeLookup: { + // Identifiers intentionally empty; the import path does not + // know the persisted identity ID yet. + Type: identity.CredentialsTypeLookup, + Config: rc, + }, + }, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + return i, codes +} + +// createIdentityWithImportedLookupViaUpdate mimics the admin identity +// import path on an existing identity (`PUT /admin/identities/{id}` with +// `credentials.lookup_secret`): it first creates an identity with a +// password credential, then attaches a lookup-secret credential via +// `Manager.Update` with `Identifiers` left empty. The persister is +// expected to default the lookup-secret identifier to the existing +// identity ID. See https://github.com/ory/kratos/issues/4561. +func createIdentityWithImportedLookupViaUpdate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: identifier, Verified: false, CreatedAt: time.Now()}, + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + }, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + codes := make([]identity.RecoveryCode, 4) + for k := range codes { + codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("imported-key-%d", k)} + } + rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes}) + require.NoError(t, err) + i.Credentials[identity.CredentialsTypeLookup] = identity.Credentials{ + // Identifiers intentionally empty; the persister must default it. + Type: identity.CredentialsTypeLookup, + Config: rc, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits)) + return i, codes +} + +func TestCompleteSettings(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.MethodEnableConfig("profile", false)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeLookup, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceSettingsRequiredAAL: "aal1", + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m", + }), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, 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(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, 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=hide recovery codes behind reveal button and show disable button", func(t *testing.T) { + id, _ := createIdentity(t, reg) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + + t.Run("case=spa", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value")) + }) + + t.Run("case=browser", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, false, publicTS) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value")) + }) + + t.Run("case=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value")) + }) + }) + + 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(ctx, t, reg, id) + + t.Run("case=spa", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) + }) + + t.Run("case=browser", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, false, publicTS) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) + }) + + t.Run("case=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) + }) + }) + + t.Run("case=should pass without csrf if API flow", func(t *testing.T) { + id, _ := createIdentity(t, reg) + + body, res := doAPIFlow(t, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupReveal, "true") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").Raw), []string{"0.attributes.value"}) + }) + + t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) { + id := createIdentityWithoutLookup(t, reg) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupReveal, "true") + }, id) + + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "reason").String(), body) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.LookupReveal, "true") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + }) + }) + + t.Run("type=can not reveal or regenerate or remove without privileged session", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + id, codes := createIdentity(t, reg) + + checkIdentity := func(t *testing.T) { + _, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String()) + require.NoError(t, err) + assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw)) + } + + for _, tc := range []struct { + v func(v url.Values) + d string + }{ + { + d: "reveal", + v: func(v url.Values) { + v.Set(node.LookupReveal, "true") + }, + }, + { + d: "regenerate", + v: func(v url.Values) { + v.Set(node.LookupRegenerate, "true") + }, + }, + { + d: "disable", + v: func(v url.Values) { + v.Set(node.LookupDisable, "true") + }, + }, + } { + t.Run("case="+tc.d, func(t *testing.T) { + payload := tc.v + t.Run("type=api", func(t *testing.T) { + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=spa", func(t *testing.T) { + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=browser", func(t *testing.T) { + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), loginTS.URL+"/login-ts") + assertx.EqualAsJSON(t, text.NewInfoLoginReAuth().Text, json.RawMessage(gjson.Get(actual, "ui.messages.0.text").Raw), actual) + checkIdentity(t) + }) + }) + } + }) + + t.Run("type=can not confirm without regenerate", func(t *testing.T) { + id, codes := createIdentity(t, reg) + payload := func(v url.Values) { + v.Set(node.LookupConfirm, "true") + } + + const reason = "You must (re-)generate recovery backup codes before you can save them." + + checkIdentity := func(t *testing.T) { + _, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String()) + require.NoError(t, err) + assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw)) + } + + t.Run("type=api", func(t *testing.T) { + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + checkIdentity(t) + }) + + t.Run("type=spa", func(t *testing.T) { + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + checkIdentity(t) + }) + + t.Run("type=browser", func(t *testing.T) { + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + checkIdentity(t) + }) + }) + + t.Run("type=regenerate but no confirmation", func(t *testing.T) { + id, codes := createIdentity(t, reg) + payload := func(v url.Values) { + v.Set(node.LookupRegenerate, "true") + } + + checkIdentity := func(t *testing.T) { + _, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String()) + require.NoError(t, err) + assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw)) + } + + t.Run("type=api", func(t *testing.T) { + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"}) + checkIdentity(t) + }) + + t.Run("type=spa", func(t *testing.T) { + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"}) + checkIdentity(t) + }) + + t.Run("type=browser", func(t *testing.T) { + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"}) + checkIdentity(t) + }) + }) + + t.Run("type=regenerate with confirmation", func(t *testing.T) { + for _, tc := range []struct { + d string + c func(t *testing.T) *identity.Identity + }{ + { + d: "with", + c: func(t *testing.T) *identity.Identity { + i, _ := createIdentity(t, reg) + return i + }, + }, + { + d: "without", + c: func(t *testing.T) *identity.Identity { + return createIdentityWithoutLookup(t, reg) + }, + }, + } { + t.Run("credentials="+tc.d, func(t *testing.T) { + payload := func(v url.Values) { + v.Del(node.LookupReveal) + v.Del(node.LookupDisable) + v.Set(node.LookupRegenerate, "true") + } + + payloadConfirm := func(v url.Values) { + v.Del(node.LookupRegenerate) + v.Del(node.LookupDisable) + v.Del(node.LookupReveal) + v.Set(node.LookupConfirm, "true") + } + + checkIdentity := func(t *testing.T, id *identity.Identity, f *kratos.SettingsFlow) { + _, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String()) + require.NoError(t, err) + assert.NotContains(t, gjson.GetBytes(cred.Config, "recovery_codes").Raw, "key-1") + assert.NotContains(t, gjson.GetBytes(cred.Config, "recovery_codes").Raw, "key-0") + + actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeLookup, lookup.InternalContextKeyRegenerated))) + } + + t.Run("type=api", func(t *testing.T) { + id, _ := createIdentity(t, reg) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + payload(values) + _, _ = testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + payloadConfirm(values) + actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + 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())) + + checkIdentity(t, id, f) + testhelpers.EnsureAAL(t, apiClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) + }) + + runBrowser := func(t *testing.T, spa bool) { + id, _ := createIdentity(t, reg) + + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + payload(values) + _, _ = testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + + payloadConfirm(values) + actual, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + assert.Equal(t, http.StatusOK, res.StatusCode) + + 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())) + checkIdentity(t, id, f) + testhelpers.EnsureAAL(t, browserClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) + } + + t.Run("type=browser", func(t *testing.T) { + runBrowser(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + runBrowser(t, true) + }) + }) + } + }) + + t.Run("type=remove lookup codes", func(t *testing.T) { + for _, tc := range []struct { + d string + c func(t *testing.T) *identity.Identity + }{ + { + d: "with", + c: func(t *testing.T) *identity.Identity { + i, _ := createIdentity(t, reg) + return i + }, + }, + { + d: "without", + c: func(t *testing.T) *identity.Identity { + return createIdentityWithoutLookup(t, reg) + }, + }, + } { + t.Run("credentials="+tc.d, func(t *testing.T) { + payloadConfirm := func(v url.Values) { + v.Del(node.LookupRegenerate) + v.Del(node.LookupReveal) + v.Set(node.LookupDisable, "true") + } + + checkIdentity := func(t *testing.T, id *identity.Identity, f *kratos.SettingsFlow) { + _, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String()) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeLookup, lookup.InternalContextKeyRegenerated)).Raw) + } + + t.Run("type=api", func(t *testing.T) { + id, _ := createIdentity(t, reg) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + payloadConfirm(values) + actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.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())) + + checkIdentity(t, id, f) + testhelpers.EnsureAAL(t, apiClient, publicTS, "aal1") + }) + + runBrowser := func(t *testing.T, spa bool) { + id, _ := createIdentity(t, reg) + + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + payloadConfirm(values) + actual, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + assert.Equal(t, http.StatusOK, res.StatusCode) + + 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())) + checkIdentity(t, id, f) + testhelpers.EnsureAAL(t, browserClient, publicTS, "aal1") + } + + t.Run("type=browser", func(t *testing.T) { + runBrowser(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + runBrowser(t, true) + }) + }) + } + }) +} diff --git a/selfservice/strategy/lookup/strategy.go b/selfservice/strategy/lookup/strategy.go new file mode 100644 index 0000000..597ebac --- /dev/null +++ b/selfservice/strategy/lookup/strategy.go @@ -0,0 +1,126 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/ory/kratos/x/nosurfx" + + "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/kratos/x/transaction" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +// var _ login.Strategy = new(Strategy) +var ( + _ settings.Strategy = (*Strategy)(nil) + _ login.AAL2FormHydrator = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + transaction.PersistenceProvider + otelx.Provider + + config.Provider + + continuity.ManagementProvider + + x.CookieProvider + + 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.ManagementProvider + + session.HandlerProvider + session.ManagementProvider +} + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +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.CredentialsLookupConfig + if err := json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + if len(conf.RecoveryCodes) > 0 { + count++ + } + } + } + return +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeLookup +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.LookupGroup +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel2, + } +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error { + return s.PopulateLoginMethod(r, identity.AuthenticatorAssuranceLevel2, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateLoginMethod(r, identity.AuthenticatorAssuranceLevel2, f) +} diff --git a/selfservice/strategy/lookup/strategy_test.go b/selfservice/strategy/lookup/strategy_test.go new file mode 100644 index 0000000..8309198 --- /dev/null +++ b/selfservice/strategy/lookup/strategy_test.go @@ -0,0 +1,75 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package lookup_test + +import ( + "fmt" + "testing" + + "github.com/ory/kratos/selfservice/strategy/lookup" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" +) + +func TestCountActiveFirstFactorCredentials(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := lookup.NewStrategy(reg) + + t.Run("first factor", func(t *testing.T) { + actual, err := strategy.CountActiveFirstFactorCredentials(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, 0, actual) + }) + + t.Run("multi factor", func(t *testing.T) { + 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(`{"recovery_codes": []}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"recovery_codes": [{}]}`), + }}, + expected: 1, + }, + { + 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.CountActiveMultiFactorCredentials(t.Context(), tc.in) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } + }) +} diff --git a/selfservice/strategy/lookup/stub/login.schema.json b/selfservice/strategy/lookup/stub/login.schema.json new file mode 100644 index 0000000..82f8581 --- /dev/null +++ b/selfservice/strategy/lookup/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/strategy/oidc/.schema/link.schema.json b/selfservice/strategy/oidc/.schema/link.schema.json new file mode 100644 index 0000000..4d40a79 --- /dev/null +++ b/selfservice/strategy/oidc/.schema/link.schema.json @@ -0,0 +1,55 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "provider": { + "type": "string", + "minLength": 1 + }, + "traits": { + "description": "DO NOT DELETE THIS FIELD. This field will be overwritten in login.go's and registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + }, + "method": { + "type": "string" + }, + "upstream_parameters": { + "type": "object", + "$comment": "Only the defined parameters are allowed. This is to prevent users from sending arbitrary parameters or craft URLs that cause unexpected behavior.", + "properties": { + "hd": { + "description": "The hd (hosted domain) parameter streamlines the login process for G Suite hosted accounts. By including the domain of the G Suite user (for example, mycollege.edu), you can indicate that the account selection UI should be optimized for accounts at that domain.", + "type": "string" + }, + "prompt": { + "description": "The prompt specifies whether the Authorization Server prompts the End-User for reauthentication and consent (for example, select_account).", + "type": "string" + }, + "auth_type": { + "description": "The `auth_type` parameter specifies the requested authentication features (as a comma-separated list).", + "type": "string" + }, + "acr_values": { + "description": "The `acr_values` parameter specifies the Authentication Context Class Reference values for the authorization request.", + "type": "string" + }, + "additionalProperties": false + } + }, + "id_token": { + "type": "string", + "description": "An optional id token provided by an OIDC provider" + }, + "id_token_nonce": { + "type": "string", + "description": "The nonce used when requesting the id_token from the provider. Required, if an id_token is given and the provider supports it." + } + } +} diff --git a/selfservice/strategy/oidc/.schema/settings.schema.json b/selfservice/strategy/oidc/.schema/settings.schema.json new file mode 100644 index 0000000..409e1ff --- /dev/null +++ b/selfservice/strategy/oidc/.schema/settings.schema.json @@ -0,0 +1,42 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/oidc/.schema/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "csrf_token": { + "type": "string" + }, + "unlink": { + "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." + }, + "upstream_parameters": { + "type": "object", + "$comment": "Only the defined parameters are allowed. This is to prevent users from sending arbitrary parameters or craft URLs that cause unexpected behavior.", + "properties": { + "hd": { + "description": "The hd (hosted domain) parameter streamlines the login process for G Suite hosted accounts. By including the domain of the G Suite user (for example, mycollege.edu), you can indicate that the account selection UI should be optimized for accounts at that domain.", + "type": "string" + }, + "prompt": { + "description": "The prompt specifies whether the Authorization Server prompts the End-User for reauthentication and consent (for example, select_account).", + "type": "string" + }, + "acr_values": { + "description": "The `acr_values` parameter specifies the Authentication Context Class Reference values for the authorization request.", + "type": "string" + }, + "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 0000000..2939b12 --- /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 0000000..2939b12 --- /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 0000000..2939b12 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.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=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 0000000..fe51488 --- /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 @@ +[] 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 0000000..a2c08bd --- /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 0000000..2939b12 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.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=no_options.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json new file mode 100644 index 0000000..2939b12 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.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=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 0000000..2939b12 --- /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 0000000..9ce3553 --- /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 0000000..19765bd --- /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 0000000..364b8ab --- /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/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..ca98dda --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with providerID", + "type": "info", + "context": { + "provider": "providerID", + "provider_id": "providerID" + } + } + } + } +] 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 new file mode 100644 index 0000000..48d0280 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..3b534e2 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050003, + "text": "Unlink google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=oryer.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=oryer.json new file mode 100644 index 0000000..c9b0b11 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=oryer.json @@ -0,0 +1,143 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..94db74f --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050002, + "text": "Link Ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_credentials.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_credentials.json new file mode 100644 index 0000000..3b534e2 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_credentials.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050003, + "text": "Unlink google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_oidc.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_oidc.json new file mode 100644 index 0000000..48d0280 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=multi_oidc.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=password_only.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=password_only.json new file mode 100644 index 0000000..94db74f --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=password_only.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050002, + "text": "Link Ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=single_oidc.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=single_oidc.json new file mode 100644 index 0000000..c9b0b11 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=api-case=API_flow_populates_link_and_unlink_nodes-identity=single_oidc.json @@ -0,0 +1,143 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..37108bf --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050003, + "text": "Unlink google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..48d0280 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..fc364ef --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json @@ -0,0 +1,165 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink google", + "type": "info", + "context": { + "provider": "google" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "ory", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink Ory", + "type": "info", + "context": { + "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 new file mode 100644 index 0000000..cc010fb --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050003, + "text": "Unlink google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050002, + "text": "Link Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..ddaaf69 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json @@ -0,0 +1,165 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link google", + "type": "info", + "context": { + "provider": "google" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "ory", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink Ory", + "type": "info", + "context": { + "provider": "Ory" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_an_non-existing_connection.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_an_non-existing_connection.json new file mode 100644 index 0000000..abb1fa2 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_an_non-existing_connection.json @@ -0,0 +1,143 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link google", + "type": "info", + "context": { + "provider": "google" + } + } + } + } +] 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 new file mode 100644 index 0000000..48d0280 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..ddaaf69 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json @@ -0,0 +1,165 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link google", + "type": "info", + "context": { + "provider": "google" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "ory", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink Ory", + "type": "info", + "context": { + "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 new file mode 100644 index 0000000..48d0280 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json @@ -0,0 +1,165 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050003, + "text": "Unlink github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "node_type": "input", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "Ory" + }, + "id": 1050003, + "text": "Unlink Ory", + "type": "info" + } + }, + "type": "input" + } +] 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 new file mode 100644 index 0000000..ddaaf69 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json @@ -0,0 +1,165 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link google", + "type": "info", + "context": { + "provider": "google" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "ory", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink Ory", + "type": "info", + "context": { + "provider": "Ory" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=fetch.json new file mode 100644 index 0000000..c9b0b11 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=fetch.json @@ -0,0 +1,143 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "email", + "disabled": false, + "name": "traits.email", + "node_type": "input", + "required": true, + "type": "email" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050002, + "text": "Link google", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=json.json new file mode 100644 index 0000000..abb1fa2 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_the_last_remaining_connection-flow=json.json @@ -0,0 +1,143 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "required": true, + "autocomplete": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link google", + "type": "info", + "context": { + "provider": "google" + } + } + } + } +] 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 0000000..0c62b4c --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..0c62b4c --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..0c62b4c --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..ae48c00 --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..ae48c00 --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..ae48c00 --- /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,297 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr" + } + } + } + }, + { + "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 0000000..f073e91 --- /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,86 @@ +{ + "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 0000000..f073e91 --- /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,86 @@ +{ + "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 0000000..f073e91 --- /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,86 @@ +{ + "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 0000000..3bfa766 --- /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,101 @@ +{ + "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 0000000..3bfa766 --- /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,101 @@ +{ + "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 0000000..3bfa766 --- /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,101 @@ +{ + "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=TestPopulateSignUpMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json new file mode 100644 index 0000000..53a24da --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json @@ -0,0 +1,248 @@ +{ + "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": 1040002, + "text": "Sign up with valid", + "type": "info", + "context": { + "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" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider", + "provider_id": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with claimsViaUserInfo", + "type": "info", + "context": { + "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" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-acr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with aal2-acr", + "type": "info", + "context": { + "provider": "aal2-acr", + "provider_id": "aal2-acr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "aal2-amr", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with aal2-amr", + "type": "info", + "context": { + "provider": "aal2-amr", + "provider_id": "aal2-amr" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer", + "provider_id": "invalid-issuer" + } + } + } + } + ] +} diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy_PopulateTestLoginFlow_UISnapshot.json b/selfservice/strategy/oidc/.snapshots/TestStrategy_PopulateTestLoginFlow_UISnapshot.json new file mode 100644 index 0000000..af56d5a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy_PopulateTestLoginFlow_UISnapshot.json @@ -0,0 +1,42 @@ +{ + "action": "", + "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": "providerID", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with My Provider", + "type": "info", + "context": { + "provider": "My Provider", + "provider_id": "providerID" + } + } + } + } + ] +} diff --git a/selfservice/strategy/oidc/const.go b/selfservice/strategy/oidc/const.go new file mode 100644 index 0000000..bd7b28c --- /dev/null +++ b/selfservice/strategy/oidc/const.go @@ -0,0 +1,8 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +const ( + sessionName = "ory_kratos_oidc_auth_code_session" +) diff --git a/selfservice/strategy/oidc/error.go b/selfservice/strategy/oidc/error.go new file mode 100644 index 0000000..6cbb2bb --- /dev/null +++ b/selfservice/strategy/oidc/error.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "io" + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/logrusx" +) + +func ErrScopeMissing() *herodot.DefaultError { + return herodot.ErrBadRequest(). + WithError("authentication failed because a required scope was not granted"). + WithReasonf(`Unable to finish because one or more permissions were not granted. Please retry and accept all permissions.`) +} + +func ErrIDTokenMissing() *herodot.DefaultError { + return herodot.ErrBadRequest(). + WithError("authentication failed because id_token is missing"). + WithReasonf(`Authentication failed because no id_token was returned. Please accept the "openid" permission and try again.`) +} + +func logUpstreamError(l *logrusx.Logger, resp *http.Response) error { + if resp.StatusCode == http.StatusOK { + return nil + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) + if err != nil { + l = l.WithError(err) + } + + l.WithField("response_code", resp.StatusCode).WithField("response_body", string(body)).Error("The upstream OIDC provider returned a non 200 status code.") + return errors.WithStack(herodot.ErrUpstreamError().WithReasonf("OpenID Connect provider returned a %d status code but 200 is expected.", resp.StatusCode)) +} diff --git a/selfservice/strategy/oidc/export_test.go b/selfservice/strategy/oidc/export_test.go new file mode 100644 index 0000000..947d140 --- /dev/null +++ b/selfservice/strategy/oidc/export_test.go @@ -0,0 +1,42 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "net/http" + + "github.com/gofrs/uuid" + + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" +) + +// ValidateFlowForTest exposes the unexported flow resolver so tests in the +// external oidc_test package can assert that OIDC callback state dispatches +// to the correct flow kind (login, registration, settings). +func (s *Strategy) ValidateFlowForTest(ctx context.Context, r *http.Request, rid uuid.UUID, kind oidcv1.FlowKind) (flow.Flow, error) { + return s.validateFlow(ctx, r, rid, kind) +} + +// ForwardErrorForTest exposes forwardError so tests can assert that +// pre-dispatch callback failures finish as a ready flow with a populated +// DebugPayload instead of panicking. +func (s *Strategy) ForwardErrorForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f flow.Flow, err error) { + s.forwardError(ctx, w, r, f, err) +} + +// ProcessTestLoginForTest exposes processTestLogin so external tests can +// exercise the dry-run OIDC callback path without routing through the full +// callback HTTP pipeline. +func (s *Strategy) ProcessTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, claims *Claims, provider Provider) error { + return s.processTestLogin(ctx, w, r, f, claims, provider) +} + +// FinishTestLoginForTest exposes finishTestLogin so external tests can +// assert the idempotency guard against an already-captured DebugPayload. +func (s *Strategy) FinishTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, dp *login.DebugPayload) error { + return s.finishTestLogin(ctx, w, r, f, dp) +} diff --git a/selfservice/strategy/oidc/fedcm/definitions.go b/selfservice/strategy/oidc/fedcm/definitions.go new file mode 100644 index 0000000..3d86f29 --- /dev/null +++ b/selfservice/strategy/oidc/fedcm/definitions.go @@ -0,0 +1,140 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fedcm + +import "encoding/json" + +type Provider struct { + // A full path of the IdP config file. + ConfigURL string `json:"config_url"` + + // The RP's client identifier, issued by the IdP. + ClientID string `json:"client_id"` + + // A random string to ensure the response is issued for this specific request. + // Prevents replay attacks. + Nonce string `json:"nonce"` + + // By specifying one of login_hints values provided by the accounts endpoints, + // the FedCM dialog selectively shows the specified account. + LoginHint string `json:"login_hint,omitempty"` + + // By specifying one of domain_hints values provided by the accounts endpoints, + // the FedCM dialog selectively shows the specified account. + DomainHint string `json:"domain_hint,omitempty"` + + // Array of strings that specifies the user information ("name", " email", + // "picture") that RP needs IdP to share with them. + // + // Note: Field API is supported by Chrome 132 and later. + Fields []string `json:"fields,omitempty"` + + // Custom object that allows to specify additional key-value parameters: + // - scope: A string value containing additional permissions that RP needs to + // request, for example " drive.readonly calendar.readonly" + // - nonce: A random string to ensure the response is issued for this specific + // request. Prevents replay attacks. + // + // Other custom key-value parameters. + // + // Note: parameters is supported from Chrome 132. + Parameters map[string]string `json:"parameters,omitempty"` +} + +// CreateFedcmFlowResponse +// +// Contains a list of all available FedCM providers. +// +// swagger:model createFedcmFlowResponse +type CreateFedcmFlowResponse struct { + Providers []Provider `json:"providers"` + CSRFToken string `json:"csrf_token"` +} + +// swagger:route GET /self-service/fed-cm/parameters frontend createFedcmFlow +// +// # Get FedCM Parameters +// +// This endpoint returns a list of all available FedCM providers. It is only supported on the Ory Network. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: createFedcmFlowResponse +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high + +type UpdateFedcmFlowBody struct { + // Token contains the result of `navigator.credentials.get`. + // + // required: true + Token string `json:"token"` + + // Nonce is the nonce that was used in the `navigator.credentials.get` call. If + // specified, it must match the `nonce` claim in the token. + // + // required: false + Nonce string `json:"nonce"` + + // CSRFToken is the anti-CSRF token. + // + // required: true + 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"` +} + +// swagger:parameters updateFedcmFlow +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateFedcmFlow struct { + // in: body + // required: true + Body UpdateFedcmFlowBody +} + +// swagger:route POST /self-service/fed-cm/token frontend updateFedcmFlow +// +// # Submit a FedCM token +// +// Use this endpoint to submit a token from a FedCM provider through +// `navigator.credentials.get` and log the user in. The parameters from +// `navigator.credentials.get` must have come from `GET +// /self-service/fed-cm/parameters`. +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Header: +// - Set-Cookie +// +// Responses: +// 200: successfulNativeLogin +// 303: emptyResponse +// 400: loginFlow +// 410: errorGeneric +// 422: errorBrowserLocationChangeRequired +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low diff --git a/selfservice/strategy/oidc/form.go b/selfservice/strategy/oidc/form.go new file mode 100644 index 0000000..1243b2d --- /dev/null +++ b/selfservice/strategy/oidc/form.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "encoding/json" + + "dario.cat/mergo" + + "github.com/ory/kratos/identity" +) + +// merge merges the JSON messages in a and b. If a value is defined in both a and b, a wins. +func merge(a, b json.RawMessage) (identity.Traits, error) { + if len(a) == 0 || string(a) == "{}" { + return identity.Traits(b), nil + } + + var aMap, bMap map[string]any + + if err := json.Unmarshal(a, &aMap); err != nil { + return nil, err + } + + if err := json.Unmarshal(b, &bMap); err != nil { + return nil, err + } + + // values in aMap (src) win. + if err := mergo.Merge(&bMap, &aMap, mergo.WithOverride); err != nil { + return nil, err + } + + traits, err := json.Marshal(bMap) + if err != nil { + return nil, err + } + + return traits, nil +} diff --git a/selfservice/strategy/oidc/form_test.go b/selfservice/strategy/oidc/form_test.go new file mode 100644 index 0000000..e1c0a7a --- /dev/null +++ b/selfservice/strategy/oidc/form_test.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMerge(t *testing.T) { + for k, tc := range []struct { + schema string + form json.RawMessage + op json.RawMessage + expect json.RawMessage + }{ + { + form: json.RawMessage("{}"), + op: json.RawMessage("{}"), + expect: json.RawMessage("{}"), + }, + { + form: json.RawMessage(`{"foo": "bar", "bool": true, "opv": "blubb"}`), + op: json.RawMessage(`{"baz":"bar","opv":"bla"}`), + expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`), + }, + { + form: json.RawMessage(`{"bool": true, "opv": "blubb"}`), + op: json.RawMessage(`{"foo":"bar","baz":"bar","opv":"bla"}`), + expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`), + }, + { + form: json.RawMessage(`{"bool": true, "opv": "blubb"}`), + op: json.RawMessage(`{"foo":"bar","baz":"bar","opv":"bla"}`), + expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`), + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + got, err := merge(tc.form, tc.op) + require.NoError(t, err) + assert.JSONEq(t, string(tc.expect), string(got)) + }) + } +} diff --git a/selfservice/strategy/oidc/json_equal_test.go b/selfservice/strategy/oidc/json_equal_test.go new file mode 100644 index 0000000..24098fe --- /dev/null +++ b/selfservice/strategy/oidc/json_equal_test.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJsonEqual(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + a, b json.RawMessage + expected bool + }{ + { + name: "both nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "both empty", + a: json.RawMessage{}, + b: json.RawMessage{}, + expected: true, + }, + { + name: "one nil one empty", + a: nil, + b: json.RawMessage{}, + expected: true, + }, + { + name: "one nil one non-empty", + a: nil, + b: json.RawMessage(`{}`), + expected: false, + }, + { + name: "identical objects", + a: json.RawMessage(`{"name":"alice","age":30}`), + b: json.RawMessage(`{"name":"alice","age":30}`), + expected: true, + }, + { + name: "same content different key order", + a: json.RawMessage(`{"name":"alice","age":30}`), + b: json.RawMessage(`{"age":30,"name":"alice"}`), + expected: true, + }, + { + name: "same content different whitespace", + a: json.RawMessage(`{"name": "alice"}`), + b: json.RawMessage(`{"name":"alice"}`), + expected: true, + }, + { + name: "different values", + a: json.RawMessage(`{"name":"alice"}`), + b: json.RawMessage(`{"name":"bob"}`), + expected: false, + }, + { + name: "nested objects equal", + a: json.RawMessage(`{"user":{"name":"alice","groups":["admin","user"]}}`), + b: json.RawMessage(`{"user":{"groups":["admin","user"],"name":"alice"}}`), + expected: true, + }, + { + name: "nested objects different", + a: json.RawMessage(`{"user":{"groups":["admin"]}}`), + b: json.RawMessage(`{"user":{"groups":["admin","user"]}}`), + expected: false, + }, + { + name: "extra key", + a: json.RawMessage(`{"name":"alice"}`), + b: json.RawMessage(`{"name":"alice","age":30}`), + expected: false, + }, + { + name: "invalid json a", + a: json.RawMessage(`{invalid`), + b: json.RawMessage(`{"name":"alice"}`), + expected: false, + }, + { + name: "invalid json b", + a: json.RawMessage(`{"name":"alice"}`), + b: json.RawMessage(`{invalid`), + expected: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, jsonEqual(tc.a, tc.b)) + }) + } +} diff --git a/selfservice/strategy/oidc/nodes.go b/selfservice/strategy/oidc/nodes.go new file mode 100644 index 0000000..3dc725e --- /dev/null +++ b/selfservice/strategy/oidc/nodes.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func NewLinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel)) +} + +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/oidcerr/oidcerr.go b/selfservice/strategy/oidc/oidcerr/oidcerr.go new file mode 100644 index 0000000..7191fe9 --- /dev/null +++ b/selfservice/strategy/oidc/oidcerr/oidcerr.go @@ -0,0 +1,75 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package oidcerr defines structured errors raised by the OIDC strategy. +// +// The Step type and its constants name the high-level callback phase that +// failed. The test login flow's debug renderer buckets failures by Step; +// non-test flows ignore it. +package oidcerr + +import "errors" + +// Step identifies the OIDC callback phase that produced an error. +type Step string + +const ( + StepStateValidate Step = "state_validate" + StepProviderDenied Step = "provider_denied" + StepTokenExchange Step = "token_exchange" + StepIDTokenVerify Step = "id_token_verify" + StepClaimsDecode Step = "claims_decode" + StepCallback Step = "callback" +) + +// stepReasons crosswalks a Step to the reason string surfaced in the test +// debug payload's StepError.Reason field. +var stepReasons = map[Step]string{ + StepStateValidate: "state_invalid", + StepProviderDenied: "access_denied", + StepTokenExchange: "exchange_failed", + StepIDTokenVerify: "verification_failed", + StepClaimsDecode: "claims_invalid", + StepCallback: "callback_failed", +} + +// Reason returns the reason string surfaced in the test debug payload's +// StepError.Reason field. Defined on Step (not *CallbackStepError) because +// herodot.ReasonCarrier picks up any error type with a Reason() method and +// would clobber the herodot DefaultError reason on the wire. +func (s Step) Reason() string { + return stepReasons[s] +} + +// CallbackStepError tags an OIDC callback error with the step it belongs to. +// It wraps the underlying error transparently: errors.As walks past it to +// find the inner *herodot.DefaultError, so existing handlers and writers +// behave identically. +type CallbackStepError struct { + Step Step + err error +} + +func (e *CallbackStepError) Error() string { return e.err.Error() } +func (e *CallbackStepError) Unwrap() error { return e.err } + +// Wrap tags err with a step hint. Pass-through for nil. The wrap is the +// outermost layer so callers that errors.As on *herodot.DefaultError keep +// working without changes. +func Wrap(step Step, err error) error { + if err == nil { + return nil + } + return &CallbackStepError{Step: step, err: err} +} + +// Unwrap walks err's chain looking for a *CallbackStepError tag. It returns +// a pointer to the tagged error if found, or a fresh fallback tagged with +// StepCallback otherwise. The returned value is never nil — callers can +// always read e.Step / e.Step.Reason() without a nil check. +func Unwrap(err error) *CallbackStepError { + if se, ok := errors.AsType[*CallbackStepError](err); ok { + return se + } + return &CallbackStepError{Step: StepCallback, err: err} +} diff --git a/selfservice/strategy/oidc/oidcerr/oidcerr_test.go b/selfservice/strategy/oidc/oidcerr/oidcerr_test.go new file mode 100644 index 0000000..3cf4c9d --- /dev/null +++ b/selfservice/strategy/oidc/oidcerr/oidcerr_test.go @@ -0,0 +1,87 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidcerr_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" +) + +func TestUnwrap(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + err error + wantStep oidcerr.Step + wantReason string + }{ + { + name: "state validate", + err: oidcerr.Wrap(oidcerr.StepStateValidate, herodot.ErrBadRequest().WithReason("invalid state")), + wantStep: oidcerr.StepStateValidate, + wantReason: "state_invalid", + }, + { + name: "provider denied", + err: oidcerr.Wrap(oidcerr.StepProviderDenied, herodot.ErrBadRequest().WithReason("access_denied: user refused")), + wantStep: oidcerr.StepProviderDenied, + wantReason: "access_denied", + }, + { + name: "token exchange", + err: oidcerr.Wrap(oidcerr.StepTokenExchange, errors.New("oauth2: cannot fetch token")), + wantStep: oidcerr.StepTokenExchange, + wantReason: "exchange_failed", + }, + { + name: "id_token verify", + err: oidcerr.Wrap(oidcerr.StepIDTokenVerify, herodot.ErrForbidden().WithReason("Could not verify id_token")), + wantStep: oidcerr.StepIDTokenVerify, + wantReason: "verification_failed", + }, + { + name: "claims decode", + err: oidcerr.Wrap(oidcerr.StepClaimsDecode, herodot.ErrUpstreamError().WithReason("provider did not return a subject")), + wantStep: oidcerr.StepClaimsDecode, + wantReason: "claims_invalid", + }, + { + name: "explicit callback bucket", + err: oidcerr.Wrap(oidcerr.StepCallback, errors.New("missing code")), + wantStep: oidcerr.StepCallback, + wantReason: "callback_failed", + }, + { + name: "untagged error falls back to callback", + err: errors.New("some unrelated failure"), + wantStep: oidcerr.StepCallback, + wantReason: "callback_failed", + }, + { + name: "untagged herodot error falls back to callback", + err: herodot.ErrInternalServerError().WithReason("oops"), + wantStep: oidcerr.StepCallback, + wantReason: "callback_failed", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + e := oidcerr.Unwrap(tc.err) + assert.Equal(t, tc.wantStep, e.Step) + assert.Equal(t, tc.wantReason, e.Step.Reason()) + }) + } +} + +func TestWrap_NilPassthrough(t *testing.T) { + t.Parallel() + assert.Nil(t, oidcerr.Wrap(oidcerr.StepCallback, nil)) +} diff --git a/selfservice/strategy/oidc/pkce.go b/selfservice/strategy/oidc/pkce.go new file mode 100644 index 0000000..206af32 --- /dev/null +++ b/selfservice/strategy/oidc/pkce.go @@ -0,0 +1,80 @@ +// 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/x/httpx" + "github.com/ory/x/logrusx" +) + +type pkceDependencies interface { + logrusx.Provider + httpx.ClientProvider +} + +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 0000000..2dbb829 --- /dev/null +++ b/selfservice/strategy/oidc/pkce_test.go @@ -0,0 +1,100 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestPKCESupport(t *testing.T) { + t.Parallel() + + 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) + + _, reg := pkg.NewFastRegistryWithMocks(t) + 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) + + flow := &login.Flow{ + ID: x.NewUUID(), + } + + stateParam, pkce, err := strat.GenerateState(t.Context(), provider, flow, "https://testhost") + require.NoError(t, err) + require.NotEmpty(t, stateParam) + + state, err := oidc.DecryptState(t.Context(), reg.Cipher(t.Context()), stateParam) + require.NoError(t, err) + assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_LOGIN, state.FlowKind) + assert.Equal(t, "https://testhost", state.RequestBaseUrl) + + 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(t.Context(), provider, ®istration.Flow{ID: x.NewUUID()}, "https://testhost") + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.DecryptState(t.Context(), reg.Cipher(t.Context()), stateParam) + require.NoError(t, err) + assert.Empty(t, oidc.PKCEVerifier(state)) + assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_REGISTRATION, state.FlowKind) + assert.Equal(t, "https://testhost", state.RequestBaseUrl) + } + }) +} diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go new file mode 100644 index 0000000..fba5a64 --- /dev/null +++ b/selfservice/strategy/oidc/provider.go @@ -0,0 +1,165 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/dghubble/oauth1" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" + "github.com/ory/kratos/x" +) + +type ( + Provider interface { + Config() *Configuration + } + 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) + } + 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) +} + +type IDTokenVerifier interface { + Verify(ctx context.Context, rawIDToken string) (*Claims, error) +} + +type NonceValidationSkipper interface { + CanSkipNonce(*Claims) bool +} + +type Claims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Object string `json:"oid,omitempty"` + Name string `json:"name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + LastName string `json:"last_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Email string `json:"email,omitempty"` + // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. + EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"` + Gender string `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Zoneinfo string `json:"zoneinfo,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"` + HD string `json:"hd,omitempty"` + Team string `json:"team,omitempty"` + Nonce string `json:"nonce,omitempty"` + NonceSupported bool `json:"nonce_supported,omitempty"` + + // ACR is the Authentication Context Class Reference reported by the + // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2. + ACR string `json:"acr,omitempty"` + + // AMR is the list of Authentication Methods References reported by the + // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2 and + // RFC 8176. + AMR []string `json:"amr,omitempty"` + + RawClaims map[string]any `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 == "" { + return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("provider did not return a subject"))) + } + if c.Issuer == "" { + return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("issuer not set in claims"))) + } + return nil +} + +// UpstreamParameters returns a list of oauth2.AuthCodeOption based on the upstream parameters. +// +// Only allowed parameters are returned and the rest is ignored. +// Allowed parameters are also defined in the `oidc/.schema/link.schema.json` file, however, +// this function also validates the parameters to prevent any potential security issues. +// +// Allowed parameters are: +// - `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`. +// - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. +// +// `login_hint` is intentionally not forwarded. A caller-supplied value cannot be +// trusted, and on IdPs that act on it (Google, Microsoft, Auth0, …) it can steer +// account, tenant, or OAuth client selection and enable a client-swap attack +// (HackerOne #3239672, ory-corp/cloud#8955). +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. + allowedParameters := map[string]struct{}{ + "hd": {}, + "prompt": {}, + "auth_type": {}, + "acr_values": {}, + } + + var params []oauth2.AuthCodeOption + for up, v := range upstreamParameters { + if _, ok := allowedParameters[up]; !ok { + continue + } + params = append(params, oauth2.SetAuthURLParam(up, v)) + } + + return params +} diff --git a/selfservice/strategy/oidc/provider_amazon.go b/selfservice/strategy/oidc/provider_amazon.go new file mode 100644 index 0000000..5d07f05 --- /dev/null +++ b/selfservice/strategy/oidc/provider_amazon.go @@ -0,0 +1,141 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "slices" + "testing" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/amazon" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" +) + +var _ OAuth2Provider = (*ProviderAmazon)(nil) + +var amazonSupportedScopes = []string{"profile", "profile:user_id", "postal_code"} + +type ProviderAmazon struct { + *ProviderGenericOIDC + amazonProfileURL string // Only overriden in tests. +} + +type amazonProfileResponse struct { + UserID string `json:"user_id"` + Email string `json:"email"` + Name string `json:"name"` + PostalCode string `json:"postal_code"` +} + +func NewProviderAmazon( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = amazon.Endpoint.AuthURL + const amazonProfileURL string = "https://api.amazon.com/user/profile" + + return &ProviderAmazon{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + amazonProfileURL: amazonProfileURL, + } +} + +func (p *ProviderAmazon) SetProfileURL(t *testing.T, url string) { + p.amazonProfileURL = url +} + +func (p *ProviderAmazon) Config() *Configuration { + return p.config +} + +func (p *ProviderAmazon) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: p.config.ClientID, + ClientSecret: p.config.ClientSecret, + Endpoint: amazon.Endpoint, + Scopes: p.config.Scope, + RedirectURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (p *ProviderAmazon) validateConfiguration() error { + for _, s := range p.config.Scope { + if !slices.Contains(amazonSupportedScopes, s) { + return errors.WithStack( + herodot.ErrMisconfiguration().WithReasonf("scope %s not supported. Supported: %+v", s, amazonSupportedScopes)) + } + } + if p.config.PKCE == "auto" { + return errors.WithStack(herodot.ErrMisconfiguration().WithReason("pkce:auto is not supported because Amazon does not support PKCE discovery")) + } + + return nil +} + +func (p *ProviderAmazon) OAuth2(ctx context.Context) (*oauth2.Config, error) { + // This is as good a place as any to validate the configuration. + if err := p.validateConfiguration(); err != nil { + return nil, err + } + + return p.oauth2(ctx), nil +} + +func (p *ProviderAmazon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { + ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderAmazon.Claims") + defer otelx.End(span, &err) + + ctx, client := httpx.SetOAuth2(ctx, p.reg.HTTPClient(ctx), p.oauth2(ctx), exchange) + + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, p.amazonProfileURL, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("failed to create HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error())) + } + req.Header.Set("x-amz-access-token", exchange.AccessToken) + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("failed to make HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error())) + } + defer func() { _ = resp.Body.Close() }() + body := io.LimitReader(resp.Body, 64*1024) // 64 KiB + + if resp.StatusCode != http.StatusOK { + rawResponse, _ := io.ReadAll(body) + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("non 200 response").WithDetail("url", p.amazonProfileURL).WithDetail("external_error", string(rawResponse)). + WithDetail("external_status_code", resp.StatusCode)) + } + + profile := amazonProfileResponse{} + if err := json.NewDecoder(body).Decode(&profile); err != nil { + rawResponse, _ := io.ReadAll(body) + return nil, errors.WithStack(herodot.ErrUpstreamError().WithDetail("url", p.amazonProfileURL).WithDetail("raw_response", rawResponse).WithError(err.Error())) + } + + claims := &Claims{ + Subject: profile.UserID, + Issuer: amazon.Endpoint.TokenURL, + Name: profile.Name, + Email: profile.Email, + Zoneinfo: profile.PostalCode, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_amazon_test.go b/selfservice/strategy/oidc/provider_amazon_test.go new file mode 100644 index 0000000..d1a191e --- /dev/null +++ b/selfservice/strategy/oidc/provider_amazon_test.go @@ -0,0 +1,59 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/oauth2/amazon" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestAmazonOidcClaims(t *testing.T) { + t.Parallel() + + handler := http.NewServeMux() + expectedAccessToken := "my-access-token" + handler.HandleFunc("GET /user/profile", func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("x-amz-access-token") + if token != expectedAccessToken { + w.WriteHeader(http.StatusForbidden) + return + } + // From the official docs: https://developer.amazon.com/docs/login-with-amazon/customer-profile.html . + userProfile := ` +{ + "user_id" : "amzn1.account.K2LI23KL2LK2", + "email" : "johndoe@gmail.com", + "name" : "John Doe", + "postal_code": "98101" +} +` + + _, err := w.Write([]byte(userProfile)) + require.NoError(t, err) + }) + amazonAPI := httptest.NewServer(handler) + t.Cleanup(amazonAPI.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := oidc.NewProviderAmazon(&oidc.Configuration{}, reg).(*oidc.ProviderAmazon) + p.SetProfileURL(t, amazonAPI.URL+"/user/profile") + + claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: expectedAccessToken}, nil) + require.NoError(t, err) + require.NotNil(t, claims) + + require.Equal(t, claims.Subject, "amzn1.account.K2LI23KL2LK2") + require.Equal(t, claims.Issuer, amazon.Endpoint.TokenURL) + require.Equal(t, claims.Name, "John Doe") + require.Equal(t, claims.Email, "johndoe@gmail.com") + require.Equal(t, claims.Zoneinfo, "98101") +} diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go new file mode 100644 index 0000000..ad17720 --- /dev/null +++ b/selfservice/strategy/oidc/provider_apple.go @@ -0,0 +1,175 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "crypto/ecdsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "net/url" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/golang-jwt/jwt/v4" + + "github.com/pkg/errors" + + "golang.org/x/oauth2" +) + +type ProviderApple struct { + *ProviderGenericOIDC + JWKSUrl string +} + +var _ OAuth2Provider = (*ProviderApple)(nil) + +func NewProviderApple( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = "https://appleid.apple.com" + return &ProviderApple{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + JWKSUrl: "https://appleid.apple.com/auth/keys", + } +} + +func (a *ProviderApple) newClientSecret() (string, error) { + // decode the pem format + block, _ := pem.Decode([]byte(a.config.PrivateKey)) + if block == nil || block.Type != "PRIVATE KEY" { + return "", errors.New("failed to decode PEM block containing private key") + } + + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", errors.Wrap(err, "Private key decoding failed") + } + privateKey, ok := parsedKey.(*ecdsa.PrivateKey) + if !ok { + return "", errors.New("Private key is not ecdsa key") + } + + now := time.Now() + expirationTime := time.Now().Add(5 * time.Minute) + + appleToken := jwt.NewWithClaims(jwt.SigningMethodES256, + jwt.RegisteredClaims{ + Audience: []string{"https://appleid.apple.com"}, + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(now), + Issuer: a.config.TeamId, + Subject: a.config.ClientID, + }) + appleToken.Header["kid"] = a.config.PrivateKeyId + + return appleToken.SignedString(privateKey) +} + +func (a *ProviderApple) oauth2(ctx context.Context) (*oauth2.Config, error) { + // Apple requires a JWT token that acts as a client secret + secret, err := a.newClientSecret() + if err != nil { + return nil, err + } + a.config.ClientSecret = secret + + endpoint := oauth2.Endpoint{ + AuthURL: "https://appleid.apple.com/auth/authorize", + TokenURL: "https://appleid.apple.com/auth/token", + AuthStyle: oauth2.AuthStyleInParams, + } + return a.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +func (a *ProviderApple) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return a.oauth2(ctx) +} + +func (a *ProviderApple) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + var options []oauth2.AuthCodeOption + + if isForced(r) { + options = append(options, oauth2.SetAuthURLParam("prompt", "login")) + } + if len(a.config.RequestedClaims) != 0 { + options = append(options, oauth2.SetAuthURLParam("claims", string(a.config.RequestedClaims))) + } + + // When requesting email or name, Apple requires the form_post response mode. + // This also means the return url will be called by Apple using POST method. + for _, scope := range a.config.Scope { + if scope == "email" || scope == "name" { + options = append(options, oauth2.SetAuthURLParam("response_mode", "form_post")) + break + } + } + + return options +} + +func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + claims, err := a.ProviderGenericOIDC.Claims(ctx, exchange, query) + if err != nil { + return claims, err + } + a.DecodeQuery(query, claims) + + return claims, nil +} + +// DecodeQuery decodes extra user info from Apple into the given `Claims`. +// The info is sent as an extra query parameter to the redirect URL. +// See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292 +// Note that there's no way to make sure the info hasn't been tampered with. +func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) { + var user struct { + Name *struct { + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` + } `json:"name"` + } + if err := json.Unmarshal([]byte(query.Get("user")), &user); err == nil { + if name := user.Name; name != nil { + if firstName := name.FirstName; firstName != nil { + if claims.GivenName == "" { + claims.GivenName = *firstName + } + } + if lastName := name.LastName; lastName != nil { + if claims.LastName == "" { + claims.LastName = *lastName + } + if claims.FamilyName == "" { + claims.FamilyName = *lastName + } + } + } + } +} + +var _ IDTokenVerifier = new(ProviderApple) + +const issuerURLApple = "https://appleid.apple.com" + +func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { + keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl) + ctx = oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient) + + return verifyToken(ctx, keySet, a.config, rawIDToken, issuerURLApple) +} + +var _ NonceValidationSkipper = new(ProviderApple) + +func (a *ProviderApple) CanSkipNonce(c *Claims) bool { + // Apple sets nonce_supported=true when the device requires nonce validation, + // so the nonce check may only be skipped when nonce_supported is false. + return !c.NonceSupported +} diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go new file mode 100644 index 0000000..53642fc --- /dev/null +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -0,0 +1,146 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + _ "embed" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestDecodeQuery(t *testing.T) { + query := url.Values{ + "user": []string{`{"name": {"firstName": "first", "lastName": "last"}, "email": "email@email.com"}`}, + } + + for k, tc := range []struct { + claims *oidc.Claims + familyName string + givenName string + lastName string + }{ + {claims: &oidc.Claims{}, familyName: "last", givenName: "first", lastName: "last"}, + {claims: &oidc.Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"}, + {claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"}, + {claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + a := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple) + a.DecodeQuery(query, tc.claims) + assert.Equal(t, tc.familyName, tc.claims.FamilyName) + assert.Equal(t, tc.givenName, tc.claims.GivenName) + assert.Equal(t, tc.lastName, tc.claims.LastName) + // Never extract email from the query, as the same info can be extracted and verified from the ID token. + assert.Empty(t, tc.claims.Email) + }) + } +} + +func TestAppleVerify(t *testing.T) { + t.Parallel() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS) + })) + + tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS2) + })) + makeClaims := func(aud string) jwt.RegisteredClaims { + return jwt.RegisteredClaims{ + Issuer: "https://appleid.apple.com", + Subject: "acme@ory.sh", + Audience: jwt.ClaimStrings{aud}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + } + } + t.Run("case=successful verification", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderApple(&oidc.Configuration{ + ClientID: "com.example.app", + }, reg).(*oidc.ProviderApple) + apple.JWKSUrl = ts.URL + token := createIDToken(t, makeClaims("com.example.app")) + + c, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + assert.Equal(t, "acme@ory.sh", c.Email) + assert.Equal(t, "acme@ory.sh", c.Subject) + assert.Equal(t, "https://appleid.apple.com", c.Issuer) + }) + + t.Run("case=fails due to client_id mismatch", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderApple(&oidc.Configuration{ + ClientID: "com.example.app", + }, reg).(*oidc.ProviderApple) + apple.JWKSUrl = ts.URL + token := createIDToken(t, makeClaims("com.different-example.app")) + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error()) + }) + + t.Run("case=fails due to jwks mismatch", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderApple(&oidc.Configuration{ + ClientID: "com.example.app", + }, reg).(*oidc.ProviderApple) + apple.JWKSUrl = tsOtherJWKS.URL + token := createIDToken(t, makeClaims("com.example.app")) + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error()) + }) + + t.Run("case=succeedes with additional id token audience", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderApple(&oidc.Configuration{ + ClientID: "something.else.app", + AdditionalIDTokenAudiences: []string{"com.example.app"}, + }, reg).(*oidc.ProviderApple) + apple.JWKSUrl = ts.URL + token := createIDToken(t, makeClaims("com.example.app")) + + _, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + }) +} + +func TestAppleCanSkipNonce(t *testing.T) { + apple := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple) + + for _, tc := range []struct { + name string + nonceSupported bool + wantSkip bool + }{ + // Apple sets nonce_supported=true when the device requires a nonce, + // so nonce validation must NOT be skipped in that case. + {name: "nonce supported means nonce is mandatory", nonceSupported: true, wantSkip: false}, + // Legacy devices report nonce_supported=false, so nonce validation may be skipped. + {name: "nonce not supported means skip is allowed", nonceSupported: false, wantSkip: true}, + } { + t.Run("case="+tc.name, func(t *testing.T) { + assert.Equal(t, tc.wantSkip, apple.CanSkipNonce(&oidc.Claims{NonceSupported: tc.nonceSupported})) + }) + } +} diff --git a/selfservice/strategy/oidc/provider_auth0.go b/selfservice/strategy/oidc/provider_auth0.go new file mode 100644 index 0000000..a6c4680 --- /dev/null +++ b/selfservice/strategy/oidc/provider_auth0.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "cmp" + "context" + "encoding/json" + "io" + "net/url" + "path" + "time" + + "github.com/ory/x/httpx" + + "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 ProviderAuth0 struct { + *ProviderGenericOIDC +} + +var _ OAuth2Provider = (*ProviderAuth0)(nil) + +func NewProviderAuth0( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderAuth0{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderAuth0) oauth2(ctx context.Context) (*oauth2.Config, error) { + endpoint, err := url.Parse(g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + authURL := *endpoint + tokenURL := *endpoint + + authURL.Path = path.Join(authURL.Path, "/authorize") + tokenURL.Path = path.Join(tokenURL.Path, "/oauth/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 *ProviderAuth0) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx) +} + +func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, err + } + + u, err := url.Parse(g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithWrap(err).WithReasonf("%s", err)) + } + u.Path = path.Join(u.Path, "/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().WithWrap(err).WithReasonf("%s", err)) + } + + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + // Once auth0 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.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + b, err = authZeroUpdatedAtWorkaround(b) + if err != nil { + return nil, 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().WithWrap(err).WithReasonf("%s", err)) + } + + claims.Issuer = cmp.Or(claims.Issuer, g.config.IssuerURL) + return &claims, nil +} + +// There is a bug in the response from Auth0. The updated_at field may be a string and not an int64. +// https://community.auth0.com/t/oidc-id-token-claim-updated-at-violates-oidc-specification-breaks-rp-implementations/24098 +// 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 authZeroUpdatedAtWorkaround(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().WithWrap(err).WithReasonf("%s", err)) + } + } + return body, nil +} diff --git a/selfservice/strategy/oidc/provider_auth0_unit_test.go b/selfservice/strategy/oidc/provider_auth0_unit_test.go new file mode 100644 index 0000000..9de0815 --- /dev/null +++ b/selfservice/strategy/oidc/provider_auth0_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 TestAuthZeroUpdatedAtWorkaround(t *testing.T) { + actual, err := authZeroUpdatedAtWorkaround([]byte("{}")) + require.NoError(t, err) + assert.Equal(t, "{}", string(actual)) + + actual, err = authZeroUpdatedAtWorkaround([]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 = authZeroUpdatedAtWorkaround(input) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf(`{"updated_at":%d}`, timestamp.Unix()), string(actual)) +} diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go new file mode 100644 index 0000000..4a14329 --- /dev/null +++ b/selfservice/strategy/oidc/provider_config.go @@ -0,0 +1,280 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "encoding/json" + "maps" + "net/url" + "slices" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/x/urlx" +) + +const ( + // AccountLinkingModeVerifyWithExistingCredential requires the user to verify their identity + // with an existing credential before accounts are linked. + AccountLinkingModeVerifyWithExistingCredential = "confirm_with_existing_credential" + // AccountLinkingModeAutomatic silently links accounts if the provider verifies email ownership. + AccountLinkingModeAutomatic = "automatic" + + // ProviderTypeApple is the provider type for Sign in with Apple. + ProviderTypeApple = "apple" + // ProviderTypeGoogle is the provider type for Sign in with Google. + ProviderTypeGoogle = "google" + + // UpdateIdentityOnLoginNever disables identity updates on login (default). + UpdateIdentityOnLoginNever = "never" + // UpdateIdentityOnLoginAutomatic re-runs the Jsonnet claims mapper on every + // OIDC login and updates the identity's traits and metadata automatically. + UpdateIdentityOnLoginAutomatic = "automatic" +) + +type Configuration struct { + // ID is the provider's ID + ID string `json:"id"` + + // Provider is either "generic" for a generic OAuth 2.0 / OpenID Connect Provider or one of: + // - generic + // - google + // - github + // - github-app + // - gitlab + // - microsoft + // - discord + // - salesforce + // - slack + // - facebook + // - auth0 + // - vk + // - yandex + // - apple + // - spotify + // - netid + // - dingtalk + // - linkedin + // - patreon + // - amazon + // - uaepass + Provider string `json:"provider"` + + // Label represents an optional label which can be used in the UI generation. + Label string `json:"label"` + + // ClientID is the application's Client ID. + ClientID string `json:"client_id"` + + // ClientSecret is the application's secret. + ClientSecret string `json:"client_secret"` + + // IssuerURL is the OpenID Connect Server URL. You can leave this empty if `provider` is not set to `generic`. + // If set, neither `auth_url` nor `token_url` are required. + IssuerURL string `json:"issuer_url"` + + // AuthURL is the authorize url, typically something like: https://example.org/oauth2/auth + // Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when + // `provider` is set to `generic`. + AuthURL string `json:"auth_url"` + + // TokenURL is the token url, typically something like: https://example.org/oauth2/token + // Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when + // `provider` is set to `generic`. + TokenURL string `json:"token_url"` + + // Tenant is the Azure AD Tenant to use for authentication, and must be set when `provider` is set to `microsoft`. + // Can be either `common`, `organizations`, `consumers` for a multitenant application or a specific tenant like + // `8eaef023-2b34-4da1-9baa-8bc8c9d6a490` or `contoso.onmicrosoft.com`. + Tenant string `json:"microsoft_tenant"` + + // SubjectSource is a flag which controls from which endpoint the subject identifier is taken by microsoft provider. + // Can be either `userinfo` or `me` or `oid`. + // 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. + // If the value is `oid` then the the oid (Object ID) is taken to identify users across different services. + // The default is `userinfo`. + SubjectSource string `json:"subject_source"` + + // TeamId is the Apple Developer Team ID that's needed for the `apple` `provider` to work. + // It can be found Apple Developer website and combined with `apple_private_key` and `apple_private_key_id` + // is used to generate `client_secret` + TeamId string `json:"apple_team_id"` + + // PrivateKeyId is the private Apple key identifier. Keys can be generated via developer.apple.com. + // This key should be generated with the `Sign In with Apple` option checked. + // This is needed when `provider` is set to `apple` + PrivateKeyId string `json:"apple_private_key_id"` + + // PrivateKeyId is the Apple private key identifier that can be downloaded during key generation. + // This is needed when `provider` is set to `apple` + PrivateKey string `json:"apple_private_key"` + + // Scope specifies optional requested permissions. + Scope []string `json:"scope"` + + // Mapper specifies the JSONNet code snippet which uses the OpenID Connect Provider's data (e.g. GitHub or Google + // profile information) to hydrate the identity's data. + // + // It can be either a URL (file://, http(s)://, base64://) or an inline JSONNet code snippet. + Mapper string `json:"mapper_url"` + + // RequestedClaims is a string encoded json object that specifies claims and optionally their properties that should be + // included in the id_token or returned from the UserInfo Endpoint. + // + // More information: https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter + RequestedClaims json.RawMessage `json:"requested_claims"` + + // An optional organization ID that this provider belongs to. + // This parameter is only effective in the Ory Network. + OrganizationID string `json:"organization_id"` + + // AdditionalIDTokenAudiences is a list of additional audiences allowed in the ID Token. + // This is only relevant in OIDC flows that submit an IDToken instead of using the callback from the OIDC provider. + AdditionalIDTokenAudiences []string `json:"additional_id_token_audiences"` + + // ClaimsSource is a flag which controls where the claims are taken from when + // using the generic provider. 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`. + 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"` + + // FedCMConfigURL is the URL to the FedCM IdP configuration file. + // This is only effective in the Ory Network. + FedCMConfigURL string `json:"fedcm_config_url"` + + // NetIDTokenOriginHeader contains the orgin header to be used when exchanging a + // NetID FedCM token for an ID token. + NetIDTokenOriginHeader string `json:"net_id_token_origin_header"` + + // AccountLinkingMode controls how account conflicts are resolved for this provider. + // Possible values: "confirm_with_existing_credential" (default), "automatic". + // This is only effective in the Ory Network. + AccountLinkingMode string `json:"account_linking_mode,omitempty"` + + // UpdateIdentityOnLogin controls whether the identity is updated from + // OIDC claims on each login. + // + // Possible values: + // - "never" (default): the identity is not updated on login. + // - "automatic": re-runs the Jsonnet claims mapper on every OIDC login + // and updates the identity's traits and metadata automatically. + UpdateIdentityOnLogin string `json:"update_identity_on_login,omitempty"` + + // AAL2ACRValues lists upstream OIDC `acr` claim values that elevate + // the resulting Kratos session to AAL2. When empty, the session AAL + // is always AAL1 regardless of the upstream `acr` claim. + AAL2ACRValues []string `json:"aal2_acr_values,omitempty"` + + // AAL2AMRValues lists upstream OIDC `amr` claim values that elevate + // the resulting Kratos session to AAL2 when any of them appears in + // the upstream `amr` array. When empty, `amr` does not influence the + // session AAL. + AAL2AMRValues []string `json:"aal2_amr_values,omitempty"` +} + +// AALForClaims returns the session AuthenticatorAssuranceLevel that the +// upstream OIDC response should be mapped to, based on the provider's +// AAL2ACRValues / AAL2AMRValues allowlists and the ACR / AMR claims +// reported by the upstream provider. +// +// The default is AAL1. AAL2 is only returned when either the upstream +// `acr` claim matches one of the configured AAL2ACRValues, or at least +// one of the upstream `amr` entries matches one of the configured +// AAL2AMRValues. An empty `acr` never matches, even if the allowlist +// contains an empty string. +func (c *Configuration) AALForClaims(claims *Claims) identity.AuthenticatorAssuranceLevel { + if claims == nil { + return identity.AuthenticatorAssuranceLevel1 + } + if claims.ACR != "" && slices.Contains(c.AAL2ACRValues, claims.ACR) { + return identity.AuthenticatorAssuranceLevel2 + } + for _, want := range c.AAL2AMRValues { + if slices.Contains(claims.AMR, want) { + return identity.AuthenticatorAssuranceLevel2 + } + } + return identity.AuthenticatorAssuranceLevel1 +} + +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) + route = strings.Replace(route, "{organization}", p.OrganizationID, 1) + return urlx.AppendPaths(public, route).String() + } + + return urlx.AppendPaths(public, strings.Replace(RouteCallback, "{provider}", p.ID, 1)).String() +} + +type ConfigurationCollection struct { + BaseRedirectURI string `json:"base_redirect_uri"` + Providers []Configuration `json:"providers"` +} + +// !!! WARNING !!! +// +// 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, + "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, + "line": NewProviderLineV21, + "jackson": NewProviderJackson, + "fedcm-test": NewProviderTestFedcm, + "amazon": NewProviderAmazon, + "uaepass": NewProviderUAEPass, +} + +func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) { + for _, p := range c.Providers { + if p.ID == id { + if f, ok := supportedProviders[p.Provider]; ok { + return f(&p, reg), nil + } + + return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, maps.Keys(supportedProviders)) + } + } + return nil, errors.WithStack(herodot.ErrNotFound().WithReasonf(`OpenID Connect Provider "%s" is unknown or has not been configured`, id)) +} diff --git a/selfservice/strategy/oidc/provider_config_test.go b/selfservice/strategy/oidc/provider_config_test.go new file mode 100644 index 0000000..7a73a78 --- /dev/null +++ b/selfservice/strategy/oidc/provider_config_test.go @@ -0,0 +1,113 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/x/configx" +) + +func TestConfig(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue( + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", + map[string]any{"providers": []map[string]any{{"provider": "generic"}}})) + + s := oidc.NewStrategy(reg) + collection, err := s.Config(t.Context()) + require.NoError(t, err) + + require.Len(t, collection.Providers, 1) + assert.Equal(t, "generic", collection.Providers[0].Provider) +} + +func TestConfiguration_AALForClaims(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + config oidc.Configuration + claims *oidc.Claims + want identity.AuthenticatorAssuranceLevel + }{ + { + name: "nil claims default to aal1", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, + claims: nil, + want: identity.AuthenticatorAssuranceLevel1, + }, + { + name: "empty config with claims stays aal1", + config: oidc.Configuration{}, + claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"mfa", "pwd"}}, + want: identity.AuthenticatorAssuranceLevel1, + }, + { + name: "acr match elevates to aal2", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa", "urn:strong"}}, + claims: &oidc.Claims{ACR: "urn:mfa"}, + want: identity.AuthenticatorAssuranceLevel2, + }, + { + name: "acr mismatch stays aal1", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}}, + claims: &oidc.Claims{ACR: "urn:basic"}, + want: identity.AuthenticatorAssuranceLevel1, + }, + { + name: "empty acr does not accidentally match empty configured value", + config: oidc.Configuration{AAL2ACRValues: []string{""}}, + claims: &oidc.Claims{ACR: ""}, + want: identity.AuthenticatorAssuranceLevel1, + }, + { + name: "amr match elevates to aal2", + config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}}, + claims: &oidc.Claims{AMR: []string{"pwd", "mfa"}}, + want: identity.AuthenticatorAssuranceLevel2, + }, + { + name: "amr mismatch stays aal1", + config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}}, + claims: &oidc.Claims{AMR: []string{"pwd"}}, + want: identity.AuthenticatorAssuranceLevel1, + }, + { + name: "any configured amr value is sufficient", + config: oidc.Configuration{AAL2AMRValues: []string{"otp", "hwk", "mfa"}}, + claims: &oidc.Claims{AMR: []string{"pwd", "hwk"}}, + want: identity.AuthenticatorAssuranceLevel2, + }, + { + name: "both acr and amr configured, acr matches", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, + claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"pwd"}}, + want: identity.AuthenticatorAssuranceLevel2, + }, + { + name: "both acr and amr configured, amr matches", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, + claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd", "mfa"}}, + want: identity.AuthenticatorAssuranceLevel2, + }, + { + name: "both configured, neither matches", + config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, + claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd"}}, + want: identity.AuthenticatorAssuranceLevel1, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, tc.config.AALForClaims(tc.claims)) + }) + } +} diff --git a/selfservice/strategy/oidc/provider_dingtalk.go b/selfservice/strategy/oidc/provider_dingtalk.go new file mode 100644 index 0000000..5b19fb1 --- /dev/null +++ b/selfservice/strategy/oidc/provider_dingtalk.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/x/httpx" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/herodot" +) + +type ProviderDingTalk struct { + config *Configuration + reg Dependencies +} + +var _ OAuth2Provider = (*ProviderDingTalk)(nil) + +func NewProviderDingTalk( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderDingTalk{ + config: config, + reg: reg, + } +} + +func (g *ProviderDingTalk) Config() *Configuration { + return g.config +} + +func (g *ProviderDingTalk) oauth2(ctx context.Context) *oauth2.Config { + endpoint := oauth2.Endpoint{ + AuthURL: "https://login.dingtalk.com/oauth2/auth", + TokenURL: "https://api.dingtalk.com/v1.0/oauth2/userAccessToken", + } + + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: endpoint, + // DingTalk only allow to set scopes: openid or openid corpid + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderDingTalk) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("prompt", "consent"), + } +} + +func (g *ProviderDingTalk) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +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, err + } + + pTokenParams := &struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + Code string `json:"code"` + GrantType string `json:"grantType"` + }{conf.ClientID, conf.ClientSecret, code, "authorization_code"} + bs, err := json.Marshal(pTokenParams) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + r := strings.NewReader(string(bs)) + client := g.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs()) + req, err := retryablehttp.NewRequest("POST", conf.Endpoint.TokenURL, r) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + req.Header.Add("Content-Type", "application/json;charset=UTF-8") + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + var dToken struct { + ErrCode int `json:"code"` + ErrMsg string `json:"message"` + AccessToken string `json:"accessToken"` // Interface call credentials + ExpiresIn int64 `json:"expireIn"` // access_token interface call credential timeout time, unit (seconds) + } + + if err := json.NewDecoder(resp.Body).Decode(&dToken); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + if dToken.ErrCode != 0 { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("dToken.ErrCode = %d, dToken.ErrMsg = %s", dToken.ErrCode, dToken.ErrMsg)) + } + + token := &oauth2.Token{ + AccessToken: dToken.AccessToken, + Expiry: time.Unix(time.Now().Unix()+int64(dToken.ExpiresIn), 0), + } + return token, nil +} + +func (g *ProviderDingTalk) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { + userInfoURL := "https://api.dingtalk.com/v1.0/contact/users/me" + accessToken := exchange.AccessToken + + client := g.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs()) + req, err := retryablehttp.NewRequest("GET", userInfoURL, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + req.Header.Add("x-acs-dingtalk-access-token", accessToken) + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + var user struct { + Nick string `json:"nick"` + OpenID string `json:"openId"` + AvatarURL string `json:"avatarUrl"` + Email string `json:"email"` + ErrMsg string `json:"message"` + ErrCode string `json:"code"` + } + + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + if user.ErrMsg != "" { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("userResp.ErrCode = %s, userResp.ErrMsg = %s", user.ErrCode, user.ErrMsg)) + } + + return &Claims{ + Issuer: userInfoURL, + Subject: user.OpenID, + Nickname: user.Nick, + Name: user.Nick, + Picture: user.AvatarURL, + Email: user.Email, + }, nil +} diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go new file mode 100644 index 0000000..5141862 --- /dev/null +++ b/selfservice/strategy/oidc/provider_discord.go @@ -0,0 +1,103 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "net/url" + "slices" + + "github.com/ory/kratos/x" + + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + + "github.com/ory/x/stringsx" +) + +var _ OAuth2Provider = (*ProviderDiscord)(nil) + +type ProviderDiscord struct { + config *Configuration + reg Dependencies +} + +func NewProviderDiscord( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderDiscord{ + config: config, + reg: reg, + } +} + +func (d *ProviderDiscord) Config() *Configuration { + return d.config +} + +func (d *ProviderDiscord) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: d.config.ClientID, + ClientSecret: d.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: discordgo.EndpointOauth2 + "authorize", + TokenURL: discordgo.EndpointOauth2 + "token", + }, + RedirectURL: d.config.Redir(d.reg.Config().OIDCRedirectURIBase(ctx)), + Scopes: d.config.Scope, + } +} + +func (d *ProviderDiscord) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return d.oauth2(ctx), nil +} + +func (d *ProviderDiscord) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + if isForced(r) { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("prompt", "consent"), + } + } + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("prompt", "none"), + } +} + +func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") + for _, check := range d.Config().Scope { + if !slices.Contains(grantedScopes, check) { + return nil, errors.WithStack(ErrScopeMissing()) + } + } + + dg, err := discordgo.New(fmt.Sprintf("Bearer %s", exchange.AccessToken)) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err)) + } + + user, err := dg.User("@me") + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims := &Claims{ + Issuer: discordgo.EndpointOauth2, + Subject: user.ID, + Name: fmt.Sprintf("%s#%s", user.Username, user.Discriminator), + Nickname: user.Username, + PreferredUsername: user.Username, + Picture: user.AvatarURL(""), + Email: user.Email, + EmailVerified: x.ConvertibleBoolean(user.Verified), + Locale: Locale(user.Locale), + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go new file mode 100644 index 0000000..8f8b9a1 --- /dev/null +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -0,0 +1,139 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + + "github.com/ory/kratos/x" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderFacebook)(nil) + +type ProviderFacebook struct { + *ProviderGenericOIDC +} + +func NewProviderFacebook( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = "https://www.facebook.com" + return &ProviderFacebook{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderFacebook) generateAppSecretProof(token *oauth2.Token) string { + secret := g.config.ClientSecret + data := token.AccessToken + + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) { + p, err := g.provider(ctx) + if err != nil { + return nil, err + } + + endpoint := p.Endpoint() + endpoint.AuthURL = "https://facebook.com/dialog/oauth" + endpoint.TokenURL = "https://graph.facebook.com/oauth/access_token" + return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +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, err + } + + appSecretProof := g.generateAppSecretProof(token) + // Do not use the versioned Graph API here. If you do, it will break once the version is deprecated. See also: + // + // When you use https://graph.facebook.com/me without specifying a version, Facebook defaults to the oldest + // available version your app supports. This behavior ensures backward compatibility but can lead to unintended + // issues if that version becomes deprecated. + 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().WithWrap(err).WithReasonf("%s", err)) + } + + 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().WithWrap(err).WithReasonf("%s", err)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + var user struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool + Picture struct { + Data struct { + URL string `json:"url,omitempty"` + } `json:"data,omitempty"` + } `json:"picture,omitempty"` + BirthDay string `json:"birthday,omitempty"` + Gender string `json:"gender,omitempty"` + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err)) + } + + if user.Email != "" { + user.EmailVerified = true + } + + return &Claims{ + Issuer: u.String(), + Subject: user.ID, + Name: user.Name, + GivenName: user.FirstName, + FamilyName: user.LastName, + MiddleName: user.MiddleName, + Nickname: user.Name, + PreferredUsername: user.Name, + Picture: user.Picture.Data.URL, + Email: user.Email, + EmailVerified: x.ConvertibleBoolean(user.EmailVerified), + Gender: user.Gender, + Birthdate: user.BirthDay, + }, nil +} diff --git a/selfservice/strategy/oidc/provider_generic_oidc.go b/selfservice/strategy/oidc/provider_generic_oidc.go new file mode 100644 index 0000000..7e1ee78 --- /dev/null +++ b/selfservice/strategy/oidc/provider_generic_oidc.go @@ -0,0 +1,219 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "net/url" + "slices" + "time" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/reqlog" +) + +var _ OAuth2Provider = (*ProviderGenericOIDC)(nil) + +type ProviderGenericOIDC struct { + p *gooidc.Provider + config *Configuration + reg Dependencies +} + +func NewProviderGenericOIDC( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderGenericOIDC{ + config: config, + reg: reg, + } +} + +const ( + ClaimsSourceIDToken = "id_token" + ClaimsSourceUserInfo = "userinfo" +) + +func (g *ProviderGenericOIDC) Config() *Configuration { + return g.config +} + +func (g *ProviderGenericOIDC) withHTTPClientContext(ctx context.Context) context.Context { + return gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient) +} + +func (g *ProviderGenericOIDC) provider(ctx context.Context) (*gooidc.Provider, error) { + if g.p == nil { + p, err := gooidc.NewProvider(g.withHTTPClientContext(ctx), g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to initialize OpenID Connect Provider: %s", err)) + } + g.p = p + } + return g.p, nil +} + +func (g *ProviderGenericOIDC) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config { + scope := g.config.Scope + if !slices.Contains(scope, gooidc.ScopeOpenID) { + scope = append(scope, gooidc.ScopeOpenID) + } + + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: endpoint, + Scopes: scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderGenericOIDC) OAuth2(ctx context.Context) (*oauth2.Config, error) { + p, err := g.provider(ctx) + if err != nil { + return nil, err + } + + endpoint := p.Endpoint() + return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +func (g *ProviderGenericOIDC) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + var options []oauth2.AuthCodeOption + + if isForced(r) { + options = append(options, oauth2.SetAuthURLParam("prompt", "login")) + } + if len(g.config.RequestedClaims) != 0 { + options = append(options, oauth2.SetAuthURLParam("claims", string(g.config.RequestedClaims))) + } + + return options +} + +func (g *ProviderGenericOIDC) verifyAndDecodeClaimsWithProvider(ctx context.Context, provider *gooidc.Provider, raw string) (*Claims, error) { + token, err := provider.VerifierContext(g.withHTTPClientContext(ctx), &gooidc.Config{ClientID: g.config.ClientID}).Verify(ctx, raw) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + + var claims Claims + if err := token.Claims(&claims); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + + var rawClaims map[string]interface{} + if err := token.Claims(&rawClaims); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + claims.RawClaims = rawClaims + + return &claims, nil +} + +func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { + switch g.config.ClaimsSource { + case ClaimsSourceIDToken, "": + return g.claimsFromIDToken(ctx, exchange) + case ClaimsSourceUserInfo: + return g.claimsFromUserInfo(ctx, exchange) + } + + return nil, errors.WithStack(herodot.ErrMisconfiguration(). + WithReasonf("Unknown claims source: %q", g.config.ClaimsSource)) +} + +func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange *oauth2.Token) (*Claims, error) { + p, err := g.provider(ctx) + if err != nil { + return nil, err + } + + t0 := time.Now() + userInfo, err := p.UserInfo(g.withHTTPClientContext(ctx), oauth2.StaticTokenSource(exchange)) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil { + return nil, err + } + + var claims Claims + if err = userInfo.Claims(&claims); err != nil { + return nil, err + } + var rawClaims map[string]interface{} + if err := userInfo.Claims(&rawClaims); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + claims.RawClaims = rawClaims + + // NOTE: Due to the possibility of token substitution attacks (see Section + // 16.11), the UserInfo Response is not guaranteed to be about the End-User + // identified by the sub (subject) element of the ID Token. The sub Claim in the + // UserInfo Response MUST be verified to exactly match the sub Claim in the ID + // Token; if they do not match, the UserInfo Response values MUST NOT be used. + // See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + idToken, err := g.verifiedIDToken(ctx, exchange) + if err != nil { + return nil, err + } + + if idToken.Subject != claims.Subject { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("sub (Subject) claim mismatch between ID token and UserInfo endpoint")) + } + + // If signed, the UserInfo Response MUST contain the Claims iss (issuer) and aud + // (audience) as members. The iss value MUST be the OP's Issuer Identifier URL. + // The aud value MUST be or include the RP's Client ID value. + // See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + // + // Consequently, the issuer might not be present in the UserInfo response and we + // need to set it here. + if claims.Issuer == "" { + claims.Issuer = idToken.Issuer + } + + return &claims, nil +} + +func (g *ProviderGenericOIDC) claimsFromIDToken(ctx context.Context, exchange *oauth2.Token) (*Claims, error) { + p, raw, err := g.idTokenAndProvider(ctx, exchange) + if err != nil { + return nil, err + } + + return g.verifyAndDecodeClaimsWithProvider(ctx, p, raw) +} + +func (g *ProviderGenericOIDC) idTokenAndProvider(ctx context.Context, exchange *oauth2.Token) (*gooidc.Provider, string, error) { + raw, ok := exchange.Extra("id_token").(string) + if !ok || len(raw) == 0 { + return nil, "", errors.WithStack(ErrIDTokenMissing()) + } + + p, err := g.provider(ctx) + if err != nil { + return nil, "", err + } + + return p, raw, nil +} + +func (g *ProviderGenericOIDC) verifiedIDToken(ctx context.Context, exchange *oauth2.Token) (*gooidc.IDToken, error) { + p, raw, err := g.idTokenAndProvider(ctx, exchange) + if err != nil { + return nil, err + } + + token, err := p.VerifierContext(g.withHTTPClientContext(ctx), &gooidc.Config{ClientID: g.config.ClientID}).Verify(ctx, raw) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + + return token, nil +} diff --git a/selfservice/strategy/oidc/provider_generic_test.go b/selfservice/strategy/oidc/provider_generic_test.go new file mode 100644 index 0000000..2ebbfcb --- /dev/null +++ b/selfservice/strategy/oidc/provider_generic_test.go @@ -0,0 +1,94 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" +) + +func makeOIDCClaims() json.RawMessage { + claims, _ := json.Marshal(map[string]interface{}{ + "id_token": map[string]interface{}{ + "email": map[string]bool{ + "essential": true, + }, + "email_verified": map[string]bool{ + "essential": true, + }, + }, + }) + return claims +} + +func makeAuthCodeURL(t *testing.T, r *login.Flow, reg *driver.RegistryDefault) string { + p := oidc.NewProviderGenericOIDC(&oidc.Configuration{ + Provider: "generic", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + IssuerURL: "https://accounts.google.com", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: makeOIDCClaims(), + }, reg) + c, err := p.(oidc.OAuth2Provider).OAuth2(t.Context()) + require.NoError(t, err) + return c.AuthCodeURL("state", p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)...) +} + +func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeyPublicBaseURL, "https://ory.sh")) + t.Run("case=redirectURI is public base url", func(t *testing.T) { + r := &login.Flow{ID: x.NewUUID(), Refresh: true} + actual, err := url.ParseRequestURI(makeAuthCodeURL(t, r, reg)) + require.NoError(t, err) + assert.Contains(t, actual.Query().Get("redirect_uri"), "https://ory.sh") + }) + + t.Run("case=redirectURI is public base url", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyOIDCBaseRedirectURL, "https://example.org") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyOIDCBaseRedirectURL, nil) + }) + r := &login.Flow{ID: x.NewUUID(), Refresh: true} + actual, err := url.ParseRequestURI(makeAuthCodeURL(t, r, reg)) + require.NoError(t, err) + assert.Contains(t, actual.Query().Get("redirect_uri"), "https://example.org") + }) + + t.Run("case=expect prompt to be login with forced flag", func(t *testing.T) { + r := &login.Flow{ + ID: x.NewUUID(), + Refresh: true, + } + assert.Contains(t, makeAuthCodeURL(t, r, reg), "prompt=login") + }) + + t.Run("case=expect prompt to not be login without forced flag", func(t *testing.T) { + r := &login.Flow{ + ID: x.NewUUID(), + } + assert.NotContains(t, makeAuthCodeURL(t, r, reg), "prompt=login") + }) + + t.Run("case=expect requested claims to be set", func(t *testing.T) { + r := &login.Flow{ + ID: x.NewUUID(), + } + assert.Contains(t, makeAuthCodeURL(t, r, reg), "claims="+url.QueryEscape(string(makeOIDCClaims()))) + }) +} diff --git a/selfservice/strategy/oidc/provider_github.go b/selfservice/strategy/oidc/provider_github.go new file mode 100644 index 0000000..8a57075 --- /dev/null +++ b/selfservice/strategy/oidc/provider_github.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "net/url" + "slices" + + "github.com/ory/kratos/x" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" + + "github.com/ory/x/httpx" + "github.com/ory/x/stringsx" + + ghapi "github.com/google/go-github/v89/github" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderGitHub)(nil) + +type ProviderGitHub struct { + config *Configuration + reg Dependencies +} + +func NewProviderGitHub( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderGitHub{ + config: config, + reg: reg, + } +} + +func (g *ProviderGitHub) Config() *Configuration { + return g.config +} + +func (g *ProviderGitHub) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: github.Endpoint, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderGitHub) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderGitHub) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",") + for _, check := range g.Config().Scope { + if !slices.Contains(grantedScopes, check) { + return nil, errors.WithStack(ErrScopeMissing()) + } + } + + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), g.oauth2(ctx), exchange) + gh, err := ghapi.NewClient(ghapi.WithHTTPClient(client.HTTPClient)) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + user, _, err := gh.Users.Get(ctx, "") + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims := &Claims{ + Subject: fmt.Sprintf("%d", user.GetID()), + Issuer: github.Endpoint.TokenURL, + Name: user.GetName(), + Nickname: user.GetLogin(), + Website: user.GetBlog(), + Picture: user.GetAvatarURL(), + Profile: user.GetHTMLURL(), + UpdatedAt: user.GetUpdatedAt().Unix(), + } + + // GitHub does not provide the user's private emails in the call to `/user`. Therefore, if scope "user:email" is set, + // we want to make another request to `/user/emails` and merge that with our claims. + if slices.Contains(grantedScopes, "user:email") { + emails, _, err := gh.Users.ListEmails(ctx, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + 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) { + claims.Email = e.GetEmail() + claims.EmailVerified = x.ConvertibleBoolean(e.GetVerified()) + break + } + } + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_github_app.go b/selfservice/strategy/oidc/provider_github_app.go new file mode 100644 index 0000000..e77ba2f --- /dev/null +++ b/selfservice/strategy/oidc/provider_github_app.go @@ -0,0 +1,100 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "net/url" + + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" + + ghapi "github.com/google/go-github/v89/github" + + "github.com/ory/herodot" +) + +type ProviderGitHubApp struct { + config *Configuration + reg Dependencies +} + +func NewProviderGitHubApp( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderGitHubApp{ + config: config, + reg: reg, + } +} + +func (g *ProviderGitHubApp) Config() *Configuration { + return g.config +} + +func (g *ProviderGitHubApp) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: github.Endpoint, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderGitHubApp) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderGitHubApp) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), g.oauth2(ctx), exchange) + gh, err := ghapi.NewClient(ghapi.WithHTTPClient(client.HTTPClient)) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + user, _, err := gh.Users.Get(ctx, "") + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims := &Claims{ + Subject: fmt.Sprintf("%d", user.GetID()), + Issuer: github.Endpoint.TokenURL, + Name: user.GetName(), + Nickname: user.GetLogin(), + Website: user.GetBlog(), + Picture: user.GetAvatarURL(), + Profile: user.GetHTMLURL(), + UpdatedAt: user.GetUpdatedAt().Unix(), + } + + // GitHub does not provide the user's private emails in the call to `/user`. Therefore, if scope "user:email" is set, + // we want to make another request to `/user/emails` and merge that with our claims. + emails, _, err := gh.Users.ListEmails(ctx, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + 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)-1 { + claims.Email = e.GetEmail() + claims.EmailVerified = x.ConvertibleBoolean(e.GetVerified()) + break + } + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_gitlab.go b/selfservice/strategy/oidc/provider_gitlab.go new file mode 100644 index 0000000..8128506 --- /dev/null +++ b/selfservice/strategy/oidc/provider_gitlab.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "cmp" + "context" + "encoding/json" + "net/url" + "path" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +const ( + defaultEndpoint = "https://gitlab.com" +) + +var _ OAuth2Provider = (*ProviderGitLab)(nil) + +type ProviderGitLab struct { + *ProviderGenericOIDC +} + +func NewProviderGitLab( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderGitLab{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderGitLab) oauth2(ctx context.Context) (*oauth2.Config, error) { + endpoint, err := g.endpoint() + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + authURL := *endpoint + tokenURL := *endpoint + + authURL.Path = path.Join(authURL.Path, "/oauth/authorize") + tokenURL.Path = path.Join(tokenURL.Path, "/oauth/token") + + return &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)), + }, nil +} + +func (g *ProviderGitLab) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx) +} + +func (g *ProviderGitLab) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, err + } + + u, err := g.endpoint() + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + u.Path = path.Join(u.Path, "/oauth/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().WithWrap(err).WithReasonf("%s", err)) + } + + req.Header.Set("Accept", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + var claims Claims + if err := json.NewDecoder(resp.Body).Decode(&claims); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims.Issuer = cmp.Or(claims.Issuer, g.config.IssuerURL) + return &claims, nil +} + +func (g *ProviderGitLab) endpoint() (*url.URL, error) { + e := defaultEndpoint + if len(g.config.IssuerURL) > 0 { + e = g.config.IssuerURL + } + return url.Parse(e) +} diff --git a/selfservice/strategy/oidc/provider_google.go b/selfservice/strategy/oidc/provider_google.go new file mode 100644 index 0000000..b339a22 --- /dev/null +++ b/selfservice/strategy/oidc/provider_google.go @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "slices" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +var _ OAuth2Provider = (*ProviderGoogle)(nil) + +type ProviderGoogle struct { + *ProviderGenericOIDC + JWKSUrl string +} + +func NewProviderGoogle( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = "https://accounts.google.com" + return &ProviderGoogle{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + JWKSUrl: "https://www.googleapis.com/oauth2/v3/certs", + } +} + +func (g *ProviderGoogle) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config { + scope := g.config.Scope + if !slices.Contains(scope, gooidc.ScopeOpenID) { + scope = append(scope, gooidc.ScopeOpenID) + } + + scope = slices.DeleteFunc(slices.Clone(scope), func(s string) bool { return s == gooidc.ScopeOfflineAccess }) + + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: endpoint, + Scopes: scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderGoogle) OAuth2(ctx context.Context) (*oauth2.Config, error) { + p, err := g.provider(ctx) + if err != nil { + return nil, err + } + + endpoint := p.Endpoint() + return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +func (g *ProviderGoogle) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + scope := g.config.Scope + options := g.ProviderGenericOIDC.AuthCodeURLOptions(r) + + if slices.Contains(scope, gooidc.ScopeOfflineAccess) { + options = append(options, oauth2.AccessTypeOffline) + } + + return options +} + +var _ IDTokenVerifier = new(ProviderGoogle) + +const issuerURLGoogle = "https://accounts.google.com" + +func (g *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { + keySet := gooidc.NewRemoteKeySet(ctx, g.JWKSUrl) + ctx = gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient) + + return verifyToken(ctx, keySet, g.config, rawIDToken, issuerURLGoogle) +} + +var _ NonceValidationSkipper = new(ProviderGoogle) + +func (g *ProviderGoogle) CanSkipNonce(c *Claims) bool { + // Not all SDKs support nonce validation, so we skip it if no nonce is present in the claims of the ID Token. + return c.Nonce == "" +} diff --git a/selfservice/strategy/oidc/provider_google_test.go b/selfservice/strategy/oidc/provider_google_test.go new file mode 100644 index 0000000..1e29395 --- /dev/null +++ b/selfservice/strategy/oidc/provider_google_test.go @@ -0,0 +1,138 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestProviderGoogle_Scope(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderGoogle(&oidc.Configuration{ + Provider: "google", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + c, _ := p.(oidc.OAuth2Provider).OAuth2(context.Background()) + assert.NotContains(t, c.Scopes, "offline_access") +} + +func TestProviderGoogle_AccessType(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderGoogle(&oidc.Configuration{ + Provider: "google", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + r := &login.Flow{ + ID: x.NewUUID(), + } + + options := p.(oidc.OAuth2Provider).AuthCodeURLOptions(r) + assert.Contains(t, options, oauth2.AccessTypeOffline) +} + +func TestGoogleVerify(t *testing.T) { + t.Parallel() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS) + })) + + tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS2) + })) + + makeClaims := func(aud string) jwt.RegisteredClaims { + return jwt.RegisteredClaims{ + Issuer: "https://accounts.google.com", + Subject: "acme@ory.sh", + Audience: jwt.ClaimStrings{aud}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + } + } + createProvider := func(jwksUrl string) *oidc.ProviderGoogle { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + p := oidc.NewProviderGoogle(&oidc.Configuration{ + Provider: "google", + ID: "valid", + ClientID: "com.example.app", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg).(*oidc.ProviderGoogle) + p.JWKSUrl = jwksUrl + return p + } + t.Run("case=successful verification", func(t *testing.T) { + p := createProvider(ts.URL) + token := createIDToken(t, makeClaims("com.example.app")) + + c, err := p.Verify(context.Background(), token) + require.NoError(t, err) + assert.Equal(t, "acme@ory.sh", c.Email) + assert.Equal(t, "acme@ory.sh", c.Subject) + assert.Equal(t, "https://accounts.google.com", c.Issuer) + }) + + t.Run("case=fails due to client_id mismatch", func(t *testing.T) { + p := createProvider(ts.URL) + token := createIDToken(t, makeClaims("com.different-example.app")) + + _, err := p.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error()) + }) + + t.Run("case=fails due to jwks mismatch", func(t *testing.T) { + p := createProvider(tsOtherJWKS.URL) + token := createIDToken(t, makeClaims("com.example.app")) + + _, err := p.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error()) + }) + + t.Run("case=succeedes with additional id token audience", func(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + google := oidc.NewProviderGoogle(&oidc.Configuration{ + ClientID: "something.else.app", + AdditionalIDTokenAudiences: []string{"com.example.app"}, + }, reg).(*oidc.ProviderGoogle) + google.JWKSUrl = ts.URL + token := createIDToken(t, makeClaims("com.example.app")) + + _, err := google.Verify(context.Background(), token) + require.NoError(t, err) + }) +} diff --git a/selfservice/strategy/oidc/provider_jackson.go b/selfservice/strategy/oidc/provider_jackson.go new file mode 100644 index 0000000..89460c8 --- /dev/null +++ b/selfservice/strategy/oidc/provider_jackson.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "net/url" + "os" + "slices" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/urlx" +) + +// jacksonTrustedIssuersEnv is a comma-separated list of token issuers accepted +// in addition to the configured issuer URL. It keeps ID tokens verifiable while +// the issuer URL migrates from one domain to another: set it to the old and new +// issuers for the duration of the migration, then unset it. +const jacksonTrustedIssuersEnv = "SAML_SP_TRUSTED_ISSUERS" + +type ProviderJackson struct { + *ProviderGenericOIDC +} + +func NewProviderJackson( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderJackson{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (j *ProviderJackson) setProvider(ctx context.Context) { + if j.p == nil { + internalHost := strings.TrimSuffix(j.config.TokenURL, "/api/oauth/token") + config := oidc.ProviderConfig{ + IssuerURL: j.config.IssuerURL, + AuthURL: j.config.AuthURL, + TokenURL: j.config.TokenURL, + DeviceAuthURL: "", + UserInfoURL: internalHost + "/api/oauth/userinfo", + JWKSURL: internalHost + "/oauth/jwks", + Algorithms: []string{"RS256"}, + } + j.p = config.NewProvider(j.withHTTPClientContext(ctx)) + } +} + +func (j *ProviderJackson) OAuth2(ctx context.Context) (*oauth2.Config, error) { + j.setProvider(ctx) + endpoint := j.p.Endpoint() + config := j.oauth2ConfigFromEndpoint(ctx, endpoint) + config.RedirectURL = urlx.AppendPaths( + j.reg.Config().SAMLRedirectURIBase(ctx), + "/self-service/methods/saml/callback/"+j.config.ID, + ).String() + + return config, nil +} + +func (j *ProviderJackson) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { + j.setProvider(ctx) + + raw, ok := exchange.Extra("id_token").(string) + if !ok || len(raw) == 0 { + return nil, errors.WithStack(ErrIDTokenMissing()) + } + + // go-oidc's SkipIssuerCheck defers issuer validation to the caller. The + // issuer is verified below against the configured issuer plus the + // JacksonTrustedIssuersEnv allowlist. + token, err := j.p.VerifierContext( + j.withHTTPClientContext(ctx), + &oidc.Config{ClientID: j.config.ClientID, SkipIssuerCheck: true}, + ).Verify(ctx, raw) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + + if token.Issuer != j.config.IssuerURL && !slices.Contains(jacksonTrustedIssuers(), token.Issuer) { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf( + "oidc: id token issued by a different provider, expected %q got %q", j.config.IssuerURL, token.Issuer, + )) + } + + var claims Claims + if err := token.Claims(&claims); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + + var rawClaims map[string]interface{} + if err := token.Claims(&rawClaims); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) + } + claims.RawClaims = rawClaims + + return &claims, nil +} + +func jacksonTrustedIssuers() []string { + var issuers []string + for iss := range strings.SplitSeq(os.Getenv(jacksonTrustedIssuersEnv), ",") { + if iss = strings.TrimSpace(iss); iss != "" { + issuers = append(issuers, iss) + } + } + return issuers +} diff --git a/selfservice/strategy/oidc/provider_jackson_test.go b/selfservice/strategy/oidc/provider_jackson_test.go new file mode 100644 index 0000000..c9ed90f --- /dev/null +++ b/selfservice/strategy/oidc/provider_jackson_test.go @@ -0,0 +1,137 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderJackson(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + j := oidc.NewProviderJackson(&oidc.Configuration{ + Provider: "jackson", + IssuerURL: "https://www.jackson.com/oauth", + AuthURL: "https://www.jackson.com/oauth/auth", + TokenURL: "https://www.jackson.com/api/oauth/token", + Mapper: "file://./stub/hydra.schema.json", + Scope: []string{"email", "profile"}, + ID: "some-id", + }, reg) + assert.NotNil(t, j) + + c, err := j.(oidc.OAuth2Provider).OAuth2(context.Background()) + require.NoError(t, err) + + assert.True(t, strings.HasSuffix(c.RedirectURL, "/self-service/methods/saml/callback/some-id")) +} + +func TestProviderJacksonClaims(t *testing.T) { + jwks := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS) + })) + t.Cleanup(jwks.Close) + + otherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(publicJWKS2) + })) + t.Cleanup(otherJWKS.Close) + + const issuer = "https://sp.example.org/saml" + + newJackson := func(t *testing.T, upstream *httptest.Server) oidc.OAuth2Provider { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + return oidc.NewProviderJackson(&oidc.Configuration{ + Provider: "jackson", + ID: "jackson", + ClientID: "client-id", + IssuerURL: issuer, + AuthURL: upstream.URL + "/api/oauth/authorize", + TokenURL: upstream.URL + "/api/oauth/token", + Mapper: "file://./stub/hydra.schema.json", + }, reg).(oidc.OAuth2Provider) + } + + makeExchange := func(t *testing.T, iss string) *oauth2.Token { + raw := createIDToken(t, jwt.RegisteredClaims{ + Issuer: iss, + Subject: "acme@ory.sh", + Audience: jwt.ClaimStrings{"client-id"}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }) + return (&oauth2.Token{}).WithExtra(map[string]interface{}{"id_token": raw}) + } + + t.Run("case=accepts the configured issuer", func(t *testing.T) { + claims, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, issuer), url.Values{}) + require.NoError(t, err) + assert.Equal(t, issuer, claims.Issuer) + assert.Equal(t, "acme@ory.sh", claims.Subject) + assert.Equal(t, "acme@ory.sh", claims.Email) + }) + + requireReasonContains := func(t *testing.T, err error, substr string) { + t.Helper() + he := new(herodot.DefaultError) + require.ErrorAs(t, err, &he) + assert.Contains(t, he.ReasonField, substr) + } + + t.Run("case=rejects an unknown issuer", func(t *testing.T) { + _, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{}) + requireReasonContains(t, err, "https://old.example.org/saml") + }) + + t.Run("case=accepts an additional trusted issuer", func(t *testing.T) { + t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml, https://other.example.org/saml") + + claims, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{}) + require.NoError(t, err) + assert.Equal(t, "https://old.example.org/saml", claims.Issuer) + + claims, err = newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://other.example.org/saml"), url.Values{}) + require.NoError(t, err) + assert.Equal(t, "https://other.example.org/saml", claims.Issuer) + + claims, err = newJackson(t, jwks).Claims(t.Context(), makeExchange(t, issuer), url.Values{}) + require.NoError(t, err) + assert.Equal(t, issuer, claims.Issuer) + }) + + t.Run("case=rejects an issuer outside the trusted list", func(t *testing.T) { + t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml") + + _, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://evil.example.org/saml"), url.Values{}) + requireReasonContains(t, err, "https://evil.example.org/saml") + }) + + t.Run("case=rejects a token signed by an unknown key even for a trusted issuer", func(t *testing.T) { + t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml") + + _, err := newJackson(t, otherJWKS).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{}) + requireReasonContains(t, err, "failed to verify signature") + }) + + t.Run("case=rejects a missing id token", func(t *testing.T) { + _, err := newJackson(t, jwks).Claims(t.Context(), &oauth2.Token{}, url.Values{}) + require.Error(t, err) + }) +} diff --git a/selfservice/strategy/oidc/provider_lark.go b/selfservice/strategy/oidc/provider_lark.go new file mode 100644 index 0000000..f8e62d5 --- /dev/null +++ b/selfservice/strategy/oidc/provider_lark.go @@ -0,0 +1,117 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" +) + +var _ OAuth2Provider = (*ProviderLark)(nil) + +type ProviderLark struct { + *ProviderGenericOIDC +} + +var ( + larkAuthEndpoint = oauth2.Endpoint{ + AuthURL: "https://passport.feishu.cn/suite/passport/oauth/authorize", + TokenURL: "https://passport.feishu.cn/suite/passport/oauth/token", + AuthStyle: oauth2.AuthStyleInParams, + } + larkUserEndpoint = "https://passport.feishu.cn/suite/passport/oauth/userinfo" +) + +func NewProviderLark( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderLark{ + &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (pl *ProviderLark) Config() *Configuration { + return pl.config +} + +func (pl *ProviderLark) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return &oauth2.Config{ + ClientID: pl.config.ClientID, + ClientSecret: pl.config.ClientSecret, + Endpoint: larkAuthEndpoint, + // Lark uses fixed scope that can not be configured in runtime + Scopes: pl.config.Scope, + RedirectURL: pl.config.Redir(pl.reg.Config().OIDCRedirectURIBase(ctx)), + }, nil +} + +func (pl *ProviderLark) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + // larkClaim is defined in the https://open.feishu.cn/document/common-capabilities/sso/api/get-user-info + type larkClaim struct { + Sub string `json:"sub"` + Name string `json:"name"` + Picture string `json:"picture"` + OpenID string `json:"open_id"` + UnionID string `json:"union_id"` + EnName string `json:"en_name"` + TenantKey string `json:"tenant_key"` + AvatarURL string `json:"avatar_url"` + AvatarThumb string `json:"avatar_thumb"` + AvatarMiddle string `json:"avatar_middle"` + AvatarBig string `json:"avatar_big"` + Email string `json:"email"` + UserID string `json:"user_id"` + Mobile string `json:"mobile"` + } + var ( + client = pl.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs()) + user larkClaim + ) + + req, err := retryablehttp.NewRequest("GET", larkUserEndpoint, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + exchange.SetAuthHeader(req.Request) + res, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = res.Body.Close() }() + + if err := logUpstreamError(pl.reg.Logger(), res); err != nil { + return nil, err + } + + if err := json.NewDecoder(res.Body).Decode(&user); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + return &Claims{ + Issuer: larkUserEndpoint, + Subject: user.OpenID, + Name: user.Name, + Nickname: user.Name, + Picture: user.AvatarURL, + Email: user.Email, + PhoneNumber: user.Mobile, + }, nil +} + +func (pl *ProviderLark) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} diff --git a/selfservice/strategy/oidc/provider_line_2_1.go b/selfservice/strategy/oidc/provider_line_2_1.go new file mode 100644 index 0000000..69018d7 --- /dev/null +++ b/selfservice/strategy/oidc/provider_line_2_1.go @@ -0,0 +1,39 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + + "golang.org/x/oauth2" +) + +type ProviderLineV21 struct { + *ProviderGenericOIDC +} + +func NewProviderLineV21( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = "https://access.line.me" + return &ProviderLineV21{ + &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderLineV21) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, err + } + + // Line login requires adding id_token_key_type=JWK when getting the token in order to issue an ES256 token. + // https://blog.miniasp.com/post/2022/04/08/LINE-Login-with-OpenID-Connect-in-ASPNET-Core (Chinese) + opts = append(opts, oauth2.SetAuthURLParam("id_token_key_type", "JWK")) + return o.Exchange(ctx, code, opts...) +} diff --git a/selfservice/strategy/oidc/provider_linkedin.go b/selfservice/strategy/oidc/provider_linkedin.go new file mode 100644 index 0000000..60dcaad --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin.go @@ -0,0 +1,200 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/ory/x/otelx" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/linkedin" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" +) + +type LinkedInProfile struct { + LocalizedLastName string `json:"localizedLastName"` + LocalizedFirstName string `json:"localizedFirstName"` + ProfilePicture *struct { + DisplayImage struct { + Elements []struct { + Identifiers []struct { + Identifier string `json:"identifier"` + } `json:"identifiers"` + } `json:"elements"` + } `json:"displayImage~"` + } `json:"profilePicture,omitempty"` + ID string `json:"id"` +} + +type LinkedInEmail struct { + Elements []struct { + Handle struct { + EmailAddress string `json:"emailAddress"` + } `json:"handle~"` + HandleUrn string `json:"handle"` + } `json:"elements"` +} + +type LinkedInIntrospection struct { + Active bool `json:"active"` + ClientID string `json:"client_id"` + AuthorizedAt uint32 `json:"authorized_at"` + CreatedAt uint32 `json:"created_at"` + ExpiresAt uint32 `json:"expires_at"` + Status string `json:"status"` + Scope string `json:"scope"` + AuthType string `json:"auth_type"` +} + +// type APIUrl string + +const ( + ProfileURL string = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))" + EmailURL string = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" + IntrospectionURL string = "https://www.linkedin.com/oauth/v2/introspectToken" +) + +var _ OAuth2Provider = (*ProviderLinkedIn)(nil) + +type ProviderLinkedIn struct { + config *Configuration + reg Dependencies +} + +func NewProviderLinkedIn( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderLinkedIn{ + config: config, + reg: reg, + } +} + +func (l *ProviderLinkedIn) Config() *Configuration { + return l.config +} + +func (l *ProviderLinkedIn) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: l.config.ClientID, + ClientSecret: l.config.ClientSecret, + Endpoint: linkedin.Endpoint, + Scopes: l.config.Scope, + RedirectURL: l.config.Redir(l.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (l *ProviderLinkedIn) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return l.oauth2(ctx), nil +} + +func (l *ProviderLinkedIn) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (l *ProviderLinkedIn) fetch(ctx context.Context, client *retryablehttp.Client, url string, result interface{}) (err error) { + ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch") + defer otelx.End(span, &err) + + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return errors.WithStack(err) + } + + res, err := client.Do(req) + if err != nil { + return errors.WithStack(err) + } + + defer func() { _ = res.Body.Close() }() + if err := logUpstreamError(l.reg.Logger(), res); err != nil { + return err + } + + if err := json.NewDecoder(res.Body).Decode(result); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (l *ProviderLinkedIn) Profile(ctx context.Context, client *retryablehttp.Client) (*LinkedInProfile, error) { + var result LinkedInProfile + if err := l.fetch(ctx, client, ProfileURL, &result); err != nil { + return nil, errors.WithStack(err) + } + + return &result, nil +} + +func (l *ProviderLinkedIn) Email(ctx context.Context, client *retryablehttp.Client) (*LinkedInEmail, error) { + var result LinkedInEmail + if err := l.fetch(ctx, client, EmailURL, &result); err != nil { + return nil, errors.WithStack(err) + } + + return &result, nil +} + +func (l *ProviderLinkedIn) ProfilePicture(profile *LinkedInProfile) string { + if profile.ProfilePicture == nil { + return "" + } + + elements := profile.ProfilePicture.DisplayImage.Elements + i := len(elements) + if i == 0 { + return "" + } else if i > 3 { + i = 3 + } + + identifiers := elements[i-1].Identifiers + if len(identifiers) == 0 { + return "" + } + + return identifiers[0].Identifier +} + +func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { + ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.Claims") + defer otelx.End(span, &err) + + o, err := l.OAuth2(ctx) + if err != nil { + return nil, err + } + + ctx, client := httpx.SetOAuth2(ctx, l.reg.HTTPClient(ctx), o, exchange) + profile, err := l.Profile(ctx, client) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + email, err := l.Email(ctx, client) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims := &Claims{ + Subject: profile.ID, + Issuer: "https://login.linkedin.com/", + Email: email.Elements[0].Handle.EmailAddress, + GivenName: profile.LocalizedFirstName, + LastName: profile.LocalizedLastName, + Picture: l.ProfilePicture(profile), + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_linkedin_test.go b/selfservice/strategy/oidc/provider_linkedin_test.go new file mode 100644 index 0000000..c1f2497 --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_test.go @@ -0,0 +1,209 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/jarcoal/httpmock" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderLinkedin_Claims(t *testing.T) { + _, base := pkg.NewFastRegistryWithMocks(t) + reg := &mockRegistry{base, retryablehttp.NewClient()} + httpmock.ActivateNonDefault(reg.cl.HTTPClient) + t.Cleanup(httpmock.DeactivateAndReset) + + httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "localizedLastName": "Doe", + "profilePicture": map[string]interface{}{ + "displayImage": "urn:li:digitalmediaAsset:D5F04AQG1h-anJtsLeA", + "displayImage~": map[string]interface{}{ + "elements": []map[string]interface{}{ + { + "identifiers": []map[string]interface{}{ + { + "identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_100_100/0/1661502148285?e=1674086400&v=beta&t=5zdsvAqGS1idrfNAai-zwp01K4hApGeJyRV8zBTjZck", + }, + }, + }, + { + "identifiers": []map[string]interface{}{ + { + "identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_200_200/0/1661502148285?e=1674086400&v=beta&t=fbHprGci4vEpeQdzRTyt9YJjNXTgv-14h9ROr_DE5R4", + }, + }, + }, + { + "identifiers": []map[string]interface{}{ + { + "identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_400_400/0/1661502148285?e=1674086400&v=beta&t=SMgXuiZ5DiiPf4iMrTPQWoCnFRh4Y-DfoWjopUZ0qCo", + }, + }, + }, + { + "identifiers": []map[string]interface{}{ + { + "identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_800_800/0/1661502148285?e=1674086400&v=beta&t=QzC9b32DmLRIwU7P27LfJUqglXsJm056WkA_TTOHxHU", + }, + }, + }, + }, + }, + }, + "localizedFirstName": "John", + "id": "5foOWOiYXD", + }) + return resp, err + }) + + httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "elements": []map[string]interface{}{ + { + "handle~": map[string]interface{}{ + "emailAddress": "john.doe@gmail.com", + }, + "handle": "urn:li:emailAddress:-1810278278", + }, + }, + }) + return resp, err + }, + ) + + httpmock.RegisterResponder("POST", "https://www.linkedin.com/oauth/v2/introspectToken", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "active": true, + "client_id": "78e533g1dq0rs5", + "authorized_at": 1668518671, + "created_at": 1668518671, + "status": "active", + "expires_at": 2673702676, + "scope": "r_emailaddress,r_liteprofile", + "auth_type": "3L", + }) + return resp, err + }, + ) + + c := &oidc.Configuration{ + ID: "linkedin", + Provider: "linkedin", + ClientID: "abcd", + ClientSecret: "secret", + Mapper: "file://./stub/oidc.linkedin.jsonnet", + Scope: []string{"r_emailaddress", "r_liteprofile"}, + } + linkedin := oidc.NewProviderLinkedIn(c, reg) + + const fakeLinkedinIDToken = "id_token_mock_" // #nosec G101 test code + 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{}, + ) + require.NoError(t, err) + + assert.Equal(t, &oidc.Claims{ + Issuer: "https://login.linkedin.com/", + Subject: "5foOWOiYXD", + GivenName: "John", + LastName: "Doe", + Email: "john.doe@gmail.com", + Picture: "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_400_400/0/1661502148285?e=1674086400&v=beta&t=SMgXuiZ5DiiPf4iMrTPQWoCnFRh4Y-DfoWjopUZ0qCo", + }, actual) +} + +func TestProviderLinkedin_No_Picture(t *testing.T) { + _, base := pkg.NewFastRegistryWithMocks(t) + reg := &mockRegistry{base, retryablehttp.NewClient()} + httpmock.ActivateNonDefault(reg.cl.HTTPClient) + t.Cleanup(httpmock.DeactivateAndReset) + + httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "localizedLastName": "Doe", + "localizedFirstName": "John", + "id": "5foOWOiYXD", + }) + return resp, err + }) + + httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "elements": []map[string]interface{}{ + { + "handle~": map[string]interface{}{ + "emailAddress": "john.doe@gmail.com", + }, + "handle": "urn:li:emailAddress:-1810278278", + }, + }, + }) + return resp, err + }, + ) + + httpmock.RegisterResponder("POST", "https://www.linkedin.com/oauth/v2/introspectToken", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "active": true, + "client_id": "78e533g1dq0rs5", + "authorized_at": 1668518671, + "created_at": 1668518671, + "status": "active", + "expires_at": 2673702676, + "scope": "r_emailaddress,r_liteprofile", + "auth_type": "3L", + }) + return resp, err + }, + ) + + c := &oidc.Configuration{ + ID: "linkedin", + Provider: "linkedin", + ClientID: "abcd", + ClientSecret: "secret", + Mapper: "file://./stub/oidc.linkedin.jsonnet", + Scope: []string{"r_emailaddress", "r_liteprofile"}, + } + linkedin := oidc.NewProviderLinkedIn(c, reg) + + const fakeLinkedinIDToken = "id_token_mock_" // #nosec G101 test code + 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{}, + ) + require.NoError(t, err) + + assert.Equal(t, &oidc.Claims{ + Issuer: "https://login.linkedin.com/", + Subject: "5foOWOiYXD", + GivenName: "John", + LastName: "Doe", + Email: "john.doe@gmail.com", + Picture: "", + }, actual) +} diff --git a/selfservice/strategy/oidc/provider_linkedin_v2.go b/selfservice/strategy/oidc/provider_linkedin_v2.go new file mode 100644 index 0000000..a71d801 --- /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 0000000..4643d98 --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_v2_test.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderLinkedInV2_Discovery(t *testing.T) { + _, reg := pkg.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(t.Context()) + 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_microsoft.go b/selfservice/strategy/oidc/provider_microsoft.go new file mode 100644 index 0000000..5a7d039 --- /dev/null +++ b/selfservice/strategy/oidc/provider_microsoft.go @@ -0,0 +1,132 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + "strings" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v4" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" +) + +var _ OAuth2Provider = (*ProviderMicrosoft)(nil) + +type ProviderMicrosoft struct { + *ProviderGenericOIDC +} + +func NewProviderMicrosoft( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderMicrosoft{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (m *ProviderMicrosoft) OAuth2(ctx context.Context) (*oauth2.Config, error) { + if strings.TrimSpace(m.config.Tenant) == "" { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("No Tenant specified for the `microsoft` oidc provider %s", m.config.ID)) + } + + endpointPrefix := "https://login.microsoftonline.com/" + m.config.Tenant + endpoint := oauth2.Endpoint{ + AuthURL: endpointPrefix + "/oauth2/v2.0/authorize", + TokenURL: endpointPrefix + "/oauth2/v2.0/token", + } + + return m.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { + raw, ok := exchange.Extra("id_token").(string) + if !ok || len(raw) == 0 { + return nil, errors.WithStack(ErrIDTokenMissing()) + } + + parser := new(jwt.Parser) + unverifiedClaims := microsoftUnverifiedClaims{} + if _, _, err := parser.ParseUnverified(raw, &unverifiedClaims); err != nil { + return nil, err + } + + if _, err := uuid.FromString(unverifiedClaims.TenantID); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("TenantID claim is not a valid UUID: %s", err)) + } + + issuer := "https://login.microsoftonline.com/" + unverifiedClaims.TenantID + "/v2.0" + ctx = context.WithValue(ctx, oauth2.HTTPClient, m.reg.HTTPClient(ctx).HTTPClient) + p, err := gooidc.NewProvider(ctx, issuer) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to initialize OpenID Connect Provider: %s", err)) + } + + claims, err := m.verifyAndDecodeClaimsWithProvider(ctx, p, raw) + if err != nil { + return nil, err + } + + return m.updateSubject(ctx, claims, exchange) +} + +func (m *ProviderMicrosoft) updateSubject(ctx context.Context, claims *Claims, exchange *oauth2.Token) (*Claims, error) { + if m.config.SubjectSource == "me" { + o, err := m.OAuth2(ctx) + if err != nil { + return nil, err + } + + ctx, client := httpx.SetOAuth2(ctx, m.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://graph.microsoft.com/v1.0/me", nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to fetch from `https://graph.microsoft.com/v1.0/me`: %s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(m.reg.Logger(), resp); err != nil { + return nil, err + } + + var user struct { + ID string `json:"id"` + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to decode JSON from `https://graph.microsoft.com/v1.0/me`: %s", err)) + } + + claims.Subject = user.ID + } + + if m.config.SubjectSource == "oid" { + claims.Subject = claims.Object + } + + return claims, nil +} + +type microsoftUnverifiedClaims struct { + TenantID string `json:"tid,omitempty"` +} + +func (c *microsoftUnverifiedClaims) Valid() error { + return nil +} diff --git a/selfservice/strategy/oidc/provider_netid.go b/selfservice/strategy/oidc/provider_netid.go new file mode 100644 index 0000000..eab9394 --- /dev/null +++ b/selfservice/strategy/oidc/provider_netid.go @@ -0,0 +1,174 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "slices" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" + "github.com/ory/x/urlx" +) + +const ( + defaultBrokerScheme = "https" + defaultBrokerHost = "broker.netid.de" +) + +var _ OAuth2Provider = (*ProviderNetID)(nil) + +type ProviderNetID struct { + *ProviderGenericOIDC +} + +func NewProviderNetID( + config *Configuration, + reg Dependencies, +) Provider { + config.IssuerURL = fmt.Sprintf("%s://%s/", defaultBrokerScheme, defaultBrokerHost) + if !slices.Contains(config.Scope, oidc.ScopeOpenID) { + config.Scope = append(config.Scope, oidc.ScopeOpenID) + } + + return &ProviderNetID{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (n *ProviderNetID) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return n.oAuth2(ctx) +} + +func (n *ProviderNetID) oAuth2(ctx context.Context) (*oauth2.Config, error) { + u := n.brokerURL() + + authURL := urlx.AppendPaths(u, "/authorize") + tokenURL := urlx.AppendPaths(u, "/token") + + return &oauth2.Config{ + ClientID: n.config.ClientID, + ClientSecret: n.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL.String(), + TokenURL: tokenURL.String(), + }, + Scopes: n.config.Scope, + RedirectURL: n.config.Redir(n.reg.Config().OIDCRedirectURIBase(ctx)), + }, nil +} + +func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { + o, err := n.OAuth2(ctx) + if err != nil { + return nil, err + } + + ctx, client := httpx.SetOAuth2(ctx, n.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", urlx.AppendPaths(n.brokerURL(), "/userinfo").String(), nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(n.reg.Logger(), resp); err != nil { + return nil, err + } + + p, err := n.provider(ctx) + if err != nil { + return nil, err + } + + raw, ok := exchange.Extra("id_token").(string) + if !ok || len(raw) == 0 { + return nil, errors.WithStack(ErrIDTokenMissing()) + } + + claims, err := n.verifyAndDecodeClaimsWithProvider(ctx, p, raw) + if err != nil { + return nil, err + } + + var userinfo Claims + if err := json.NewDecoder(resp.Body).Decode(&userinfo); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + userinfo.Issuer = claims.Issuer + userinfo.Subject = claims.Subject + return &userinfo, nil +} + +func (n *ProviderNetID) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { + provider, err := n.provider(ctx) + if err != nil { + return nil, err + } + + req, err := retryablehttp.NewRequestWithContext(ctx, "POST", urlx.AppendPaths(n.brokerURL(), "/token").String(), strings.NewReader(url.Values{ + "grant_type": {"netid_fedcm"}, + "fedcm_token": {rawIDToken}, + }.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", n.config.NetIDTokenOriginHeader) + res, err := n.reg.HTTPClient(ctx).Do(req) + if err != nil { + return nil, err + } + + token := struct { + IDToken string `json:"id_token"` + }{} + + if err := json.NewDecoder(res.Body).Decode(&token); err != nil { + return nil, err + } + + idToken, err := provider.VerifierContext( + n.withHTTPClientContext(ctx), + &oidc.Config{ClientID: n.config.ClientID}, + ).Verify(ctx, token.IDToken) + if err != nil { + return nil, err + } + + var ( + claims Claims + rawClaims map[string]any + ) + + if err = idToken.Claims(&claims); err != nil { + return nil, err + } + if err = idToken.Claims(&rawClaims); err != nil { + return nil, err + } + claims.RawClaims = rawClaims + + return &claims, nil +} + +func (n *ProviderNetID) brokerURL() *url.URL { + return &url.URL{Scheme: defaultBrokerScheme, Host: defaultBrokerHost} +} diff --git a/selfservice/strategy/oidc/provider_netid_test.go b/selfservice/strategy/oidc/provider_netid_test.go new file mode 100644 index 0000000..2909fad --- /dev/null +++ b/selfservice/strategy/oidc/provider_netid_test.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestNetidProvider(t *testing.T) { + t.Skip("can't test this automatically, because the token is only valid for a short time") + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderNetID(&oidc.Configuration{ + ClientID: "9b56b26a-e93d-4fce-8f16-951a9858f23e", + }, reg) + + rawToken := `...` + + claims, err := p.(oidc.IDTokenVerifier).Verify(context.Background(), rawToken) + require.NoError(t, err) + require.NotNil(t, claims) +} diff --git a/selfservice/strategy/oidc/provider_patreon.go b/selfservice/strategy/oidc/provider_patreon.go new file mode 100644 index 0000000..8f4242f --- /dev/null +++ b/selfservice/strategy/oidc/provider_patreon.go @@ -0,0 +1,124 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderPatreon)(nil) + +type ProviderPatreon struct { + config *Configuration + reg Dependencies +} + +type PatreonIdentityResponse struct { + Data struct { + Attributes struct { + Email string `json:"email"` + FirstName string `json:"first_name"` + FullName string `json:"full_name"` + ImageURL string `json:"image_url"` + LastName string `json:"last_name"` + } `json:"attributes"` + ID string `json:"id"` + Type string `json:"type"` + } `json:"data"` +} + +func NewProviderPatreon( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderPatreon{ + config: config, + reg: reg, + } +} + +func (d *ProviderPatreon) Config() *Configuration { + return d.config +} + +func (d *ProviderPatreon) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: d.config.ClientID, + ClientSecret: d.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://www.patreon.com/oauth2/authorize", + TokenURL: "https://www.patreon.com/api/oauth2/token", + }, + RedirectURL: d.config.Redir(d.reg.Config().OIDCRedirectURIBase(ctx)), + Scopes: d.config.Scope, + } +} + +func (d *ProviderPatreon) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return d.oauth2(ctx), nil +} + +func (d *ProviderPatreon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + if isForced(r) { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("prompt", "consent"), + } + } + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("prompt", "none"), + } +} + +func (d *ProviderPatreon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + identityURL := "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=first_name,last_name,url,full_name,email,image_url" + + o := d.oauth2(ctx) + ctx, client := httpx.SetOAuth2(ctx, d.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", identityURL, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err)) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+exchange.AccessToken) + + res, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = res.Body.Close() }() + + if err := logUpstreamError(d.reg.Logger(), res); err != nil { + return nil, err + } + + data := PatreonIdentityResponse{} + jsonErr := json.NewDecoder(res.Body).Decode(&data) + if jsonErr != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(jsonErr).WithReasonf("%s", jsonErr)) + } + + claims := &Claims{ + Issuer: "https://www.patreon.com/", + Subject: data.Data.ID, + Name: data.Data.Attributes.FullName, + Email: data.Data.Attributes.Email, + GivenName: data.Data.Attributes.FirstName, + FamilyName: data.Data.Attributes.LastName, + LastName: data.Data.Attributes.LastName, + Picture: data.Data.Attributes.ImageURL, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go new file mode 100644 index 0000000..11c2340 --- /dev/null +++ b/selfservice/strategy/oidc/provider_private_net_test.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/x/configx" +) + +const ( + wellknownJWKs = "https://raw.githubusercontent.com/aeneasr/private-oidc/master/jwks" + fakeJWTJWKS = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTksImF1ZCI6ImFiY2QiLCJpc3MiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYWVuZWFzci9wcml2YXRlLW9pZGMvbWFzdGVyL2p3a3MifQ.RLR3dSRGGIjbRqyOMGMYFGTzcVHi7hPuFs_IKYywVWJ_XMyzWozTW4M8uuvBUPiVoNDNs7osm-AkRl7cBfw0by1XEcnEKZStCjdEh7Q0IGGb4hgq8rRqm1d3uJwNIGU5h7-s7tMnDED2ZTZhp304U99YWz7Ozl_TA9tqolBLLZEmIfXSY_RR3rMoDwtHZvWhI0OZtPdcBh86vWS9zG6QPHM5qGtRMMIs-ljXrrgS8LulUI5CAVEeHlQLXroBIe9v89IkKi07A7YRrk1SxFxlojcZ2v0z-0iTI3WL8mUoocF-RYy1RgJTK_dPYkSJebaN0R5MmBax5MXLKy4baNHKsg" +) + +func TestProviderPrivateIP(t *testing.T) { + t.Parallel() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("the request should not have reached the test server") + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(ts.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + + generic := func(c *oidc.Configuration) oidc.Provider { + return oidc.NewProviderGenericOIDC(c, reg) + } + auth0 := func(c *oidc.Configuration) oidc.Provider { + return oidc.NewProviderAuth0(c, reg) + } + gitlab := func(c *oidc.Configuration) oidc.Provider { + return oidc.NewProviderGitLab(c, reg) + } + + // We do not test the Auth URL as the Auth URL is not vulnerable to SSRF attacks. + // The AuthURL is only given to the user's browser, thus it is not possible to cause SSRF. + // We only care about Token URLs and Issuer URLs. + for k, tc := range []struct { + c *oidc.Configuration + e string + p func(c *oidc.Configuration) oidc.Provider + id string + }{ + // Apple uses a fixed token URL and does not use the issuer. + + {p: auth0, c: &oidc.Configuration{IssuerURL: ts.URL}, e: "no route to host"}, + // The TokenURL is fixed in Auth0 to {issuer_url}/token. Since the issuer is called first, any local token fails also. + + // If the issuer URL is local, we fail + {p: generic, c: &oidc.Configuration{IssuerURL: ts.URL}, e: "no route to host", id: fakeJWTJWKS}, + {p: generic, c: &oidc.Configuration{IssuerURL: ts.URL, ClaimsSource: "userinfo"}, e: "no route to host", id: fakeJWTJWKS}, + {p: generic, c: &oidc.Configuration{IssuerURL: ts.URL, ClaimsSource: "invalid"}, e: "Unknown claims source: \"invalid\"", id: fakeJWTJWKS}, + + // If the issuer URL has a local JWKs URL, we fail + {p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownJWKs}, e: "no route to host", id: fakeJWTJWKS}, + {p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownJWKs, ClaimsSource: "userinfo"}, e: "no route to host", id: fakeJWTJWKS}, + + // The next call does not fail because the provider uses only the ID JSON Web Token to verify this call and does + // not use the TokenURL at all! + // {p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownToken, TokenURL: "http://127.0.0.3/"}, e: "127.0.0.3 is not a public IP address", id: fakeJWTToken}, + + // Discord uses a fixed token URL and does not use the issuer. + // Facebook uses a fixed token URL and does not use the issuer. + // 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: ts.URL}, e: "no route to host"}, + // The TokenURL is fixed in GitLab to {issuer_url}/token. Since the issuer is called first, any local token fails also. + + // Google uses a fixed token URL and does not use the issuer. + // Microsoft uses a fixed token URL and does not use the issuer. + // Slack uses a fixed token URL and does not use the issuer. + // Spotify uses a fixed token URL and does not use the issuer. + // 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. + // Line v2.1 uses a fixed token URL and does not use the issuer. + // UAE PASS uses fixed token URL and userinfo URL 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.(oidc.OAuth2Provider).Claims(t.Context(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{ + "id_token": tc.id, + }), url.Values{}) + require.Error(t, err) + assert.Contains(t, fmt.Sprintf("%+v", err), tc.e) + }) + } +} diff --git a/selfservice/strategy/oidc/provider_salesforce.go b/selfservice/strategy/oidc/provider_salesforce.go new file mode 100644 index 0000000..79da355 --- /dev/null +++ b/selfservice/strategy/oidc/provider_salesforce.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "cmp" + "context" + "encoding/json" + "io" + "net/url" + "path" + "time" + + "github.com/ory/x/httpx" + + "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" +) + +var _ OAuth2Provider = (*ProviderSalesforce)(nil) + +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().WithWrap(err).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, err + } + + u, err := url.Parse(g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithWrap(err).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().WithWrap(err).WithReasonf("%s", err)) + } + + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = 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.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + b, err = salesforceUpdatedAtWorkaround(b) + if err != nil { + return nil, 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().WithWrap(err).WithReasonf("%s", err)) + } + + claims.Issuer = cmp.Or(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().WithWrap(err).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 0000000..81638fb --- /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_slack.go b/selfservice/strategy/oidc/provider_slack.go new file mode 100644 index 0000000..685592e --- /dev/null +++ b/selfservice/strategy/oidc/provider_slack.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "net/url" + "slices" + + "github.com/ory/herodot" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/x/stringsx" + + "github.com/slack-go/slack" +) + +var _ OAuth2Provider = (*ProviderSlack)(nil) + +type ProviderSlack struct { + config *Configuration + reg Dependencies +} + +func NewProviderSlack( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderSlack{ + config: config, + reg: reg, + } +} + +func (d *ProviderSlack) Config() *Configuration { + return d.config +} + +func (d *ProviderSlack) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: d.config.ClientID, + ClientSecret: d.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + // slack's oauth v2 does not implement the oauth2 standard so we use the old version. + // to use v2 we would need to rename the request 'scope' field to 'user_scope'. + AuthURL: "https://slack.com/oauth/authorize", + TokenURL: slack.APIURL + "oauth.access", + }, + RedirectURL: d.config.Redir(d.reg.Config().OIDCRedirectURIBase(ctx)), + Scopes: d.config.Scope, + } +} + +func (d *ProviderSlack) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return d.oauth2(ctx), nil +} + +func (d *ProviderSlack) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (d *ProviderSlack) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",") + for _, check := range d.Config().Scope { + if !slices.Contains(grantedScopes, check) { + return nil, errors.WithStack(ErrScopeMissing()) + } + } + + api := slack.New(exchange.AccessToken) + identity, err := api.GetUserIdentity() + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + claims := &Claims{ + Issuer: "https://slack.com/oauth/", + Subject: identity.User.ID, + Name: identity.User.Name, + PreferredUsername: identity.User.Name, + Nickname: identity.User.Name, + Email: identity.User.Email, + EmailVerified: true, + Picture: identity.User.Image512, + Team: identity.Team.ID, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go new file mode 100644 index 0000000..0909abd --- /dev/null +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -0,0 +1,103 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "net/url" + "slices" + + "golang.org/x/oauth2/spotify" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/x/httpx" + + "github.com/ory/x/stringsx" + + spotifyapi "github.com/zmb3/spotify/v2" + spotifyauth "github.com/zmb3/spotify/v2/auth" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderSpotify)(nil) + +type ProviderSpotify struct { + config *Configuration + reg Dependencies +} + +func NewProviderSpotify( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderSpotify{ + config: config, + reg: reg, + } +} + +func (g *ProviderSpotify) Config() *Configuration { + return g.config +} + +func (g *ProviderSpotify) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: spotify.Endpoint, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderSpotify) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderSpotify) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") + for _, check := range g.Config().Scope { + if !slices.Contains(grantedScopes, check) { + return nil, errors.WithStack(ErrScopeMissing()) + } + } + + auth := spotifyauth.New( + spotifyauth.WithRedirectURL(g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx))), + spotifyauth.WithScopes(spotifyauth.ScopeUserReadPrivate)) + + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), auth, exchange) + spotifyClient := spotifyapi.New(client.HTTPClient) + + user, err := spotifyClient.CurrentUser(ctx) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + var userPicture string + if len(user.Images) > 0 { + userPicture = user.Images[0].URL + } + + claims := &Claims{ + Subject: user.ID, + Issuer: spotify.Endpoint.TokenURL, + Name: user.DisplayName, + Nickname: user.DisplayName, + Email: user.Email, + Picture: userPicture, + Profile: user.ExternalURLs["spotify"], + Birthdate: user.Birthdate, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go new file mode 100644 index 0000000..5345c91 --- /dev/null +++ b/selfservice/strategy/oidc/provider_test.go @@ -0,0 +1,176 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" +) + +func TestClaimsValidate(t *testing.T) { + require.Error(t, new(Claims).Validate()) + require.Error(t, (&Claims{Issuer: "not-empty"}).Validate()) + require.Error(t, (&Claims{Issuer: "not-empty"}).Validate()) + require.Error(t, (&Claims{Subject: "not-empty"}).Validate()) + require.Error(t, (&Claims{Subject: "not-empty"}).Validate()) + require.NoError(t, (&Claims{Issuer: "not-empty", Subject: "not-empty"}).Validate()) +} + +type TestProvider struct { + *ProviderGenericOIDC +} + +func NewTestProvider(c *Configuration, reg Dependencies) Provider { + return &TestProvider{ + ProviderGenericOIDC: NewProviderGenericOIDC(c, reg).(*ProviderGenericOIDC), + } +} + +func RegisterTestProvider(t *testing.T, id string) { + supportedProviders[id] = func(c *Configuration, reg Dependencies) Provider { + return NewTestProvider(c, reg) + } + t.Cleanup(func() { + delete(supportedProviders, id) + }) +} + +var _ IDTokenVerifier = new(TestProvider) + +func (t *TestProvider) Verify(_ context.Context, token string) (*Claims, error) { + if token == "error" { + return nil, fmt.Errorf("stub error") + } + c := Claims{} + if err := json.Unmarshal([]byte(token), &c); err != nil { + return nil, err + } + return &c, nil +} + +// SetUserInfoURL allows overriding the userinfo URL for testing purposes. +func (p *ProviderUAEPass) SetUserInfoURL(t *testing.T, url string) { + t.Helper() + p.userinfoURL = url +} + +// upstreamParam reports whether the option list produced by +// UpstreamParameters contains a given upstream parameter, and its value. +// The options are applied through a real oauth2.Config.AuthCodeURL so the +// test observes exactly what is sent to the upstream provider. +func upstreamParam(t *testing.T, opts []oauth2.AuthCodeOption, key string) (string, bool) { + t.Helper() + conf := &oauth2.Config{ + ClientID: "client", + Endpoint: oauth2.Endpoint{AuthURL: "https://idp.example.com/auth"}, + } + authURL, err := url.Parse(conf.AuthCodeURL("state", opts...)) + require.NoError(t, err) + q := authURL.Query() + if !q.Has(key) { + return "", false + } + return q.Get(key), true +} + +func TestUpstreamParameters(t *testing.T) { + t.Parallel() + + upstream := map[string]string{ + "login_hint": "victim@example.com", + "hd": "example.com", + "prompt": "select_account", + "auth_type": "reauthenticate", + "acr_values": "urn:example:acr", + "unknown": "dropped", + } + + opts := UpstreamParameters(upstream) + + t.Run("case=login_hint is never forwarded", func(t *testing.T) { + // A caller-supplied login_hint cannot be trusted and can steer + // account/tenant/client selection on some IdPs, enabling a client-swap + // attack (HackerOne #3239672, ory-corp/cloud#8955). + _, ok := upstreamParam(t, opts, "login_hint") + assert.False(t, ok, "login_hint must not be forwarded") + }) + + t.Run("case=other allowed parameters are forwarded", func(t *testing.T) { + for key, want := range map[string]string{ + "hd": "example.com", + "prompt": "select_account", + "auth_type": "reauthenticate", + "acr_values": "urn:example:acr", + } { + got, ok := upstreamParam(t, opts, key) + assert.True(t, ok, "%s must be forwarded", key) + assert.Equal(t, want, got) + } + }) + + t.Run("case=unknown parameters are dropped", func(t *testing.T) { + _, ok := upstreamParam(t, opts, "unknown") + assert.False(t, ok, "unknown parameters must not be forwarded") + }) +} + +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_test_fedcm.go b/selfservice/strategy/oidc/provider_test_fedcm.go new file mode 100644 index 0000000..518b8fa --- /dev/null +++ b/selfservice/strategy/oidc/provider_test_fedcm.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + + "github.com/golang-jwt/jwt/v5" +) + +// ProviderTestFedcm is a mock provider to test FedCM. +type ProviderTestFedcm struct { + *ProviderGenericOIDC +} + +var _ OAuth2Provider = (*ProviderTestFedcm)(nil) + +func NewProviderTestFedcm( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderTestFedcm{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderTestFedcm) Verify(_ context.Context, rawIDToken string) (claims *Claims, err error) { + rawClaims := &struct { + Claims + jwt.MapClaims + }{} + _, err = jwt.ParseWithClaims(rawIDToken, rawClaims, func(token *jwt.Token) (interface{}, error) { + return []byte(`xxxxxxx`), nil + }, jwt.WithoutClaimsValidation()) + if err != nil { + return nil, err + } + rawClaims.Issuer = "https://example.com/fedcm" + + if err = rawClaims.Validate(); err != nil { + return nil, err + } + + return &rawClaims.Claims, nil +} diff --git a/selfservice/strategy/oidc/provider_test_fedcm_test.go b/selfservice/strategy/oidc/provider_test_fedcm_test.go new file mode 100644 index 0000000..5f0ce3a --- /dev/null +++ b/selfservice/strategy/oidc/provider_test_fedcm_test.go @@ -0,0 +1,26 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestFedcmTestProvider(t *testing.T) { + _, reg := pkg.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderTestFedcm(&oidc.Configuration{}, reg) + + rawToken := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NWVlMjgxNC02ZTQ4LTRmZTktYWIzNS1mM2QxYzczM2I3ZTciLCJub25jZSI6ImVkOWM0ZDcyMDZkMDc1YTg4NjY0ZmE3YjMwY2Q5ZGE2NGU4ZTkwMjY5MGJhZmI2YjNmMmY2OWU5YzU1ZGUyNTcwOTFlYTk3ZTFiZTFiYjdiNDZmMjJjYzY0ZSIsImV4cCI6MTczNzU1ODM4MTk3MSwiaWF0IjoxNzM3NDcxOTgxOTcxLCJlbWFpbCI6InhweGN3dnU1YjRuemZvdGZAZXhhbXBsZS5jb20iLCJuYW1lIjoiVXNlciBOYW1lIiwicGljdHVyZSI6Imh0dHBzOi8vYXBpLmRpY2ViZWFyLmNvbS83LngvYm90dHRzL3BuZz9zZWVkPSUyNDJiJTI0MTAlMjR5WEs3eWozNEg4SkhCNm8zOG1sc2xlYzl1WkozZ2F2UGlDaFdaeFFIbnk3VkFKRlouS3RGZSJ9.GnSP_x8J_yS5wrTwtB6B-BydYYljrpVjQjS2vZ5D8Hg` // #nosec G101 -- test code + + claims, err := p.(oidc.IDTokenVerifier).Verify(context.Background(), rawToken) + require.NoError(t, err) + require.NotNil(t, claims) +} diff --git a/selfservice/strategy/oidc/provider_uaepass.go b/selfservice/strategy/oidc/provider_uaepass.go new file mode 100644 index 0000000..12f7e2a --- /dev/null +++ b/selfservice/strategy/oidc/provider_uaepass.go @@ -0,0 +1,182 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" +) + +var _ OAuth2Provider = (*ProviderUAEPass)(nil) + +// UAE PASS scopes for different user types +// See: https://docs.uaepass.ae/feature-guides/authentication/web-application/endpoints +// +// For UAE Citizens/Residents: +// - urn:uae:digitalid:profile:general - General profile information +// - urn:uae:digitalid:profile:general:profileType - Profile type (SOP1/SOP2/SOP3) +// - urn:uae:digitalid:profile:general:unifiedId - Unified ID +// +// For Visitors: +// - urn:uae:digitalid:profile - Basic profile +// +// Common scopes: +// - openid - Required for OIDC flows (returns id_token) + +// ProviderUAEPass implements the OAuth2Provider interface for UAE PASS. +// UAE PASS is the UAE's official digital identity platform that allows +// citizens, residents, and visitors to authenticate with government and +// private sector services. +type ProviderUAEPass struct { + *ProviderGenericOIDC + baseURL string // Base URL for UAE PASS (e.g., https://id.uaepass.ae or https://stg-id.uaepass.ae) + userinfoURL string // Can be overridden in tests +} + +// NewProviderUAEPass creates a new UAE PASS provider. +// The IssuerURL configuration determines which UAE PASS environment to use: +// - Production: https://id.uaepass.ae (default if not set) +// - Staging: https://stg-id.uaepass.ae +func NewProviderUAEPass( + config *Configuration, + reg Dependencies, +) Provider { + // Use IssuerURL if provided, otherwise default to production + baseURL := config.IssuerURL + if baseURL == "" { + baseURL = "https://id.uaepass.ae" + } + + // Normalize: remove trailing slash if present + baseURL = strings.TrimSuffix(baseURL, "/") + + // UAE PASS doesn't support PKCE discovery, so treat "auto" (or unset) as "force" + // since UAE PASS does support PKCE. + if config.PKCE == "" || config.PKCE == "auto" { + config.PKCE = "force" + } + + return &ProviderUAEPass{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + baseURL: baseURL, + userinfoURL: baseURL + "/idshub/userinfo", + } +} + +// oauth2 returns the OAuth2 configuration for UAE PASS. +func (p *ProviderUAEPass) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: p.config.ClientID, + ClientSecret: p.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: p.baseURL + "/idshub/authorize", + TokenURL: p.baseURL + "/idshub/token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + Scopes: p.config.Scope, + RedirectURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +// OAuth2 returns the OAuth2 configuration for UAE PASS. +func (p *ProviderUAEPass) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return p.oauth2(ctx), nil +} + +// UAE PASS acr_values for different authentication levels +// UAEPassACRWeb is the default authentication level for web-based authentication +const UAEPassACRWeb = "urn:safelayer:tws:policies:authentication:level:low" //nolint:gosec // not a credential, this is an ACR URN + +// AuthCodeURLOptions returns OAuth2 authorization URL options. +// UAE PASS requires acr_values parameter for authentication. +// The default is web-based authentication. To use mobile app-to-app +// authentication, pass acr_values via upstream_parameters. +func (p *ProviderUAEPass) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("acr_values", UAEPassACRWeb), + } +} + +// Claims fetches user claims from the UAE PASS userinfo endpoint. +// UAE PASS requires the access token to be passed as a Bearer token in the Authorization header. +func (p *ProviderUAEPass) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { + ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderUAEPass.Claims") + defer otelx.End(span, &err) + + ctx, client := httpx.SetOAuth2(ctx, p.reg.HTTPClient(ctx), p.oauth2(ctx), exchange) + + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, p.userinfoURL, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError(). + WithReason("failed to create HTTP request"). + WithDetail("url", p.userinfoURL). + WithError(err.Error())) + } + + // UAE PASS expects the access token as a Bearer token in the Authorization header + req.Header.Set("Authorization", "Bearer "+exchange.AccessToken) + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError(). + WithReason("failed to make HTTP request to UAE PASS userinfo endpoint"). + WithDetail("url", p.userinfoURL). + WithError(err.Error())) + } + defer func() { _ = resp.Body.Close() }() + + body := io.LimitReader(resp.Body, 64*1024) // 64 KiB limit + + if resp.StatusCode != http.StatusOK { + rawResponse, _ := io.ReadAll(body) + return nil, errors.WithStack(herodot.ErrUpstreamError(). + WithReason("UAE PASS userinfo endpoint returned non-200 status"). + WithDetail("url", p.userinfoURL). + WithDetail("external_error", string(rawResponse)). + WithDetail("external_status_code", resp.StatusCode)) + } + + var rawClaims map[string]interface{} + if err := json.NewDecoder(body).Decode(&rawClaims); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError(). + WithReason("failed to decode UAE PASS userinfo response"). + WithDetail("url", p.userinfoURL). + WithError(err.Error())) + } + + str := func(key string) string { + if v, ok := rawClaims[key].(string); ok { + return v + } + return "" + } + + // Map UAE PASS profile to standard OIDC claims + claims := &Claims{ + Subject: str("sub"), + Issuer: p.baseURL, + Email: str("email"), + Name: str("fullnameEN"), + GivenName: str("firstnameEN"), + LastName: str("lastnameEN"), + RawClaims: rawClaims, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_uaepass_test.go b/selfservice/strategy/oidc/provider_uaepass_test.go new file mode 100644 index 0000000..6454337 --- /dev/null +++ b/selfservice/strategy/oidc/provider_uaepass_test.go @@ -0,0 +1,297 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestUAEPassOIDCClaims(t *testing.T) { + t.Parallel() + + handler := http.NewServeMux() + expectedAccessToken := "my-access-token" + + handler.HandleFunc("GET /userinfo", func(w http.ResponseWriter, r *http.Request) { + // Verify Bearer token authentication + authHeader := r.Header.Get("Authorization") + if authHeader != "Bearer "+expectedAccessToken { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error": "invalid_token"}`)) + return + } + + // UAE PASS citizen/resident profile response + userProfile := `{ + "sub": "uaepass-user-123456", + "email": "user@example.ae", + "fullnameEN": "Ahmed Mohammed Al Rashid", + "fullnameAR": "أحمد محمد الراشد", + "firstnameEN": "Ahmed", + "firstnameAR": "أحمد", + "lastnameEN": "Al Rashid", + "lastnameAR": "الراشد", + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "unifiedId": "784-1990-1234567-1", + "idn": "784199012345671", + "userType": "SOP1", + "nationalityEN": "ARE", + "gender": "M", + "dob": "1990-05-15", + "mobile": "+971501234567" + }` + + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(userProfile)) + require.NoError(t, err) + }) + + uaePassAPI := httptest.NewServer(handler) + t.Cleanup(uaePassAPI.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + p.SetUserInfoURL(t, uaePassAPI.URL+"/userinfo") + + claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: expectedAccessToken}, nil) + require.NoError(t, err) + require.NotNil(t, claims) + + // Verify standard claims + assert.Equal(t, "uaepass-user-123456", claims.Subject) + assert.Equal(t, "user@example.ae", claims.Email) + assert.Equal(t, "Ahmed Mohammed Al Rashid", claims.Name) + assert.Equal(t, "Ahmed", claims.GivenName) + assert.Equal(t, "Al Rashid", claims.LastName) + + // Verify raw claims contain UAE PASS specific data + require.NotNil(t, claims.RawClaims) + assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", claims.RawClaims["uuid"]) + assert.Equal(t, "784-1990-1234567-1", claims.RawClaims["unifiedId"]) + assert.Equal(t, "784199012345671", claims.RawClaims["idn"]) + assert.Equal(t, "SOP1", claims.RawClaims["userType"]) + assert.Equal(t, "ARE", claims.RawClaims["nationalityEN"]) + assert.Equal(t, "M", claims.RawClaims["gender"]) + assert.Equal(t, "1990-05-15", claims.RawClaims["dob"]) + assert.Equal(t, "+971501234567", claims.RawClaims["mobile"]) + assert.Equal(t, "أحمد محمد الراشد", claims.RawClaims["fullnameAR"]) +} + +func TestUAEPassOIDCClaimsVisitor(t *testing.T) { + t.Parallel() + + handler := http.NewServeMux() + expectedAccessToken := "visitor-access-token" + + handler.HandleFunc("GET /userinfo", func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader != "Bearer "+expectedAccessToken { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // UAE PASS visitor profile response (limited data) + userProfile := `{ + "sub": "uaepass-visitor-789", + "email": "visitor@example.com", + "fullnameEN": "John Smith", + "firstnameEN": "John", + "lastnameEN": "Smith", + "uuid": "660e8400-e29b-41d4-a716-446655440001", + "userType": "SOP3" + }` + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(userProfile)) + }) + + uaePassAPI := httptest.NewServer(handler) + t.Cleanup(uaePassAPI.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile"}, + }, reg).(*oidc.ProviderUAEPass) + p.SetUserInfoURL(t, uaePassAPI.URL+"/userinfo") + + claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: expectedAccessToken}, nil) + require.NoError(t, err) + require.NotNil(t, claims) + + assert.Equal(t, "uaepass-visitor-789", claims.Subject) + assert.Equal(t, "visitor@example.com", claims.Email) + assert.Equal(t, "John Smith", claims.Name) + assert.Equal(t, "SOP3", claims.RawClaims["userType"]) +} + +func TestUAEPassOIDCClaimsError(t *testing.T) { + t.Parallel() + + handler := http.NewServeMux() + + handler.HandleFunc("GET /userinfo", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error": "invalid_token", "error_description": "The access token is expired"}`)) + }) + + uaePassAPI := httptest.NewServer(handler) + t.Cleanup(uaePassAPI.Close) + + _, reg := pkg.NewFastRegistryWithMocks(t) + p := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + p.SetUserInfoURL(t, uaePassAPI.URL+"/userinfo") + + claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: "invalid-token"}, nil) + require.Error(t, err) + require.Nil(t, claims) +} + +func TestUAEPassIssuerURL(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + + // Test staging via IssuerURL + pStaging := oidc.NewProviderUAEPass(&oidc.Configuration{ + IssuerURL: "https://stg-id.uaepass.ae", + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err := pStaging.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "https://stg-id.uaepass.ae/idshub/authorize", oauth2Config.Endpoint.AuthURL) + assert.Equal(t, "https://stg-id.uaepass.ae/idshub/token", oauth2Config.Endpoint.TokenURL) + + // Test production via IssuerURL + pProduction := oidc.NewProviderUAEPass(&oidc.Configuration{ + IssuerURL: "https://id.uaepass.ae", + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err = pProduction.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "https://id.uaepass.ae/idshub/authorize", oauth2Config.Endpoint.AuthURL) + assert.Equal(t, "https://id.uaepass.ae/idshub/token", oauth2Config.Endpoint.TokenURL) + + // Test default (production) when IssuerURL is not set + pDefault := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err = pDefault.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "https://id.uaepass.ae/idshub/authorize", oauth2Config.Endpoint.AuthURL) + assert.Equal(t, "https://id.uaepass.ae/idshub/token", oauth2Config.Endpoint.TokenURL) + + // Test trailing slash is handled correctly + pTrailingSlash := oidc.NewProviderUAEPass(&oidc.Configuration{ + IssuerURL: "https://stg-id.uaepass.ae/", + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err = pTrailingSlash.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "https://stg-id.uaepass.ae/idshub/authorize", oauth2Config.Endpoint.AuthURL) + assert.Equal(t, "https://stg-id.uaepass.ae/idshub/token", oauth2Config.Endpoint.TokenURL) +} + +func TestUAEPassConfigurationValidation(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + + // Test PKCE auto falls back to force (no error) + pPKCEAuto := oidc.NewProviderUAEPass(&oidc.Configuration{ + PKCE: "auto", + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + _, err := pPKCEAuto.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "force", pPKCEAuto.Config().PKCE) + + // Test PKCE unset also falls back to force + pPKCEUnset := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + _, err = pPKCEUnset.OAuth2(t.Context()) + require.NoError(t, err) + assert.Equal(t, "force", pPKCEUnset.Config().PKCE) + + // Test valid configuration + pValid := oidc.NewProviderUAEPass(&oidc.Configuration{ + PKCE: "never", + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err := pValid.OAuth2(t.Context()) + require.NoError(t, err) + require.NotNil(t, oauth2Config) +} + +func TestUAEPassAuthFlowDefaultsToWeb(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + + p := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + options := p.AuthCodeURLOptions(nil) + require.Len(t, options, 1) + + oauth2Config, err := p.OAuth2(t.Context()) + require.NoError(t, err) + + authURL := oauth2Config.AuthCodeURL("test-state", options...) + parsedURL, err := url.Parse(authURL) + require.NoError(t, err) + + acrValue := parsedURL.Query().Get("acr_values") + assert.Equal(t, oidc.UAEPassACRWeb, acrValue) +} + +func TestUAEPassMobileACRViaUpstreamParams(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + + p := oidc.NewProviderUAEPass(&oidc.Configuration{ + Scope: []string{"urn:uae:digitalid:profile:general"}, + }, reg).(*oidc.ProviderUAEPass) + + oauth2Config, err := p.OAuth2(t.Context()) + require.NoError(t, err) + + // Simulate the ordering in getAuthRedirectURL: provider defaults first, then upstream params. + // Upstream acr_values should override the provider's default web ACR value. + var opts []oauth2.AuthCodeOption + opts = append(opts, p.AuthCodeURLOptions(nil)...) + opts = append(opts, oidc.UpstreamParameters(map[string]string{ + "acr_values": "urn:digitalid:authentication:flow:mobileondevice", + })...) + + authURL := oauth2Config.AuthCodeURL("test-state", opts...) + parsedURL, err := url.Parse(authURL) + require.NoError(t, err) + + acrValue := parsedURL.Query().Get("acr_values") + assert.Equal(t, "urn:digitalid:authentication:flow:mobileondevice", acrValue) +} diff --git a/selfservice/strategy/oidc/provider_userinfo_test.go b/selfservice/strategy/oidc/provider_userinfo_test.go new file mode 100644 index 0000000..929af91 --- /dev/null +++ b/selfservice/strategy/oidc/provider_userinfo_test.go @@ -0,0 +1,380 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "testing" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/x/configx" + "github.com/ory/x/httpx" +) + +type mockRegistry struct { + *driver.RegistryDefault + cl *retryablehttp.Client +} + +func (s *mockRegistry) HTTPClient(context.Context, ...httpx.ResilientOptions) *retryablehttp.Client { + return s.cl +} + +func TestProviderClaimsRespectsErrorCodes(t *testing.T) { + t.Parallel() + + _, base := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + reg := &mockRegistry{base, retryablehttp.NewClient()} + + token := &oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)} + + expectedClaims := &oidc.Claims{ + Issuer: "ignore-me", + Subject: "123456789012345", + Name: "John Doe", + GivenName: "John", + FamilyName: "Doe", + Nickname: "John Doe", + PreferredUsername: "John Doe", + Email: "john.doe@example.com", + EmailVerified: true, + Birthdate: "01/01/1990", + } + + defaultUserinfoHandler := func(req *http.Request) (*http.Response, error) { + if head := req.Header.Get("Authorization"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, expectedClaims) + return resp, err + } + + for _, tc := range []struct { + name string + issuer string + userInfoEndpoint string + config *oidc.Configuration + provider oidc.Provider + userInfoHandler func(req *http.Request) (*http.Response, error) + expectedClaims *oidc.Claims + useToken *oauth2.Token + hook func(t *testing.T) + }{ + { + name: "auth0", + userInfoHandler: defaultUserinfoHandler, + userInfoEndpoint: "https://www.auth0.com/userinfo", + provider: oidc.NewProviderAuth0(&oidc.Configuration{ + IssuerURL: "https://www.auth0.com", + ID: "auth0", + 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, + userInfoEndpoint: "https://broker.netid.de/userinfo", + provider: oidc.NewProviderNetID(&oidc.Configuration{ + IssuerURL: "https://broker.netid.de", + ID: "netid", + Provider: "netid", + ClientID: "foo", + }, reg), + useToken: token.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwidGlkIjoiYTliODYzODUtZjMyYy00ODAzLWFmYzgtNGIyMzEyZmJkZjI0IiwiaXNzIjoiaHR0cHM6Ly9icm9rZXIubmV0aWQuZGUvIiwiYXVkIjpbImZvbyJdLCJleHAiOjQwNzE3Mjg1MDR9.Zt_-9jULoENQ7pq6rKrevhecBlWKR2rzNti42EJti_OelCrGbl5zyHYRfIg264VzEY0Zp9BAZTWmcF6Z-cpuD05RddTXZDrrC_47bJeYDL-bjDfKjoSZUt_1JnNFgqeyMA1Ji6HCIsEf_g8QnuSELAO0c-qb2ANmDLVL8_dY6oUmCN5oWLJS2q6xO-Iaz-AuKaGTbLZBcjCe2NB_--kIx4IrgLm78U90X3ePOV0CcYZLNvDgzEsVJ2M4ixKp87bYUaZZ3JJEjuxgnHqKRMExDKron3mvfQtC1L-dZQyeDo3-mCFJL_JhEw9zLmoWFBD7aARrVL_yAe31o26FB3S-Dg"}), + hook: func(t *testing.T) { + httpmock.RegisterResponder("GET", "https://broker.netid.de/.well-known/openid-configuration", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "issuer": "https://broker.netid.de/", + "jwks_uri": "https://broker.netid.de/.well-known/jwks.json", + }) + }, + ) + httpmock.RegisterResponder("GET", "https://broker.netid.de/.well-known/jwks.json", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, json.RawMessage(`{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "alg": "RS256", + "use": "sig", + "n": "uw0hrmZpA5CH5lDnjbCK6ZQD-I0BGxde-ChDzUz5eK4r7VtyMGvAxwD-k-mWw4FJ2NgYmT_T89sAtE6NQqT5u9HAe-CI22lf5LMvmqvMzekcmBAvXNw8VeTV_N6CbS9INJrxf20cObf-kpTxVxlYtYxYwIhYdOw3DwX8y31vI38qHQ4_OzTRo4KFVLCr68MzIHHRI4d5EHrFv1VFjiaa_ATwuwCMUfg0RMnK09FpMCgvp0E6bQeptXhBBNQVQkoC5whT1GzikfSxyeugjQ_XuTt1MKoyYsN2pmfrBdcfWrPYvV_JPgO1MkEtqErvtCByairINfXrHTMOxNWe3sYlXQ" + } + ] +}`)) + }, + ) + }, + expectedClaims: &oidc.Claims{Issuer: "https://broker.netid.de/", Subject: "1234567890", Name: "John Doe", GivenName: "John", FamilyName: "Doe", LastName: "", MiddleName: "", Nickname: "John Doe", PreferredUsername: "John Doe", Profile: "", Picture: "", Website: "", Email: "john.doe@example.com", EmailVerified: true, Gender: "", Birthdate: "01/01/1990", Zoneinfo: "", Locale: "", PhoneNumber: "", PhoneNumberVerified: false, UpdatedAt: 0, HD: "", Team: ""}, + }, + { + name: "vk", + userInfoEndpoint: "https://api.vk.com/method/users.get", + provider: oidc.NewProviderVK(&oidc.Configuration{ + IssuerURL: "https://oauth.vk.com", + ID: "vk", + Provider: "vk", + }, reg), + useToken: token.WithExtra(map[string]interface{}{"email": "john.doe@example.com"}), + userInfoHandler: func(req *http.Request) (*http.Response, error) { + if head := req.URL.Query().Get("access_token"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "response": []map[string]interface{}{{"id": 123456789012345}}, + }) + return resp, err + }, + + expectedClaims: &oidc.Claims{ + Issuer: "https://api.vk.com/method/users.get", + Subject: "123456789012345", + Email: "john.doe@example.com", + }, + }, + { + name: "yandex", + userInfoEndpoint: "https://login.yandex.ru/info", + provider: oidc.NewProviderYandex(&oidc.Configuration{ + IssuerURL: "https://oauth.yandex.com", + ID: "vk", + Provider: "vk", + }, reg), + useToken: token.WithExtra(map[string]interface{}{"email": "john.doe@example.com"}), + userInfoHandler: func(req *http.Request) (*http.Response, error) { + if head := req.URL.Query().Get("oauth_token"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": "123456789012345", + "default_email": "john.doe@example.com", + }) + return resp, err + }, + + expectedClaims: &oidc.Claims{ + Issuer: "https://login.yandex.ru/info", + Subject: "123456789012345", + Email: "john.doe@example.com", + Picture: "https://avatars.yandex.net/get-yapic//islands-200", + }, + }, + { + name: "facebook", + hook: func(t *testing.T) { + httpmock.RegisterResponder("GET", "https://www.facebook.com/.well-known/openid-configuration", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "issuer": "https://www.facebook.com", + }) + return resp, err + }, + ) + }, + userInfoEndpoint: "https://graph.facebook.com/me", + provider: oidc.NewProviderFacebook(&oidc.Configuration{ + ID: "facebook", + Provider: "facebook", + }, reg), + useToken: token, + userInfoHandler: func(req *http.Request) (*http.Response, error) { + if head := req.Header.Get("Authorization"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + if _, ok := req.URL.Query()["appsecret_proof"]; !ok { + resp, err := httpmock.NewJsonResponse(400, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": "123456789012345", + "name": "John Doe", + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "birthday": "01/01/1990", + }) + return resp, err + }, + expectedClaims: &oidc.Claims{ + Issuer: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=0c0d98f7e3d9d45e72e8877bc1b104327efb9c07b18f2ffeced76d81307f1fff", + Subject: "123456789012345", + Name: "John Doe", + GivenName: "John", + FamilyName: "Doe", + Nickname: "John Doe", + PreferredUsername: "John Doe", + Email: "john.doe@example.com", + EmailVerified: true, + Birthdate: "01/01/1990", + }, + }, + { + name: "gitlab", + userInfoHandler: defaultUserinfoHandler, + userInfoEndpoint: "https://www.gitlab.com/oauth/userinfo", + provider: oidc.NewProviderGitLab(&oidc.Configuration{ + IssuerURL: "https://www.gitlab.com", + ID: "gitlab", + Provider: "gitlab", + }, reg), + }, + // Microsoft is a more complicated set up because it actually verifies the ID Token before using the userinfo endpoint. + { + name: "microsoft", + userInfoHandler: func(req *http.Request) (*http.Response, error) { + if head := req.Header.Get("Authorization"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, json.RawMessage(`{"id":"new-id"}`)) + return resp, err + }, + userInfoEndpoint: "https://graph.microsoft.com/v1.0/me", + provider: oidc.NewProviderMicrosoft(&oidc.Configuration{ + IssuerURL: "https://login.microsoftonline.com/", + ID: "microsoft", + Provider: "microsoft", + Tenant: "a9b86385-f32c-4803-afc8-4b2312fbdf24", + ClientID: "foo", + ClientSecret: "bar", + SubjectSource: "me", + }, reg), + useToken: token.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwidGlkIjoiYTliODYzODUtZjMyYy00ODAzLWFmYzgtNGIyMzEyZmJkZjI0IiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tL2E5Yjg2Mzg1LWYzMmMtNDgwMy1hZmM4LTRiMjMxMmZiZGYyNC92Mi4wIiwiYXVkIjpbImZvbyJdLCJleHAiOjQwNzE3Mjg1MDR9.LRgolO5_-26uMrBo89NCzUfi87a8jf7rXlWgZVYnpfowzqn-U0JhNGVzEOXOACoPdX9HsEtYj4hZxYgYcd6z7yqgmOZXE-y58L5BHYZU1kk37O1Dl_VnN-BmeCWs_JZiXF2KEnu7BW8btYnb26qCnc3_8RGbyJwI4UU6ynbJzAmLCPUPgoMZ2Jpahx_K8vqLe-4rveLyUvVVHMoUAV16I-Wg08GcuA5cY0_91-QpA5Kq0AA58wbrUbAOEAOYOpa_63QqYcCcZLbnX_w09Z1YCXGGVSbbpzjr3cJ8EFA6doloBKRLWtNtEnGk4hyjHyp_ls89ZYqJ1ngy95AEswzwJQ"}), + hook: func(t *testing.T) { + httpmock.RegisterResponder("GET", "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/openid-configuration", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "issuer": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", + "jwks_uri": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/jwks.json", + }) + }, + ) + httpmock.RegisterResponder("GET", "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/jwks.json", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, json.RawMessage(`{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "alg": "RS256", + "n": "xGnYx5u9S7LCrtINTI1Kav6pqzFj72JhYunqrDhnG18Iu-MJdCSmd26xiiZFY6UsJoG-te_DcUE_YHvhx6vwC0tJO-Z2uxwKRxWsMiyW8v3V0Bfbtu7jlf0kpQYeAeAIAHih7GU_v5jtxaGLNR9JIbmENUGOoIfydfTWLKHtwI0MB8tNVwjqP8e6ZSr6DNIjsKKuz98BeRlZ576jyH2GcmM9DguLZv73qU1m1OLoHFW5rAPH70-nhr9V67TTR1A1is9v85uLlKBnZgKp-EdYtBEvKRWSpw5vxCnKggJjEYATOB2H5m_c8F1e1tsJLAehaXKMKZYI-VlWWxj7KixDKw" + } + ] +}`)) + }, + ) + }, + expectedClaims: &oidc.Claims{ + Issuer: "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", Subject: "new-id", Name: "John Doe", Email: "john.doe@example.com", + RawClaims: map[string]interface{}{"aud": []interface{}{"foo"}, "exp": 4.071728504e+09, "iat": 1.516239022e+09, "iss": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", "email": "john.doe@example.com", "name": "John Doe", "sub": "1234567890", "tid": "a9b86385-f32c-4803-afc8-4b2312fbdf24"}, + }, + }, + { + name: "dingtalk", + userInfoHandler: func(req *http.Request) (*http.Response, error) { + if head := req.Header.Get("x-acs-dingtalk-access-token"); len(head) == 0 { + resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""}) + return resp, err + } + + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "openId": "123456789012345", + "email": "john.doe@example.com", + }) + return resp, err + }, + userInfoEndpoint: "https://api.dingtalk.com/v1.0/contact/users/me", + provider: oidc.NewProviderDingTalk(&oidc.Configuration{ + IssuerURL: "https://www.dingtalk.com", + ID: "dingtalk", + Provider: "dingtalk", + }, reg), + expectedClaims: &oidc.Claims{ + Issuer: "https://api.dingtalk.com/v1.0/contact/users/me", + Subject: "123456789012345", + Email: "john.doe@example.com", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + token := token + if tc.useToken != nil { + token = tc.useToken + } + + t.Run("http error is respected", func(t *testing.T) { + httpmock.ActivateNonDefault(reg.cl.HTTPClient) + t.Cleanup(httpmock.DeactivateAndReset) + + if tc.hook != nil { + tc.hook(t) + } + + httpmock.RegisterResponder("GET", tc.userInfoEndpoint, func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(455, map[string]interface{}{}) + }) + + _, err := tc.provider.(oidc.OAuth2Provider).Claims(t.Context(), 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(), "%+v", err) + }) + + t.Run("call is successful", func(t *testing.T) { + httpmock.ActivateNonDefault(reg.cl.HTTPClient) + t.Cleanup(httpmock.DeactivateAndReset) + + if tc.hook != nil { + tc.hook(t) + } + + httpmock.RegisterResponder("GET", tc.userInfoEndpoint, tc.userInfoHandler) + + claims, err := tc.provider.(oidc.OAuth2Provider).Claims(t.Context(), token, url.Values{}) + require.NoError(t, err) + if tc.expectedClaims == nil { + assert.Equal(t, expectedClaims, claims) + } else { + assert.Equal(t, tc.expectedClaims, claims) + } + }) + }) + } +} diff --git a/selfservice/strategy/oidc/provider_vk.go b/selfservice/strategy/oidc/provider_vk.go new file mode 100644 index 0000000..282be10 --- /dev/null +++ b/selfservice/strategy/oidc/provider_vk.go @@ -0,0 +1,134 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderVK)(nil) + +type ProviderVK struct { + config *Configuration + reg Dependencies +} + +func NewProviderVK( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderVK{ + config: config, + reg: reg, + } +} + +func (g *ProviderVK) Config() *Configuration { + return g.config +} + +func (g *ProviderVK) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://oauth.vk.com/authorize", + TokenURL: "https://oauth.vk.com/access_token", + }, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderVK) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderVK) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderVK) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, err + } + + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://api.vk.com/method/users.get?fields=photo_200,nickname,bdate,sex&access_token="+exchange.AccessToken+"&v=5.103", nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + type User struct { + ID int `json:"id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + Picture string `json:"photo_200,omitempty"` + Email string `json:"-"` + Gender int `json:"sex,omitempty"` + BirthDay string `json:"bdate,omitempty"` + } + + var response struct { + Result []User `json:"response,omitempty"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + if len(response.Result) == 0 { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("VK did not return a user in the userinfo request.")) + } + + user := response.Result[0] + + if email, ok := exchange.Extra("email").(string); ok { + user.Email = email + } + + gender := "" + switch user.Gender { + case 1: + gender = "female" + case 2: + gender = "male" + } + + return &Claims{ + Issuer: "https://api.vk.com/method/users.get", + Subject: strconv.Itoa(user.ID), + GivenName: user.FirstName, + FamilyName: user.LastName, + Nickname: user.Nickname, + Picture: user.Picture, + Email: user.Email, + Gender: gender, + Birthdate: user.BirthDay, + }, nil +} diff --git a/selfservice/strategy/oidc/provider_x.go b/selfservice/strategy/oidc/provider_x.go new file mode 100644 index 0000000..d71f443 --- /dev/null +++ b/selfservice/strategy/oidc/provider_x.go @@ -0,0 +1,173 @@ +// 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 _ OAuth1Provider = (*ProviderX)(nil) + +const ( + xUserInfoBase = "https://api.twitter.com/1.1/account/verify_credentials.json" + 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.ErrUpstreamError().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.ErrMisconfiguration().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.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = 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.ErrUpstreamError().WithWrap(err).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/provider_yandex.go b/selfservice/strategy/oidc/provider_yandex.go new file mode 100644 index 0000000..e522b73 --- /dev/null +++ b/selfservice/strategy/oidc/provider_yandex.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/x/httpx" + + "github.com/ory/herodot" +) + +var _ OAuth2Provider = (*ProviderYandex)(nil) + +type ProviderYandex struct { + config *Configuration + reg Dependencies +} + +func NewProviderYandex( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderYandex{ + config: config, + reg: reg, + } +} + +func (g *ProviderYandex) Config() *Configuration { + return g.config +} + +func (g *ProviderYandex) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://oauth.yandex.com/authorize", + TokenURL: "https://oauth.yandex.com/token", + }, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderYandex) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderYandex) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderYandex) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, err + } + + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://login.yandex.ru/info?format=json&oauth_token="+exchange.AccessToken, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + defer func() { _ = resp.Body.Close() }() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + var user struct { + ID string `json:"id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Email string `json:"default_email,omitempty"` + Picture string `json:"default_avatar_id,omitempty"` + PictureEmpty bool `json:"is_avatar_empty,omitempty"` + Gender string `json:"sex,omitempty"` + BirthDay string `json:"birthday,omitempty"` + } + + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) + } + + if !user.PictureEmpty { + user.Picture = "https://avatars.yandex.net/get-yapic/" + user.Picture + "/islands-200" + } else { + user.Picture = "" + } + + return &Claims{ + Issuer: "https://login.yandex.ru/info", + Subject: user.ID, + GivenName: user.FirstName, + FamilyName: user.LastName, + Picture: user.Picture, + Email: user.Email, + Gender: user.Gender, + Birthdate: user.BirthDay, + }, nil +} diff --git a/selfservice/strategy/oidc/schema.go b/selfservice/strategy/oidc/schema.go new file mode 100644 index 0000000..b62d9f2 --- /dev/null +++ b/selfservice/strategy/oidc/schema.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + _ "embed" +) + +//go:embed .schema/link.schema.json +var linkSchema []byte diff --git a/selfservice/strategy/oidc/state.go b/selfservice/strategy/oidc/state.go new file mode 100644 index 0000000..5b736d0 --- /dev/null +++ b/selfservice/strategy/oidc/state.go @@ -0,0 +1,91 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "crypto/sha512" + "crypto/subtle" + + "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/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/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, fl flow.Flow, requestBaseURL string) (stateParam string, pkce []oauth2.AuthCodeOption, err error) { + // The customer-facing base URL is captured at flow *init* (where the + // proxy / custom-domain headers are present) and stashed on the flow's + // InternalContext. The provider-submit request that reaches here does + // not necessarily carry those headers, so the flow-captured value wins + // over the request-derived fallback. Mirrors the legacy host-rewrite + // webhook, which also captured at flow init. + if ic, ok := fl.(flow.InternalContexter); ok { + if captured := flow.GetRequestBaseURL(ic); captured != "" { + requestBaseURL = captured + } + } + + state := oidcv1.State{ + FlowId: fl.GetID().Bytes(), + SessionTokenExchangeCodeSha512: x.NewUUID().Bytes(), + ProviderId: p.Config().ID, + PkceVerifier: maybePKCE(ctx, s.d, p), + RequestBaseUrl: requestBaseURL, + } + + switch fl.(type) { + case *login.Flow: + state.FlowKind = oidcv1.FlowKind_FLOW_KIND_LOGIN + case *registration.Flow: + state.FlowKind = oidcv1.FlowKind_FLOW_KIND_REGISTRATION + case *settings.Flow: + state.FlowKind = oidcv1.FlowKind_FLOW_KIND_SETTINGS + default: + state.FlowKind = oidcv1.FlowKind_FLOW_KIND_UNSPECIFIED + } + + if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, fl.GetID()); 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 0000000..8ec3eb4 --- /dev/null +++ b/selfservice/strategy/oidc/state_test.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cipher" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestGenerateState(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strat := oidc.NewStrategy(reg) + ciph := reg.Cipher(t.Context()) + _, ok := ciph.(*cipher.Noop) + require.False(t, ok) + + flow := ®istration.Flow{ + ID: x.NewUUID(), + } + + stateParam, pkce, err := strat.GenerateState(t.Context(), &testProvider{}, flow, "https://teststatehost") + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.DecryptState(t.Context(), ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, flow.GetID().Bytes(), state.FlowId) + assert.Empty(t, oidc.PKCEVerifier(state)) + assert.Equal(t, "test-provider", state.ProviderId) + assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_REGISTRATION, state.FlowKind) + assert.Equal(t, "https://teststatehost", state.RequestBaseUrl) +} + +// TestGenerateStateUsesFlowCapturedBaseURL is the regression test for the +// init→submit bridge. The Ory CLI / Tunnel only sends the +// Ory-Base-URL-Rewrite header on the flow-*init* request; the provider +// submit that reaches GenerateState does not carry it. The base URL is +// therefore captured onto the flow's InternalContext at init, and +// GenerateState must prefer that captured value over the request-derived +// fallback — otherwise the social-sign-in callback never gets redirected +// back onto the developer host (the bug this guards against). +func TestGenerateStateUsesFlowCapturedBaseURL(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strat := oidc.NewStrategy(reg) + ciph := reg.Cipher(t.Context()) + + t.Run("flow-captured base URL wins over the submit fallback", func(t *testing.T) { + f := ®istration.Flow{ID: x.NewUUID(), InternalContext: []byte("{}")} + require.NoError(t, flow.SetRequestBaseURL(f, "http://localhost:4000")) + + // The fallback (what the submit request resolved to, e.g. the bare + // projects.oryapis host) must be ignored in favor of the captured value. + stateParam, _, err := strat.GenerateState(t.Context(), &testProvider{}, f, "https://slug.projects.oryapis.com") + require.NoError(t, err) + state, err := oidc.DecryptState(t.Context(), ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, "http://localhost:4000", state.RequestBaseUrl, + "http scheme + host captured at init must survive to the callback") + }) + + t.Run("no captured base URL falls back to the passed value", func(t *testing.T) { + f := ®istration.Flow{ID: x.NewUUID(), InternalContext: []byte("{}")} + stateParam, _, err := strat.GenerateState(t.Context(), &testProvider{}, f, "https://slug.projects.oryapis.com") + require.NoError(t, err) + state, err := oidc.DecryptState(t.Context(), ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, "https://slug.projects.oryapis.com", state.RequestBaseUrl) + }) +} + +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 new file mode 100644 index 0000000..cedb46a --- /dev/null +++ b/selfservice/strategy/oidc/strategy.go @@ -0,0 +1,997 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "cmp" + "context" + "encoding/json" + "maps" + "net/http" + "net/url" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "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/sessiontokenexchange" + "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" + "github.com/ory/kratos/session" + "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/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/clock" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/reqlog" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +const ( + RouteBase = "/self-service/methods/oidc" + + RouteAuth = RouteBase + "/auth/{flow}" + RouteCallback = RouteBase + "/callback/{provider}" + RouteCallbackGeneric = RouteBase + "/callback" + RouteOrganizationCallback = RouteBase + "/organization/{organization}/callback/{provider}" +) + +var ( + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) + _ x.Handler = (*Strategy)(nil) +) + +type Dependencies interface { + clock.Provider + + errorx.ManagementProvider + + config.Provider + + logrusx.Provider + x.CookieProvider + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + httpx.ClientProvider + otelx.Provider + + identity.ValidationProvider + identity.PrivilegedPoolProvider + identity.ActiveCredentialsCounterStrategyProvider + identity.ManagementProvider + + session.ManagementProvider + session.HandlerProvider + sessiontokenexchange.PersistenceProvider + + login.HookExecutorProvider + login.FlowPersistenceProvider + login.HooksProvider + login.StrategyProvider + login.HandlerProvider + login.ErrorHandlerProvider + + registration.HookExecutorProvider + registration.FlowPersistenceProvider + registration.HooksProvider + registration.StrategyProvider + registration.HandlerProvider + registration.ErrorHandlerProvider + + settings.ErrorHandlerProvider + settings.FlowPersistenceProvider + settings.HookExecutorProvider + + continuity.ManagementProvider + + cipher.Provider + + jsonnetsecure.VMProvider +} + +func isForced(req interface{}) bool { + f, ok := req.(interface { + IsRefresh() bool + }) + return ok && f.IsRefresh() +} + +// ConflictingIdentityVerdict encodes the decision on what to do on a oconflict +// between an existing and a new identity. +type ConflictingIdentityVerdict int + +const ( + // ConflictingIdentityVerdictUnknown is the default value and should not be used. + ConflictingIdentityVerdictUnknown ConflictingIdentityVerdict = iota + + // ConflictingIdentityVerdictReject rejects the new identity. The flow will + // continue with an explicit account linking step, where the user will need to + // confirm an existing credential on the identity. + ConflictingIdentityVerdictReject + + // ConflictingIdentityVerdictMerge merges the new identity into the existing. + ConflictingIdentityVerdictMerge +) + +// Strategy implements selfservice.LoginStrategy, selfservice.RegistrationStrategy and selfservice.SettingsStrategy. +// It supports login, registration and settings via OpenID Providers. +type Strategy struct { + d Dependencies + validator *schema.Validator + credType identity.CredentialsType + handleUnknownProviderError func(err error) error + handleMethodNotAllowedError func(err error) error + + conflictingIdentityPolicy ConflictingIdentityPolicy +} +type ConflictingIdentityPolicy func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, provider Provider, claims *Claims) ConflictingIdentityVerdict + +type AuthCodeContainer struct { + FlowID string `json:"flow_id"` + State string `json:"state"` + IdentitySchema flow.IdentitySchema `json:"identity_schema_id,omitempty"` + Traits json.RawMessage `json:"traits"` + TransientPayload json.RawMessage `json:"transient_payload"` +} + +func (s *Strategy) CountActiveFirstFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return CountActiveFirstFactorCredentials(ctx, s.ID(), cc, false) +} + +func CountActiveFirstFactorCredentials(_ context.Context, id identity.CredentialsType, cc map[identity.CredentialsType]identity.Credentials, withOrgs bool) (count int, err error) { + for _, c := range cc { + if c.Type == id && gjson.ValidBytes(c.Config) { + var conf identity.CredentialsOIDC + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + for _, identifier := range c.Identifiers { + provider, sub, ok := strings.Cut(identifier, ":") + if !ok { + continue + } + + for _, prov := range conf.Providers { + if withOrgs && len(prov.Organization) == 0 { + continue + } else if !withOrgs && len(prov.Organization) > 0 { + continue + } + + if provider == prov.Provider && sub == prov.Subject && prov.Subject != "" && prov.Provider != "" { + count++ + } + } + } + } + } + return +} + +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) RegisterPublicRoutes(r *httprouterx.RouterPublic) { + wrappedHandleCallback := strategy.IsDisabled(s.d, s.ID().String(), s.HandleCallback) + r.GET(RouteCallback, wrappedHandleCallback) + r.GET(RouteCallbackGeneric, wrappedHandleCallback) + + // Apple can use the POST request method when calling the callback + // Apple is the only (known) provider that sometimes does a form POST to the callback URL. + // This is a workaround to handle this case. + // But since the URL contains the `id` of the provider, we just allow all OIDC provider callbacks to bypass CSRF. + // This is fine, because all other providers seem to use GET, which is CSRF safe. + s.d.CSRFHandler().IgnoreGlob(RouteBase + "/callback/*") + + // When handler is called using POST method, the cookies are not attached to the request + // by the browser. So here we just redirect the request to the same location rewriting the + // form fields to query params. This second GET request should have the cookies attached. + r.POST(RouteCallback, s.redirectToGET) +} + +func (s *Strategy) RegisterAdminRoutes(*httprouterx.RouterAdmin) {} + +// Redirect POST request to GET rewriting form fields to query params. +func (s *Strategy) redirectToGET(w http.ResponseWriter, r *http.Request) { + publicURL := s.d.Config().SelfPublicURL(r.Context()) + dest := *r.URL + dest.Host = publicURL.Host + dest.Scheme = publicURL.Scheme + if err := r.ParseForm(); err == nil { + q := dest.Query() + for key, values := range r.Form { + for _, value := range values { + q.Set(key, value) + } + } + dest.RawQuery = q.Encode() + } + dest.Path = filepath.Join(publicURL.Path, dest.Path) + + http.Redirect(w, r, dest.String(), http.StatusFound) +} + +type NewStrategyOpt func(s *Strategy) + +// ForCredentialType overrides the credentials type for this strategy. +func ForCredentialType(ct identity.CredentialsType) NewStrategyOpt { + return func(s *Strategy) { s.credType = ct } +} + +// WithUnknownProviderHandler overrides the error returned when the provider +// cannot be found. +func WithUnknownProviderHandler(handler func(error) error) NewStrategyOpt { + return func(s *Strategy) { s.handleUnknownProviderError = handler } +} + +// WithHandleMethodNotAllowedError overrides the error returned when method is +// not allowed. +func WithHandleMethodNotAllowedError(handler func(error) error) NewStrategyOpt { + return func(s *Strategy) { s.handleMethodNotAllowedError = handler } +} + +// WithOnConflictingIdentity sets a policy handler for deciding what to do when a +// new identity conflicts with an existing one during login. +func WithOnConflictingIdentity(handler ConflictingIdentityPolicy) NewStrategyOpt { + return func(s *Strategy) { s.conflictingIdentityPolicy = handler } +} + +// SetOnConflictingIdentity sets a policy handler for deciding what to do when a +// new identity conflicts with an existing one during login. This should only be +// called in tests. +func (s *Strategy) SetOnConflictingIdentity(t testing.TB, handler ConflictingIdentityPolicy) { + if t == nil { + panic("this should only be called in tests") + } + s.conflictingIdentityPolicy = handler +} + +func NewStrategy(d Dependencies, opts ...NewStrategyOpt) *Strategy { + s := &Strategy{ + d: d, + validator: schema.NewValidator(), + credType: identity.CredentialsTypeOIDC, + handleUnknownProviderError: func(err error) error { return err }, + handleMethodNotAllowedError: func(err error) error { return err }, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +func (s *Strategy) ID() identity.CredentialsType { + return s.credType +} + +func (s *Strategy) validateFlow(ctx context.Context, r *http.Request, rid uuid.UUID, kind oidcv1.FlowKind) (f flow.Flow, err error) { + if rid.IsNil() { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("The session cookie contains invalid values and the flow could not be executed. Please try again.")) + } + + switch kind { + + case oidcv1.FlowKind_FLOW_KIND_LOGIN: + lf, err := s.d.LoginFlowPersister().GetLoginFlow(ctx, rid) + if err != nil { + return nil, err + } + return lf, lf.Valid(s.d.Clock()) + + case oidcv1.FlowKind_FLOW_KIND_REGISTRATION: + rf, err := s.d.RegistrationFlowPersister().GetRegistrationFlow(ctx, rid) + if err != nil { + return nil, err + } + return rf, rf.Valid(s.d.Clock()) + + case oidcv1.FlowKind_FLOW_KIND_SETTINGS: + sf, err := s.d.SettingsFlowPersister().GetSettingsFlow(ctx, rid) + if err != nil { + return nil, err + } + // The flow validation only reads the session's identity ID, so skip loading the + // devices and the identity's associations. + sess, err := s.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandDefault, identity.ExpandNothing) + if err != nil { + return sf, err + } + return sf, sf.Valid(s.d.Clock(), sess) + + } + + // fallback to the old behavior for backwards compatibility + for _, kind := range []oidcv1.FlowKind{oidcv1.FlowKind_FLOW_KIND_LOGIN, oidcv1.FlowKind_FLOW_KIND_REGISTRATION, oidcv1.FlowKind_FLOW_KIND_SETTINGS} { + if f, err = s.validateFlow(ctx, r, rid, kind); f != nil { + return f, err + } + } + return f, err +} + +func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flow.Flow, *oidcv1.State, *AuthCodeContainer, error) { + var ( + codeParam = cmp.Or(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) + stateParam = r.URL.Query().Get("state") + errorParam = r.URL.Query().Get("error") + ) + + if stateParam == "" { + return nil, nil, nil, oidcerr.Wrap(oidcerr.StepStateValidate, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the state query parameter.`))) + } + + state, err := DecryptState(r.Context(), s.d.Cipher(r.Context()), stateParam) + if err != nil { + return nil, nil, nil, oidcerr.Wrap(oidcerr.StepStateValidate, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to complete OpenID Connect flow because the state parameter is invalid.`))) + } + + if providerFromURL := r.PathValue("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, oidcerr.Wrap(oidcerr.StepStateValidate, 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, oidcerr.Wrap(oidcerr.StepStateValidate, 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, uuid.FromBytesOrNil(state.FlowId), state.FlowKind) + if err != nil { + return nil, state, nil, err + } + + tokenCode, hasSessionTokenCode, err := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.GetID()) + if err != nil { + return nil, state, nil, err + } + + cntnr := AuthCodeContainer{} + + var refStore continuity.ContainerReferenceStore + if f.GetType() == flow.TypeBrowser || !hasSessionTokenCode { + refStore = continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(r.Context())) + } else { + if !codeMatches(state, tokenCode.InitCode) { + return nil, state, &cntnr, oidcerr.Wrap(oidcerr.StepStateValidate, 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 ic, ok := f.(flow.InternalContexter); ok { + refStore = continuity.NewInternalContextReferenceStore(ic) + } else { + // This should never happen, because all flows implement the InternalContexter interface, but we need to handle this case anyway to avoid panics. + return nil, state, &cntnr, oidcerr.Wrap(oidcerr.StepCallback, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Native flow does not support InternalContext."))) + } + } + + if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, refStore, + continuity.WithPayload(&cntnr), + continuity.WithExpireInsteadOfDelete(time.Minute), + ); err != nil { + return nil, state, nil, err + } + + if f.GetType() == flow.TypeBrowser || !hasSessionTokenCode { + if stateParam != cntnr.State { + return nil, state, &cntnr, oidcerr.Wrap(oidcerr.StepStateValidate, 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 { + cntnr.State = stateParam + cntnr.FlowID = uuid.FromBytesOrNil(state.FlowId).String() + } + + if errorParam != "" { + return f, state, &cntnr, oidcerr.Wrap(oidcerr.StepProviderDenied, 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, state, &cntnr, oidcerr.Wrap(oidcerr.StepCallback, 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, nil +} + +func registrationOrLoginFlowID(flow any) (uuid.UUID, bool) { + switch f := flow.(type) { + case *registration.Flow: + return f.ID, true + case *login.Flow: + return f.ID, true + default: + return uuid.Nil, false + } +} + +func (s *Strategy) alreadyAuthenticated(ctx context.Context, w http.ResponseWriter, r *http.Request, f interface{}) (bool, error) { + // Only the session ID is read below, so skip loading the devices and the identity. + if sess, _ := s.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandNothing, identity.ExpandNothing); sess != nil { + if _, ok := f.(*settings.Flow); ok { + // ignore this if it's a settings flow + } else if !isForced(f) { + returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(ctx) + if redirecter, ok := f.(flow.FlowWithRedirect); ok { + r, err := redir.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.d)...) + if err == nil { + returnTo = r + } + } + if flowID, ok := registrationOrLoginFlowID(f); ok { + if codes, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, flowID); hasCode { + if err := s.d.SessionTokenExchangePersister().UpdateSessionOnExchanger(ctx, flowID, sess.ID); err != nil { + return false, err + } + q := returnTo.Query() + q.Set("code", codes.ReturnToCode) + returnTo.RawQuery = q.Encode() + } + } + http.Redirect(w, r, returnTo.String(), http.StatusSeeOther) + return true, nil + } + } + + return false, nil +} + +func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request) { + var ( + code = cmp.Or(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) + err error + ) + + ctx := r.Context() + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.HandleCallback") + defer otelx.End(span, &err) + r = r.WithContext(ctx) + + req, state, cntnr, err := s.ValidateCallback(w, r) + if err != nil { + if req != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, err)) + } else { + s.d.SelfServiceErrorManager().Forward(ctx, w, r, s.HandleError(ctx, w, r, nil, "", nil, err)) + } + return + } + + 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(ctx, state.ProviderId) + if err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } + + var claims *Claims + var et *identity.CredentialsOIDCEncryptedTokens + switch p := provider.(type) { + case OAuth2Provider: + t0 := time.Now() + token, err := s.exchangeCode(ctx, p, code, PKCEVerifier(state)) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, oidcerr.Wrap(oidcerr.StepTokenExchange, 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, oidcerr.Wrap(oidcerr.StepCallback, err))) + return + } + + t0 = time.Now() + claims, err = p.Claims(ctx, token, r.URL.Query()) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, oidcerr.Wrap(oidcerr.StepClaimsDecode, err))) + return + } + case OAuth1Provider: + t0 := time.Now() + token, err := p.ExchangeToken(ctx, r) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, oidcerr.Wrap(oidcerr.StepTokenExchange, err))) + return + } + + t0 = time.Now() + claims, err = p.Claims(ctx, token) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, oidcerr.Wrap(oidcerr.StepClaimsDecode, err))) + return + } + } + + if err = claims.Validate(); err != nil { + s.forwardError(ctx, w, r, req, s.HandleError(ctx, w, r, req, state.ProviderId, nil, oidcerr.Wrap(oidcerr.StepClaimsDecode, err))) + return + } + + span.SetAttributes(attribute.StringSlice("claims", slices.Collect(maps.Keys(claims.RawClaims)))) + + switch a := req.(type) { + case *login.Flow: + a.Active = s.ID() + a.TransientPayload = cntnr.TransientPayload + a.IdentitySchema = cntnr.IdentitySchema + // Test login flows short-circuit here: capture debug payload, do not + // touch identity or session state, do not run ProcessLogin. + if a.IsTest() { + if err := s.processTestLogin(ctx, w, r, a, claims, provider); err != nil { + s.forwardError(ctx, w, r, a, err) + } + return + } + 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(ctx, w, r, ff, err) + return + } + s.forwardError(ctx, w, r, a, err) + } + return + case *registration.Flow: + a.Active = s.ID() + a.TransientPayload = cntnr.TransientPayload + a.IdentitySchema = cntnr.IdentitySchema + if ff, err := s.processRegistration(ctx, w, r, a, et, claims, provider, cntnr); errors.Is(err, flow.ErrCompletedByStrategy) { + return + } else if err != nil { + if ff != nil { + s.forwardError(ctx, w, r, ff, err) + return + } + s.forwardError(ctx, w, r, a, err) + } + return + case *settings.Flow: + a.Active = sqlxx.NullString(s.ID()) + a.TransientPayload = cntnr.TransientPayload + // The full expansion is required: the settings flow update reads the session's identity. + sess, err := s.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandEverything, identity.ExpandEverything) + if err != nil { + s.forwardError(ctx, w, r, a, s.HandleError(ctx, w, r, a, state.ProviderId, nil, err)) + return + } + if err := s.linkProvider(ctx, 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(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 OAuth2Provider, code string, opts []oauth2.AuthCodeOption) (token *oauth2.Token, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.exchangeCode", trace.WithAttributes( + attribute.String("provider_id", provider.Config().ID), + attribute.String("provider_label", provider.Config().Label))) + defer otelx.End(span, &err) + + te, ok := provider.(OAuth2TokenExchanger) + if !ok { + te, err = provider.OAuth2(ctx) + if err != nil { + return nil, err + } + } + + client := s.d.HTTPClient(ctx) + ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) + return te.Exchange(ctx, code, opts...) +} + +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 + } + + f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(f.GetUI(), conf.Providers, message, s.ID()) + + return nil +} + +func (s *Strategy) Config(ctx context.Context) (*ConfigurationCollection, error) { + var c ConfigurationCollection + + conf := s.d.Config().SelfServiceStrategy(ctx, string(s.ID())).Config + if err := json. + NewDecoder(bytes.NewBuffer(conf)). + Decode(&c); err != nil { + s.d.Logger().WithError(err).WithField("config", conf) + return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to decode OpenID Connect Provider configuration: %s", err)) + } + + return &c, nil +} + +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 { + return nil, s.handleUnknownProviderError(err) + } else { + return provider, nil + } +} + +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: + // Test login flows never surface an error page — the admin wants a + // DebugPayload populated with whatever was captured before the + // failure. Route to finishTestLoginWithError which redirects to the + // admin test UI with Error.Step + Error.Message set. + if ff.IsTest() { + if finishErr := s.finishTestLoginWithError(ctx, w, r, ff, err); finishErr != nil { + s.d.SelfServiceErrorManager().Forward(ctx, w, r, finishErr) + } + return + } + s.d.LoginFlowErrorHandler().WriteFlowError(w, r, ff, s.ID(), s.NodeGroup(), err) + case *registration.Flow: + s.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, ff, s.ID(), s.NodeGroup(), err) + case *settings.Flow: + var i *identity.Identity + var sess *session.Session + if currentSession, err := s.d.SessionManager().FetchFromRequest(ctx, r, session.ExpandEverything, identity.ExpandEverything); err == nil { + i = currentSession.Identity + sess = currentSession + } + s.d.SettingsFlowErrorHandler().WriteFlowError(ctx, w, r, s.NodeGroup(), ff, i, sess, err) + default: + panic(errors.Errorf("unexpected type: %T", ff)) + } +} + +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 + case *registration.Flow: + // Reset all nodes to not confuse users. + // This is kinda hacky and will probably need to be updated at some point. + + if dup := new(identity.ErrDuplicateCredentials); errors.As(err, &dup) { + err = schema.NewDuplicateCredentialsError(dup) + + if validationErr := new(schema.ValidationError); errors.As(err, &validationErr) { + for _, m := range validationErr.Messages { + m := m + rf.UI.Messages.Add(&m) + } + } else { + rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) + } + + 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. + var redirectURL *url.URL + if lf.Type == flow.TypeAPI { + returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(ctx) + if redirecter, ok := f.(flow.FlowWithRedirect); ok { + secureReturnTo, err := redir.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.d)...) + if err == nil { + returnTo = secureReturnTo + } + } + 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 + } + } + x.SendFlowErrorAsRedirectOrJSON(w, r, s.d.Writer(), lf, redirectURL.String()) + // ensure the function does not continue to execute + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + rf.UI.Nodes = node.Nodes{} + + // Adds the "Continue" button + rf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProvider(rf.UI, usedProviderID, text.NewInfoRegistrationContinue(), s.ID()) + + group := node.DefaultGroup + if s.d.Config().SelfServiceLegacyOIDCRegistrationGroup(ctx) { + group = node.OpenIDConnectGroup + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "legacy_oidc_registration_group")) + } + + if traits != nil { + ds, err := rf.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + traitNodes, err := container.NodesFromJSONSchema(ctx, group, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(ctx)) + if err != nil { + return err + } + + rf.UI.Nodes = append(rf.UI.Nodes, traitNodes...) + rf.UI.UpdateNodeValuesFromJSON(traits, "traits", group) + } + + return err + case *settings.Flow: + return err + } + + 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) + default: + // do nothing + } + + // 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(context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) ProcessIDToken(r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { + verifier, ok := provider.(IDTokenVerifier) + if !ok { + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("The provider %s does not support id_token verification", provider.Config().Provider))) + } + t0 := time.Now() + claims, err := verifier.Verify(r.Context(), idToken) + reqlog.AccumulateExternalLatency(r.Context(), time.Since(t0)) + if err != nil { + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrForbidden().WithReasonf("Could not verify id_token").WithWrap(err).WithError(err.Error()))) + } + + if err := claims.Validate(); err != nil { + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrForbidden().WithReasonf("The id_token claims were invalid").WithWrap(err))) + } + + // First check if the JWT contains the nonce claim. + if claims.Nonce == "" { + // If it doesn't, check if the provider supports nonces. + if nonceSkipper, ok := verifier.(NonceValidationSkipper); !ok || !nonceSkipper.CanSkipNonce(claims) { + // If the provider supports nonces, abort the flow! + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("No nonce was included in the id_token but is required by the provider"))) + } + // If the provider does not support nonces, we don't do validation and return the claim. + // This case only applies to Apple, as some of their devices do not support nonces. + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple + } else if idTokenNonce == "" { + // A nonce was present in the JWT token, but no nonce was submitted in the flow + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("No nonce was provided but is required by the provider"))) + } else if idTokenNonce != claims.Nonce { + // The nonce from the JWT token does not match the nonce from the flow. + return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("The supplied nonce does not match the nonce from the id_token"))) + } + // Nonce checking was successful + + return claims, nil +} + +func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, tokens *identity.CredentialsOIDCEncryptedTokens, provider, subject, organization string) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.linkCredentials", trace.WithAttributes( + attribute.String("provider", provider), + // attribute.String("subject", subject), // PII + attribute.String("organization", organization))) + defer otelx.End(span, &err) + + if len(i.Credentials) == 0 { + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { + return err + } + } + + var conf identity.CredentialsOIDC + creds, err := i.ParseCredentials(s.ID(), &conf) + if errors.Is(err, herodot.ErrNotFound()) { + var err error + if creds, err = identity.NewOIDCLikeCredentials(tokens, s.ID(), provider, subject, organization); err != nil { + return err + } + } else if err != nil { + return err + } else { + creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider, subject)) + conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ + Subject: subject, + Provider: provider, + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), + InitialIDToken: tokens.GetIDToken(), + Organization: organization, + }) + + creds.Config, err = json.Marshal(conf) + if err != nil { + return err + } + } + + i.Credentials[s.ID()] = *creds + if orgID, err := uuid.FromString(organization); err == nil { + i.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} + } + + 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, p.AuthCodeURLOptions(req)...) + opts = append(opts, UpstreamParameters(upstreamParameters)...) + + 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 new file mode 100644 index 0000000..22c918c --- /dev/null +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -0,0 +1,388 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "bytes" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/phayes/freeport" + "github.com/rakutentech/jwk-go/jwk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + + "github.com/ory/dockertest/v4" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + "github.com/ory/x/ioutilx" +) + +type idTokenClaims struct { + traits struct { + website string + groups []string + } + metadataPublic struct { + picture string + } + metadataAdmin struct { + phoneNumber string + } +} + +func (token idTokenClaims) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + IDToken struct { + Website string `json:"website,omitempty"` + Groups []string `json:"groups,omitempty"` + Picture string `json:"picture,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + } `json:"id_token"` + }{ + IDToken: struct { + Website string `json:"website,omitempty"` + Groups []string `json:"groups,omitempty"` + Picture string `json:"picture,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + }{ + Website: token.traits.website, + Groups: token.traits.groups, + Picture: token.metadataPublic.picture, + PhoneNumber: token.metadataAdmin.phoneNumber, + }, + }) +} + +func createClient(t *testing.T, remote string, redir []string) (id, secret string) { + client := testhelpers.NewTestClient(t) + require.EventuallyWithT(t, func(t *assert.CollectT) { + 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"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` + }{ + GrantTypes: []string{"authorization_code", "refresh_token"}, + ResponseTypes: []string{"code"}, + Scope: "offline offline_access openid", + 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/pkg/token.go#L227-L242 + TokenEndpointAuthMethod: "client_secret_post", + })) + + res, err := client.Post(remote+"/admin/clients", "application/json", &b) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body := ioutilx.MustReadAll(res.Body) + require.Equal(t, http.StatusCreated, res.StatusCode) + + id = gjson.GetBytes(body, "client_id").String() + secret = gjson.GetBytes(body, "client_secret").String() + }, time.Minute, time.Second) + return +} + +type hydraFlowParams struct { + subject string + claims idTokenClaims + scope []string + // acr, when non-nil, is returned as the upstream `acr` + // claim on the login accept. The empty string is omitted from the + // payload (the harness uses `omitempty` on the wire format). + acr *string + // amr, when non-nil, is returned as the upstream `amr` + // claim on the login accept. An empty slice is omitted from the + // payload (the harness uses `omitempty` on the wire format). + amr []string +} + +func wrapClientForHydraLoginConsent(t *testing.T, wrapped *http.Client, hydraAdmin string, params hydraFlowParams) *http.Client { + adminClient := testhelpers.NewTestClient(t) + doPut := func(href, payload string) string { + req, err := http.NewRequest("PUT", hydraAdmin+href, strings.NewReader(payload)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + res, err := adminClient.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body := ioutilx.MustReadAll(res.Body) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + + redirectTo := gjson.GetBytes(body, "redirect_to") + require.Truef(t, redirectTo.Exists(), "%s", body) + require.NotEmptyf(t, redirectTo.Str, "%s", body) + + return redirectTo.Str + } + + c := *wrapped + tsp := c.Transport + if tsp == nil { + tsp = http.DefaultTransport + } + c.Transport = httpx.TransportFunc(func(req *http.Request) (*http.Response, error) { + if strings.HasPrefix(req.URL.String(), hydraLoginURL) { + challenge := req.URL.Query().Get("login_challenge") + require.NotEmptyf(t, challenge, "%s", req.URL) + + payload := map[string]any{ + "subject": params.subject, + } + if params.acr != nil { + payload["acr"] = *params.acr + } + if params.amr != nil { + payload["amr"] = params.amr + } + body, err := json.Marshal(payload) + require.NoError(t, err) + + redirectTo := doPut("/admin/oauth2/auth/requests/login/accept?login_challenge="+challenge, string(body)) + req, err := http.NewRequest("GET", redirectTo, nil) + require.NoError(t, err) + return c.Do(req) + } + if strings.HasPrefix(req.URL.String(), hydraConsentURL) { + challenge := req.URL.Query().Get("consent_challenge") + require.NotEmptyf(t, challenge, "%s", req.URL) + + body, err := json.Marshal(map[string]any{ + "grant_scope": params.scope, + "session": params.claims, + }) + require.NoError(t, err) + redirectTo := doPut("/admin/oauth2/auth/requests/consent/accept?consent_challenge="+challenge, string(body)) + req, err := http.NewRequest("GET", redirectTo, nil) + require.NoError(t, err) + return c.Do(req) + } + return tsp.RoundTrip(req) + }) + return &c +} + +func newUI(t *testing.T, reg driver.Registry) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var e interface{} + var err error + switch r.URL.Path { + case "/login": + e, err = reg.LoginFlowPersister().GetLoginFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + case "/registration": + e, err = reg.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + case "/settings": + e, err = reg.SettingsFlowPersister().GetSettingsFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow"))) + } + + if err != nil { + reg.Writer().Write(w, r, err) + return + } + reg.Writer().Write(w, r, e) + })) + t.Cleanup(ts.Close) + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, ts.URL+"/login") + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceRegistrationUI, ts.URL+"/registration") + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings") + return ts +} + +// These URLs are not actually resolving, but instead the client is catching the redirect and handling it. +const ( + hydraLoginURL = "https://hydra-placeholder-ui/login" + hydraConsentURL = "https://hydra-placeholder-ui/consent" +) + +func newHydra(t *testing.T) (remoteAdmin, remotePublic string) { + remoteAdmin = os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_ADMIN") + remotePublic = os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_PUBLIC") + + start := time.Now() + if remotePublic == "" && remoteAdmin == "" { + t.Logf("Environment did not provide Ory Hydra, starting fresh.") + pool := dockertest.NewPoolT(t, "") + + // The public host port must be known before the container starts because + // it is part of URLS_SELF_ISSUER. freeport releases the port before Docker + // binds it, so another process can grab it in between — retry on that race. + var hydra dockertest.ClosableResource + ctx := t.Context() + require.EventuallyWithT(t, func(t *assert.CollectT) { + publicPort, err := freeport.GetFreePort() + require.NoError(t, err) + + hydra, err = pool.Run(ctx, "oryd/hydra", + // Keep tag in sync with the version in ci.yaml + dockertest.WithTag("v2.2.0"), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{ + "DSN=memory", + fmt.Sprintf("URLS_SELF_ISSUER=http://127.0.0.1:%d/", publicPort), + "URLS_LOGIN=" + hydraLoginURL, + "URLS_CONSENT=" + hydraConsentURL, + "LOG_LEAK_SENSITIVE_VALUES=true", + "SECRETS_SYSTEM=someverylongsecretthatis32byteslong", + }), + dockertest.WithCmd([]string{"serve", "all", "--dev"}), + dockertest.WithContainerConfig(func(cc *container.Config) { + cc.ExposedPorts = network.PortSet{ + network.MustParsePort("4444/tcp"): struct{}{}, + network.MustParsePort("4445/tcp"): struct{}{}, + } + }), + dockertest.WithHostConfig(func(hc *container.HostConfig) { + hc.PortBindings = network.PortMap{ + network.MustParsePort("4444/tcp"): {{HostPort: strconv.Itoa(publicPort)}}, + network.MustParsePort("4445/tcp"): {{HostPort: ""}}, + } + }), + ) + require.NoError(t, err) + }, 2*time.Minute, 100*time.Millisecond) + hydra.Cleanup(t) + require.NotEmpty(t, hydra.GetPort("4444/tcp"), "%+v", hydra.Container().NetworkSettings.Ports) + require.NotEmpty(t, hydra.GetPort("4445/tcp"), "%+v", hydra.Container) + + remotePublic = "http://127.0.0.1:" + hydra.GetPort("4444/tcp") + remoteAdmin = "http://127.0.0.1:" + hydra.GetPort("4445/tcp") + + client := testhelpers.NewTestClient(t) + require.EventuallyWithT(t, func(t *assert.CollectT) { + res, err := client.Get(remotePublic + "/health/ready") + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = client.Get(remotePublic + "/.well-known/openid-configuration") + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = client.Get(remoteAdmin + "/health/ready") + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + }, 30*time.Second, 100*time.Millisecond) + } + + t.Logf("Ory Hydra started after %s at: %s %s", time.Since(start), remotePublic, remoteAdmin) + + return remoteAdmin, remotePublic +} + +func newOIDCProvider( + t *testing.T, + kratos *httptest.Server, + hydraPublic string, + hydraAdmin string, + id string, + opts ...func(*oidc.Configuration), +) oidc.Configuration { + clientID, secret := createClient(t, hydraAdmin, []string{kratos.URL + oidc.RouteBase + "/callback/" + id, kratos.URL + oidc.RouteCallbackGeneric}) + + cfg := oidc.Configuration{ + Provider: "generic", + ID: id, + ClientID: clientID, + ClientSecret: secret, + IssuerURL: hydraPublic + "/", + Mapper: "file://./stub/oidc.hydra.jsonnet", + } + for _, opt := range opts { + opt(&cfg) + } + + return cfg +} + +func setProviderConfig(t *testing.T, conf *config.Config, providers ...oidc.Configuration) { + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + currentConfig := conf.GetProvider(t.Context()).Get(baseKey + ".config") + currentEnabled := conf.GetProvider(t.Context()).Get(baseKey + ".enabled") + + conf.MustSet(t.Context(), baseKey+".config", &oidc.ConfigurationCollection{Providers: providers}) + conf.MustSet(t.Context(), baseKey+".enabled", true) + + t.Cleanup(func() { + conf.MustSet(t.Context(), baseKey+".config", currentConfig) + conf.MustSet(t.Context(), baseKey+".enabled", currentEnabled) + }) +} + +// AssertSystemError asserts an error ui response +func AssertSystemError(t *testing.T, errTS *httptest.Server, res *http.Response, body []byte, code int, reason string) { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + + assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "reason").String(), reason, "%s", body) +} + +//go:embed stub/jwk.json +var rawKey []byte + +//go:embed stub/jwks_public.json +var publicJWKS []byte + +// Just a public key set, to be able to test what happens if an ID token was issued by a different private key. +// +//go:embed stub/jwks_public2.json +var publicJWKS2 []byte + +func createIDToken(t *testing.T, cl jwt.RegisteredClaims) string { + key := &jwk.KeySpec{} + require.NoError(t, json.Unmarshal(rawKey, key)) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, &struct { + *jwt.RegisteredClaims + Email string `json:"email"` + }{ + RegisteredClaims: &cl, + Email: "acme@ory.sh", + }) + token.Header["kid"] = key.KeyID + s, err := token.SignedString(key.Key) + require.NoError(t, err) + return s +} diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go new file mode 100644 index 0000000..9eeb5c1 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_login.go @@ -0,0 +1,717 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "cmp" + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/json" + "net/http" + "reflect" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "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/kratos/x/nosurfx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +var ( + _ login.AAL1FormHydrator = (*Strategy)(nil) + _ login.Strategy = (*Strategy)(nil) +) + +// Update Login Flow with OpenID Connect Method +// +// swagger:model updateLoginFlowWithOidcMethod +type UpdateLoginFlowWithOidcMethod struct { + // The provider to register with + // + // required: true + Provider string `json:"provider"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // Method to use + // + // This field must be set to `oidc` when using the oidc method. + // + // required: true + Method string `json:"method"` + + // The identity traits. This is a placeholder for the registration flow. + Traits json.RawMessage `json:"traits"` + + // 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: + // - `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`. + // - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + // + // required: false + UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // 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 + // required: false + 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. + // + // 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) handleConflictingIdentity(ctx context.Context, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (verdict ConflictingIdentityVerdict, id *identity.Identity, credentials *identity.Credentials, err error) { + if s.conflictingIdentityPolicy == nil && s.d.Config().SelfServiceFlowRegistrationEnabled(ctx) { + // Without a conflicting-identity policy there is nothing to merge. The + // conflict lookup below is still required when registration is + // disabled, because ProcessLogin uses its result to distinguish + // account linking from a genuine new sign-up. + return ConflictingIdentityVerdictReject, nil, nil, nil + } + + // Find out if there is a conflicting identity + newIdentity, va, err := s.newIdentityFromClaims(ctx, claims, provider, container, loginFlow.IdentitySchema) + if err != nil { + return ConflictingIdentityVerdictReject, nil, nil, nil + } + + // Validate the identity itself + // We ignore the error here because the claims may not fulfil the requirements + // of the identity schema. + // + // However, this is not a problem because the identity will be merged with the existing + // identity and the existing identity will be updated with the new credentials, but not any traits. + // + // We do need the validation step however, to "hydrate" the verifiable address of the user, which is then + // used in subsequent calls to match the existing with the new identity. + _ = s.d.IdentityValidator().Validate(ctx, newIdentity) + + for n := range newIdentity.VerifiableAddresses { + verifiable := &newIdentity.VerifiableAddresses[n] + for _, verified := range va { + if verifiable.Via == verified.Via && verifiable.Value == verified.Value { + verifiable.Status = identity.VerifiableAddressStatusCompleted + verifiable.Verified = true + t := sqlxx.NullTime(time.Now().UTC().Round(time.Second)) + verifiable.VerifiedAt = &t + } + } + } + + creds, err := identity.NewOIDCLikeCredentials(token, s.ID(), provider.Config().ID, claims.Subject, provider.Config().OrganizationID) + if err != nil { + return ConflictingIdentityVerdictUnknown, nil, nil, err + } + + newIdentity.SetCredentials(s.ID(), *creds) + + existingIdentity, _, _, err := s.d.IdentityManager().ConflictingIdentity(ctx, newIdentity) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + // No existing identity matches the new identity's identifiers. + return ConflictingIdentityVerdictReject, nil, nil, nil + } + // Propagate infrastructure errors instead of misreporting them as + // "no conflicting identity", which would surface to the user as a + // misleading rejection (e.g. "registration disabled"). + return ConflictingIdentityVerdictUnknown, nil, nil, err + } + + if s.conflictingIdentityPolicy == nil { + return ConflictingIdentityVerdictReject, existingIdentity, creds, nil + } + + verdict = s.conflictingIdentityPolicy(ctx, existingIdentity, newIdentity, provider, claims) + if verdict == ConflictingIdentityVerdictMerge { + if err = existingIdentity.MergeOIDCCredentials(s.ID(), *creds); err != nil { + return ConflictingIdentityVerdictUnknown, nil, nil, err + } + + if err = s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, existingIdentity); err != nil { + return ConflictingIdentityVerdictUnknown, nil, nil, err + } + } + + return verdict, existingIdentity, creds, nil +} + +func verifiableAddressHash(i *identity.Identity) [sha256.Size]byte { + h := sha256.New() + for _, a := range i.VerifiableAddresses { + h.Write([]byte(a.Signature())) // sha256 Write never returns an error + } + return [sha256.Size]byte(h.Sum(nil)) +} + +func recoveryAddressHash(i *identity.Identity) [sha256.Size]byte { + h := sha256.New() + for _, a := range i.RecoveryAddresses { + h.Write([]byte(a.Signature())) // sha256 Write never returns an error + } + return [sha256.Size]byte(h.Sum(nil)) +} + +// jsonEqual reports whether two JSON values are deeply equal, ignoring key +// order and whitespace differences. Array element order is significant. +func jsonEqual(a, b json.RawMessage) bool { + if len(a) == 0 && len(b) == 0 { + return true + } + if len(a) == 0 || len(b) == 0 { + return false + } + var aVal, bVal any + if err := json.Unmarshal(a, &aVal); err != nil { + return false + } + if err := json.Unmarshal(b, &bVal); err != nil { + return false + } + return reflect.DeepEqual(aVal, bVal) +} + +// UpdateIdentityFromClaims re-runs the Jsonnet claims mapper and applies the +// result to an existing identity. It returns true if traits or metadata changed. +// Unlike registration, user-supplied form traits are not merged — only the +// mapper output is applied. +func (s *Strategy) UpdateIdentityFromClaims(ctx context.Context, claims *Claims, provider Provider, i *identity.Identity) (changed bool, err error) { + evaluated, _, err := s.EvaluateClaimsMapper(ctx, claims, provider, i) + if err != nil { + return false, err + } + + // Save the current state for comparison. + oldTraits := json.RawMessage(i.Traits) + oldMetadataPublic := json.RawMessage(i.MetadataPublic) + oldMetadataAdmin := json.RawMessage(i.MetadataAdmin) + oldVerifiableHash := verifiableAddressHash(i) + oldRecoveryHash := recoveryAddressHash(i) + + // Merge the mapper output with the existing identity traits. The mapper + // output takes precedence, but existing traits that the mapper does not + // output are preserved. + jsonTraits := gjson.Get(evaluated, "identity.traits") + if !jsonTraits.IsObject() { + return false, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!")) + } + // merge(override, base) merges base into override, with override winning. + // Pass mapper output as override so it takes precedence over existing traits, + // while traits not in the mapper output are preserved from the base. + mergedTraits, err := merge(json.RawMessage(jsonTraits.Raw), json.RawMessage(i.Traits)) + if err != nil { + return false, err + } + i.Traits = mergedTraits + + // Only update metadata if the mapper explicitly outputs the key. When the + // mapper omits metadata_public or metadata_admin (common for mappers + // written for registration only), we preserve the existing values rather + // than wiping them. + if gjson.Get(evaluated, string(PublicMetadata)).Exists() { + if err = s.setMetadata(evaluated, i, PublicMetadata); err != nil { + return false, err + } + } + if gjson.Get(evaluated, string(AdminMetadata)).Exists() { + if err = s.setMetadata(evaluated, i, AdminMetadata); err != nil { + return false, err + } + } + + // Save existing addresses before Validate regenerates them from the schema. + oldVerifiableAddresses := make([]identity.VerifiableAddress, len(i.VerifiableAddresses)) + copy(oldVerifiableAddresses, i.VerifiableAddresses) + oldRecoveryAddresses := make([]identity.RecoveryAddress, len(i.RecoveryAddresses)) + copy(oldRecoveryAddresses, i.RecoveryAddresses) + + // Validate the updated identity against the schema to prevent persisting + // invalid traits produced by a broken mapper. + if err = s.d.IdentityValidator().Validate(ctx, i); err != nil { + return false, err + } + + // Restore existing verification and recovery state for addresses that + // survived validation. Validate regenerates both VerifiableAddresses and + // RecoveryAddresses from the schema, losing IDs, verified/verifiedAt + // status, etc. Carry over the state for matching addresses. + for n := range i.VerifiableAddresses { + addr := &i.VerifiableAddresses[n] + for _, old := range oldVerifiableAddresses { + if addr.Via == old.Via && addr.Value == old.Value { + *addr = old + break + } + } + } + for n := range i.RecoveryAddresses { + addr := &i.RecoveryAddresses[n] + for _, old := range oldRecoveryAddresses { + if addr.Via == old.Via && addr.Value == old.Value { + *addr = old + break + } + } + } + + // Apply verified addresses from the mapper output, matching the + // registration behavior. This lets the mapper carry over the verified + // status from the SSO provider on every login (e.g., when the user's + // email changes upstream). + va, err := s.extractVerifiedAddresses(evaluated) + if err != nil { + return false, err + } + for n := range i.VerifiableAddresses { + verifiable := &i.VerifiableAddresses[n] + for _, verified := range va { + if verifiable.Via == verified.Via && verifiable.Value == verified.Value { + if !verifiable.Verified { + verifiable.Status = identity.VerifiableAddressStatusCompleted + verifiable.Verified = true + t := sqlxx.NullTime(time.Now().UTC().Round(time.Second)) + verifiable.VerifiedAt = &t + } + } + } + } + + changed = !jsonEqual(oldTraits, json.RawMessage(i.Traits)) || + !jsonEqual(oldMetadataPublic, json.RawMessage(i.MetadataPublic)) || + !jsonEqual(oldMetadataAdmin, json.RawMessage(i.MetadataAdmin)) || + oldVerifiableHash != verifiableAddressHash(i) || + oldRecoveryHash != recoveryAddressHash(i) + + s.d.Logger(). + WithField("oidc_provider", provider.Config().ID). + WithField("identity_id", i.ID). + WithField("identity_changed", changed). + Debug("Re-evaluated OpenID Connect claims mapper on login.") + + return changed, nil +} + +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, s.ID(), identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + var verdict ConflictingIdentityVerdict + verdict, i, c, err = s.handleConflictingIdentity(ctx, loginFlow, token, claims, provider, container) + switch verdict { + case ConflictingIdentityVerdictMerge: + // Do nothing + case ConflictingIdentityVerdictReject: + // If no account was found we're "manually" creating a new registration flow and redirecting the browser + // to that endpoint. + + // That will execute the "pre registration" hook which allows to e.g. disallow this request. The registration + // ui however will NOT be shown, instead the user is directly redirected to the auth path. That should then + // do a silent re-request. While this might be a bit excessive from a network perspective it should usually + // happen without any downsides to user experience as the flow has already been authorized and should + // not need additional consent/login. + + // This is kinda hacky but the only way to ensure seamless login/registration flows when using OIDC. + s.d. + Logger(). + WithField("provider", provider.Config().ID). + WithField("subject", claims.Subject). + Debug("Received successful OpenID Connect callback but user is not registered. Re-initializing registration flow now.") + + if !s.d.Config().SelfServiceFlowRegistrationEnabled(ctx) && i == nil { + // Registration is disabled and no existing identity matches the + // provider claims, so this is a genuine new sign-up and not an + // account linking attempt. Reject it instead of converting the + // flow. + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(registration.ErrRegistrationDisabled())) + } + + var opts []registration.FlowOption + if i != nil { + // A conflicting identity exists, so submitting this flow can + // only link to that identity, never create a new one. Mark it + // as an internal login → registration conversion for account + // linking: such flows may exist while registration is disabled. + // Genuine sign-ups (no conflicting identity) stay unmarked and + // are only reachable while registration is enabled. + opts = append(opts, registration.WithFlowAccountLinking()) + } + // If return_to was set before, we need to preserve it. + if len(loginFlow.ReturnTo) > 0 { + opts = append(opts, registration.WithFlowReturnTo(loginFlow.ReturnTo)) + } + + if loginFlow.OAuth2LoginChallenge.String() != "" { + opts = append(opts, registration.WithFlowOAuth2LoginChallenge(loginFlow.OAuth2LoginChallenge.String())) + } + + registrationFlow, err := s.d.RegistrationHandler().NewRegistrationFlow(w, r, loginFlow.Type, opts...) + if err != nil { + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + } + + err = s.d.SessionTokenExchangePersister().MoveToNewFlow(ctx, loginFlow.ID, registrationFlow.ID) + if err != nil { + 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.TransientPayload = loginFlow.TransientPayload + registrationFlow.Active = s.ID() + registrationFlow.IdentitySchema = loginFlow.IdentitySchema + + // We are converting the flow here, but want to retain the original request URL. + registrationFlow.RequestURL = loginFlow.RequestURL + + if _, err := s.processRegistration(ctx, w, r, registrationFlow, token, claims, provider, container); err != nil { + return registrationFlow, err + } + + return nil, nil + case ConflictingIdentityVerdictUnknown: + fallthrough + default: + // This should never happen if err == nil, but just for safety: + if err != nil { + return nil, err + } + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("The OpenID Connect identity merge policy returned an unknown verdict without other error details, which prevents the sign up from completing. Please report this as a bug.")) + } + + } else { + 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(ctx, w, r, loginFlow, provider.Config().ID, nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("The OpenID Connect credentials could not be decoded properly").WithDebug(err.Error())), i.ID)) + } + + sess := session.NewInactiveSession() + sess.CompletedLoginForOIDC( + s.ID(), + provider.Config().AALForClaims(claims), + provider.Config().ID, + provider.Config().OrganizationID, + claims.ACR, + claims.AMR, + ) + + for _, c := range oidcCredentials.Providers { + if c.Subject == claims.Subject && c.Provider == provider.Config().ID { + if provider.Config().UpdateIdentityOnLogin == UpdateIdentityOnLoginAutomatic { + identityChanged, err := s.UpdateIdentityFromClaims(ctx, claims, provider, i) + if err != nil { + return nil, x.WrapWithIdentityIDError(s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err), i.ID) + } + if identityChanged { + if err := s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i); err != nil { + return nil, x.WrapWithIdentityIDError(s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err), i.ID) + } + } + } + + if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { + return nil, x.WrapWithIdentityIDError(s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err), i.ID) + } + return nil, nil + } + } + + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, x.WrapWithIdentityIDError(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)), i.ID)) +} + +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") + 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(ctx, &p, r, &f.IdentitySchema); err != nil { + return nil, s.HandleError(ctx, w, r, f, "", nil, err) + } + + f.IDToken = p.IDToken + f.RawIDTokenNonce = p.IDTokenNonce + f.TransientPayload = p.TransientPayload + + // Test flows use a flow-owned random UUID as the CSRF bearer (no browser + // cookie is involved in their creation). Verify it in constant time so + // that an attacker who discovers the flow ID alone cannot initiate a + // parallel OIDC round-trip against the flow. + if f.IsTest() { + if subtle.ConstantTimeCompare([]byte(p.CSRFToken), []byte(f.CSRFToken)) != 1 { + return nil, s.HandleError(ctx, w, r, f, "", nil, errors.WithStack(nosurfx.ErrInvalidCSRFToken())) + } + } + + 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) + } + + 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.Logger(). + WithRequest(r). + 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(ctx, w, r, f, pid, nil, s.handleMethodNotAllowedError(err)) + } + + provider, err := s.Provider(ctx, pid) + if err != nil { + return nil, s.HandleError(ctx, w, r, f, pid, nil, err) + } + + req, err := s.validateFlow(ctx, r, f.ID, oidcv1.FlowKind_FLOW_KIND_LOGIN) + if err != nil { + return nil, s.HandleError(ctx, 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(r, provider, p.IDToken, p.IDTokenNonce) + if err != nil { + return nil, s.HandleError(ctx, w, r, f, pid, nil, err) + } + _, err = s.ProcessLogin(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ + FlowID: f.ID.String(), + Traits: p.Traits, + }) + if errors.Is(err, flow.ErrCompletedByStrategy) { + return nil, err + } else if err != nil { + return nil, s.HandleError(ctx, w, r, f, pid, nil, err) + } + return nil, errors.WithStack(flow.ErrCompletedByStrategy) + } + + state, pkce, err := s.GenerateState(ctx, provider, f, x.RequestBaseURL(r)) + if err != nil { + return nil, s.HandleError(ctx, w, r, f, pid, nil, err) + } + var refStore continuity.ContainerReferenceStore + if f.Type == flow.TypeAPI { + refStore = continuity.NewInternalContextReferenceStore(f) + } else { + refStore = continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)) + } + + if _, err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, refStore, + continuity.WithPayload(&AuthCodeContainer{ + State: state, + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, + IdentitySchema: f.IdentitySchema, + }), + continuity.WithLifespan(time.Minute*30), + ); err != nil { + 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(ctx, w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("Could not update flow").WithWrap(err))) + } + + var up map[string]string + if err := json.NewDecoder(bytes.NewBuffer(p.UpstreamParameters)).Decode(&up); err != nil { + return nil, err + } + + 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 { + http.Redirect(w, r, codeURL, http.StatusSeeOther) + } + + return nil, errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, lf *login.Flow, _ *session.Session) 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, s.ID()) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} + +func (s *Strategy) removeProviders(conf *ConfigurationCollection, f *login.Flow) { + for _, l := range conf.Providers { + group := node.OpenIDConnectGroup + if s.ID() == identity.CredentialsTypeSAML { + group = node.SAMLGroup + } + + if l.OrganizationID != "" { + continue + } + + f.GetUI().Nodes.RemoveMatching(&node.Node{ + Group: group, + Type: node.Input, + Attributes: &node.InputAttributes{ + Name: "provider", + FieldValue: l.ID, + }, + }) + } +} + +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 + } + + if o.IdentityHint != nil { + // We found no credentials. We remove all the providers and tell the strategy that we found nothing. + // We only execute this, if the identity hint is set, otherwise we do not know if the user has any credentials and we likely stay on the `provide_credentials` screen. + // The OIDC method is special in that regard, as it's the only method showing buttons on that screen. + s.removeProviders(conf, f) + } + 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! + s.removeProviders(conf, f) + + for _, l := range linked { + lc := l.Config() + + // Organizations are handled differently. + if lc.OrganizationID != "" { + continue + } + AddProvider(f.UI, lc.ID, text.NewInfoLoginWith(cmp.Or(lc.Label, lc.ID), lc.ID), s.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 0000000..b514a5a --- /dev/null +++ b/selfservice/strategy/oidc/strategy_login_test.go @@ -0,0 +1,154 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "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() + providerID := "test-provider" + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, true)), + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config.providers", + []map[string]any{{ + "provider": "generic", + "id": providerID, + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }}, + ), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/stub.schema.json")), + ) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + fh, ok := s.(login.AAL1FormHydrator) + 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(reg, r, flow.TypeBrowser) + require.NoError(t, err) + return r, 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(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + 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.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + 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.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + 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 := contextx.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity(providerID) + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + 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 := contextx.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 new file mode 100644 index 0000000..00e7901 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -0,0 +1,584 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "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/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/decoderx" + "github.com/ory/x/fetcher" + "github.com/ory/x/otelx" + "github.com/ory/x/region" + "github.com/ory/x/sqlxx" +) + +var ( + _ registration.Strategy = (*Strategy)(nil) + _ registration.FormHydrator = (*Strategy)(nil) +) + +var jsonnetCache, _ = ristretto.NewCache(&ristretto.Config[[]byte, []byte]{ + MaxCost: 100 << 20, // 100MB, + NumCounters: 1_000_000, // 1kB per snippet -> 100k snippets -> 1M counters + BufferItems: 64, +}) + +type MetadataType string + +type VerifiedAddress struct { + Value string `json:"value"` + Via string `json:"via"` +} + +const ( + VerifiedAddressesKey = "identity.verified_addresses" + + PublicMetadata MetadataType = "identity.metadata_public" + AdminMetadata MetadataType = "identity.metadata_admin" +) + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + return s.populateMethod(r, f, text.NewInfoRegistrationWith) +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + return s.populateMethod(r, f, text.NewInfoRegistrationWith) +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + return s.populateMethod(r, f, text.NewInfoRegistrationWith) +} + +// Update Registration Flow with OpenID Connect Method +// +// swagger:model updateRegistrationFlowWithOidcMethod +type UpdateRegistrationFlowWithOidcMethod struct { + // The provider to register with + // + // required: true + Provider string `json:"provider"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // The identity traits + Traits json.RawMessage `json:"traits"` + + // Method to use + // + // This field must be set to `oidc` when using the oidc method. + // + // required: true + Method string `json:"method"` + + // 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: + // - `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`. + // - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + // + // required: false + UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // 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 + // required: false + 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. + // + // 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(ctx context.Context, p interface{}, r *http.Request, identitySchema *flow.IdentitySchema) error { + ds, err := identitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + raw, err := sjson.SetBytes(linkSchema, "properties.traits.$ref", ds.String()+"#/properties/traits") + if err != nil { + return errors.WithStack(err) + } + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(raw) + if err != nil { + return errors.WithStack(err) + } + + if err := decoderx.Decode(r, &p, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return errors.WithStack(err) + } + + return nil +} + +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(ctx, &p, r, &f.IdentitySchema); err != nil { + return s.HandleError(ctx, w, r, f, "", nil, err) + } + + 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.Logger(). + WithRequest(r). + 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(ctx, w, r, f, pid, nil, s.handleMethodNotAllowedError(err)) + } + + provider, err := s.Provider(ctx, pid) + if err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + + req, err := s.validateFlow(ctx, r, f.ID, oidcv1.FlowKind_FLOW_KIND_REGISTRATION) + if err != nil { + return s.HandleError(ctx, 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(r, provider, p.IDToken, p.IDTokenNonce) + if err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + _, err = s.processRegistration(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, + IdentitySchema: f.IdentitySchema, + }) + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } else if err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + + state, pkce, err := s.GenerateState(ctx, provider, f, x.RequestBaseURL(r)) + if err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + + var refStore continuity.ContainerReferenceStore + if f.Type == flow.TypeAPI { + refStore = continuity.NewInternalContextReferenceStore(f) + } else { + refStore = continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)) + } + + if _, err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, refStore, + continuity.WithPayload(&AuthCodeContainer{ + State: state, + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, + IdentitySchema: f.IdentitySchema, + }), + continuity.WithLifespan(time.Minute*30), + ); err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + + // Native/API flows have no cookie — the continuity container ID was written to + // the flow's in-memory InternalContext. Persist the flow so the callback can + // restore it from the database. + if f.Type == flow.TypeAPI { + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return s.HandleError(ctx, w, r, f, pid, nil, err) + } + } + + var up map[string]string + if err := json.NewDecoder(bytes.NewBuffer(p.UpstreamParameters)).Decode(&up); err != nil { + return err + } + + 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 { + http.Redirect(w, r, codeURL, http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +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 { + opts = append(opts, login.WithFlowReturnTo(rf.ReturnTo)) + } + + if len(rf.UI.Messages) > 0 { + opts = append(opts, login.WithFormErrorMessage(rf.UI.Messages)) + } + + opts = append(opts, login.WithInternalContext(rf.InternalContext), login.WithIsAccountLinking()) + if rf.OAuth2LoginChallenge != "" { + opts = append(opts, login.WithLoginChallenge(rf.OAuth2LoginChallenge.String())) + } + + lf, _, err := s.d.LoginHandler().NewLoginFlow(w, r, rf.Type, opts...) + if err != nil { + return nil, err + } + + err = s.d.SessionTokenExchangePersister().MoveToNewFlow(ctx, rf.ID, lf.ID) + if err != nil { + return nil, err + } + + // In this scenario, we are performing account linking. The request URL is set to the "original" registration URL + // the user used to try and sign up (which triggered the account linking flow). + // + // In this scenario we want to keep the original request url instead of the current request url, as the current + // request url is an "in-between" state where we are half-way through performing account linking. + lf.RequestURL = rf.RequestURL + lf.TransientPayload = rf.TransientPayload + lf.Active = s.ID() + lf.OrganizationID = rf.OrganizationID + lf.IdentitySchema = rf.IdentitySchema + + return lf, 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, s.ID(), 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 + // ui however will NOT be shown, instead the user is directly redirected to the auth path. That should then + // do a silent re-request. While this might be a bit excessive from a network perspective it should usually + // happen without any downsides to user experience as the request has already been authorized and should + // not need additional consent/login. + + // This is kinda hacky but the only way to ensure seamless login/registration flows when using OIDC. + s.d.Logger().WithRequest(r).WithField("Provider", provider.Config().ID). + WithField("subject", claims.Subject). + Debug("Received successful OpenID Connect callback but user is already registered. Re-initializing login flow now.") + + lf, err := s.registrationToLogin(ctx, w, r, rf) + if err != nil { + return nil, s.HandleError(ctx, 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 + } + + i, va, err := s.newIdentityFromClaims(ctx, claims, provider, container, rf.IdentitySchema) + if err != nil { + return nil, s.HandleError(ctx, w, r, rf, provider.Config().ID, nil, err) + } + + // Validate the identity itself + 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 { + verifiable := &i.VerifiableAddresses[n] + for _, verified := range va { + if verifiable.Via == verified.Via && verifiable.Value == verified.Value { + verifiable.Status = identity.VerifiableAddressStatusCompleted + verifiable.Verified = true + t := sqlxx.NullTime(time.Now().UTC().Round(time.Second)) + verifiable.VerifiedAt = &t + } + } + } + + creds, err := identity.NewOIDCLikeCredentials(token, s.ID(), provider.Config().ID, claims.Subject, provider.Config().OrganizationID) + if err != nil { + return nil, s.HandleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) + } + + i.SetCredentials(s.ID(), *creds) + if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, rf, i, session.AuthenticationMethod{ + Method: s.ID(), + AAL: provider.Config().AALForClaims(claims), + Provider: provider.Config().ID, + Organization: provider.Config().OrganizationID, + UpstreamACR: claims.ACR, + UpstreamAMR: claims.AMR, + }); err != nil { + return nil, s.HandleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) + } + + return nil, nil +} + +// EvaluateClaimsMapper runs the Jsonnet mapper for the given claims and returns +// the evaluated JSON string. If currentIdentity is non-nil, its traits and +// metadata are made available in the Jsonnet context as std.extVar('identity'). +func (s *Strategy) EvaluateClaimsMapper(ctx context.Context, claims *Claims, provider Provider, currentIdentity *identity.Identity) (evaluated string, claimsJSON []byte, err error) { + fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(ctx)), fetcher.WithCache(jsonnetCache, 60*time.Minute)) + jsonnetSnippet, err := fetch.FetchContext(ctx, provider.Config().Mapper) + if err != nil { + return "", nil, err + } + + var jsonClaims bytes.Buffer + if err = json.NewEncoder(&jsonClaims).Encode(claims); err != nil { + return "", nil, err + } + + defer func() { + if err != nil { + trace.SpanFromContext(ctx).AddEvent(events.NewJsonnetMappingFailed( + ctx, err, jsonClaims.Bytes(), evaluated, provider.Config().Provider, s.ID().String(), + )) + } + }() + + vm, err := s.d.JsonnetVM(ctx) + if err != nil { + return "", nil, err + } + + vm.ExtCode("claims", jsonClaims.String()) + + if currentIdentity != nil { + // Coerce nil/empty fields to empty JSON objects so that Jsonnet + // mappers can safely access identity.traits, identity.metadata_public, + // and identity.metadata_admin without null-reference errors. + coalesce := func(b json.RawMessage) json.RawMessage { + if len(b) == 0 { + return json.RawMessage("{}") + } + return b + } + identityCtx, err := json.Marshal(map[string]json.RawMessage{ + "traits": coalesce(json.RawMessage(currentIdentity.Traits)), + "metadata_public": coalesce(json.RawMessage(currentIdentity.MetadataPublic)), + "metadata_admin": coalesce(json.RawMessage(currentIdentity.MetadataAdmin)), + }) + if err != nil { + return "", nil, err + } + vm.ExtCode("identity", string(identityCtx)) + } else { + vm.ExtCode("identity", `{"traits": {}, "metadata_public": {}, "metadata_admin": {}}`) + } + + evaluated, err = vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jsonnetSnippet.String()) + if err != nil { + return "", nil, err + } + + return evaluated, jsonClaims.Bytes(), nil +} + +func (s *Strategy) newIdentityFromClaims(ctx context.Context, claims *Claims, provider Provider, container *AuthCodeContainer, schema flow.IdentitySchema) (_ *identity.Identity, _ []VerifiedAddress, err error) { + evaluated, _, err := s.EvaluateClaimsMapper(ctx, claims, provider, nil) + if err != nil { + return nil, nil, err + } + + i := identity.NewIdentity(schema.ID(ctx, s.d.Config())) + if err = s.setTraits(provider, container, evaluated, i); err != nil { + return nil, nil, err + } + + if err = s.setMetadata(evaluated, i, PublicMetadata); err != nil { + return nil, nil, err + } + + if err = s.setMetadata(evaluated, i, AdminMetadata); err != nil { + return nil, nil, err + } + + if err = setRegion(evaluated, i); err != nil { + return nil, nil, err + } + + va, err := s.extractVerifiedAddresses(evaluated) + if err != nil { + return nil, nil, err + } + + if orgID, parseErr := uuid.FromString(provider.Config().OrganizationID); parseErr == nil { + i.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} + } + + s.d.Logger(). + WithField("oidc_provider", provider.Config().ID). + WithSensitiveField("oidc_claims", claims). + WithSensitiveField("mapper_jsonnet_output", evaluated). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Debug("OpenID Connect Jsonnet mapper completed.") + return i, va, nil +} + +func (s *Strategy) setTraits(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!")) + } + + if container != nil { + traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw)) + if err != nil { + return err + } + + i.Traits = traits + } else { + i.Traits = identity.Traits(jsonTraits.Raw) + } + s.d.Logger(). + WithField("oidc_provider", provider.Config().ID). + WithSensitiveField("identity_traits", i.Traits). + WithSensitiveField("mapper_jsonnet_output", evaluated). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Debug("Merged form values and OpenID Connect Jsonnet output.") + return nil +} + +func (s *Strategy) setMetadata(evaluated string, i *identity.Identity, m MetadataType) error { + if m != PublicMetadata && m != AdminMetadata { + return errors.Errorf("undefined metadata type: %s", m) + } + + metadata := gjson.Get(evaluated, string(m)) + if metadata.Exists() && !metadata.IsObject() { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key %s. Please check your Jsonnet code!", m)) + } + + switch m { + case PublicMetadata: + i.MetadataPublic = []byte(metadata.Raw) + case AdminMetadata: + i.MetadataAdmin = []byte(metadata.Raw) + } + + return nil +} + +// setRegion applies identity.region from the Jsonnet mapper to i. Absent +// or empty leaves Region unchanged so the persister's fallback chain runs. +func setRegion(evaluated string, ident *identity.Identity) error { + r := gjson.Get(evaluated, "identity.region") + if !r.Exists() { + return nil + } + if r.Type != gjson.String { + return errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("OpenID Connect Jsonnet mapper did not return a string for key identity.region. Please check your Jsonnet code!")) + } + candidate := region.Region(r.String()) + if !candidate.Valid() { + return errors.WithStack(region.NewErrInvalid()) + } + ident.Region = candidate + return nil +} + +func (s *Strategy) extractVerifiedAddresses(evaluated string) ([]VerifiedAddress, error) { + if verifiedAddresses := gjson.Get(evaluated, VerifiedAddressesKey); verifiedAddresses.Exists() { + if !verifiedAddresses.IsArray() { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("OpenID Connect Jsonnet mapper did not return an array for key %s. Please check your Jsonnet code!", VerifiedAddressesKey)) + } + + var va []VerifiedAddress + if err := json.Unmarshal([]byte(verifiedAddresses.Raw), &va); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Failed to unmarshal value for key %s. Please check your Jsonnet code!", VerifiedAddressesKey).WithDebugf("%s", err)) + } + + for i := range va { + va := &va[i] + if va.Via == identity.AddressTypeEmail { + va.Value = strings.ToLower(strings.TrimSpace(va.Value)) + } + } + + return va, nil + } + + return nil, nil +} diff --git a/selfservice/strategy/oidc/strategy_registration_region_test.go b/selfservice/strategy/oidc/strategy_registration_region_test.go new file mode 100644 index 0000000..939a9b5 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_registration_region_test.go @@ -0,0 +1,67 @@ +// Copyright © 2025 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/identity" + "github.com/ory/x/region" +) + +func TestSetRegion(t *testing.T) { + t.Parallel() + + t.Run("case=sets region from mapper output", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"region": "eu-central", "traits": {"subject": "alice@example.com"}}}` + require.NoError(t, setRegion(evaluated, i)) + assert.Equal(t, region.EUCentral, i.Region) + }) + + t.Run("case=leaves region empty when mapper output omits region", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"traits": {"subject": "alice@example.com"}}}` + require.NoError(t, setRegion(evaluated, i)) + assert.Equal(t, region.Region(""), i.Region) + }) + + t.Run("case=rejects empty region", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"region": "", "traits": {"subject": "alice@example.com"}}}` + err := setRegion(evaluated, i) + require.Error(t, err) + }) + + t.Run("case=rejects unknown region value", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"region": "not-a-region", "traits": {"subject": "alice@example.com"}}}` + err := setRegion(evaluated, i) + require.Error(t, err) + assert.Equal(t, region.Region(""), i.Region) + }) + + t.Run("case=rejects non-string region", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"region": 42, "traits": {"subject": "alice@example.com"}}}` + err := setRegion(evaluated, i) + require.Error(t, err) + }) + + t.Run("case=accepts super-region", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{} + evaluated := `{"identity": {"region": "eu", "traits": {"subject": "alice@example.com"}}}` + require.NoError(t, setRegion(evaluated, i)) + assert.Equal(t, region.EU, i.Region) + }) +} diff --git a/selfservice/strategy/oidc/strategy_registration_test.go b/selfservice/strategy/oidc/strategy_registration_test.go new file mode 100644 index 0000000..d44f78a --- /dev/null +++ b/selfservice/strategy/oidc/strategy_registration_test.go @@ -0,0 +1,119 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/snapshotx" +) + +func TestPopulateRegistrationMethod(t *testing.T) { + t.Parallel() + + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/registration.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, true)), + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config.providers", + []map[string]any{{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }}), + ) + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src")) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(ctx, t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSON(t, snapshots[1], snapshots[3]) + }) + }) +} diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go new file mode 100644 index 0000000..460671d --- /dev/null +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -0,0 +1,699 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "cmp" + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "slices" + "time" + + "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/kratos/continuity" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "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/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +//go:embed .schema/settings.schema.json +var settingsSchema []byte + +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: "#/", +} + +var UnlinkAllFirstFactorConnectionsError = &jsonschema.ValidationError{ + Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/", +} + +func (s *Strategy) SettingsStrategyID() string { + return s.ID().String() +} + +func (s *Strategy) decoderSettings(ctx context.Context, p *updateSettingsFlowWithOidcMethod, r *http.Request, settingsFlow *settings.Flow) error { + schema := flow.IdentitySchema(settingsFlow.Identity.SchemaID) + ds, err := schema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + raw, err := sjson.SetBytes(settingsSchema, + "properties.traits.$ref", ds.String()+"#/properties/traits") + if err != nil { + return errors.WithStack(err) + } + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(raw) + if err != nil { + return errors.WithStack(err) + } + + if err := decoderx.Decode(r, &p, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *Strategy) linkedProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { + creds, ok := confidential.GetCredentials(s.ID()) + if !ok { + return nil, nil + } + + var available identity.CredentialsOIDC + if err := json.Unmarshal(creds.Config, &available); err != nil { + return nil, errors.WithStack(err) + } + + var result []Provider + for _, p := range available.Providers { + prov, err := conf.Provider(p.Provider, s.d) + if errors.Is(err, herodot.ErrNotFound()) { + continue + } else if err != nil { + return nil, err + } + result = append(result, prov) + } + + return result, nil +} + +func (s *Strategy) linkableProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { + var available identity.CredentialsOIDC + creds, ok := confidential.GetCredentials(s.ID()) + if ok { + if err := json.Unmarshal(creds.Config, &available); err != nil { + return nil, errors.WithStack(err) + } + } + + var result []Provider + for _, p := range conf.Providers { + var found bool + for _, pp := range available.Providers { + if pp.Provider == p.ID { + found = true + break + } + } + + if !found { + prov, err := conf.Provider(p.ID, s.d) + if err != nil { + return nil, err + } + result = append(result, prov) + } + } + + return result, nil +} + +// settingsNodeGroup returns the UI node group for this strategy's settings +// link/unlink nodes. SAML nodes use the SAML group so a sibling strategy's +// group-scoped removal does not clobber them (both strategies share the OSS +// PopulateSettingsMethod, which names every link/unlink node "link"/"unlink"). +func (s *Strategy) settingsNodeGroup() node.UiNodeGroup { + if s.credType == identity.CredentialsTypeSAML { + return node.SAMLGroup + } + return node.OpenIDConnectGroup +} + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, sr *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + conf, err := s.Config(ctx) + if err != nil { + return err + } + + linkable, err := s.linkableProviders(conf, id) + if err != nil { + return err + } + + linked, err := s.linkedProviders(conf, id) + if err != nil { + return err + } + + group := s.settingsNodeGroup() + sr.UI.GetNodes().RemoveInGroup(group, "unlink", "link") + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + if sr.OrganizationID.Valid { + boundToFlowOrg := id.OrganizationID.Valid && id.OrganizationID.UUID == sr.OrganizationID.UUID + + // Org-scoped settings: render only providers belonging to this org. + // Already-linked providers render as disabled unlink nodes (the org + // owns the binding, so the user cannot remove it). Unlinked providers + // render as link nodes — disabled when the identity is already bound + // to this org, active otherwise (first-time bind). + linkedIDs := make(map[string]struct{}, len(linked)) + for _, p := range linked { + linkedIDs[p.Config().ID] = struct{}{} + } + for _, p := range conf.Providers { + if !flowOrgMatchesProvider(sr, p.OrganizationID) { + continue + } + _, alreadyLinked := linkedIDs[p.ID] + var n *node.Node + if alreadyLinked { + n = NewUnlinkNode(p.ID, cmp.Or(p.Label, p.ID)) + } else { + n = NewLinkNode(p.ID, cmp.Or(p.Label, p.ID)) + } + n.Group = group + if alreadyLinked || boundToFlowOrg { + if attrs, ok := n.Attributes.(*node.InputAttributes); ok { + attrs.Disabled = true + } + } + sr.UI.GetNodes().Append(n) + } + if boundToFlowOrg { + // Cloud OIDC and SAML org strategies both delegate here; dedupe so + // the managed-by-organization message appears exactly once. + hasMsg := slices.ContainsFunc(sr.UI.Messages, func(m text.Message) bool { + return m.ID == text.InfoSelfServiceSettingsManagedByOrganization + }) + if !hasMsg { + sr.UI.Messages.Add(text.NewInfoSelfServiceSettingsManagedByOrganization()) + } + } + return nil + } + + // Non-org settings flow: existing behavior. + for _, l := range linkable { + // We do not want to offer to link SSO providers in the regular settings flow. + if l.Config().OrganizationID != "" { + continue + } + sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, cmp.Or(l.Config().Label, l.Config().ID))) + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, id) + if err != nil { + return err + } + + if count > 1 { + // 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, cmp.Or(l.Config().Label, l.Config().ID))) + } + } + + return nil +} + +// Update Settings Flow with OpenID Connect Method +// +// swagger:model updateSettingsFlowWithOidcMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateSettingsFlowWithOidcMethod struct { + // Method + // + // Should be set to profile when trying to update a profile. + // + // required: true + Method string `json:"method"` + + // Link this provider + // + // Either this or `unlink` must be set. + // + // type: string + // in: body + Link string `json:"link"` + + // Unlink this provider + // + // Either this or `link` must be set. + // + // type: string + // in: body + Unlink string `json:"unlink"` + + // Flow ID is the flow's ID. + // + // in: query + FlowID string `json:"flow"` + + // The identity's traits + // + // in: body + Traits json.RawMessage `json:"traits"` + + // 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: + // - `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`. + // - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request. + // + // 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 { + return x.ParseUUID(p.FlowID) +} + +func (p *updateSettingsFlowWithOidcMethod) SetFlowID(rid uuid.UUID) { + p.FlowID = rid.String() +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.Settings") + defer otelx.End(span, &err) + + var p updateSettingsFlowWithOidcMethod + if err := s.decoderSettings(ctx, &p, r, f); err != nil { + return nil, err + } + f.TransientPayload = p.TransientPayload + + if f.OrganizationID.Valid && len(p.Unlink) > 0 { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("Cannot unlink an organization-scoped credential through the settings flow.")) + } + + 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(ctx, s.SettingsStrategyID()).Enabled { + return nil, s.handleMethodNotAllowedError(errors.WithStack(herodot.ErrNotFound().WithReason(strategy.EndpointDisabledMessage))) + } + + if len(p.Link) > 0 { + if err := s.initLinkProvider(ctx, w, r, ctxUpdate, &p); err != nil { + return nil, err + } + + return ctxUpdate, nil + } else if len(p.Unlink) > 0 { + if err := s.unlinkProvider(ctx, w, r, ctxUpdate, &p); err != nil { + return nil, err + } + + return ctxUpdate, nil + } + + return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, errors.WithStack(herodot.ErrBadRequest().WithReason("Expected either link or unlink to be set when continuing flow but both are unset."))) + } else if err != nil { + return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, err) + } + + 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(ctx, s.SettingsStrategyID()).Enabled { + return nil, s.handleMethodNotAllowedError(errors.WithStack(herodot.ErrNotFound().WithReason(strategy.EndpointDisabledMessage))) + } + + switch l, u := len(p.Link), len(p.Unlink); { + case l > 0 && u > 0: + return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, errors.WithStack(&jsonschema.ValidationError{ + Message: "it is not possible to link and unlink providers in the same request", + InstancePtr: "#/", + })) + case l > 0: + if err := s.initLinkProvider(ctx, w, r, ctxUpdate, &p); err != nil { + return nil, err + } + return ctxUpdate, nil + case u > 0: + if err := s.unlinkProvider(ctx, w, r, ctxUpdate, &p); err != nil { + return nil, err + } + return ctxUpdate, nil + } + // 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(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(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return nil, err + } + + linkable, err := s.linkableProviders(providers, i) + if err != nil { + return nil, err + } + + var found bool + for _, available := range linkable { + if toLink == available.Config().ID { + // The provider must match the flow's organization scope. An + // org-scoped flow may link only providers of that organization; a + // non-org flow may link only providers with no organization. We + // reuse ConnectionExistValidationError rather than a distinct + // "wrong organization" message so an attacker cannot probe which + // providers another organization has configured. + if !flowOrgMatchesProvider(ctxUpdate.Flow, available.Config().OrganizationID) { + return nil, errors.WithStack(ConnectionExistValidationError) + } + found = true + break + } + } + + if !found { + return nil, errors.WithStack(ConnectionExistValidationError) + } + + return i, nil +} + +// flowOrgMatchesProvider reports whether a provider with organization id +// providerOrg may be linked through flow f. An org-scoped flow accepts only +// providers of that organization; a non-org flow accepts only providers with +// no organization. +func flowOrgMatchesProvider(f *settings.Flow, providerOrg string) bool { + if !f.OrganizationID.Valid { + return providerOrg == "" + } + providerOrgID, err := uuid.FromString(providerOrg) + if err != nil { + return false + } + return providerOrgID == f.OrganizationID.UUID +} + +func (s *Strategy) initLinkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error { + if _, err := s.isLinkable(ctx, ctxUpdate, p.Link); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) + } + + provider, err := s.Provider(ctx, p.Link) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + req, err := s.validateFlow(ctx, r, ctxUpdate.Flow.ID, oidcv1.FlowKind_FLOW_KIND_SETTINGS) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + state, pkce, err := s.GenerateState(ctx, provider, ctxUpdate.Flow, x.RequestBaseURL(r)) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + if _, err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, + continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), + continuity.WithPayload(&AuthCodeContainer{ + State: state, + FlowID: ctxUpdate.Flow.ID.String(), + Traits: p.Traits, + }), + continuity.WithLifespan(time.Minute*30)); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + var up map[string]string + if err := json.NewDecoder(bytes.NewBuffer(p.UpstreamParameters)).Decode(&up); err != nil { + return err + } + + codeURL, err := getAuthRedirectURL(ctx, provider, req, state, up, pkce) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) + } else { + http.Redirect(w, r, codeURL, http.StatusSeeOther) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) linkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider) error { + p := &updateSettingsFlowWithOidcMethod{ + Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(), + } + + if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) + } + + i, err := s.isLinkable(ctx, ctxUpdate, p.Link) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if err := s.linkCredentials(ctx, i, token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + // Add authentication method to session after + // linking with 3rd party OIDC provider + if err := s.d.SessionManager().SessionAddAuthenticationMethods( + ctx, + ctxUpdate.Session.ID, + session.AuthenticationMethod{ + Method: s.ID(), + AAL: provider.Config().AALForClaims(claims), + Provider: provider.Config().ID, + Organization: provider.Config().OrganizationID, + UpstreamACR: claims.ACR, + UpstreamAMR: claims.AMR, + }); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if err := s.d.SettingsHookExecutor().PostSettingsHook(ctx, w, r, s.SettingsStrategyID(), ctxUpdate, i, settings.WithCallback(func(ctxUpdate *settings.UpdateContext) error { + // Credential population is done by PostSettingsHook on ctxUpdate.Session.Identity + return s.PopulateSettingsMethod(ctx, r, ctxUpdate.Session.Identity, ctxUpdate.Flow) + })); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return nil +} + +func (s *Strategy) unlinkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error { + if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) + } + + providers, err := s.Config(ctx) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + availableProviders, err := s.linkedProviders(providers, i) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + var cc identity.CredentialsOIDC + creds, err := i.ParseCredentials(s.ID(), &cc) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if count < 2 { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(UnlinkAllFirstFactorConnectionsError)) + } + + var found bool + var updatedProviders []identity.CredentialsOIDCProvider + var updatedIdentifiers []string + for _, available := range availableProviders { + if p.Unlink == available.Config().ID { + for _, link := range cc.Providers { + if link.Provider != p.Unlink { + updatedIdentifiers = append(updatedIdentifiers, identity.OIDCUniqueID(link.Provider, link.Subject)) + updatedProviders = append(updatedProviders, link) + } else { + found = true + } + } + } + } + + if !found { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(UnknownConnectionValidationError)) + } + + creds.Identifiers = updatedIdentifiers + creds.Config, err = json.Marshal(&identity.CredentialsOIDC{Providers: updatedProviders}) + if err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(err)) + } + + i.Credentials[s.ID()] = *creds + if err := s.d.SettingsHookExecutor().PostSettingsHook(ctx, w, r, s.SettingsStrategyID(), ctxUpdate, i, settings.WithCallback(func(ctxUpdate *settings.UpdateContext) error { + // Credential population is done by PostSettingsHook on ctxUpdate.Session.Identity + return s.PopulateSettingsMethod(ctx, r, ctxUpdate.Session.Identity, ctxUpdate.Flow) + })); err != nil { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) handleSettingsError(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod, 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()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), settings.ContinuityOptions(p, ctxUpdate.Session.Identity)...); err != nil { + return err + } + } + + if ctxUpdate.Flow != nil { + ctxUpdate.Flow.UI.ResetMessages() + ctxUpdate.Flow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + return err +} + +func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsConfig sqlxx.JSONRawMessage) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.Link") + defer otelx.End(span, &err) + + var credentialsOIDCConfig identity.CredentialsOIDC + if err := json.Unmarshal(credentialsConfig, &credentialsOIDCConfig); err != nil { + return err + } + if len(credentialsOIDCConfig.Providers) != 1 { + return errors.New("no oidc provider was set") + } + credentialsOIDCProvider := credentialsOIDCConfig.Providers[0] + + if err := s.linkCredentials( + ctx, + i, + // 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, + ); err != nil { + return err + } + + if err := s.d.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits); err != nil { + return err + } + + return nil +} + +func (s *Strategy) CompletedLogin(sess *session.Session, data *flow.DuplicateCredentialsData) error { + var credentialsOIDCConfig identity.CredentialsOIDC + if err := json.Unmarshal(data.CredentialsConfig, &credentialsOIDCConfig); err != nil { + return err + } + if len(credentialsOIDCConfig.Providers) != 1 { + return errors.New("no oidc provider was set") + } + credentialsOIDCProvider := credentialsOIDCConfig.Providers[0] + + sess.CompletedLoginForWithProvider( + s.ID(), + identity.AuthenticatorAssuranceLevel1, + credentialsOIDCProvider.Provider, + credentialsOIDCProvider.Organization, + ) + + return nil +} + +func (s *Strategy) SetDuplicateCredentials(f flow.InternalContexter, duplicateIdentifier string, credentials identity.Credentials, provider string) error { + var credentialsOIDCConfig identity.CredentialsOIDC + if err := json.Unmarshal(credentials.Config, &credentialsOIDCConfig); err != nil { + return err + } + + // We want to only set the provider in the credentials config that was used to authenticate the user. + for _, p := range credentialsOIDCConfig.Providers { + if p.Provider == provider { + credentialsOIDCConfig.Providers = []identity.CredentialsOIDCProvider{p} + config, err := json.Marshal(credentialsOIDCConfig) + if err != nil { + return err + } + return flow.SetDuplicateCredentials(f, flow.DuplicateCredentialsData{ + CredentialsType: s.ID(), + CredentialsConfig: config, + DuplicateIdentifier: duplicateIdentifier, + }) + } + } + return fmt.Errorf("provider %q not found in credentials", provider) +} diff --git a/selfservice/strategy/oidc/strategy_settings_organization_test.go b/selfservice/strategy/oidc/strategy_settings_organization_test.go new file mode 100644 index 0000000..b2a547a --- /dev/null +++ b/selfservice/strategy/oidc/strategy_settings_organization_test.go @@ -0,0 +1,348 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/sqlxx" +) + +// TestSettingsOrganizationFlow exercises the org-scoped settings flow end to +// end at the HTTP layer in the OSS package. Coverage: +// +// - case=init flow with ?organization= persists OrganizationID on the flow. +// - case=session identity org id overrides ?organization= (session wins). +// - case=link redirects through RouteOrganizationCallback for the flow's org. +// - case=unlink under org-scoped flow is rejected (focused integration check +// complementing the existing TestSettingsStrategy/suite=unlink case). +// +// The full OIDC callback dance — completing the upstream auth and persisting +// identity.OrganizationID via linkCredentials — is exercised end-to-end in the +// Playwright e2e suite (test/e2epw). It cannot run inside this OSS package +// because RouteOrganizationCallback is registered only by the cloud +// organization.Strategy in kratos/kratos/internal/organization/strategy_login.go. +// +// The URL-path mismatch case (Task 11) is covered by +// kratos/kratos/internal/organization/hook_test.go (TestExecuteSettingsPostPersistHook) +// because it lives in the cloud org hook. +func TestSettingsOrganizationFlow(t *testing.T) { + t.Parallel() + + orgA := uuid.Must(uuid.NewV4()) + orgB := uuid.Must(uuid.NewV4()) + + // PrepareOrganizations only honors the ?organization= query parameter when + // one of the identity's email addresses is under the organization's + // configured domains, so the orgs claim a domain and the test identities + // carry a matching email verifiable address. + const orgADomain = "org-a.example" + + privilegedSession := 5 * time.Minute + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.com/kratos", + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: privilegedSession, + config.ViperKeyOrganizations: []map[string]any{ + {"id": orgA.String(), "domains": []string{orgADomain}}, + {"id": orgB.String(), "domains": []string{"org-b.example"}}, + }, + }), + ) + + hydraAdmin, hydraPublic := newHydra(t) + + _ = newUI(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + // newOrgOIDCProvider mirrors newOIDCProvider but additionally registers the + // org-scoped callback URL (RouteOrganizationCallback) with the upstream + // hydra client. The non-helper provider only registers /callback/; an + // org-scoped flow uses /organization//callback/. + newOrgOIDCProvider := func(id string, organizationID uuid.UUID, opts ...func(*oidc.Configuration)) oidc.Configuration { + clientID, secret := createClient(t, hydraAdmin, []string{ + publicTS.URL + oidc.RouteBase + "/callback/" + id, + publicTS.URL + oidc.RouteBase + "/organization/" + organizationID.String() + "/callback/" + id, + publicTS.URL + oidc.RouteCallbackGeneric, + }) + cfg := oidc.Configuration{ + Provider: "generic", + ID: id, + ClientID: clientID, + ClientSecret: secret, + IssuerURL: hydraPublic + "/", + Mapper: "file://./stub/oidc.hydra.jsonnet", + OrganizationID: organizationID.String(), + } + for _, opt := range opts { + opt(&cfg) + } + return cfg + } + + setProviderConfig( + t, + conf, + newOIDCProvider(t, publicTS, hydraPublic, hydraAdmin, "ory"), + newOrgOIDCProvider("orgA-google", orgA, func(c *oidc.Configuration) { + c.Label = "Org A Google" + }), + newOrgOIDCProvider("orgB-google", orgB), + ) + + // Identity with a password credential and no OrganizationID — the typical + // case for an initial org binding through the settings flow. The email + // lives under orgA's claimed domain and is carried as a verifiable + // address so PrepareOrganizations honors ?organization=. + newPasswordIdentity := func(t *testing.T) *identity.Identity { + t.Helper() + email := "user-" + x.NewUUID().String() + "@" + orgADomain + return &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + VerifiableAddresses: []identity.VerifiableAddress{ + *identity.NewVerifiableEmailAddress(email, uuid.Nil), + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, + } + } + + // initBrowserSettingsFlow creates a settings flow via the public browser + // endpoint with an optional ?organization= query parameter, then loads it + // back from the persister. + initBrowserSettingsFlow := func(t *testing.T, client *http.Client, orgQuery string) *settings.Flow { + t.Helper() + u := publicTS.URL + settings.RouteInitBrowserFlow + if orgQuery != "" { + u += "?organization=" + orgQuery + } + req, err := http.NewRequest("GET", u, nil) + require.NoError(t, err) + + res, err := client.Do(req) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + flowID := res.Request.URL.Query().Get("flow") + require.NotEmpty(t, flowID, "browser-init redirected without a flow id (response URL: %s)", res.Request.URL.String()) + + f, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), x.ParseUUID(flowID)) + require.NoError(t, err) + return f + } + + t.Run("case=init flow with organization query persists OrganizationID on the flow", func(t *testing.T) { + _, client := testhelpers.AddAndLoginIdentity(t, reg, newPasswordIdentity(t)) + + f := initBrowserSettingsFlow(t, client, orgA.String()) + + require.True(t, f.OrganizationID.Valid, "flow should be org-scoped") + assert.Equal(t, orgA, f.OrganizationID.UUID) + }) + + t.Run("case=init flow with organization query but non-matching email stays unscoped", func(t *testing.T) { + // The ?organization= parameter is free-form caller input. When none of + // the identity's email addresses are under the org's configured + // domains, the flow must stay unscoped — otherwise any authenticated + // identity could enumerate the org's SSO providers. + email := "user-" + x.NewUUID().String() + "@nomatch.example" + outsideIdentity := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + VerifiableAddresses: []identity.VerifiableAddress{ + *identity.NewVerifiableEmailAddress(email, uuid.Nil), + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, + } + _, client := testhelpers.AddAndLoginIdentity(t, reg, outsideIdentity) + + f := initBrowserSettingsFlow(t, client, orgA.String()) + + assert.False(t, f.OrganizationID.Valid, + "flow must not be org-scoped when the identity's email is not under the organization's domains") + }) + + t.Run("case=session identity org id overrides organization query", func(t *testing.T) { + // An identity already bound to an organization cannot be silently + // re-bound through this path (PrepareOrganizations: session wins). + boundIdentity := newPasswordIdentity(t) + boundIdentity.OrganizationID = uuid.NullUUID{UUID: orgA, Valid: true} + _, client := testhelpers.AddAndLoginIdentity(t, reg, boundIdentity) + + f := initBrowserSettingsFlow(t, client, orgB.String()) + + require.True(t, f.OrganizationID.Valid) + assert.Equal(t, orgA, f.OrganizationID.UUID, + "session identity organization must override the organization query parameter") + }) + + t.Run("case=link redirects through RouteOrganizationCallback for the flow's org", func(t *testing.T) { + _, client := testhelpers.AddAndLoginIdentity(t, reg, newPasswordIdentity(t)) + + // Init via HTTP so PrepareOrganizations populates OrganizationID. + f := initBrowserSettingsFlow(t, client, orgA.String()) + require.True(t, f.OrganizationID.Valid) + require.Equal(t, orgA, f.OrganizationID.UUID) + + // Disable redirects so we can inspect the location header that points + // at the upstream provider's auth URL. + c := *client + c.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + + values := url.Values{ + "csrf_token": {nosurfx.FakeCSRFToken}, + "link": {"orgA-google"}, + } + res, err := c.PostForm(publicTS.URL+settings.RouteSubmitFlow+"?flow="+f.ID.String(), values) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusSeeOther, res.StatusCode) + + loc, err := res.Location() + require.NoError(t, err) + + // The upstream auth URL embeds the redirect_uri pointing back at our + // org-scoped callback path. + redirectURI := loc.Query().Get("redirect_uri") + require.NotEmpty(t, redirectURI, "expected redirect_uri on upstream auth URL: %s", loc.String()) + assert.Contains(t, redirectURI, "/self-service/methods/oidc/organization/"+orgA.String()+"/callback/orgA-google", + "upstream redirect_uri should target RouteOrganizationCallback for orgA") + }) + + t.Run("case=unlink rejection under org-scoped flow", func(t *testing.T) { + // Identity that already has the orgA-google credential linked, so an + // unlink request would otherwise be a candidate for processing. + email := "user-" + x.NewUUID().String() + "@" + orgADomain + linkedIdentity := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + VerifiableAddresses: []identity.VerifiableAddress{ + *identity.NewVerifiableEmailAddress(email, uuid.Nil), + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"orgA-google:" + email}, + Config: sqlxx.JSONRawMessage(fmt.Sprintf( + `{"providers":[{"provider":"orgA-google","subject":"%s","organization":"%s"}]}`, + email, orgA.String())), + }, + }, + } + _, client := testhelpers.AddAndLoginIdentity(t, reg, linkedIdentity) + + f := initBrowserSettingsFlow(t, client, orgA.String()) + require.True(t, f.OrganizationID.Valid) + + body, res := testhelpers.HTTPPostForm(t, client, publicTS.URL+settings.RouteSubmitFlow+"?flow="+f.ID.String(), + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "unlink": {"orgA-google"}}) + + // The handler bubbles the unlink rejection through the settings UI; + // the response payload carries the rejection message rendered into the + // flow. + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, + gjson.GetBytes(body, "ui.messages.0.text").String(), + "Cannot unlink", + "%s", body) + }) + + t.Run("case=link rejection when provider belongs to a different organization", func(t *testing.T) { + // A flow scoped to orgA must not accept a provider that belongs to + // orgB: otherwise the user is redirected to orgB's IdP and bound to + // orgB through an orgA-scoped flow. + _, client := testhelpers.AddAndLoginIdentity(t, reg, newPasswordIdentity(t)) + + f := initBrowserSettingsFlow(t, client, orgA.String()) + require.True(t, f.OrganizationID.Valid) + require.Equal(t, orgA, f.OrganizationID.UUID) + + body, res := testhelpers.HTTPPostForm(t, client, publicTS.URL+settings.RouteSubmitFlow+"?flow="+f.ID.String(), + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "link": {"orgB-google"}}) + + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, + gjson.GetBytes(body, "ui.messages.0.text").String(), + "can not link unknown or already existing OpenID Connect connection", + "%s", body) + }) + + t.Run("case=link rejection when org-scoped flow links a non-org provider", func(t *testing.T) { + // An org-scoped flow must also reject a provider that has no + // organization at all (the "ory" provider). Only providers of the + // flow's own organization are linkable. + _, client := testhelpers.AddAndLoginIdentity(t, reg, newPasswordIdentity(t)) + + f := initBrowserSettingsFlow(t, client, orgA.String()) + require.True(t, f.OrganizationID.Valid) + require.Equal(t, orgA, f.OrganizationID.UUID) + + body, res := testhelpers.HTTPPostForm(t, client, publicTS.URL+settings.RouteSubmitFlow+"?flow="+f.ID.String(), + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "link": {"ory"}}) + + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, + gjson.GetBytes(body, "ui.messages.0.text").String(), + "can not link unknown or already existing OpenID Connect connection", + "%s", body) + }) + + t.Run("case=link rejection when non-org flow links an org provider", func(t *testing.T) { + // The symmetric direction: a non-org settings flow must not accept an + // org-scoped provider on the submit path. The UI already hides it; the + // server must enforce it too. + _, client := testhelpers.AddAndLoginIdentity(t, reg, newPasswordIdentity(t)) + + f := initBrowserSettingsFlow(t, client, "") + require.False(t, f.OrganizationID.Valid, "flow should not be org-scoped") + + body, res := testhelpers.HTTPPostForm(t, client, publicTS.URL+settings.RouteSubmitFlow+"?flow="+f.ID.String(), + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "link": {"orgA-google"}}) + + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + assert.Contains(t, + gjson.GetBytes(body, "ui.messages.0.text").String(), + "can not link unknown or already existing OpenID Connect connection", + "%s", body) + }) +} diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go new file mode 100644 index 0000000..3299f20 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -0,0 +1,1065 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/url" + "slices" + "strconv" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +func init() { + corpx.RegisterFakes() +} + +func TestSettingsStrategy(t *testing.T) { + t.Parallel() + + normalPrivilegedSessionFor := 5 * time.Minute + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.com/kratos", + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: normalPrivilegedSessionFor, + }), + ) + + hydraAdmin, hydraPublic := newHydra(t) + uiTS := newUI(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + setProviderConfig(t, conf, + newOIDCProvider(t, publicTS, hydraPublic, hydraAdmin, "ory", func(c *oidc.Configuration) { c.Label = "Ory" }), + newOIDCProvider(t, publicTS, hydraPublic, hydraAdmin, "ory-sso", func(c *oidc.Configuration) { c.OrganizationID = "org-1" }), + newOIDCProvider(t, publicTS, hydraPublic, hydraAdmin, "google"), + newOIDCProvider(t, publicTS, hydraPublic, hydraAdmin, "github"), + ) + + type userDataFunc = func() (string, *identity.Identity) + passwordIdentity := func() (string, *identity.Identity) { + e := testhelpers.RandomEmail() + return e, &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, e)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{e}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, + } + } + singleOIDCIdentity := func() (string, *identity.Identity) { + e := testhelpers.RandomEmail() + return e, &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, e)), + SchemaID: config.DefaultIdentityTraitsSchemaID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"ory:" + e}, + Config: sqlxx.JSONRawMessage(fmt.Sprintf(`{"providers":[{"provider":"ory","subject":"%s"}]}`, e)), + }, + }, + } + } + multiOIDCIdentity := func() (string, *identity.Identity) { + e := testhelpers.RandomEmail() + return e, &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, e)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"ory:" + e, "github:" + e}, + Config: sqlxx.JSONRawMessage(fmt.Sprintf(`{"providers":[{"provider":"ory","subject":"%s"},{"provider":"github","subject":"%s"}]}`, e, e)), + }, + }, + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + } + multiCredentialIdentity := func() (string, *identity.Identity) { + e := testhelpers.RandomEmail() + return e, &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, e)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{e}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"ory:" + e, "google:" + e}, + Config: sqlxx.JSONRawMessage(fmt.Sprintf(`{"providers":[{"provider":"ory","subject":"%s"},{"provider":"google","subject":"%s"}]}`, e, e)), + }, + }, + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + } + + newProfileFlow := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { + req, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), + x.ParseUUID(testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS).Id)) + require.NoError(t, err) + assert.Empty(t, req.Active) + + if redirectTo != "" { + req.RequestURL = redirectTo + } + req.ExpiresAt = time.Now().Add(exp) + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(t.Context(), req)) + + // sanity check + got, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), req.ID) + require.NoError(t, err) + require.Len(t, got.UI.Nodes, len(req.UI.Nodes)) + + return req + } + + // does the same as new profile request but uses the SDK + nprSDK := func(t *testing.T, client *http.Client) *kratos.SettingsFlow { + return testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS) + } + + _, sharedPasswordUser := passwordIdentity() + sharedUserID, sharedUserClient := testhelpers.AddAndLoginIdentity(t, reg, sharedPasswordUser) + + t.Run("case=should not be able to continue a flow with a malformed ID", func(t *testing.T) { + body, res := testhelpers.HTTPPostForm(t, sharedUserClient, publicTS.URL+settings.RouteSubmitFlow+"?flow=i-am-not-a-uuid", nil) + AssertSystemError(t, errTS, res, body, 400, "malformed") + }) + + t.Run("case=should not be able to continue a flow without the flow query parameter", func(t *testing.T) { + body, res := testhelpers.HTTPPostForm(t, sharedUserClient, publicTS.URL+settings.RouteSubmitFlow, nil) + AssertSystemError(t, errTS, res, body, 400, "query parameter is missing") + }) + + t.Run("case=should not be able to continue a flow with a non-existing ID", func(t *testing.T) { + body, res := testhelpers.HTTPPostForm(t, sharedUserClient, publicTS.URL+settings.RouteSubmitFlow+"?flow="+x.NewUUID().String(), nil) + AssertSystemError(t, errTS, res, body, 404, "not be found") + }) + + t.Run("case=should not be able to continue a flow that is expired", func(t *testing.T) { + req := newProfileFlow(t, sharedUserClient, "", -time.Hour) + body, res := testhelpers.HTTPPostForm(t, sharedUserClient, publicTS.URL+settings.RouteSubmitFlow+"?flow="+req.ID.String(), nil) + + require.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow=") + assert.NotContains(t, res.Request.URL.String(), req.ID.String(), "should initialize a new flow") + assert.Contains(t, gjson.GetBytes(body, `ui.messages.0.text`).String(), "expired") + }) + + t.Run("case=should not be able to fetch another user's data", func(t *testing.T) { + _, otherUserData := passwordIdentity() + _, otherUserClient := testhelpers.AddAndLoginIdentity(t, reg, otherUserData) + + req := newProfileFlow(t, sharedUserClient, "", time.Hour) + + _, _, err := testhelpers.NewSDKCustomClient(publicTS, otherUserClient).FrontendAPI.GetSettingsFlow(t.Context()).Id(req.ID.String()).Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "403") + }) + + t.Run("case=should fetch the settings request and expect data to be set appropriately", func(t *testing.T) { + req := newProfileFlow(t, sharedUserClient, "", time.Hour) + + rs, _, err := testhelpers.NewSDKCustomClient(publicTS, sharedUserClient).FrontendAPI.GetSettingsFlow(t.Context()).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? + assert.Equal(t, publicTS.URL+"/self-service/settings/browser", req.RequestURL) + assert.Empty(t, req.Active) + assert.NotEmpty(t, req.IssuedAt) + assert.EqualValues(t, sharedUserID, req.Identity.ID) + assert.EqualValues(t, sharedPasswordUser.Traits, req.Identity.Traits) + assert.EqualValues(t, sharedPasswordUser.SchemaID, req.Identity.SchemaID) + + assert.EqualValues(t, req.ID.String(), rs.Id) + assert.EqualValues(t, req.RequestURL, rs.RequestUrl) + assert.EqualValues(t, req.Identity.ID.String(), rs.Identity.Id) + assert.EqualValues(t, req.IssuedAt, rs.IssuedAt) + + require.NotNil(t, identity.CredentialsTypeOIDC.String(), rs.Ui) + require.EqualValues(t, "POST", rs.Ui.Method) + require.EqualValues(t, publicTS.URL+settings.RouteSubmitFlow+"?flow="+req.ID.String(), + rs.Ui.Action) + }) + + t.Run("case=should adjust linkable providers based on linked credentials", func(t *testing.T) { + _, pw := passwordIdentity() + _, ory := singleOIDCIdentity() + _, gh := multiOIDCIdentity() + _, multi := multiCredentialIdentity() + userData := map[string]*identity.Identity{ + "password": pw, + "oryer": ory, + "githuber": gh, + "multiuser": multi, + } + users := testhelpers.AddAndLoginIdentities(t, reg, userData) + for name, user := range users { + t.Run("agent="+name, func(t *testing.T) { + rs := nprSDK(t, user.Client) + snapshotx.SnapshotT(t, rs.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + } + }) + + checkCredentials := func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), iid) + require.NoError(t, err) + + var cc identity.CredentialsOIDC + creds, err := actual.ParseCredentials(identity.CredentialsTypeOIDC, &cc) + require.NoError(t, err) + + if shouldExist { + assert.Contains(t, creds.Identifiers, provider+":"+subject) + } else { + assert.NotContains(t, creds.Identifiers, provider+":"+subject) + } + + var found bool + for _, p := range cc.Providers { + if p.Provider == provider && p.Subject == subject { + found = true + if expectTokens { + assert.NotEmpty(t, p.InitialIDToken) + assert.NotEmpty(t, p.InitialAccessToken) + assert.NotEmpty(t, p.InitialRefreshToken) + } + break + } + } + + require.EqualValues(t, shouldExist, found) + } + + t.Run("suite=unlink", func(t *testing.T) { + unlink := func(t *testing.T, agent *http.Client, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + req = nprSDK(t, agent) + body, res = testhelpers.HTTPPostForm(t, agent, req.Ui.Action, + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "unlink": {provider}}) + return + } + + unlinkInvalid := func(userData userDataFunc, provider, errorMessage string) func(t *testing.T) { + return func(t *testing.T) { + _, identityData := userData() + _, client := testhelpers.AddAndLoginIdentity(t, reg, identityData) + + body, res, req := unlink(t, client, provider) + + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) + + // The original options to link google and github are still there + t.Run("flow=fetch", func(t *testing.T) { + snapshotx.SnapshotT(t, req.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + + t.Run("flow=json", func(t *testing.T) { + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, `ui.nodes`).Raw), snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), publicTS.URL+settings.RouteSubmitFlow+"?flow=") + assert.Contains(t, gjson.GetBytes(body, `ui.messages.0.text`).String(), errorMessage) + } + } + + t.Run("case=should not be able to unlink the last remaining connection", + unlinkInvalid(singleOIDCIdentity, "ory", "can not unlink OpenID Connect connection because it is the last remaining first factor credential")) + + t.Run("case=should not be able to unlink an non-existing connection", + unlinkInvalid(multiOIDCIdentity, "i-do-not-exist", "can not unlink non-existing OpenID Connect connection")) + + t.Run("case=should not be able to unlink a connection not yet linked", + unlinkInvalid(multiOIDCIdentity, "google", "can not unlink non-existing OpenID Connect connection")) + + t.Run("case=should unlink a connection", func(t *testing.T) { + email, userData := multiOIDCIdentity() + // only keep the relevant identity + userID, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + provider := "github" + + body, res, req := unlink(t, client, provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) + require.Equalf(t, "success", gjson.GetBytes(body, "state").String(), "%s", body) + + checkCredentials(t, false, userID, provider, email, false) + checkCredentials(t, true, userID, "ory", email, false) + }) + + t.Run("case=should not be able to unlink under an org-scoped settings flow", func(t *testing.T) { + _, userData := multiOIDCIdentity() + _, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + + req, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), + x.ParseUUID(testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS).Id)) + require.NoError(t, err) + + req.OrganizationID = uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(t.Context(), req)) + + body, res := testhelpers.HTTPPostForm(t, client, publicTS.URL+settings.RouteSubmitFlow+"?flow="+req.ID.String(), + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "unlink": {"github"}}) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, gjson.GetBytes(body, `ui.messages.0.text`).String(), "Cannot unlink") + }) + + t.Run("case=should not be able to unlink a connection without a privileged session", func(t *testing.T) { + userEmail, userData := multiOIDCIdentity() + userID, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + provider := "github" + + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + }) + _, res, req := unlink(t, client, provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") + + fa := testhelpers.NewSDKCustomClient(publicTS, client).FrontendAPI + lf, _, err := fa.GetLoginFlow(t.Context()).Id(res.Request.URL.Query()["flow"][0]).Execute() + require.NoError(t, err) + + for _, n := range lf.Ui.Nodes { + if n.Group == "oidc" && n.Attributes.UiNodeInputAttributes.Name == "provider" { + assert.Contains(t, []string{"ory", "github"}, n.Attributes.UiNodeInputAttributes.Value) + } + } + + rs, _, err := fa.GetSettingsFlow(t.Context()).Id(req.Id).Execute() + require.NoError(t, err) + require.EqualValues(t, flow.StateShowForm, rs.State) + + checkCredentials(t, true, userID, provider, userEmail, false) + + return req + } + + t.Run("subcase=should not update without re-auth", func(t *testing.T) { + _ = runUnauthed(t) + }) + + t.Run("subcase=should update after re-auth", func(t *testing.T) { + req := runUnauthed(t) + + // fake login by allowing longer sessions... + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + + body, res := testhelpers.HTTPPostForm(t, client, req.Ui.Action, + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "unlink": {provider}}) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) + + assert.Equal(t, "success", gjson.GetBytes(body, "state").String()) + + checkCredentials(t, false, userID, provider, userEmail, false) + checkCredentials(t, true, userID, "ory", userEmail, false) + }) + }) + }) + + t.Run("suite=link", func(t *testing.T) { + userEmail, userData := multiOIDCIdentity() + _, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + + link := func(t *testing.T, client *http.Client, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + req = nprSDK(t, client) + body, res = testhelpers.HTTPPostForm(t, client, req.Ui.Action, + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "link": {provider}}) + return + } + + linkInvalid := func(userData userDataFunc, provider string) func(t *testing.T) { + return func(t *testing.T) { + _, identityData := userData() + _, client := testhelpers.AddAndLoginIdentity(t, reg, identityData) + + body, res, req := link(t, client, provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) + + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), publicTS.URL+settings.RouteSubmitFlow+"?flow=") + + // The original options to link google and github are still there + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").Raw), snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + + assert.Contains(t, gjson.GetBytes(body, `ui.messages.0.text`).String(), + "can not link unknown or already existing OpenID Connect connection") + } + } + + t.Run("case=should not be able to link an non-existing connection", + linkInvalid(singleOIDCIdentity, "i-do-not-exist")) + + t.Run("case=should not be able to link a connection which already exists", + linkInvalid(multiOIDCIdentity, "github")) + + t.Run("case=should not be able to link a connection already linked by another identity", func(t *testing.T) { + // While this theoretically allows for account enumeration - because we see an error indicator if an + // OIDC connection is being linked that exists already - it would require the attacker to already + // have control over the social profile, in which case account enumeration is the least of our worries. + // Instead of using the OIDC profile for enumeration, the attacker would use it for account takeover. + _, otherUserData := singleOIDCIdentity() + _, otherClient := testhelpers.AddAndLoginIdentity(t, reg, otherUserData) + + subject := userEmail + scope := []string{"openid"} + + c := wrapClientForHydraLoginConsent(t, otherClient, hydraAdmin, hydraFlowParams{subject: subject, scope: scope}) + + body, res, _ := link(t, c, "github") + + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Containsf(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "An account with the same identifier (email, phone, username, ...) exists already.", "%s", body) + }) + + t.Run("case=should not be able to link a connection which is missing the ID token", func(t *testing.T) { + c := wrapClientForHydraLoginConsent(t, client, hydraAdmin, hydraFlowParams{subject: "hackerman+scope-missing", scope: []string{}}) + + body, res, _ := link(t, c, "google") + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + + assert.Containsf(t, gjson.GetBytes(body, `ui.messages.0.text`).String(), + "no id_token was returned", + "%s", body) + }) + + t.Run("case=should link a connection", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + provider := "google" + + _, userData := multiOIDCIdentity() + userID, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + + updatedFlow, res, originalFlow := link(t, wrapClientForHydraLoginConsent(t, client, hydraAdmin, hydraFlowParams{subject: subject, scope: scope}), provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + + updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, client).FrontendAPI.GetSettingsFlow(t.Context()).Id(originalFlow.Id).Execute() + require.NoError(t, err) + require.EqualValues(t, flow.StateSuccess, updatedFlowSDK.State) + + t.Run("flow=original", func(t *testing.T) { + snapshotx.SnapshotT(t, originalFlow.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + t.Run("flow=response", func(t *testing.T) { + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(updatedFlow, "ui.nodes").Raw), snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + t.Run("flow=fetch", func(t *testing.T) { + snapshotx.SnapshotT(t, updatedFlowSDK.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + + checkCredentials(t, true, userID, provider, subject, true) + + t.Run("session should have new auth method", func(t *testing.T) { + // Get the specific session for this agent using SDK + sess, _, err := testhelpers.NewSDKCustomClient(publicTS, client). + FrontendAPI. + ToSession(t.Context()). + Execute() + require.NoError(t, err) + require.NotNil(t, sess) + + // Check that the session has the expected auth method + found := slices.ContainsFunc(sess.AuthenticationMethods, func(am kratos.SessionAuthenticationMethod) bool { + return am.Method != nil && + am.Provider != nil && + *am.Method == string(identity.CredentialsTypeOIDC) && + *am.Provider == provider + }) + + require.Truef(t, found, "session should contain OIDC auth method for provider %s", provider) + }) + }) + + t.Run("case=should link a connection even if user does not have oidc credentials yet", func(t *testing.T) { + _, userData := passwordIdentity() + userID, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + provider := "google" + + subject := testhelpers.RandomEmail() + + _, res, req := link(t, wrapClientForHydraLoginConsent(t, client, hydraAdmin, hydraFlowParams{subject: subject, scope: []string{"openid", "offline"}}), provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + + rs, _, err := testhelpers.NewSDKCustomClient(publicTS, client).FrontendAPI.GetSettingsFlow(t.Context()).Id(req.Id).Execute() + require.NoError(t, err) + require.EqualValues(t, flow.StateSuccess, rs.State) + + snapshotx.SnapshotT(t, rs.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + + checkCredentials(t, true, userID, provider, subject, true) + }) + + t.Run("case=upstream parameters", func(t *testing.T) { + t.Run("case=should pass upstream parameters when linking a connection", func(t *testing.T) { + req := nprSDK(t, client) + // copy over the client so we can disable redirects + c := *client + // We need to disable redirects because the upstream parameters are only passed on to the provider + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + values := &url.Values{} + values.Set("csrf_token", nosurfx.FakeCSRFToken) + values.Set("link", "google") + values.Set("upstream_parameters.login_hint", "foo@bar.com") + values.Set("upstream_parameters.hd", "bar.com") + values.Set("upstream_parameters.prompt", "consent") + + resp, err := c.PostForm(req.Ui.Action, *values) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, resp.StatusCode) + + loc, err := resp.Location() + require.NoError(t, err) + + // login_hint is never forwarded: a caller-supplied value cannot be + // trusted (HackerOne #3239672, ory-corp/cloud#8955). + assert.Empty(t, loc.Query().Get("login_hint")) + assert.EqualValues(t, "bar.com", loc.Query().Get("hd")) + assert.EqualValues(t, "consent", loc.Query().Get("prompt")) + }) + + t.Run("case=invalid query parameters should be ignored", func(t *testing.T) { + req := nprSDK(t, client) + // copy over the client so we can disable redirects + c := *client + // We need to disable redirects because the upstream parameters are only passed on to the provider + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + values := &url.Values{} + values.Set("csrf_token", nosurfx.FakeCSRFToken) + values.Set("link", "google") + values.Set("upstream_parameters.lol", "invalid") + + resp, err := c.PostForm(req.Ui.Action, *values) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, resp.StatusCode) + + loc, err := resp.Location() + require.NoError(t, err) + + assert.False(t, loc.Query().Has("lol")) + }) + }) + + t.Run("case=should not be able to link a connection without a privileged session", func(t *testing.T) { + _, userData := singleOIDCIdentity() + userID, client := testhelpers.AddAndLoginIdentity(t, reg, userData) + provider := "google" + + subject := testhelpers.RandomEmail() + + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + }) + _, res, req := link(t, client, provider) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") + + fa := testhelpers.NewSDKCustomClient(publicTS, client).FrontendAPI + lf, _, err := fa.GetLoginFlow(t.Context()).Id(res.Request.URL.Query()["flow"][0]).Execute() + require.NoError(t, err) + + var allProviders []any + for _, n := range lf.Ui.Nodes { + if n.Group == "oidc" && n.Attributes.UiNodeInputAttributes.Name == "provider" { + allProviders = append(allProviders, n.Attributes.UiNodeInputAttributes.Value) + } + } + assert.ElementsMatch(t, []string{"ory"}, allProviders) + + rs, _, err := fa.GetSettingsFlow(t.Context()).Id(req.Id).Execute() + require.NoError(t, err) + require.EqualValues(t, flow.StateShowForm, rs.State) + + checkCredentials(t, false, userID, provider, subject, true) + + return req + } + + t.Run("subcase=should not update without re-auth", func(t *testing.T) { + _ = runUnauthed(t) + }) + + t.Run("subcase=should update after re-auth", func(t *testing.T) { + req := runUnauthed(t) + + // fake login by allowing longer sessions... + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + + body, res := testhelpers.HTTPPostForm(t, wrapClientForHydraLoginConsent(t, client, hydraAdmin, hydraFlowParams{subject: subject, scope: []string{"openid", "offline"}}), req.Ui.Action, + &url.Values{"csrf_token": {nosurfx.FakeCSRFToken}, "unlink": {provider}}) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) + + assert.Equal(t, "success", gjson.GetBytes(body, "state").String()) + + checkCredentials(t, true, userID, provider, subject, true) + }) + }) + }) + + t.Run("suite=api", func(t *testing.T) { + newAPIClient := func(t *testing.T, userData userDataFunc) (uuid.UUID, *http.Client) { + _, id := userData() + id.State = identity.StateActive + return id.ID, testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + } + + nprAPISDK := func(t *testing.T, client *http.Client) *kratos.SettingsFlow { + return testhelpers.InitializeSettingsFlowViaAPI(t, client, publicTS) + } + + t.Run("case=API flow populates link and unlink nodes", func(t *testing.T) { + for name, userData := range map[string]userDataFunc{ + "password_only": passwordIdentity, + "single_oidc": singleOIDCIdentity, + "multi_oidc": multiOIDCIdentity, + "multi_credentials": multiCredentialIdentity, + } { + t.Run("identity="+name, func(t *testing.T) { + _, client := newAPIClient(t, userData) + rs := nprAPISDK(t, client) + snapshotx.SnapshotT(t, rs.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value", "1.attributes.value")) + }) + } + }) + + makeOIDCPayload := func(rs *kratos.SettingsFlow, key, provider string) string { + values := url.Values{ + "csrf_token": {nosurfx.FakeCSRFToken}, + key: {provider}, + } + return testhelpers.EncodeFormAsJSON(t, true, values) + } + + t.Run("case=API flow link returns 422 BrowserLocationChangeRequired", func(t *testing.T) { + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "google" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "link", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode, "%s", actual) + assert.NotEmpty(t, gjson.Get(actual, "redirect_browser_to").String(), "%s", actual) + + var changeLocation flow.BrowserLocationChangeRequiredError + require.NoError(t, json.Unmarshal([]byte(actual), &changeLocation)) + assert.Contains(t, changeLocation.RedirectBrowserTo, "oauth2/auth") + }) + + t.Run("case=API flow unlink succeeds", func(t *testing.T) { + email, id := multiOIDCIdentity() + id.State = identity.StateActive + client := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + provider := "github" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "unlink", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusOK, res.StatusCode, "%s", actual) + assert.EqualValues(t, "success", gjson.Get(actual, "state").String(), "%s", actual) + + // id.ID is set by NewHTTPClientWithIdentitySessionToken after persistence + checkCredentials(t, false, id.ID, provider, email, false) + checkCredentials(t, true, id.ID, "ory", email, false) + }) + + t.Run("case=API flow unlink last connection fails", func(t *testing.T) { + _, client := newAPIClient(t, singleOIDCIdentity) + provider := "ory" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "unlink", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), + "can not unlink OpenID Connect connection because it is the last remaining first factor credential", "%s", actual) + }) + + t.Run("case=API flow unlink non-existing connection fails", func(t *testing.T) { + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "i-do-not-exist" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "unlink", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), + "can not unlink non-existing OpenID Connect connection", "%s", actual) + }) + + t.Run("case=API flow unlink connection not yet linked fails", func(t *testing.T) { + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "google" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "unlink", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), + "can not unlink non-existing OpenID Connect connection", "%s", actual) + }) + + t.Run("case=API flow link non-existing provider fails", func(t *testing.T) { + _, client := newAPIClient(t, singleOIDCIdentity) + provider := "i-do-not-exist" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "link", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), + "can not link unknown or already existing OpenID Connect connection", "%s", actual) + }) + + t.Run("case=API flow link already linked provider fails", func(t *testing.T) { + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "github" + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "link", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), + "can not link unknown or already existing OpenID Connect connection", "%s", actual) + }) + + t.Run("case=API flow privileged session requirement for unlink returns 403", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Nanosecond) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + }) + + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "github" + + // Ensure the privileged session window has expired. + time.Sleep(time.Millisecond) + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "unlink", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + assert.Equal(t, http.StatusForbidden, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), + publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + }) + + t.Run("case=API flow privileged session requirement for link returns 403", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Nanosecond) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, normalPrivilegedSessionFor) + }) + + _, client := newAPIClient(t, multiOIDCIdentity) + provider := "google" + + // Ensure the privileged session window has expired. + time.Sleep(time.Millisecond) + + rs := nprAPISDK(t, client) + payload := makeOIDCPayload(rs, "link", provider) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, rs, client, payload) + assert.Equal(t, http.StatusForbidden, res.StatusCode, "%s", actual) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), + publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + }) + }) +} + +func TestPopulateSettingsMethod(t *testing.T) { + t.Parallel() + nCtx := func(t *testing.T, conf *oidc.ConfigurationCollection) (*driver.RegistryDefault, context.Context) { + _, reg := pkg.NewFastRegistryWithMocks(t) + ctx := context.Background() + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/") + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + + ctx = contextx.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, ctx + } + + 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) + } + + nr := func() *settings.Flow { + return &settings.Flow{Type: flow.TypeBrowser, ID: x.NewUUID(), UI: container.New("")} + } + + 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(ctx, req, 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{ + {Provider: "generic", ID: "facebook"}, + {Provider: "generic", ID: "google"}, + {Provider: "generic", ID: "github"}, + } + + t.Run("case=should populate API flow with link nodes", func(t *testing.T) { + 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(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(ctx, req, i, f)) + require.NotEmpty(t, f.UI.Nodes) + assert.EqualValues(t, node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("github", "github"), + }, f.UI.Nodes) + }) + + for k, tc := range []struct { + c []oidc.Configuration + i *identity.Credentials + e node.Nodes + withpw bool + }{ + { + c: []oidc.Configuration{}, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + }, + }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "github"}, + }, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("github", "github"), + }, + }, + { + c: defaultConfig, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), + }, + }, + { + c: defaultConfig, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), + }, + i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{}, Config: []byte(`{}`)}, + }, + { + c: defaultConfig, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), + }, + i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + }, Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`)}, + }, + { + c: defaultConfig, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), + }, + withpw: true, + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`), + }, + }, + { + c: defaultConfig, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), + oidc.NewUnlinkNode("facebook", "facebook"), + }, + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + "facebook:1234", + }, + 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(nosurfx.FakeCSRFToken), + oidc.NewLinkNode("labeled", "Labeled"), + }, + }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + {Provider: "generic", ID: "facebook"}, + }, + e: node.Nodes{ + node.NewCSRFNode(nosurfx.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) { + 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), + } + if tc.i != nil { + i.Credentials[identity.CredentialsTypeOIDC] = *tc.i + } + if tc.withpw { + i.Credentials[identity.CredentialsTypePassword] = identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{"foo@bar.com"}, + Config: []byte(`{"hashed_password":"$argon2id$..."}`), + } + } + actual := populate(t, reg, ctx, i, nr()) + assert.EqualValues(t, tc.e, actual.Nodes) + }) + } + + t.Run("case=org-scoped flow renders only org providers and surfaces linked ones as disabled unlink nodes", func(t *testing.T) { + t.Parallel() + orgA := uuid.Must(uuid.NewV4()) + orgB := uuid.Must(uuid.NewV4()) + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{ + {Provider: "generic", ID: "public-github"}, + {Provider: "generic", ID: "orgA-google", Label: "Org A Google", OrganizationID: orgA.String()}, + {Provider: "generic", ID: "orgA-facebook", OrganizationID: orgA.String()}, + {Provider: "generic", ID: "orgB-github", OrganizationID: orgB.String()}, + }}) + i := &identity.Identity{ + Traits: []byte(`{"subject":"foo@bar.com"}`), + Credentials: make(map[identity.CredentialsType]identity.Credentials, 1), + } + // Identity has already linked orgA-google. + i.Credentials[identity.CredentialsTypeOIDC] = identity.Credentials{ + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"orgA-google:1234"}, + Config: []byte(`{"providers":[{"provider":"orgA-google","subject":"1234"}]}`), + } + + f := nr() + f.OrganizationID = uuid.NullUUID{UUID: orgA, Valid: true} + + ui := populate(t, reg, ctx, i, f) + + // Expected nodes: CSRF + an unlink node for the already-linked orgA-google + // (disabled — the org owns the binding) + a link node for the unlinked + // orgA-facebook. Public/other-org providers are filtered out. + linkedNode := oidc.NewUnlinkNode("orgA-google", "Org A Google") + linkedNode.Attributes.(*node.InputAttributes).Disabled = true + expected := node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + linkedNode, + oidc.NewLinkNode("orgA-facebook", "orgA-facebook"), + } + assert.EqualValues(t, expected, ui.Nodes) + + // Sanity: no provider from another org or public provider leaks in. + for _, n := range ui.Nodes { + attrs, ok := n.Attributes.(*node.InputAttributes) + if !ok { + continue + } + assert.NotEqual(t, "public-github", attrs.FieldValue) + assert.NotEqual(t, "orgB-github", attrs.FieldValue) + } + }) +} diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go new file mode 100644 index 0000000..ba417ec --- /dev/null +++ b/selfservice/strategy/oidc/strategy_test.go @@ -0,0 +1,2592 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "golang.org/x/oauth2" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "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/hook/hooktest" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" + "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" + "github.com/ory/x/uuidx" +) + +func TestStrategy(t *testing.T) { + t.Parallel() + + returnTSRouter := http.NewServeMux() + returnTS := httptest.NewServer(returnTSRouter) + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeyPublicBaseURL: "https://foo/", // deterministic value for snapshots + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: "file://./stub/registration.schema.json"}, + {ID: "email", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://./stub/registration-phone.schema.json", SelfserviceSelectable: true}, + {ID: "extra_data", URL: "file://./stub/registration-multi-schema-extra-fields.schema.json", SelfserviceSelectable: true}, + {ID: "aal", URL: "file://./stub/registration-aal.schema.json", SelfserviceSelectable: true}, + {ID: "verifiable_email", URL: "file://./stub/registration-verifiable-email.schema.json", SelfserviceSelectable: true}, + }, + config.ViperKeyDefaultIdentitySchemaID: "default", + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String()): []config.SelfServiceHook{{Name: "session"}}, + config.ViperKeyURLsAllowedReturnToDomains: []string{returnTS.URL}, + config.ViperKeySelfServiceBrowserDefaultReturnTo: returnTS.URL, + config.ViperKeyOAuth2ProviderURL: "http://fake-hydra", + }), + ) + + returnTSRouter.HandleFunc("/app_code", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) }) + returnTSRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + if err != nil { + reg.Writer().WriteError(w, r, err) + return + } + reg.Writer().Write(w, r, sess) + }) + + hydraAdmin, hydraPublic := newHydra(t) + uiTS := newUI(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP, routerA := httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + invalid := newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "invalid-issuer") + + orgID := uuidx.NewV4() + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid"), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid2"), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "secondProvider"), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "claimsViaUserInfo", func(c *oidc.Configuration) { c.ClaimsSource = oidc.ClaimsSourceUserInfo }), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "neverPKCE", func(c *oidc.Configuration) { c.PKCE = "never" }), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "autoPKCE", func(c *oidc.Configuration) { c.PKCE = "auto" }), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "forcePKCE", func(c *oidc.Configuration) { c.PKCE = "force" }), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "aal2-acr", func(c *oidc.Configuration) { c.AAL2ACRValues = []string{"urn:mfa", "urn:strong"} }), + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "aal2-amr", func(c *oidc.Configuration) { c.AAL2AMRValues = []string{"mfa", "otp"} }), + oidc.Configuration{ + Provider: "generic", + ID: "invalid-issuer", + ClientID: invalid.ClientID, + ClientSecret: invalid.ClientSecret, + // We replace this URL to cause an issuer validation mismatch. + IssuerURL: strings.Replace(hydraPublic, "127.0.0.1", "localhost", 1) + "/", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }, + ) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + t.Logf("Hydra Public URL: %s", hydraPublic) + t.Logf("Hydra Admin URL: %s", hydraAdmin) + t.Logf("Return URL: %s", returnTS.URL) + + // assert form values + assertFormValues := func(t *testing.T, flowID uuid.UUID, provider string) (action string) { + var cfg *container.Container + if req, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), flowID); err == nil { + require.EqualValues(t, req.ID, flowID) + cfg = req.UI + require.NotNil(t, cfg) + } else if req, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), flowID); err == nil { + require.EqualValues(t, req.ID, flowID) + cfg = req.UI + require.NotNil(t, cfg) + } else { + require.NoError(t, err) + return + } + + assert.Equal(t, "POST", cfg.Method) + + var providers []interface{} + for _, nodes := range cfg.Nodes { + if strings.Contains(nodes.ID(), "provider") { + providers = append(providers, nodes.GetValue()) + } + } + require.Containsf(t, providers, provider, "%+v", assertx.PrettifyJSONPayload(t, cfg)) + + return cfg.Action + } + + registerAction := func(flowID uuid.UUID) string { + return ts.URL + registration.RouteSubmitFlow + "?flow=" + flowID.String() + } + loginAction := func(flowID uuid.UUID) string { + return ts.URL + login.RouteSubmitFlow + "?flow=" + flowID.String() + } + + makeRequestWithClient := func(t *testing.T, client *http.Client, action, provider string, params hydraFlowParams, fv url.Values) (*http.Response, []byte) { + if fv == nil { + fv = make(url.Values) + } + fv.Set("provider", provider) + res, err := wrapClientForHydraLoginConsent(t, client, hydraAdmin, params). + 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.Equalf(t, http.StatusOK, res.StatusCode, "%s: %s\n\t%s", action, res.Request.URL.String(), body) + + return res, body + } + + makeRequest := func(t *testing.T, action, provider string, params hydraFlowParams, fv url.Values) (*http.Response, []byte) { + return makeRequestWithClient(t, testhelpers.NewClientWithCookies(t), action, provider, params, fv) + } + + makeJSONRequest := func(t *testing.T, action, provider, subject string, scope []string, fv url.Values) (*http.Response, []byte) { + if fv == nil { + fv = make(url.Values) + } + fv.Set("provider", provider) + req, err := http.NewRequest("POST", action, strings.NewReader(fv.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + res, err := wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, hydraFlowParams{subject: subject, scope: scope}). + Do(req) + require.NoError(t, err, action) + + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + + require.Equalf(t, 422, res.StatusCode, "%s: %s\n\t%s", action, res.Request.URL.String(), body) + + return res, body + } + + makeAPICodeFlowRequest := func(t *testing.T, client *http.Client, action, provider string, params hydraFlowParams) (returnToURL *url.URL) { + res, err := testhelpers.NewTestClient(t).Post( + action, + "application/json", + strings.NewReader(fmt.Sprintf(` +{ + "method": "oidc", + "provider": %q +}`, provider)), + ) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + var changeLocation flow.BrowserLocationChangeRequiredError + require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) + + if client == nil { + client = testhelpers.NewClientWithCookies(t) + } + res, err = wrapClientForHydraLoginConsent(t, client, hydraAdmin, params). + Get(changeLocation.RedirectBrowserTo) + require.NoError(t, err) + + returnToURL = res.Request.URL + assert.Truef(t, strings.HasPrefix(returnToURL.String(), returnTS.URL+"/app_code"), "expected %s to have prefix %s/app_code", returnToURL, returnTS.URL) + + return returnToURL + } + + exchangeCodeForToken := func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse, err error) { + tokenURL := urlx.ParseOrPanic(ts.URL) + tokenURL.Path = "/sessions/token-exchange" + tokenURL.RawQuery = fmt.Sprintf("init_code=%s&return_to_code=%s", codes.InitCode, codes.ReturnToCode) + res, err := ts.Client().Get(tokenURL.String()) + if err != nil { + return codeResponse, err + } + if res.StatusCode != 200 { + return codeResponse, fmt.Errorf("got status code %d", res.StatusCode) + } + require.NoError(t, json.NewDecoder(res.Body).Decode(&codeResponse)) + + return + } + + assertSystemErrorWithReason := func(t *testing.T, res *http.Response, body []byte, code int, reason string) { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + + assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", prettyJSON(t, body)) + assert.Contains(t, gjson.GetBytes(body, "reason").String(), reason, "%s", prettyJSON(t, body)) + } + + // assert system error (redirect to error endpoint) + assertSystemErrorWithMessage := func(t *testing.T, res *http.Response, body []byte, code int, message string) { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + + assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "message").String(), message, "%s", body) + } + + // 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, "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)) + } + + // assert identity (success) + assertIdentity := func(t *testing.T, res *http.Response, body []byte, subject string, claims idTokenClaims) { + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", prettyJSON(t, body)) + assert.Equal(t, claims.traits.website, gjson.GetBytes(body, "identity.traits.website").String(), "%s", prettyJSON(t, body)) + assert.Equal(t, claims.metadataPublic.picture, gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", prettyJSON(t, body)) + } + + newLoginFlow := func(t *testing.T, requestURL string, exp time.Duration, flowType flow.Type) (req *login.Flow) { + // Use NewLoginFlow to instantiate the request but change the things we need to control a copy of it. + req, _, err := reg.LoginHandler().NewLoginFlow(httptest.NewRecorder(), + &http.Request{URL: urlx.ParseOrPanic(requestURL)}, flowType) + require.NoError(t, err) + req.RequestURL = requestURL + req.ExpiresAt = time.Now().Add(exp) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), req)) + + // sanity check + got, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), req.ID) + require.NoError(t, err) + + require.Len(t, got.UI.Nodes, len(req.UI.Nodes), "%+v", got) + + return + } + newBrowserLoginFlow := func(t *testing.T, redirectTo string, exp time.Duration) (req *login.Flow) { + return newLoginFlow(t, redirectTo, exp, flow.TypeBrowser) + } + newAPILoginFlow := func(t *testing.T, redirectTo string, exp time.Duration) (req *login.Flow) { + return newLoginFlow(t, redirectTo, exp, flow.TypeAPI) + } + + newRegistrationFlow := func(t *testing.T, redirectTo string, exp time.Duration, flowType flow.Type, opts ...registration.FlowOption) *registration.Flow { + req, err := reg.RegistrationHandler().NewRegistrationFlow(httptest.NewRecorder(), + &http.Request{URL: urlx.ParseOrPanic(redirectTo)}, flowType, opts...) + require.NoError(t, err) + req.RequestURL = redirectTo + req.ExpiresAt = time.Now().Add(exp) + require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(t.Context(), req)) + + // sanity check + got, err := reg.RegistrationFlowPersister().GetRegistrationFlow(t.Context(), req.ID) + require.NoError(t, err) + require.Lenf(t, got.UI.Nodes, len(req.UI.Nodes), "%+v", req) + + return req + } + newBrowserRegistrationFlow := func(t *testing.T, redirectTo string, exp time.Duration, opts ...registration.FlowOption) *registration.Flow { + return newRegistrationFlow(t, redirectTo, exp, flow.TypeBrowser, opts...) + } + newAPIRegistrationFlow := func(t *testing.T, redirectTo string, exp time.Duration, opts ...registration.FlowOption) *registration.Flow { + return newRegistrationFlow(t, redirectTo, exp, flow.TypeAPI, opts...) + } + + t.Run("case=should fail because provider does not exist", func(t *testing.T) { + for name, action := range map[string]string{ + "login": loginAction(newBrowserLoginFlow(t, returnTS.URL, time.Minute).ID), + "registration": registerAction(newBrowserRegistrationFlow(t, returnTS.URL, time.Minute).ID), + } { + t.Run("case="+name, func(t *testing.T) { + res, body := makeRequest(t, action, "provider-does-not-exist", hydraFlowParams{}, nil) + assertSystemErrorWithReason(t, res, body, http.StatusNotFound, "is unknown or has not been configured") + }) + } + }) + + t.Run("case=should fail because the issuer is mismatching", func(t *testing.T) { + for name, action := range map[string]string{ + "login": loginAction(newBrowserLoginFlow(t, returnTS.URL, time.Minute).ID), + "registration": registerAction(newBrowserRegistrationFlow(t, returnTS.URL, time.Minute).ID), + } { + t.Run("case="+name, func(t *testing.T) { + res, body := makeRequest(t, action, "invalid-issuer", hydraFlowParams{}, nil) + assertSystemErrorWithReason(t, res, body, http.StatusInternalServerError, "issuer did not match the issuer returned by provider") + }) + } + }) + + t.Run("case=should fail because flow does not exist", func(t *testing.T) { + for name, action := range map[string]string{ + "login": loginAction(x.NewUUID()), + "registration": registerAction(x.NewUUID()), + } { + t.Run("case="+name, func(t *testing.T) { + res, body := makeRequest(t, action, "valid", hydraFlowParams{}, nil) + assertSystemErrorWithMessage(t, res, body, http.StatusNotFound, "Unable to locate the resource") + }) + } + }) + + t.Run("case=should fail because the flow is expired", func(t *testing.T) { + for name, flw := range map[string]uuid.UUID{ + "login": newBrowserLoginFlow(t, returnTS.URL, -time.Minute).ID, + "registration": newBrowserRegistrationFlow(t, returnTS.URL, -time.Minute).ID, + } { + t.Run("case="+name, func(t *testing.T) { + action := assertFormValues(t, flw, "valid") + res, body := makeRequest(t, action, "valid", hydraFlowParams{}, nil) + + assert.NotEqual(t, flw, gjson.GetBytes(body, "id")) + require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "flow expired", "%s", body) + }) + } + }) + + t.Run("case=should fail because scope was not provided", func(t *testing.T) { + for name, flowID := range map[string]uuid.UUID{ + "login": newBrowserLoginFlow(t, returnTS.URL, time.Minute).ID, + "registration": newBrowserRegistrationFlow(t, returnTS.URL, time.Minute).ID, + } { + t.Run("case="+name, func(t *testing.T) { + action := assertFormValues(t, flowID, "valid") + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: "foo@bar.com", scope: []string{}}, nil) + assertUIError(t, res, body, "no id_token was returned") + }) + } + }) + + t.Run("case=should fail because oidc does not satisfy AAL2", func(t *testing.T) { + user := testhelpers.AddAndLoginIdentities(t, reg, map[string]*identity.Identity{"u": { + SchemaID: "aal", + Traits: identity.Traits(`{"email":"just-aal1-identity@ory.sh"}`), + }})["u"] + client := wrapClientForHydraLoginConsent(t, user.Client, hydraAdmin, hydraFlowParams{subject: "foo@bar.com", scope: []string{"openid"}}) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, client, ts, true) + + update, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), update)) + + req, err := http.NewRequest("POST", f.Ui.Action, strings.NewReader(`{"method":"oidc"}`)) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", body) + + assert.Contains(t, res.Request.URL.String(), ts.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(body, "ui.messages.0.text").String()) + }) + + t.Run("case=should fail registration flow because subject is not an email", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: "not-an-email", scope: []string{"openid"}}, nil) + + require.Containsf(t, res.Request.URL.String(), uiTS.URL, "%s", body) + assert.Containsf(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.subject).messages.0.text").String(), "Enter a valid email address", "%s\n%s", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.subject)").Raw, body) + }) + + t.Run("case=cannot register multiple accounts with the same OIDC account", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + + expectTokens := func(t *testing.T, provider string, body []byte) { + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String())) + require.NoError(t, err) + c := i.Credentials[identity.CredentialsTypeOIDC].Config + assert.NotEmpty(t, gjson.GetBytes(c, "providers.0.initial_access_token").String()) + assertx.EqualAsJSONExcept( + t, + json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), + json.RawMessage(c), + []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, + ) + } + + t.Run("case=should pass registration", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postRegistrationWebhook.SetConfig(t, conf.GetProvider(t.Context()), 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{"transient_payload": {transientPayload}}) + assertIdentity(t, res, body, subject, idTokenClaims{}) + 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(t.Context()), 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{"transient_payload": {transientPayload}}) + assert.Equal(t, returnTo, res.Request.URL.String()) + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "valid", body) + + postLoginWebhook.AssertTransientPayload(t, transientPayload) + }) + }) + + expectTokens := func(t *testing.T, provider, subject string, body []byte) uuid.UUID { + id := uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String()) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id) + require.NoError(t, err) + c := i.Credentials[identity.CredentialsTypeOIDC].Config + assert.NotEmpty(t, gjson.GetBytes(c, "providers.0.initial_access_token").String()) + assertx.EqualAsJSONExcept( + t, + json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), + json.RawMessage(c), + []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, + ) + 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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + var redirects []*http.Request + client := testhelpers.NewClientWithCookieJar(t, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + res, body := makeRequestWithClient(t, client, action, "forcePKCE", hydraFlowParams{subject: subject, scope: scope}, nil) + require.Len(t, redirects, 2) + assert.Contains(t, redirects[0].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[0].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[0].URL.String(), "code_challenge=") + assert.Equal(t, redirects[1].URL.Path, "/self-service/methods/oidc/callback") + + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "forcePKCE", subject, 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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + verifierFalsified := false + client := testhelpers.NewClientWithCookieJar(t, 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 + }) + res, body := makeRequestWithClient(t, client, action, "forcePKCE", hydraFlowParams{subject: subject, scope: scope}, 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(t.Context()).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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + challengeParamsRemoved := false + client := testhelpers.NewClientWithCookieJar(t, 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 + }) + res, body := makeRequestWithClient(t, client, action, "forcePKCE", hydraFlowParams{subject: subject, scope: scope}, 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(t.Context()).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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + var code string + client := wrapClientForHydraLoginConsent(t, 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 + }), hydraAdmin, hydraFlowParams{subject: subject, scope: scope}) + _, err := client.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 + client = wrapClientForHydraLoginConsent(t, 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 + }), hydraAdmin, hydraFlowParams{subject: subject, scope: scope}) + _, err = client.PostForm(action, url.Values{"provider": {"forcePKCE"}}) + require.ErrorContains(t, err, "stop") + require.NotEmpty(t, redirectURI) + require.NotEmpty(t, state) + + client = wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookieJar(t, jar, nil), hydraAdmin, hydraFlowParams{subject: subject, scope: scope}) + res, err := client.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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + redirectConfused := false + client := wrapClientForHydraLoginConsent(t, 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 + }), hydraAdmin, hydraFlowParams{subject: subject, scope: scope}) + res, err := client.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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + var redirects []*http.Request + client := testhelpers.NewClientWithCookieJar(t, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + res, body := makeRequestWithClient(t, client, action, "autoPKCE", hydraFlowParams{subject: subject, scope: scope}, nil) + require.Len(t, redirects, 2) + assert.Contains(t, redirects[0].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[0].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[0].URL.String(), "code_challenge=") + assert.Equal(t, redirects[1].URL.Path, "/self-service/methods/oidc/callback/autoPKCE") + + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "autoPKCE", subject, 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 := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + var redirects []*http.Request + client := testhelpers.NewClientWithCookieJar(t, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + res, body := makeRequestWithClient(t, client, action, "neverPKCE", hydraFlowParams{subject: subject, scope: scope}, nil) + require.Len(t, redirects, 2) + assert.Contains(t, redirects[0].URL.String(), "/oauth2/auth") + assert.NotContains(t, redirects[0].URL.String(), "code_challenge_method=") + assert.NotContains(t, redirects[0].URL.String(), "code_challenge=") + assert.Equal(t, redirects[1].URL.Path, "/self-service/methods/oidc/callback/neverPKCE") + + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "neverPKCE", subject, 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(t.Context()), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(t.Context()), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + + subject := testhelpers.RandomEmail() + 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{ + "transient_payload": {transientPayload}, + }) + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "valid", subject, 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{ + "transient_payload": {transientPayload}, + }) + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "valid", subject, 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) (*http.Response, []byte, []string) { + fv := make(url.Values) + fv.Set("provider", provider) + + var lastVia []*http.Request + hc := &http.Client{ + Jar: j, + Transport: testhelpers.NewTestTransport(t), + CheckRedirect: func(req *http.Request, via []*http.Request) error { + lastVia = via + return nil + }, + } + res, err := wrapClientForHydraLoginConsent(t, hc, hydraAdmin, hydraFlowParams{subject: subject, scope: scope}).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) + assertIdentity(t, res, body, subject, idTokenClaims{}) + expectTokens(t, "valid", subject, 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, Transport: testhelpers.NewTestTransport(t)}).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, subject, idTokenClaims{}) + expectTokens(t, "valid", subject, 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, Transport: testhelpers.NewTestTransport(t)}).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=carries upstream acr/amr into session AAL", func(t *testing.T) { + // Registers the user through the provider and returns the decoded + // session JSON body from the return handler. + registerAndReturnSession := func(t *testing.T, provider, email string, acr *string, amr []string) []byte { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, provider) + res, body := makeRequest(t, action, provider, hydraFlowParams{ + subject: email, + scope: []string{"openid", "offline"}, + acr: acr, + amr: amr, + }, nil) + assertIdentity(t, res, body, email, idTokenClaims{}) + return body + } + + t.Run("case=matching acr elevates session to aal2", func(t *testing.T) { + body := registerAndReturnSession(t, "aal2-acr", testhelpers.RandomEmail(), new("urn:mfa"), nil) + + assert.Equal(t, "aal2", gjson.GetBytes(body, "authenticator_assurance_level").String(), "%s", body) + assert.Equal(t, "aal2", gjson.GetBytes(body, "authentication_methods.0.aal").String(), "%s", body) + assert.Equal(t, "aal2-acr", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + assert.Equal(t, "urn:mfa", gjson.GetBytes(body, "authentication_methods.0.upstream_acr").String(), "%s", body) + }) + + t.Run("case=mismatching acr stays aal1", func(t *testing.T) { + body := registerAndReturnSession(t, "aal2-acr", testhelpers.RandomEmail(), new("urn:basic"), nil) + + assert.Equal(t, "aal1", gjson.GetBytes(body, "authenticator_assurance_level").String(), "%s", body) + assert.Equal(t, "aal1", gjson.GetBytes(body, "authentication_methods.0.aal").String(), "%s", body) + assert.Equal(t, "urn:basic", gjson.GetBytes(body, "authentication_methods.0.upstream_acr").String(), "%s", body) + }) + + t.Run("case=matching amr elevates session to aal2", func(t *testing.T) { + body := registerAndReturnSession(t, "aal2-amr", testhelpers.RandomEmail(), nil, []string{"pwd", "mfa"}) + + assert.Equal(t, "aal2", gjson.GetBytes(body, "authenticator_assurance_level").String(), "%s", body) + assert.Equal(t, "aal2", gjson.GetBytes(body, "authentication_methods.0.aal").String(), "%s", body) + assert.Equal(t, "aal2-amr", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + // Upstream amr is persisted for auditing. + var gotAMR []string + require.NoError(t, json.Unmarshal([]byte(gjson.GetBytes(body, "authentication_methods.0.upstream_amr").Raw), &gotAMR)) + assert.ElementsMatch(t, []string{"pwd", "mfa"}, gotAMR) + }) + + t.Run("case=mismatching amr stays aal1", func(t *testing.T) { + body := registerAndReturnSession(t, "aal2-amr", testhelpers.RandomEmail(), nil, []string{"pwd"}) + + assert.Equal(t, "aal1", gjson.GetBytes(body, "authenticator_assurance_level").String(), "%s", body) + assert.Equal(t, "aal1", gjson.GetBytes(body, "authentication_methods.0.aal").String(), "%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(t.Context()), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(t.Context()), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + + subject := testhelpers.RandomEmail() + scope := []string{"openid"} + + t.Run("case=should pass login", func(t *testing.T) { + transientPayload := `{"data": "login to registration"}` + + r := newBrowserLoginFlow(t, "https://example.com?identity_schema=phone", time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{ + "transient_payload": {transientPayload}, + }) + assertIdentity(t, res, body, subject, idTokenClaims{}) + assert.Equal(t, "phone", gjson.GetBytes(body, "identity.schema_id").String(), "%s", 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) + }) + }) + + t.Run("case=login with Browser+JSON", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid"} + + t.Run("case=should pass login", func(t *testing.T) { + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeJSONRequest(t, action, "valid", subject, scope, nil) + + assert.Equal(t, "browser_location_change_required", gjson.GetBytes(body, "error.id").String(), "%s", body) + + continuityCookie := res.Header.Get("Set-Cookie") + key, val, ok := strings.Cut(continuityCookie, "=") + require.True(t, ok) + assert.Equal(t, "ory_kratos_continuity", key) + assert.NotEmpty(t, val) + }) + }) + + t.Run("suite=API with session token exchange code", func(t *testing.T) { + loginOrRegister := func(t *testing.T, client *http.Client, flowID uuid.UUID, code string, params hydraFlowParams) { + _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) + require.Error(t, err) + + action := assertFormValues(t, flowID, "valid") + returnToURL := makeAPICodeFlowRequest(t, client, action, "valid", params) + 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, + }) + require.NoError(t, err) + + assert.NotEmpty(t, codeResponse.Token) + assert.Equal(t, params.subject, gjson.GetBytes(codeResponse.Session.Identity.Traits, "subject").String()) + } + performRegistration := func(t *testing.T, client *http.Client, params hydraFlowParams) { + f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) + loginOrRegister(t, client, f.ID, f.SessionTokenExchangeCode, params) + } + performLogin := func(t *testing.T, client *http.Client, params hydraFlowParams) { + f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) + loginOrRegister(t, client, f.ID, f.SessionTokenExchangeCode, params) + } + + for _, tc := range []struct { + name string + first, then func(t *testing.T, client *http.Client, params hydraFlowParams) + }{{ + name: "login-twice", + first: performLogin, then: performLogin, + }, { + name: "login-then-register", + first: performLogin, then: performRegistration, + }, { + name: "register-then-login", + first: performRegistration, then: performLogin, + }, { + name: "register-twice", + first: performRegistration, then: performRegistration, + }} { + t.Run("case="+tc.name, func(t *testing.T) { + params := hydraFlowParams{subject: testhelpers.RandomEmail(), scope: []string{"openid"}} + tc.first(t, nil, params) + tc.then(t, nil, params) + }) + } + + params := hydraFlowParams{subject: testhelpers.RandomEmail(), scope: []string{"openid"}} + t.Run("case=should pass transient payload to registration webhook", func(t *testing.T) { + transientPayload := `{"data": "api-registration"}` + + webhook := hooktest.NewServer() + t.Cleanup(webhook.Close) + webhook.SetConfig(t, conf.GetProvider(t.Context()), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + + f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, f.ID, "valid") + + res, err := testhelpers.NewTestClient(t).Post(action, "application/json", + strings.NewReader(fmt.Sprintf(`{"method":"oidc","provider":"valid","transient_payload":%s}`, transientPayload))) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + + var changeLocation flow.BrowserLocationChangeRequiredError + require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) + + res, err = wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, params). + Get(changeLocation.RedirectBrowserTo) + require.NoError(t, err) + + returnToCode := res.Request.URL.Query().Get("code") + require.NotEmpty(t, returnToCode, "code query param was empty in the return_to URL") + + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ + InitCode: f.SessionTokenExchangeCode, + ReturnToCode: returnToCode, + }) + require.NoError(t, err) + assert.NotEmpty(t, codeResponse.Token) + + webhook.AssertTransientPayload(t, transientPayload) + }) + + t.Run("case=should pass transient payload to login webhook", func(t *testing.T) { + // subject is the same as above, so this triggers a login (already registered) + transientPayload := `{"data": "api-login"}` + + webhook := hooktest.NewServer() + t.Cleanup(webhook.Close) + webhook.SetConfig(t, conf.GetProvider(t.Context()), config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, identity.CredentialsTypeOIDC.String())) + + f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, f.ID, "valid") + + res, err := testhelpers.NewTestClient(t).Post(action, "application/json", + strings.NewReader(fmt.Sprintf(`{"method":"oidc","provider":"valid","transient_payload":%s}`, transientPayload))) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + + var changeLocation flow.BrowserLocationChangeRequiredError + require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) + + res, err = wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, params).Get(changeLocation.RedirectBrowserTo) + require.NoError(t, err) + + returnToCode := res.Request.URL.Query().Get("code") + require.NotEmpty(t, returnToCode, "code query param was empty in the return_to URL") + + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ + InitCode: f.SessionTokenExchangeCode, + ReturnToCode: returnToCode, + }) + require.NoError(t, err) + assert.NotEmpty(t, codeResponse.Token) + + webhook.AssertTransientPayload(t, transientPayload) + }) + + t.Run("case=should register with incomplete data mapper on API flow", func(t *testing.T) { + t.Run("case=should fail registration on first attempt and succeed on second", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid"}, + } + params.claims.traits.website = "https://www.ory.com/kratos" + params.claims.traits.groups = []string{"group1", "group2"} + params.claims.metadataPublic.picture = "picture.png" + params.claims.metadataAdmin.phoneNumber = "911" + + // First attempt with a too-short name trait. + f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code&identity_schema", time.Minute) + action := assertFormValues(t, f.ID, "valid") + + res, err := testhelpers.NewTestClient(t).Post(action, "application/json", + strings.NewReader(`{"method":"oidc","provider":"valid","traits.name":"i"}`)) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + + var changeLocation flow.BrowserLocationChangeRequiredError + require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) + + _, err = wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, params).Get(changeLocation.RedirectBrowserTo) + require.NoError(t, err) + // The callback redirects back but registration fails due to validation (name too short). + // Somehow we still end up at the default return_to URL. Please investigate and fix the test or code. + // assert.Truef(t, strings.HasPrefix(res.Request.URL.String(), errTS.URL), "expected %q to have prefix %q", res.Request.URL.String(), "error-ts") + + // Second attempt with valid data on a new flow. + f = newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action = assertFormValues(t, f.ID, "valid") + + res, err = testhelpers.NewTestClient(t).Post(action, "application/json", + strings.NewReader(`{"method":"oidc","provider":"valid","traits.name":"valid-name"}`)) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + + require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) + + res, err = wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, params).Get(changeLocation.RedirectBrowserTo) + require.NoError(t, err) + + returnToCode := res.Request.URL.Query().Get("code") + require.NotEmpty(t, returnToCode, "code query param was empty in the return_to URL") + + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ + InitCode: f.SessionTokenExchangeCode, + ReturnToCode: returnToCode, + }) + require.NoError(t, err) + assert.NotEmptyf(t, codeResponse.Token, "%+v", codeResponse) + assert.Equalf(t, "valid-name", gjson.GetBytes(codeResponse.Session.Identity.Traits, "name").String(), "%+v", codeResponse.Session) + assert.Equalf(t, "https://www.ory.com/kratos", gjson.GetBytes(codeResponse.Session.Identity.Traits, "website").String(), "%+v", codeResponse.Session) + }) + }) + + t.Run("case=should return exchange code even if already authenticated", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid"}, + } + cl := testhelpers.NewClientWithCookies(t) + + // step 1: register and create a session", + returnTo := "/foo" + r := newBrowserLoginFlow(t, fmt.Sprintf("%s?return_to=%s", returnTS.URL, returnTo), time.Minute) + action := assertFormValues(t, r.ID, "valid") + + res, body := makeRequestWithClient(t, cl, action, "valid", params, nil) + assert.True(t, strings.HasSuffix(res.Request.URL.String(), returnTo)) + assertIdentity(t, res, body, params.subject, idTokenClaims{}) + + // step 2: perform login and get exchange code + performLogin(t, cl, params) + }) + + t.Run("case=should use redirect_to URL on failure", func(t *testing.T) { + subject := testhelpers.RandomEmail() + + 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(t.Context(), 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, nil, action, "valid", hydraFlowParams{subject: subject, scope: []string{"openid"}}) + returnedFlow := returnToURL.Query().Get("flow") + + require.NotEmpty(t, returnedFlow, "flow query param was empty in the return_to URL") + loginFlow, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(returnedFlow)) + require.NoError(t, err) + assert.Equal(t, text.InfoSelfServiceLoginLink, loginFlow.UI.Messages[0].ID) + }) + + t.Run("case=should return flow ID in return_to when registration has missing required traits", func(t *testing.T) { + // Use a schema that requires "extra_data" which is NOT mapped by the OIDC jsonnet mapper, + // so the registration will fail with a validation error. + + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid"}, + } + params.claims.traits.website = "https://www.ory.com/kratos" + params.claims.traits.groups = []string{"group1", "group2"} + params.claims.metadataPublic.picture = "picture.png" + params.claims.metadataAdmin.phoneNumber = "911" + + f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code&identity_schema=extra_data", time.Minute) + + _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: f.SessionTokenExchangeCode}) + require.Error(t, err) + + action := assertFormValues(t, f.ID, "valid") + returnToURL := makeAPICodeFlowRequest(t, nil, action, "valid", params) + + returnToCode := returnToURL.Query().Get("code") + require.NotEmpty(t, returnToCode, "code query param was empty in the return_to URL") + + returnedFlow := returnToURL.Query().Get("flow") + require.NotEmpty(t, returnedFlow, "flow query param was empty in the return_to URL") + + // Verify the flow contains validation errors and trait input nodes + regFlow, err := reg.RegistrationFlowPersister().GetRegistrationFlow(t.Context(), uuid.FromStringOrNil(returnedFlow)) + require.NoError(t, err) + assert.Equal(t, f.ID, regFlow.ID, "returned flow ID should match the original registration flow") + + // The flow should have trait input nodes for the "extra_data" field that failed validation + body, err := json.Marshal(regFlow.UI) + require.NoError(t, err) + assert.NotEmpty(t, gjson.GetBytes(body, "nodes.#(attributes.name==traits.extra_data)").Raw, "flow UI should contain a traits.extra_data input node: %s", body) + }) + }) + + t.Run("case=submit id_token during registration or login", func(t *testing.T) { + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid"), + oidc.Configuration{ + Provider: "test-provider", + ID: "test-provider", + ClientID: invalid.ClientID, + ClientSecret: invalid.ClientSecret, + IssuerURL: hydraPublic + "/", + Mapper: "file://./stub/oidc.facebook.jsonnet", + }) + oidc.RegisterTestProvider(t, "test-provider") + + cl := http.Client{Transport: testhelpers.NewTestTransport(t)} + + type testCase struct { + name string + idToken string + provider string + v func(string, string, string) url.Values + expect func(t *testing.T, res *http.Response, body []byte) + } + + prep := func(tc *testCase) (provider string, token string, nonce string) { + provider = tc.provider + if provider == "" { + provider = "test-provider" + } + token = tc.idToken + token = strings.ReplaceAll(token, "{{sub}}", testhelpers.RandomEmail()) + nonce = randx.MustString(16, randx.Alpha) + token = strings.ReplaceAll(token, "{{nonce}}", nonce) + return + } + + for _, tc := range []testCase{ + { + name: "should fail if provider does not support id_token submission", + idToken: "error", + provider: "valid", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "The provider generic does not support id_token verification", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail because id_token is invalid", + idToken: "error", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "Could not verify id_token", gjson.GetBytes(body, "error.reason").String(), "%s", body) + require.Equal(t, "stub error", gjson.GetBytes(body, "error.message").String(), "%s", body) + }, + }, + { + name: "should fail because claims are invalid", + idToken: "{}", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "The id_token claims were invalid", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail if no nonce is included in the id_token", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "No nonce was included in the id_token but is required by the provider", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail if no nonce is supplied in request", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}", + "nonce": "{{nonce}}" + }`, + v: func(provider, token, _ string) url.Values { + return url.Values{ + "id_token": {token}, + "provider": {provider}, + } + }, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "No nonce was provided but is required by the provider", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should pass if claims are valid", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}", + "nonce": "{{nonce}}" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.NotEmpty(t, gjson.GetBytes(body, "session_token").String(), "%s", body) + }, + }, + { + name: "nonce mismatch", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}", + "nonce": "random-nonce" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "The supplied nonce does not match the nonce from the id_token", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + } { + t.Run(fmt.Sprintf("flow=registration/case=%s", tc.name), func(t *testing.T) { + f := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + provider, token, nonce := prep(&tc) + action := assertFormValues(t, f.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=login/case=%s", tc.name), func(t *testing.T) { + provider, token, nonce := prep(&tc) + rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, rf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + _, err := cl.PostForm(action, v) + require.NoError(t, err) + + lf := newAPILoginFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, lf.ID, "test-provider") + + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=login_without_registration/case=%s", tc.name), func(t *testing.T) { + provider, token, nonce := prep(&tc) + rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, rf.ID, "test-provider") + + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + _, err := cl.PostForm(action, v) + require.NoError(t, err) + + lf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, lf.ID, "test-provider") + + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=login_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { + provider, token, nonce := prep(&tc) + lf := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, lf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=registration_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { + provider, token, nonce := prep(&tc) + lf := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, lf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + } + }) + + t.Run("case=login without registered account with return_to", func(t *testing.T) { + t.Run("case=should pass login", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid"}, + } + 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, action, "valid", params, nil) + assert.True(t, strings.HasSuffix(res.Request.URL.String(), returnTo)) + assertIdentity(t, res, body, params.subject, idTokenClaims{}) + }) + + t.Run("case=should pass login and carry over login_challenge to registration", func(t *testing.T) { + client := wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid"}, + }) + + reg.SetHydra(hydra.NewFake()) + r := newBrowserLoginFlow(t, fmt.Sprintf("%s?login_challenge=%s", returnTS.URL, hydra.FakeValidLoginChallenge), time.Minute) + action := assertFormValues(t, r.ID, "valid") + fv := url.Values{} + fv.Set("provider", "valid") + res, err := client.PostForm(action, fv) + require.NoError(t, err) + // Expect to be returned to the hydra instance, that instantiated the request + assert.Equal(t, hydra.FakePostLoginURL, strings.TrimSuffix(res.Request.URL.String(), "/")) + }) + }) + + t.Run("case=register and register again but login", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + 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(t.Context()), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + + r := newBrowserRegistrationFlow(t, returnTS.URL+"?identity_schema=phone", time.Minute) + action := assertFormValues(t, r.ID, "valid") + transientPayload := `{"data": "registration-one"}` + res, body := makeRequest(t, action, "valid", params, url.Values{"transient_payload": {transientPayload}}) + assertIdentity(t, res, body, params.subject, idTokenClaims{}) + assert.Equalf(t, "phone", gjson.GetBytes(body, "identity.schema_id").String(), "%s", 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(t.Context()), config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, identity.CredentialsTypeOIDC.String())) + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + transientPayload := `{"data": "registration-two"}` + res, body := makeRequest(t, action, "valid", params, url.Values{"transient_payload": {transientPayload}}) + assertIdentity(t, res, body, params.subject, idTokenClaims{}) + 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(t.Context()), 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") + transientPayload := `{"data": "registration-three"}` + res, body := makeRequest(t, action, "valid", params, url.Values{"transient_payload": {transientPayload}}) + assert.True(t, strings.HasSuffix(res.Request.URL.String(), returnTo)) + assertIdentity(t, res, body, params.subject, idTokenClaims{}) + 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"}, + } { + claims := idTokenClaims{} + claims.traits.website = "https://www.ory.com/kratos" + claims.traits.groups = []string{"group1", "group2"} + claims.metadataPublic.picture = "picture.png" + claims.metadataAdmin.phoneNumber = "911" + + makeRequest := func(t *testing.T, action, subject, name string, fv url.Values) (*http.Response, []byte) { + if fv == nil { + fv = make(url.Values) + } + fv.Set("traits.name", name) + fv.Set("provider", tc.provider) + res, err := wrapClientForHydraLoginConsent(t, testhelpers.NewClientWithCookies(t), hydraAdmin, + hydraFlowParams{subject: subject, claims: claims, scope: []string{"openid"}}, + ).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.Equalf(t, http.StatusOK, res.StatusCode, "%s: %s\n\t%s", action, res.Request.URL.String(), body) + + return res, body + } + + t.Run(fmt.Sprintf("ClaimsSource=%s", tc.name), func(t *testing.T) { + t.Run("case=should fail registration on first attempt", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, tc.provider) + + res, body := makeRequest(t, action, testhelpers.RandomEmail(), "i", nil) + + require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body) + + assert.Equalf(t, "length must be >= 2, but got 1", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).messages.0.text").String(), "%s", body) // make sure the field is being echoed + assert.Equalf(t, "traits.name", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).attributes.name").String(), "%s", body) // make sure the field is being echoed + assert.Equalf(t, "i", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).attributes.value").String(), "%s", body) // make sure the field is being echoed + assert.Equalf(t, "https://www.ory.com/kratos", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.website).attributes.value").String(), "%s", body) // make sure the field is being echoed + }) + + t.Run("case=should pass registration with valid data", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, tc.provider) + + subject := testhelpers.RandomEmail() + res, body := makeRequest(t, action, subject, "valid-name", nil) + + assertIdentity(t, res, body, subject, claims) + assert.Equalf(t, "https://www.ory.com/kratos", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + assert.Equalf(t, "valid-name", gjson.GetBytes(body, "identity.traits.name").String(), "%s", body) + assert.JSONEqf(t, `["group1","group2"]`, gjson.GetBytes(body, "identity.traits.groups").Raw, "%s", body) + }) + + t.Run("case=should fail registration on first attempt with multi-schema select", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL+"?identity_schema=extra_data", time.Minute) + action := assertFormValues(t, r.ID, tc.provider) + res, body := makeRequest(t, action, testhelpers.RandomEmail(), "i", nil) + require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body) + + assert.Equal(t, "length must be >= 2, but got 1", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).messages.0.text").String(), "%s", body) // make sure the field is being echoed + assert.Equal(t, "traits.name", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).attributes.name").String(), "%s", body) // make sure the field is being echoed + assert.Equal(t, "traits.extra_data", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.extra_data).attributes.name").String(), "%s", body) // make sure the field is being echoed + assert.Equal(t, "i", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.name).attributes.value").String(), "%s", body) // make sure the field is being echoed + assert.Equal(t, "https://www.ory.com/kratos", gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.website).attributes.value").String(), "%s", body) // make sure the field is being echoed + }) + + t.Run("case=should pass registration with selected schema with valid data", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL+"?identity_schema=extra_data", time.Minute) + action := assertFormValues(t, r.ID, tc.provider) + subject := testhelpers.RandomEmail() + res, body := makeRequest(t, action, subject, "valid-name-2", url.Values{"traits.extra_data": {"extra-data"}}) + assertIdentity(t, res, body, subject, claims) + assert.Equalf(t, "https://www.ory.com/kratos", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + assert.Equalf(t, "valid-name-2", gjson.GetBytes(body, "identity.traits.name").String(), "%s", body) + assert.Equalf(t, "extra-data", gjson.GetBytes(body, "identity.traits.extra_data").String(), "%s", body) + assert.Equalf(t, `["group1","group2"]`, gjson.GetBytes(body, "identity.traits.groups").String(), "%s", body) + }) + }) + } + }) + + 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(t.Context(), config.ViperKeySelfServiceRegistrationLoginHints, loginHintsEnabled) + + 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(t.Context(), i)) + + 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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.MustSet(t.Context(), config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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 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(t.Context(), config.ViperKeySelfServiceRegistrationLoginHints, loginHintsEnabled) + + 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, action, "secondProvider", hydraFlowParams{subject: subject, scope: scope}, nil) + require.Equal(t, http.StatusOK, res.StatusCode) + }) + + 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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.MustSet(t.Context(), config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + 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 := testhelpers.RandomEmail() + scope := []string{"openid"} + + fv := url.Values{"traits.name": {"valid-name"}} + client := testhelpers.NewClientWithCookies(t) + r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + res1, body1 := makeRequestWithClient(t, client, assertFormValues(t, r1.ID, "valid"), "valid", hydraFlowParams{subject: subject, scope: scope}, fv) + assertIdentity(t, res1, body1, subject, idTokenClaims{}) + r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + res2, body2 := makeRequestWithClient(t, client, assertFormValues(t, r2.ID, "valid"), "valid", hydraFlowParams{subject: subject, scope: scope}, fv) + assertIdentity(t, res2, body2, subject, idTokenClaims{}) + assert.Equal(t, body1, body2) + }) + + t.Run("case=should reauthenticate when sending authenticated login flow with forced flag", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid"} + + fv := url.Values{"traits.name": {"valid-name"}} + client := testhelpers.NewClientWithCookies(t) + r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + res1, body1 := makeRequestWithClient(t, client, assertFormValues(t, r1.ID, "valid"), "valid", hydraFlowParams{subject: subject, scope: scope}, fv) + assertIdentity(t, res1, body1, subject, idTokenClaims{}) + r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + require.NoError(t, reg.LoginFlowPersister().ForceLoginFlow(t.Context(), r2.ID)) + res2, body2 := makeRequestWithClient(t, client, assertFormValues(t, r2.ID, "valid"), "valid", hydraFlowParams{subject: subject, scope: scope}, fv) + assertIdentity(t, res2, body2, subject, idTokenClaims{}) + assert.NotEqual(t, gjson.GetBytes(body1, "id"), gjson.GetBytes(body2, "id")) + authAt1, err := time.Parse(time.RFC3339, gjson.GetBytes(body1, "authenticated_at").String()) + require.NoError(t, err) + authAt2, err := time.Parse(time.RFC3339, gjson.GetBytes(body2, "authenticated_at").String()) + require.NoError(t, err) + // authenticated at is newer in the second body + assert.Greater(t, authAt2.Sub(authAt1).Milliseconds(), int64(0), "%s - %s : %s - %s", authAt2, authAt1, body2, body1) + }) + + t.Run("case=upstream parameters should be passed on to provider", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid", "offline"} + + // We need to disable redirects because the upstream parameters are only passed on to the provider + c := &http.Client{ + Transport: testhelpers.NewTestTransport(t), + CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + } + + t.Run("case=should pass when registering", func(t *testing.T) { + f := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, f.ID, "valid") + + fv := url.Values{} + + fv.Set("provider", "valid") + fv.Set("upstream_parameters.login_hint", "oidc-upstream-parameters@ory.sh") + fv.Set("upstream_parameters.hd", "ory.sh") + fv.Set("upstream_parameters.prompt", "select_account") + fv.Set("upstream_parameters.auth_type", "reauthenticate") + + res, err := wrapClientForHydraLoginConsent(t, c, hydraAdmin, hydraFlowParams{subject: subject, scope: scope}).PostForm(action, fv) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, res.StatusCode) + + loc, err := res.Location() + require.NoError(t, err) + + // login_hint is never forwarded: a caller-supplied value cannot be + // trusted (HackerOne #3239672, ory-corp/cloud#8955). + require.Empty(t, loc.Query().Get("login_hint")) + require.Equal(t, "ory.sh", loc.Query().Get("hd")) + require.Equal(t, "select_account", loc.Query().Get("prompt")) + require.Equal(t, "reauthenticate", loc.Query().Get("auth_type")) + }) + + t.Run("case=should pass when logging in", func(t *testing.T) { + f := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + + action := assertFormValues(t, f.ID, "valid") + + fv := url.Values{} + + fv.Set("provider", "valid") + fv.Set("upstream_parameters.login_hint", "oidc-upstream-parameters@ory.sh") + fv.Set("upstream_parameters.hd", "ory.sh") + fv.Set("upstream_parameters.prompt", "select_account") + + res, err := c.PostForm(action, fv) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, res.StatusCode) + + loc, err := res.Location() + require.NoError(t, err) + + // login_hint is never forwarded: a caller-supplied value cannot be + // trusted (HackerOne #3239672, ory-corp/cloud#8955). + require.Empty(t, loc.Query().Get("login_hint")) + require.Equal(t, "ory.sh", loc.Query().Get("hd")) + require.Equal(t, "select_account", loc.Query().Get("prompt")) + }) + + t.Run("case=should ignore invalid parameters when logging in", func(t *testing.T) { + f := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, f.ID, "valid") + + fv := url.Values{} + fv.Set("upstream_parameters.lol", "invalid") + + res, err := c.PostForm(action, fv) + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, res.StatusCode) + + loc, err := res.Location() + require.NoError(t, err) + + // upstream parameters that are not on the allow list will be ignored and not passed on to the upstream provider. + require.Empty(t, loc.Query().Get("lol")) + }) + + t.Run("case=should ignore invalid parameters when registering", func(t *testing.T) { + f := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, f.ID, "valid") + + fv := url.Values{} + fv.Set("upstream_parameters.lol", "invalid") + + res, err := c.PostForm(action, fv) + require.NoError(t, err) + require.Equal(t, http.StatusFound, res.StatusCode) + + loc, err := res.Location() + require.NoError(t, err) + + // upstream parameters that are not on the allow list will be ignored and not passed on to the upstream provider. + require.Empty(t, loc.Query().Get("lol")) + }) + }) + + t.Run("case=verified addresses should be respected", func(t *testing.T) { + scope := []string{"openid"} + + assertVerifiedEmail := func(t *testing.T, body []byte, subject string, verified bool) { + b := prettyJSON(t, body) + assert.Lenf(t, gjson.GetBytes(body, "identity.verifiable_addresses").Array(), 1, "%s", b) + assert.Equalf(t, "email", gjson.GetBytes(body, "identity.verifiable_addresses.0.via").String(), "%s", b) + assert.Equalf(t, subject, gjson.GetBytes(body, "identity.verifiable_addresses.0.value").String(), "%s", b) + assert.Equalf(t, verified, gjson.GetBytes(body, "identity.verifiable_addresses.0.verified").Bool(), "%s", b) + } + + t.Run("case=should have verified address when subject matches", func(t *testing.T) { + subject := testhelpers.RandomEmail() + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute, registration.WithFlowIdentitySchema("verifiable_email")) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, nil) + assertIdentity(t, res, body, subject, idTokenClaims{}) + assertVerifiedEmail(t, body, subject, true) + }) + + t.Run("case=should have verified address when subject matches after normalization", func(t *testing.T) { + subject := " Denormalized-Verified-Email@ory.sh " + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute, registration.WithFlowIdentitySchema("verifiable_email")) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{"traits.subject": {"denormalized-verified-EMAIL@ory.sh"}}) + subject = "denormalized-verified-EMAIL@ory.sh" + assertIdentity(t, res, body, subject, idTokenClaims{}) + subject = "denormalized-verified-email@ory.sh" + assertVerifiedEmail(t, body, subject, true) + }) + + t.Run("case=should have unverified address when subject does not match", func(t *testing.T) { + subject := testhelpers.RandomEmail() + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute, registration.WithFlowIdentitySchema("verifiable_email")) + action := assertFormValues(t, r.ID, "valid") + newSubject := testhelpers.RandomEmail() + res, body := makeRequest(t, action, "valid", hydraFlowParams{subject: subject, scope: scope}, url.Values{"traits.subject": {newSubject}}) + assertIdentity(t, res, body, newSubject, idTokenClaims{}) + assertVerifiedEmail(t, body, newSubject, false) + }) + }) + + loginWithOIDC := func(t *testing.T, c *http.Client, flowID uuid.UUID, provider, subject string, scope []string) (*http.Response, []byte) { + action := assertFormValues(t, flowID, provider) + res, err := wrapClientForHydraLoginConsent(t, c, hydraAdmin, hydraFlowParams{subject: subject, scope: scope}).PostForm(action, url.Values{"provider": {provider}}) + require.NoError(t, err, action) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + return res, body + } + + checkCredentialsLinked := func(t *testing.T, res *http.Response, body []byte, identityID uuid.UUID, provider, subject string) { + assert.Containsf(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, strings.ToLower(subject), gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), identityID) + require.NoError(t, err) + assert.NotEmptyf(t, i.Credentials["oidc"], "%+v", i.Credentials) + assert.Truef(t, gjson.GetBytes(i.Credentials["oidc"].Config, fmt.Sprintf("providers.#(provider=%q)", provider)).Exists(), + "%s", string(i.Credentials["oidc"].Config[:])) + assert.Contains(t, gjson.GetBytes(body, "authentication_methods").String(), "oidc", "%s", body) + } + + t.Run("case=registration should start new login flow if duplicate credentials detected", func(t *testing.T) { + reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceRegistrationLoginHints, true) + + t.Run("case=second login is password", func(t *testing.T) { + subject := "new-login-if-email-exist-with-password-strategy@ory.sh" + subject2 := "new-login-subject2@ory.sh" + scope := []string{"openid"} + password := "lwkj52sdkjf" // #nosec G101 + + var i *identity.Identity + t.Run("step=create password identity", func(t *testing.T) { + i = identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + p, err := reg.Hasher(t.Context()).Generate(t.Context(), []byte(password)) + require.NoError(t, err) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + + i2 := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i2.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject2}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }) + i2.Traits = identity.Traits(`{"subject":"` + subject2 + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i2)) + }) + + client := testhelpers.NewClientWithCookies(t) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + loginFlow.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), loginFlow)) + + var linkingLoginFlow struct { + ID string + UIAction string + CSRFToken string + } + + t.Run("step=should fail login and start a new flow", func(t *testing.T) { + // To test that the subject is normalized properly + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid", strings.ToUpper(subject), scope) + assert.True(t, res.Request.URL.Query().Has("no_org_ui")) + 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()) + assert.Equal(t, gjson.GetBytes(body, "organization_id").String(), orgID.String()) + assert.NotContains(t, gjson.GetBytes(body, "request_url").String(), "code=") + 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() + assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") + }) + + t.Run("step=should fail login if existing identity identifier doesn't match", func(t *testing.T) { + res, err := client.PostForm(linkingLoginFlow.UIAction, url.Values{ + "csrf_token": {linkingLoginFlow.CSRFToken}, + "method": {"password"}, + "identifier": {subject2}, + "password": {password}, + }) + require.NoError(t, err, linkingLoginFlow.UIAction) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + assert.EqualValues(t, + text.ErrorValidationLoginLinkedCredentialsDoNotMatch, + gjson.GetBytes(body, "ui.messages.0.id").Int(), + prettyJSON(t, body), + ) + }) + + t.Run("step=should link oidc credentials to existing identity", func(t *testing.T) { + res, err := client.PostForm(linkingLoginFlow.UIAction, url.Values{ + "csrf_token": {linkingLoginFlow.CSRFToken}, + "method": {"password"}, + "identifier": {subject}, + "password": {password}, + }) + require.NoError(t, err, linkingLoginFlow.UIAction) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + checkCredentialsLinked(t, res, body, i.ID, "valid", subject) + }) + }) + + t.Run("case=second login is OIDC", func(t *testing.T) { + email1 := "existing-oidc-identity-1@ory.sh" + email2 := "existing-oidc-identity-2@ory.sh" + scope := []string{"openid", "offline"} + + var identityID uuid.UUID + t.Run("step=create OIDC identity", func(t *testing.T) { + r := newRegistrationFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + action := assertFormValues(t, r.ID, "secondProvider") + res, body := makeRequest(t, action, "secondProvider", hydraFlowParams{subject: email1, scope: scope}, nil) + assertIdentity(t, res, body, email1, idTokenClaims{}) + identityID = expectTokens(t, "secondProvider", email1, body) + + r = newRegistrationFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + action = assertFormValues(t, r.ID, "valid") + res, body = makeRequest(t, action, "valid", hydraFlowParams{subject: email2, scope: scope}, nil) + assertIdentity(t, res, body, email2, idTokenClaims{}) + expectTokens(t, "valid", email2, body) + }) + + client := testhelpers.NewClientWithCookieJar(t, nil, nil) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + var linkingLoginFlowID string + t.Run("step=should fail login and start a new login", func(t *testing.T) { + // To test the identifier mismatch + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationLoginHints, false) + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid2", email1, scope) + 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.`) + linkingLoginFlowID = gjson.GetBytes(body, "id").String() + assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlowID, "should have started a new flow") + }) + + t.Run("step=should fail login if existing identity identifier doesn't match", func(t *testing.T) { + require.NotNil(t, linkingLoginFlowID) + require.NotEmpty(t, linkingLoginFlowID) + res, body := loginWithOIDC(t, client, uuid.Must(uuid.FromString(linkingLoginFlowID)), "valid", email2, scope) + assertUIError(t, res, body, "Linked credentials do not match.") + }) + + t.Run("step=should link oidc credentials to existing identity", func(t *testing.T) { + res, body := loginWithOIDC(t, client, uuid.Must(uuid.FromString(linkingLoginFlowID)), "secondProvider", email1, scope) + checkCredentialsLinked(t, res, body, identityID, "secondProvider", email1) + }) + }) + }) + + t.Run("case=account linking still works when registration is disabled", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationEnabled, false) + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationLoginHints, true) + t.Cleanup(func() { + ctx := context.WithoutCancel(t.Context()) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + }) + + subject := "linking-registration-disabled@ory.sh" + password := "lwkj52sdkjf" // #nosec G101 + scope := []string{"openid"} + + var i *identity.Identity + t.Run("step=create password identity", func(t *testing.T) { + i = identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + p, err := reg.Hasher(t.Context()).Generate(t.Context(), []byte(password)) + require.NoError(t, err) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + }) + + client := testhelpers.NewClientWithCookies(t) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + + var linkingLoginFlow struct { + UIAction string + CSRFToken string + } + + t.Run("step=converts to a linking login flow despite disabled registration", func(t *testing.T) { + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid", subject, scope) + assertUIError(t, res, body, "You tried to sign in with") + assert.Equal(t, subject, gjson.GetBytes(body, "ui.messages.#(id==1010016).context.duplicateIdentifier").String(), "%s", body) + linkingLoginFlow.UIAction = gjson.GetBytes(body, "ui.action").String() + linkingLoginFlow.CSRFToken = gjson.GetBytes(body, `ui.nodes.#(attributes.name=="csrf_token").attributes.value`).String() + }) + + t.Run("step=confirm with password and link", func(t *testing.T) { + res, err := client.PostForm(linkingLoginFlow.UIAction, url.Values{ + "csrf_token": {linkingLoginFlow.CSRFToken}, + "method": {"password"}, + "identifier": {subject}, + "password": {password}, + }) + require.NoError(t, err, linkingLoginFlow.UIAction) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + checkCredentialsLinked(t, res, body, i.ID, "valid", subject) + }) + }) + + t.Run("case=unknown user is still rejected when registration is disabled", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationEnabled, false) + t.Cleanup(func() { + ctx := context.WithoutCancel(t.Context()) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, true) + }) + + client := testhelpers.NewClientWithCookies(t) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid", testhelpers.RandomEmail(), []string{"openid"}) + assertUIError(t, res, body, "Registration is not allowed because it was disabled") + }) + + t.Run("suite=auto link policy", func(t *testing.T) { + t.Run("case=should automatically link credential if policy says so", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid"} + + reg.AllLoginStrategies().MustStrategy("oidc").(*oidc.Strategy).SetOnConflictingIdentity(t, + func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *oidc.Claims) oidc.ConflictingIdentityVerdict { + return oidc.ConflictingIdentityVerdictMerge + }) + + var i *identity.Identity + t.Run("step=create identity in org without credentials", func(t *testing.T) { + i = identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{}`), + }) + i.OrganizationID = uuid.NullUUID{ + UUID: orgID, + Valid: true, + } + i.VerifiableAddresses = []identity.VerifiableAddress{{Value: subject, Via: "email", Verified: true}} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + }) + + t.Run("step=log in with OIDC", func(t *testing.T) { + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + loginFlow.OrganizationID = i.OrganizationID + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), loginFlow)) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) + + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid", subject, scope) + checkCredentialsLinked(t, res, body, i.ID, "valid", subject) + }) + }) + + t.Run("case=should remove use_auto_link credential if policy says so", func(t *testing.T) { + subject := testhelpers.RandomEmail() + scope := []string{"openid"} + + reg.AllLoginStrategies().MustStrategy("oidc").(*oidc.Strategy).SetOnConflictingIdentity(t, + func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *oidc.Claims) oidc.ConflictingIdentityVerdict { + return oidc.ConflictingIdentityVerdictMerge + }) + + var i *identity.Identity + + t.Run("step=create identity with use_auto_link", func(t *testing.T) { + i = identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{}`), + }) + i.SetCredentials(identity.CredentialsTypeOIDC, identity.Credentials{ + Type: identity.CredentialsTypeOIDC, + Identifiers: []string{"valid:stub", "other:other-identifier"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{ + "subject": "stub", + "provider": "valid", + "use_auto_link": true +},{ + "subject": "other-identifier", + "provider": "other", + "use_auto_link": true +}]}`), + }) + i.VerifiableAddresses = []identity.VerifiableAddress{{Value: subject, Via: "email", Verified: true}} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i)) + }) + + t.Run("step=log in with OIDC", func(t *testing.T) { + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), loginFlow)) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) + + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid", subject, scope) + checkCredentialsLinked(t, res, body, i.ID, "valid", subject) + }) + + t.Run("step=should remove use_auto_link", func(t *testing.T) { + var err error + i, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), i.ID) + require.NoError(t, err) + assert.False(t, gjson.GetBytes(i.Credentials["oidc"].Config, `providers.#(provider=="valid").use_auto_link`).Bool()) + assert.NotContains(t, i.Credentials["oidc"].Identifiers, "valid:stub") + assert.Contains(t, i.Credentials["oidc"].Identifiers, "valid:"+subject) + assert.True(t, gjson.GetBytes(i.Credentials["oidc"].Config, `providers.#(provider=="other").use_auto_link`).Bool()) + }) + }) + }) + + t.Run("case=update identity on login", func(t *testing.T) { + t.Run("case=should update traits when claims change", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://original.example.com" + params.claims.metadataPublic.picture = "original.png" + params.claims.metadataAdmin.phoneNumber = "111" + + // Set up a provider with update_identity_on_login: automatic. + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + }), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with changed claims updates identity", func(t *testing.T) { + params := params + params.claims.traits.website = "https://updated.example.com" + params.claims.metadataPublic.picture = "updated.png" + params.claims.metadataAdmin.phoneNumber = "222" + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, params.subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) + // as per mapper the original website trait should remain unchanged + assert.Equal(t, "https://original.example.com", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + assert.Equal(t, "updated.png", gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", body) + + // Verify metadata_admin was updated by reading the identity from the admin API. + identityID := gjson.GetBytes(body, "identity.id").String() + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), uuid.FromStringOrNil(identityID)) + require.NoError(t, err) + assert.Equal(t, "222", gjson.GetBytes(i.MetadataAdmin, "phone_number").String()) + }) + }) + + t.Run("case=should not update identity when claims are unchanged", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://stable.example.com" + params.claims.metadataPublic.picture = "stable.png" + + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + }), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with same claims does not error", func(t *testing.T) { + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assertIdentity(t, res, body, params.subject, params.claims) + }) + }) + + t.Run("case=should not update identity when update_identity_on_login is never", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://original.example.com" + params.claims.metadataPublic.picture = "original.png" + + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid"), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with changed claims does not update identity", func(t *testing.T) { + params := params + params.claims.traits.website = "https://changed.example.com" + params.claims.metadataPublic.picture = "changed.png" + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, params.subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) + // Traits should still have the original values from registration. + assert.Equal(t, "https://original.example.com", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + assert.Equal(t, "original.png", gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", body) + }) + }) + + t.Run("case=should update only metadata without changing traits", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://example.com" + params.claims.metadataPublic.picture = "old-pic.png" + params.claims.metadataAdmin.phoneNumber = "111" + + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + }), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with changed metadata only", func(t *testing.T) { + // Traits stay the same, only metadata changes. + params := params + params.claims.metadataPublic.picture = "new-pic.png" + params.claims.metadataAdmin.phoneNumber = "222" + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, "https://example.com", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + assert.Equal(t, "new-pic.png", gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", body) + + // Verify metadata_admin was updated by reading the identity from the admin API. + identityID := gjson.GetBytes(body, "identity.id").String() + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), uuid.FromStringOrNil(identityID)) + require.NoError(t, err) + assert.Equal(t, "222", gjson.GetBytes(i.MetadataAdmin, "phone_number").String()) + }) + }) + + t.Run("case=should fail login when mapper is invalid", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://example.com" + + // Register with a valid mapper. + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + }), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with broken mapper causes error", func(t *testing.T) { + // Reconfigure with a mapper that produces invalid output. + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + c.Mapper = "file://./stub/oidc.invalid-mapper.jsonnet" + }), + ) + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assertSystemErrorWithReason(t, res, body, http.StatusInternalServerError, + "OpenID Connect Jsonnet mapper did not return an object for key identity.traits") + }) + }) + + t.Run("case=should preserve existing traits when mapper omits them", func(t *testing.T) { + params := hydraFlowParams{ + subject: testhelpers.RandomEmail(), + scope: []string{"openid", "offline"}, + } + params.claims.traits.website = "https://example.com" + + // Register with the standard mapper (includes subject and website in traits). + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + }), + ) + + t.Run("step=register", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + assertIdentity(t, res, body, params.subject, params.claims) + }) + + t.Run("step=login with mapper that omits subject preserves it via merge", func(t *testing.T) { + // Reconfigure with a mapper that omits the "subject" trait but + // outputs a new website. Merge should preserve subject from the + // existing identity. + params := params + params.claims.traits.website = "https://new.example.com" + setProviderConfig(t, conf, + newOIDCProvider(t, ts, hydraPublic, hydraAdmin, "valid", func(c *oidc.Configuration) { + c.UpdateIdentityOnLogin = oidc.UpdateIdentityOnLoginAutomatic + c.Mapper = "file://./stub/oidc.missing-subject.jsonnet" + }), + ) + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, action, "valid", params, nil) + + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + // Subject should be preserved from the existing identity via merge. + assert.Equal(t, params.subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) + // Website should be updated from the new mapper output. + assert.Equal(t, "https://new.example.com", gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) + }) + }) + }) + + t.Run("method=TestPopulateSignUpMethod", func(t *testing.T) { + sr, err := registration.NewFlow(reg, &http.Request{URL: urlx.ParseOrPanic("/")}, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.RegistrationStrategies(t.Context()).MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).PopulateRegistrationMethod(&http.Request{}, sr)) + + snapshotx.SnapshotT(t, sr.UI, snapshotx.ExceptPaths("action", "nodes.0.attributes.value")) + }) +} + +func prettyJSON(t *testing.T, body []byte) string { + var out bytes.Buffer + require.NoError(t, json.Indent(&out, body, "", "\t")) + + return out.String() +} + +func TestCountActiveFirstFactorCredentials(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := oidc.NewStrategy(reg) + + toJSON := func(c identity.CredentialsOIDC) []byte { + out, err := json.Marshal(&c) + require.NoError(t, err) + return out + } + + 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{}, + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{""}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"bar:"}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{":foo"}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"not-bar:foo"}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"bar:not-foo"}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"bar:foo"}, + Config: toJSON(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + expected: 1, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + in := make(map[identity.CredentialsType]identity.Credentials) + for _, v := range tc.in { + in[v.Type] = v + } + actual, err := strategy.CountActiveFirstFactorCredentials(t.Context(), in) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestDisabledEndpoint(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, false)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/stub.schema.json")), + ) + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + t.Run("case=should not callback when oidc method is disabled", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + res, err := c.Get(publicTS.URL + oidc.RouteCallback) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + + t.Run("case=should not auth when oidc method is disabled", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + + t.Run("flow=settings", func(t *testing.T) { + c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t.Context(), t, reg) + f := testhelpers.InitializeSettingsFlowViaAPI(t, c, publicTS) + + res, err := c.PostForm(f.Ui.Action, url.Values{"link": {"oidc"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + + t.Run("flow=login", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, c, publicTS, false) + res, err := c.PostForm(f.Ui.Action, url.Values{"provider": {"oidc"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + + t.Run("flow=registration", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, c, publicTS) + res, err := c.PostForm(f.Ui.Action, url.Values{"provider": {"oidc"}}) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b := ioutilx.MustReadAll(res.Body) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + }) +} + +func TestPostEndpointRedirect(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, true)), + ) + + remoteAdmin, remotePublic := newHydra(t) + + publicTS, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + + setProviderConfig(t, conf, newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "apple")) + + for _, providerID := range []string{"apple", "apple-asd123"} { + t.Run("case=should redirect to GET and preserve parameters/id="+providerID, func(t *testing.T) { + // create a client that does not follow redirects + c := &http.Client{ + Transport: testhelpers.NewTestTransport(t), + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + res, err := c.PostForm(publicTS.URL+"/self-service/methods/oidc/callback/"+providerID, url.Values{"state": {"foo"}, "test": {"3"}}) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusFound, res.StatusCode) + + location, err := res.Location() + require.NoError(t, err) + assert.Equal(t, publicTS.URL+"/self-service/methods/oidc/callback/"+providerID+"?state=foo&test=3", location.String()) + + // We don't want to add/override CSRF cookie when redirecting + testhelpers.AssertNoCSRFCookieInResponse(t, 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/strategy_test_login.go b/selfservice/strategy/oidc/strategy_test_login.go new file mode 100644 index 0000000..24dc6ba --- /dev/null +++ b/selfservice/strategy/oidc/strategy_test_login.go @@ -0,0 +1,272 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "cmp" + "context" + "encoding/json" + stderrors "errors" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "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/login" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" +) + +// PopulateTestLoginFlow renders a single-provider submit button for an +// admin-created test login flow. Implements login.TestStrategy. +// +// Unlike PopulateLoginMethodFirstFactor, which renders one button per +// configured provider, a test login flow is pre-scoped to a single provider +// at creation time and renders only that button. The flow's own CSRF bearer +// token is pre-generated by the admin-create handler and baked into the UI +// so submit authorization works without a browser cookie. +func (s *Strategy) PopulateTestLoginFlow(r *http.Request, f *login.Flow, providerID string) error { + // Record which strategy performed the lookup and what it saw. A wrong + // strategy (or an unexpectedly empty provider list) is otherwise + // undiagnosable from a trace. + span := trace.SpanFromContext(r.Context()) + span.SetAttributes(attribute.String("test_login_flow.strategy", s.ID().String())) + + conf, err := s.Config(r.Context()) + if err != nil { + return errors.WithStack(err) + } + span.SetAttributes(attribute.Int("test_login_flow.available_providers", len(conf.Providers))) + + var cfg *Configuration + for i := range conf.Providers { + if conf.Providers[i].ID == providerID { + cfg = &conf.Providers[i] + break + } + } + span.SetAttributes(attribute.Bool("test_login_flow.provider_found", cfg != nil)) + if cfg == nil { + return errors.WithStack(herodot.ErrNotFound().WithReasonf("OpenID Connect provider %q is not configured", providerID)) + } + + f.UI.SetCSRF(f.CSRFToken) + AddProvider(f.UI, cfg.ID, text.NewInfoLoginWith(cmp.Or(cfg.Label, cfg.ID), cfg.ID), s.ID()) + return nil +} + +// processTestLogin captures the outcome of an OIDC test login flow's provider +// round-trip into a DebugPayload on f.TestContext. No identity is persisted +// and no session is issued. Runtime errors in mapping or schema validation +// are recorded on DebugPayload.Error so the admin UI can render a full debug +// view. Only infrastructure failures cause this function to return an error. +func (s *Strategy) processTestLogin( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + f *login.Flow, + claims *Claims, + provider Provider, +) error { + if f.State != flow.StateChooseMethod { + return errors.WithStack(herodot.ErrConflict().WithReason("test login flow already captured")) + } + if err := f.LoadTestContext(); err != nil { + return err + } + if f.TestContext == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReason("flow is marked as a test flow but has no test context")) + } + + dp := &login.DebugPayload{ + JsonnetMapperURL: provider.Config().Mapper, + } + // Raw bearer tokens are intentionally not captured. The GET endpoint is + // CSRF-bearer-authorized, which would make a 24h window for live + // credentials an unnecessary risk. The id_token claims below give admins + // enough to validate an OIDC integration. + + if claims == nil { + dp.Error = &login.StepError{Step: "claims_decode", Reason: "missing_claims", Message: "OIDC provider returned no claims"} + return s.finishTestLogin(ctx, w, r, f, dp) + } + + claimsMap, err := claimsToMap(claims) + if err != nil { + dp.Error = &login.StepError{Step: "claims_decode", Reason: "marshal_failed", Message: err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + dp.IDTokenClaims = claimsMap + + evaluated, claimsJSON, err := s.EvaluateClaimsMapper(ctx, claims, provider, nil) + if err != nil { + dp.Error = &login.StepError{Step: "jsonnet_run", Reason: "evaluation_failed", Message: err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + + if len(claimsJSON) > 0 { + var in map[string]any + if err := json.Unmarshal(claimsJSON, &in); err == nil { + dp.JsonnetInput = in + } + } + + var output map[string]any + if err := json.Unmarshal([]byte(evaluated), &output); err != nil { + dp.Error = &login.StepError{Step: "jsonnet_run", Reason: "invalid_output", Message: "Jsonnet mapper did not return a JSON object: " + err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + dp.JsonnetOutput = output + + // Build an identity and run schema validation without persisting. + i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(ctx)) + if err := s.setTraits(provider, nil, evaluated, i); err != nil { + dp.Error = &login.StepError{Step: "jsonnet_run", Reason: "invalid_traits", Message: err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + if err := s.setMetadata(evaluated, i, PublicMetadata); err != nil { + dp.Error = &login.StepError{Step: "jsonnet_run", Reason: "invalid_metadata_public", Message: err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + if err := s.setMetadata(evaluated, i, AdminMetadata); err != nil { + dp.Error = &login.StepError{Step: "jsonnet_run", Reason: "invalid_metadata_admin", Message: err.Error()} + return s.finishTestLogin(ctx, w, r, f, dp) + } + + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + dp.SchemaValidationErrors = extractSchemaValidationErrorsForLogin(err) + dp.Error = &login.StepError{Step: "schema_validate", Reason: "traits_invalid", Message: err.Error()} + } + + return s.finishTestLogin(ctx, w, r, f, dp) +} + +// finishTestLoginWithError converts a callback-side error into a DebugPayload +// tagged with the step and finishes the flow through finishTestLogin. +func (s *Strategy) finishTestLoginWithError(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, err error) error { + e := oidcerr.Unwrap(err) + dp := &login.DebugPayload{ + Error: &login.StepError{Step: string(e.Step), Reason: e.Step.Reason(), Message: err.Error()}, + } + return s.finishTestLogin(ctx, w, r, f, dp) +} + +// finishTestLogin persists the DebugPayload into the flow's test context, +// marks the flow as completed, and redirects to the admin test UI. +func (s *Strategy) finishTestLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, dp *login.DebugPayload) error { + if f.TestContext == nil { + if err := f.LoadTestContext(); err != nil { + return err + } + } + if f.TestContext != nil && f.TestContext.DebugPayload != nil { + http.Redirect(w, r, testLoginReturnURL(r, f), http.StatusSeeOther) + return nil + } + + tc := &login.TestContext{DebugPayload: dp} + if f.TestContext != nil { + tc.ProviderID = f.TestContext.ProviderID + } + if err := f.SetTestContext(tc); err != nil { + return err + } + f.State = flow.StatePassedChallenge + + // Rotate the CSRF cookie and pin the new token onto the flow. Subsequent + // reads/deletes of the captured DebugPayload (which may contain PII) only + // succeed for the browser that completed the OIDC round trip — its cookie + // holds the new token, and the GET/DELETE handlers verify the cookie + // against f.CSRFToken via nosurf.VerifyToken. + f.CSRFToken = s.d.CSRFHandler().RegenerateToken(w, r) + if err := s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + http.Redirect(w, r, testLoginReturnURL(r, f), http.StatusSeeOther) + return nil +} + +// testLoginReturnURL builds the test results page URL the browser lands on +// after a test login flow callback completes. The host comes from +// x.RequestBaseURL, which prefers the customer-facing base URL captured from +// the gateway-validated request context and falls back to the request host. +// This serves the results page from the same public domain the flow was +// started on, rather than the configured login UI host — which may be a +// different domain (for example a self-hosted Account Experience) that does +// not serve this route. The OIDC redirect_uri for the same flow is built from +// the same source (see Strategy.processLogin / GenerateState), so this trusts +// the request host no more than the existing round-trip already does. +func testLoginReturnURL(r *http.Request, f *login.Flow) string { + base, err := url.Parse(x.RequestBaseURL(r)) + if err != nil { + // RequestBaseURL always returns a well-formed scheme://host string, so + // this guard is defensive only. Degrade to a host-relative redirect that + // keeps the browser on the current domain rather than failing the + // callback. + base = new(url.URL) + } + target := &url.URL{Scheme: base.Scheme, Host: base.Host, Path: "/session/test-oidc"} + q := target.Query() + q.Set("flow", f.ID.String()) + target.RawQuery = q.Encode() + return target.String() +} + +func extractSchemaValidationErrorsForLogin(err error) []login.SchemaValidationError { + if err == nil { + return nil + } + if wrapped, ok := stderrors.AsType[*schema.ValidationError](err); ok && wrapped.ValidationError != nil { + if out := collectLeafValidationErrorsForLogin(wrapped.ValidationError); len(out) > 0 { + return out + } + } + if raw, ok := stderrors.AsType[*jsonschema.ValidationError](err); ok { + if out := collectLeafValidationErrorsForLogin(raw); len(out) > 0 { + return out + } + } + return []login.SchemaValidationError{{Path: "", Message: err.Error()}} +} + +func collectLeafValidationErrorsForLogin(v *jsonschema.ValidationError) []login.SchemaValidationError { + if v == nil { + return nil + } + if len(v.Causes) == 0 { + return []login.SchemaValidationError{{ + Path: strings.TrimPrefix(v.InstancePtr, "#"), + Message: v.Message, + }} + } + var out []login.SchemaValidationError + for _, c := range v.Causes { + out = append(out, collectLeafValidationErrorsForLogin(c)...) + } + return out +} + +// claimsToMap serializes OIDC id_token claims to a generic map so they can be +// recorded verbatim on DebugPayload.IDTokenClaims for the admin debug view. +func claimsToMap(claims *Claims) (map[string]any, error) { + raw, err := json.Marshal(claims) + if err != nil { + return nil, errors.WithStack(err) + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + return nil, errors.WithStack(err) + } + return m, nil +} diff --git a/selfservice/strategy/oidc/strategy_test_login_test.go b/selfservice/strategy/oidc/strategy_test_login_test.go new file mode 100644 index 0000000..10d19ac --- /dev/null +++ b/selfservice/strategy/oidc/strategy_test_login_test.go @@ -0,0 +1,637 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +func newTestRegistryForTestLogin(t *testing.T, providers ...map[string]any) (*config.Config, *driver.RegistryDefault) { + t.Helper() + return pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/registration.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeOIDC, true)), + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config.providers", providers), + configx.WithValue(config.ViperKeySelfServiceLoginUI, "http://admin.example.com/ui/login"), + ) +} + +func newTestStrategyForTestLogin(t *testing.T, providers ...map[string]any) *oidc.Strategy { + t.Helper() + _, reg := newTestRegistryForTestLogin(t, providers...) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + return s.(*oidc.Strategy) +} + +// fakeTestFlowProvider is a minimal Provider stub returning a fixed +// Configuration. Tests exercise processTestLogin directly, so no OAuth2 or +// Claims methods are exercised here. +type fakeTestFlowProvider struct { + config *oidc.Configuration +} + +func (p *fakeTestFlowProvider) Config() *oidc.Configuration { return p.config } + +// newTestLoginFlow builds an admin-created test login flow with TestContext +// populated and persists it. +func newTestLoginFlow(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, providerID string) *login.Flow { + t.Helper() + f := &login.Flow{ + ID: x.NewUUID(), + Type: flow.TypeBrowser, + State: flow.StateChooseMethod, + RequestURL: "https://example.com/self-service/login/browser", + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(time.Hour), + CSRFToken: nosurfx.FakeCSRFToken, + UI: container.New(""), + InternalContext: sqlxx.JSONRawMessage(`{}`), + } + require.NoError(t, f.SetTestContext(&login.TestContext{ProviderID: providerID})) + require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f)) + return f +} + +func providerNode(nodes node.Nodes) *node.Node { + for i := range nodes { + if nodes[i].ID() == "provider" { + return nodes[i] + } + } + return nil +} + +func TestStrategy_PopulateTestLoginFlow_AddsProviderNode(t *testing.T) { + t.Parallel() + + s := newTestStrategyForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "label": "My Provider", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }) + + f := &login.Flow{ + ID: x.NewUUID(), + CSRFToken: "csrf-token", + UI: container.New(""), + } + r := httptest.NewRequest(http.MethodGet, "/self-service/login/browser", nil) + + require.NoError(t, s.PopulateTestLoginFlow(r, f, "providerID")) + + csrf := f.UI.GetNodes().Find("csrf_token") + require.NotNil(t, csrf, "csrf_token node should be present") + + p := providerNode(f.UI.Nodes) + require.NotNil(t, p, "provider submit node should be present") + attrs, ok := p.Attributes.(*node.InputAttributes) + require.True(t, ok) + assert.Equal(t, "providerID", attrs.FieldValue) + assert.Equal(t, node.InputAttributeTypeSubmit, attrs.Type) + assert.Equal(t, node.OpenIDConnectGroup, p.Group) + + // Exactly one provider node was added. + var providerCount int + for i := range f.UI.Nodes { + if f.UI.Nodes[i].ID() == "provider" { + providerCount++ + } + } + assert.Equal(t, 1, providerCount) +} + +func TestStrategy_PopulateTestLoginFlow_UISnapshot(t *testing.T) { + t.Parallel() + + s := newTestStrategyForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "label": "My Provider", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }) + + f := &login.Flow{ + ID: x.NewUUID(), + CSRFToken: nosurfx.FakeCSRFToken, + UI: container.New(""), + } + r := httptest.NewRequest(http.MethodGet, "/self-service/login/browser", nil) + + require.NoError(t, s.PopulateTestLoginFlow(r, f, "providerID")) + + // CSRF token value is masked per-request by nosurf and varies across + // runs; the node's presence is asserted in another test. + f.UI.Nodes.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f.UI) +} + +func TestStrategy_PopulateTestLoginFlow_UnknownProvider(t *testing.T) { + t.Parallel() + + s := newTestStrategyForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }) + + f := &login.Flow{ + ID: x.NewUUID(), + CSRFToken: "csrf-token", + UI: container.New(""), + } + r := httptest.NewRequest(http.MethodGet, "/self-service/login/browser", nil) + + err := s.PopulateTestLoginFlow(r, f, "nonesuch") + require.Error(t, err) + var herr *herodot.DefaultError + require.ErrorAs(t, err, &herr) + assert.Equal(t, http.StatusNotFound, herr.CodeField) + assert.Contains(t, herr.ReasonField, "nonesuch") +} + +func TestStrategy_ProcessTestLogin_HappyPath(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Email: "alice@example.com", + Website: "https://example.com", + Picture: "https://example.com/alice.png", + RawClaims: map[string]any{ + "groups": []any{"admin", "user"}, + }, + } + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + // Baseline identity count — processTestLogin must not persist anything. + before, err := reg.Persister().CountIdentities(ctx) + require.NoError(t, err) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, claims, provider)) + + after, err := reg.Persister().CountIdentities(ctx) + require.NoError(t, err) + assert.Equal(t, before, after, "processTestLogin must never create an identity") + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + assert.Equal(t, flow.StatePassedChallenge, got.State) + + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + dp := got.TestContext.DebugPayload + require.NotNil(t, dp) + assert.Nil(t, dp.Error, "happy path should have no error: %+v", dp.Error) + assert.Empty(t, dp.SchemaValidationErrors) + assert.Equal(t, "file://./stub/oidc.hydra.jsonnet", dp.JsonnetMapperURL) + assert.Equal(t, "alice@example.com", dp.IDTokenClaims["sub"]) + require.NotNil(t, dp.JsonnetInput) + assert.Equal(t, "alice@example.com", dp.JsonnetInput["sub"]) + require.NotNil(t, dp.JsonnetOutput) + ident, ok := dp.JsonnetOutput["identity"].(map[string]any) + require.True(t, ok) + traits, ok := ident["traits"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "alice@example.com", traits["subject"]) + + // Browser redirected to the admin test UI with the flow ID. + assert.Equal(t, http.StatusSeeOther, w.Code) + loc := w.Header().Get("Location") + assert.Contains(t, loc, "/session/test-oidc") + assert.Contains(t, loc, "flow="+f.ID.String()) + // The results page is served from the host the callback arrived on, not + // the login UI host (admin.example.com). + assert.Contains(t, loc, "//example.com/session/test-oidc") + assert.NotContains(t, loc, "admin.example.com") + + // CSRF token rotated on the callback and pinned onto the flow, so the + // post-callback cookie value matches f.CSRFToken. Subsequent GET/DELETE + // of the captured payload checks the CSRF cookie against this token. + assert.Equal(t, nosurfx.FakeCSRFToken, got.CSRFToken, + "finishTestLogin must regenerate the CSRF token onto the flow") +} + +// TestStrategy_ProcessTestLogin_ReturnsToRequestHost asserts the test flow's +// results page is served from the public host the browser used to start the +// flow (the OIDC callback host), not from the configured login UI host. A +// project whose login UI lives on a different domain than its public API — for +// example a self-hosted Account Experience — must still land on the domain it +// launched the test from, otherwise the results page renders on a host that +// does not serve the /session/test-oidc route. +func TestStrategy_ProcessTestLogin_ReturnsToRequestHost(t *testing.T) { + t.Parallel() + ctx := t.Context() + + // The helper configures the login UI on admin.example.com. The callback + // below arrives on a different public host, signin.example.com. + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + claims := &oidc.Claims{Subject: "alice@example.com", Email: "alice@example.com"} + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + w := httptest.NewRecorder() + // The OIDC provider redirects the browser back to the public host it used + // to start the flow, which differs from the login UI host. + r := httptest.NewRequest(http.MethodGet, "https://signin.example.com/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, claims, provider)) + + assert.Equal(t, http.StatusSeeOther, w.Code) + loc := w.Header().Get("Location") + assert.Contains(t, loc, "/session/test-oidc") + assert.Contains(t, loc, "flow="+f.ID.String()) + assert.Contains(t, loc, "signin.example.com", + "results page must be served from the host the flow started on") + assert.NotContains(t, loc, "admin.example.com", + "results page must not jump to the login UI host") +} + +// TestStrategy_ProcessTestLogin_PrefersCapturedBaseURL covers the dominant Ory +// Network path: a gateway-validated middleware captures the customer-facing +// base URL onto the request context. That captured value must win over both +// the raw request host and the login UI host, so the results page is always +// served from the trusted customer-facing domain rather than anything an +// untrusted X-Forwarded-Host could inject. +func TestStrategy_ProcessTestLogin_PrefersCapturedBaseURL(t *testing.T) { + t.Parallel() + ctx := t.Context() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + claims := &oidc.Claims{Subject: "alice@example.com", Email: "alice@example.com"} + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + // The gateway captured the customer-facing base URL onto the context. The + // request itself arrives on an unrelated internal host with a spoofed + // X-Forwarded-Host that must be ignored in favor of the captured value. + captured, perr := url.Parse("https://signin.example.com") + require.NoError(t, perr) + reqCtx := x.WithBaseURL(ctx, captured) + r := httptest.NewRequest(http.MethodGet, "https://internal-host.local/self-service/methods/oidc/callback?code=x&state=y", nil) + r = r.WithContext(reqCtx) + r.Header.Set("X-Forwarded-Host", "evil.example.com") + + w := httptest.NewRecorder() + require.NoError(t, strat.ProcessTestLoginForTest(reqCtx, w, r, f, claims, provider)) + + assert.Equal(t, http.StatusSeeOther, w.Code) + loc := w.Header().Get("Location") + assert.Contains(t, loc, "//signin.example.com/session/test-oidc", + "results page must use the gateway-captured base URL") + assert.NotContains(t, loc, "evil.example.com", + "a spoofed X-Forwarded-Host must not override the captured base URL") + assert.NotContains(t, loc, "internal-host.local") + assert.NotContains(t, loc, "admin.example.com") +} + +func TestStrategy_ProcessTestLogin_AlreadyCaptured(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + f.State = flow.StatePassedChallenge + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(ctx, f)) + + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + err = strat.ProcessTestLoginForTest(ctx, w, r, f, &oidc.Claims{Subject: "alice"}, provider) + require.Error(t, err) + var herr *herodot.DefaultError + require.ErrorAs(t, err, &herr) + assert.Equal(t, http.StatusConflict, herr.CodeField) +} + +func TestStrategy_ProcessTestLogin_RawTokensNotStored(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + + claims := &oidc.Claims{ + Subject: "alice@example.com", + RawClaims: map[string]any{ + "groups": []any{"admin"}, + }, + } + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, claims, provider)) + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + require.NotNil(t, got.TestContext.DebugPayload) + + b, err := json.Marshal(got.TestContext.DebugPayload) + require.NoError(t, err) + payload := string(b) + assert.NotContains(t, payload, "raw_id_token") + assert.NotContains(t, payload, "access_token") + assert.NotContains(t, payload, "refresh_token") +} + +func TestStrategy_ProcessTestLogin_NilClaims(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, nil, provider)) + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + dp := got.TestContext.DebugPayload + require.NotNil(t, dp) + require.NotNil(t, dp.Error) + assert.Equal(t, "claims_decode", dp.Error.Step) + assert.Equal(t, "missing_claims", dp.Error.Reason) +} + +func TestStrategy_ProcessTestLogin_MapperEvaluationFailure(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + // Point the provider at a mapper file that does not exist so + // EvaluateClaimsMapper fails on fetch. + provider := &fakeTestFlowProvider{config: &oidc.Configuration{ + ID: "providerID", + Provider: "generic", + Mapper: "file://./stub/oidc.does-not-exist.jsonnet", + }} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, &oidc.Claims{Subject: "alice"}, provider)) + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + dp := got.TestContext.DebugPayload + require.NotNil(t, dp) + require.NotNil(t, dp.Error) + assert.Equal(t, "jsonnet_run", dp.Error.Step) + assert.Equal(t, "evaluation_failed", dp.Error.Reason) +} + +func TestStrategy_FinishTestLogin_Idempotent(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + // Seed a captured test flow directly so a subsequent finishTestLogin + // call observes the idempotency guard. + f := newTestLoginFlow(t, ctx, reg, "providerID") + originalDP := &login.DebugPayload{ + IDTokenClaims: map[string]any{"sub": "first"}, + } + require.NoError(t, f.SetTestContext(&login.TestContext{ + ProviderID: "providerID", + DebugPayload: originalDP, + })) + f.State = flow.StatePassedChallenge + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(ctx, f)) + + // Call finishTestLogin with a different DebugPayload. The guard must + // refuse to overwrite the captured payload. + replacementDP := &login.DebugPayload{ + IDTokenClaims: map[string]any{"sub": "second"}, + Error: &login.StepError{Step: "callback", Reason: "callback_failed", Message: "should not win"}, + } + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + require.NoError(t, strat.FinishTestLoginForTest(ctx, w, r, f, replacementDP)) + + // Browser is still redirected to the admin test UI. + assert.Equal(t, http.StatusSeeOther, w.Code) + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + require.NotNil(t, got.TestContext.DebugPayload) + assert.Equal(t, "first", got.TestContext.DebugPayload.IDTokenClaims["sub"]) + assert.Nil(t, got.TestContext.DebugPayload.Error) + assert.Equal(t, flow.StatePassedChallenge, got.State) +} + +func TestStrategy_ForwardError_TestLoginFlow(t *testing.T) { + t.Parallel() + ctx := context.Background() + + _, reg := newTestRegistryForTestLogin(t, map[string]any{ + "provider": "generic", + "id": "providerID", + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.hydra.jsonnet", + }) + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + strat := s.(*oidc.Strategy) + + f := newTestLoginFlow(t, ctx, reg, "providerID") + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) + + // Feed a step-tagged error to exercise the classifier and the + // test-login branch of forwardError. + strat.ForwardErrorForTest(ctx, w, r, f, oidcerr.Wrap(oidcerr.StepProviderDenied, errors.New("access_denied: user refused"))) + + // The browser must be redirected to the admin test UI, not the + // standard login error handler. + assert.Equal(t, http.StatusSeeOther, w.Code) + loc := w.Header().Get("Location") + assert.Contains(t, loc, "/session/test-oidc") + assert.Contains(t, loc, "flow="+f.ID.String()) + // The results page is served from the host the callback arrived on, not + // the login UI host (admin.example.com). + assert.Contains(t, loc, "//example.com/session/test-oidc") + assert.NotContains(t, loc, "admin.example.com") + + got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) + require.NoError(t, err) + assert.Equal(t, flow.StatePassedChallenge, got.State) + + require.NoError(t, got.LoadTestContext()) + require.NotNil(t, got.TestContext) + dp := got.TestContext.DebugPayload + require.NotNil(t, dp) + require.NotNil(t, dp.Error) + assert.Equal(t, "provider_denied", dp.Error.Step) + assert.Equal(t, "access_denied", dp.Error.Reason) +} diff --git a/selfservice/strategy/oidc/strategy_update_identity_test.go b/selfservice/strategy/oidc/strategy_update_identity_test.go new file mode 100644 index 0000000..7494b34 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_update_identity_test.go @@ -0,0 +1,513 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/configx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +type testUpdateProvider struct { + config *oidc.Configuration +} + +func (p *testUpdateProvider) Config() *oidc.Configuration { + return p.config +} + +func TestEvaluateClaimsMapper(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + s := oidc.NewStrategy(reg) + ctx := context.Background() + + provider := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Email: "alice@example.com", + Website: "https://example.com", + Picture: "https://example.com/alice.png", + RawClaims: map[string]any{ + "groups": []any{"admin", "user"}, + }, + } + + t.Run("case=without current identity", func(t *testing.T) { + t.Parallel() + evaluated, claimsJSON, err := s.EvaluateClaimsMapper(ctx, claims, provider, nil) + require.NoError(t, err) + assert.NotEmpty(t, evaluated) + assert.NotEmpty(t, claimsJSON) + assert.Contains(t, evaluated, "alice@example.com") + assert.Contains(t, evaluated, "https://example.com") + }) + + t.Run("case=with current identity", func(t *testing.T) { + t.Parallel() + currentIdentity := &identity.Identity{ + Traits: identity.Traits(`{"subject":"alice@example.com","website":"https://old.example.com"}`), + MetadataPublic: []byte(`{"picture":"https://example.com/old.png"}`), + MetadataAdmin: []byte(`{}`), + } + + providerWithIdentity := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.update-identity.jsonnet", + }} + + evaluated, _, err := s.EvaluateClaimsMapper(ctx, claims, providerWithIdentity, currentIdentity) + require.NoError(t, err) + assert.NotEmpty(t, evaluated) + assert.Contains(t, evaluated, "alice@example.com") + }) +} + +func TestUpdateIdentityFromClaims(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: "file://./stub/registration.schema.json"}, + }, + config.ViperKeyDefaultIdentitySchemaID: "default", + })) + s := oidc.NewStrategy(reg) + ctx := context.Background() + + provider := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.hydra.jsonnet", + }} + + t.Run("case=detects changed traits", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com","website":"https://old.example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://new.example.com", + RawClaims: map[string]any{ + "groups": []any{"admin"}, + }, + } + + changed, err := s.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + assert.True(t, changed, "identity should be marked as changed") + + // The mapper keeps the existing website, so only the new group is added. + assert.Contains(t, string(i.Traits), "https://old.example.com", "existing website should be preserved") + assert.Contains(t, string(i.Traits), "admin") + }) + + t.Run("case=detects changed metadata", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Picture: "https://example.com/new-pic.png", + PhoneNumber: "+1234567890", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := s.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + assert.True(t, changed, "identity should be marked as changed due to metadata") + + assert.Contains(t, string(i.MetadataPublic), "new-pic.png") + assert.Contains(t, string(i.MetadataAdmin), "+1234567890") + }) + + t.Run("case=no change when claims match existing identity", func(t *testing.T) { + t.Parallel() + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com","website":"https://example.com"}`), + MetadataPublic: []byte(`{"picture":"https://example.com/alice.png"}`), + MetadataAdmin: []byte(`{"phone_number":"+1234567890"}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://example.com", + Picture: "https://example.com/alice.png", + PhoneNumber: "+1234567890", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := s.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + assert.False(t, changed, "identity should not be marked as changed when claims match") + }) + + t.Run("case=does not touch credentials", func(t *testing.T) { + t.Parallel() + originalCreds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + Config: []byte(`{"providers":[{"subject":"alice","provider":"test"}]}`), + }, + } + i := &identity.Identity{ + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + Credentials: originalCreds, + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://new.example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + _, err := s.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + + // Credentials must remain unchanged. + assert.Equal(t, originalCreds, i.Credentials) + }) + + t.Run("case=identity context available in jsonnet", func(t *testing.T) { + t.Parallel() + providerWithIdentityMapper := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.update-identity.jsonnet", + }} + + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com","website":"https://old.example.com"}`), + MetadataPublic: []byte(`{"existing_field":"keep-me","picture":"https://example.com/old.png"}`), + MetadataAdmin: []byte(`{}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://new.example.com", + Picture: "https://example.com/new.png", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := s.UpdateIdentityFromClaims(ctx, claims, providerWithIdentityMapper, i) + require.NoError(t, err) + assert.True(t, changed) + + // The mapper should preserve existing_field from the current identity's metadata. + assert.Contains(t, string(i.MetadataPublic), "keep-me") + // The mapper should update picture from the new claims. + assert.Contains(t, string(i.MetadataPublic), "new.png") + }) + + t.Run("case=mapper that omits traits preserves existing via merge", func(t *testing.T) { + t.Parallel() + providerMissingSubject := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + // This mapper outputs website but omits the required "subject" trait. + Mapper: "file://./stub/oidc.missing-subject.jsonnet", + }} + + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com","website":"https://example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://new.example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + // The mapper omits "subject", but merge preserves it from the existing identity. + changed, err := s.UpdateIdentityFromClaims(ctx, claims, providerMissingSubject, i) + require.NoError(t, err) + assert.True(t, changed, "website changed") + assert.Contains(t, string(i.Traits), "alice@example.com", "subject should be preserved") + assert.Contains(t, string(i.Traits), "https://new.example.com", "website should be updated") + }) + + t.Run("case=nil metadata fields are coalesced to empty objects", func(t *testing.T) { + t.Parallel() + // Identity with nil metadata — simulates an identity that was created + // without metadata (e.g., password registration). + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: nil, + MetadataAdmin: nil, + } + + providerWithIdentity := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.update-identity.jsonnet", + }} + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + // Should not crash even though metadata fields are nil — the coalesce + // function converts nil to {} before passing to Jsonnet. + _, err := s.UpdateIdentityFromClaims(ctx, claims, providerWithIdentity, i) + require.NoError(t, err) + }) + + t.Run("case=verified addresses are applied from mapper output", func(t *testing.T) { + t.Parallel() + // Use a schema with verification enabled so Validate populates VerifiableAddresses. + _, verifyReg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "with-verification", URL: "file://./stub/registration-with-verification.schema.json"}, + }, + config.ViperKeyDefaultIdentitySchemaID: "with-verification", + })) + verifyStrategy := oidc.NewStrategy(verifyReg) + + i := &identity.Identity{ + SchemaID: "with-verification", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + RawClaims: map[string]any{ + "groups": []any{"admin"}, + }, + } + + // The hydra mapper outputs verified_addresses: [{via: "email", value: claims.sub}]. + changed, err := verifyStrategy.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + assert.True(t, changed) + + // The address should now be verified. + require.Len(t, i.VerifiableAddresses, 1) + assert.True(t, i.VerifiableAddresses[0].Verified) + assert.Equal(t, identity.VerifiableAddressStatusCompleted, i.VerifiableAddresses[0].Status) + assert.NotNil(t, i.VerifiableAddresses[0].VerifiedAt) + }) + + t.Run("case=already verified addresses are not re-verified", func(t *testing.T) { + t.Parallel() + _, verifyReg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "with-verification", URL: "file://./stub/registration-with-verification.schema.json"}, + }, + config.ViperKeyDefaultIdentitySchemaID: "with-verification", + })) + verifyStrategy := oidc.NewStrategy(verifyReg) + + // Use subject-only mapper so traits don't change, isolating the + // verified address behavior. + providerSubjectOnly := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.subject-only.jsonnet", + }} + + originalVerifiedAt := sqlxx.NullTime(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) + i := &identity.Identity{ + SchemaID: "with-verification", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: "alice@example.com", + Via: identity.AddressTypeEmail, + Status: identity.VerifiableAddressStatusCompleted, + Verified: true, + VerifiedAt: &originalVerifiedAt, + }, + }, + RecoveryAddresses: []identity.RecoveryAddress{ + { + Value: "alice@example.com", + Via: identity.AddressTypeEmail, + }, + }, + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := verifyStrategy.UpdateIdentityFromClaims(ctx, claims, providerSubjectOnly, i) + require.NoError(t, err) + // No change — traits are the same, address was already verified. + assert.False(t, changed) + + // VerifiedAt should not be updated. + require.Len(t, i.VerifiableAddresses, 1) + assert.True(t, i.VerifiableAddresses[0].Verified) + assert.Equal(t, &originalVerifiedAt, i.VerifiableAddresses[0].VerifiedAt) + }) + + t.Run("case=mapper that omits metadata preserves existing values", func(t *testing.T) { + t.Parallel() + // This mapper only outputs traits, not metadata_public or metadata_admin. + providerTraitsOnly := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.traits-only.jsonnet", + }} + + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{"preserved":"yes"}`), + MetadataAdmin: []byte(`{"secret":"admin-data"}`), + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + Website: "https://new.example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := s.UpdateIdentityFromClaims(ctx, claims, providerTraitsOnly, i) + require.NoError(t, err) + assert.True(t, changed, "traits changed (website added)") + + // Metadata should be preserved since the mapper doesn't output metadata keys. + assert.Equal(t, `{"preserved":"yes"}`, string(i.MetadataPublic)) + assert.Equal(t, `{"secret":"admin-data"}`, string(i.MetadataAdmin)) + }) + + t.Run("case=recovery addresses survive UpdateIdentityFromClaims", func(t *testing.T) { + t.Parallel() + _, verifyReg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "with-verification", URL: "file://./stub/registration-with-verification.schema.json"}, + }, + config.ViperKeyDefaultIdentitySchemaID: "with-verification", + })) + verifyStrategy := oidc.NewStrategy(verifyReg) + + recoveryID := uuid.Must(uuid.NewV4()) + i := &identity.Identity{ + SchemaID: "with-verification", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + RecoveryAddresses: []identity.RecoveryAddress{ + { + ID: recoveryID, + Value: "alice@example.com", + Via: identity.AddressTypeEmail, + }, + }, + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + _, err := verifyStrategy.UpdateIdentityFromClaims(ctx, claims, provider, i) + require.NoError(t, err) + + // Recovery address should survive the Validate call. + require.Len(t, i.RecoveryAddresses, 1) + assert.Equal(t, "alice@example.com", i.RecoveryAddresses[0].Value) + assert.Equal(t, identity.AddressTypeEmail, i.RecoveryAddresses[0].Via) + // The ID should be preserved from the original. + assert.Equal(t, recoveryID, i.RecoveryAddresses[0].ID) + }) + + t.Run("case=verified status is sticky when mapper stops asserting it", func(t *testing.T) { + t.Parallel() + _, verifyReg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "with-verification", URL: "file://./stub/registration-with-verification.schema.json"}, + }, + config.ViperKeyDefaultIdentitySchemaID: "with-verification", + })) + verifyStrategy := oidc.NewStrategy(verifyReg) + + // Use a mapper that does NOT output verified_addresses. + providerNoVerify := &testUpdateProvider{config: &oidc.Configuration{ + ID: "test", + Provider: "generic", + Mapper: "file://./stub/oidc.subject-only.jsonnet", + }} + + originalVerifiedAt := sqlxx.NullTime(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) + i := &identity.Identity{ + SchemaID: "with-verification", + Traits: identity.Traits(`{"subject":"alice@example.com"}`), + MetadataPublic: []byte(`{}`), + MetadataAdmin: []byte(`{}`), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: "alice@example.com", + Via: identity.AddressTypeEmail, + Status: identity.VerifiableAddressStatusCompleted, + Verified: true, + VerifiedAt: &originalVerifiedAt, + }, + }, + RecoveryAddresses: []identity.RecoveryAddress{ + { + Value: "alice@example.com", + Via: identity.AddressTypeEmail, + }, + }, + } + + claims := &oidc.Claims{ + Subject: "alice@example.com", + RawClaims: map[string]any{"_placeholder": true}, + } + + changed, err := verifyStrategy.UpdateIdentityFromClaims(ctx, claims, providerNoVerify, i) + require.NoError(t, err) + assert.False(t, changed, "nothing should change") + + // Verified status should be sticky — the mapper not asserting it + // should not revoke verification. + require.Len(t, i.VerifiableAddresses, 1) + assert.True(t, i.VerifiableAddresses[0].Verified) + assert.Equal(t, identity.VerifiableAddressStatusCompleted, i.VerifiableAddresses[0].Status) + assert.Equal(t, &originalVerifiedAt, i.VerifiableAddresses[0].VerifiedAt) + }) +} diff --git a/selfservice/strategy/oidc/stub/extension/payload.json b/selfservice/strategy/oidc/stub/extension/payload.json new file mode 100644 index 0000000..13b59f4 --- /dev/null +++ b/selfservice/strategy/oidc/stub/extension/payload.json @@ -0,0 +1,7 @@ +{ + "email": "someone@email.org", + "names": [ + "peter", + "pan" + ] +} \ No newline at end of file diff --git a/selfservice/strategy/oidc/stub/extension/schema.json b/selfservice/strategy/oidc/stub/extension/schema.json new file mode 100644 index 0000000..1d57ae2 --- /dev/null +++ b/selfservice/strategy/oidc/stub/extension/schema.json @@ -0,0 +1,49 @@ +{ + "$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": { + "mappings": { + "identity": { + "traits": [ + { + "path": "email" + } + ] + } + } + } + }, + "names": { + "type": "array", + "items": { + "type": "string", + "ory.sh/kratos": { + "mappings": { + "identity": { + "traits": [ + { + "path": "names.-1" + } + ] + } + } + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/selfservice/strategy/oidc/stub/jwk.json b/selfservice/strategy/oidc/stub/jwk.json new file mode 100644 index 0000000..7922b73 --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwk.json @@ -0,0 +1,14 @@ +{ + "p": "8Bh18gHHaVBWHgHX2s9eAsTHSgFq1kSvAuhvWpipsfbrDMBYZ2nwPCB1g3hb-L6cIMzxTmsv8bxwC0l316I8f77NgjSB4mlZAHjXZp861Z9xAFQ_Kx9ZcRldGmbUQ0NOQaHxMCSh5C1hmr8X54BzTEuMTlOnjVQrKUAPoaxhRpU", + "kty": "RSA", + "q": "4id2MrhpalEruyQUIDeLts0rqqanYhny1PU_K7CibTmAFm88U2npceffxg0o6RLYccybx51VYvzPqp01uGL5-TWAJLHihpXbVt8pgidok21mN54IXScG_EOblrp6sPjzhn39dpiCzAgIZhgOQR-IHepWSYwEuKbD8mVnKkYD44c", + "d": "LsaTOZr2KEUMppL92JtlDatcVSXSDYBTqhG4Sr2Pn-Pbq-VVpHTIRtvHeAz3Kiyei8MYG7jhOLzoA2zQGq7DqNP3BGOAPjPgPqW7nQ33TFIAlS8Q-8iJnmZC3NNG4G0go5YJoNgu7aQeBmOqL_jngCwAxvF2KvmOEOpfZzG9Nt0XMBMnAWa5NskKrtnB7b8oxxjghPyLsFa_N9JfjYPUDh_zZWfgv866k2UEqLaWOUMDRhfFpXeEu_YL4i8xwGmX-mDAxESXEjRZQrK29_SldIhTAopzGoacmic9CJwntGENOt5hihlDzCpMUEKafiFzODCQVYZSl0GaYupopZoH2Q", + "e": "AQAB", + "use": "sig", + "kid": "RTSkc-Jk-iD1lx2TK9WtYDtBtWs0MSyCG0nQSR1mglU", + "qi": "ini3TIy3h4Lm85imQRXVjd5VyzA4_b7CkRhIAbj2Q-Pv2xoHJ6xhDgE6jRa1rher4DNrcAuBuOel5f_2U6EcVe6IoQU5-IvqS7hfo0JCWTjCjBKNTqpNFjUC2kE8kaWKaP_tfDESmW-ZO0iKAbuRCneSyNHgj2RQIcXFbq1bIRQ", + "dp": "nk7ClgtuPJZn8ektNm08g37UGIvOsfEfpD82DPpUCa_RU9sPb0B-0mZklYcqvVyQ_V-kTByIxE-HYSnUBy5FzcU1JAETEwJ7WMBU5qle1bQHgjwKWpiVFOmwZdQfaSpb0xLAQQomZJk3nh0Z2d7sJwY5QPwPojQ5MT24ENXkXfE", + "alg": "RS256", + "dq": "bbifi_QUkNRY1y6l5QuN6V6ZdO3t_5Z_Tfq-bz__Tea70iadqgqUjALnensgAhR2lp-iZLJcnu3xAuHLEm5SwSnHxgXX1VwXUopq5Q6hmgVVtl4hyLAKn5Fdhz9qDzp5TCMMOeG8c6jiCkZZhBb8PydWPdCE6eFe59dyufvGHzk", + "n": "1BqatHWJWPTN4mnkrhns2pSk4LRe7W0cyjs8vp7INp3PtgzQ0-KuUibnE0v0k-6DQcu-3hkP88fUOGlYm2z0x9y2urMoAepBwiWybs1v0xqaQx_eLxtkCiElF3i9zYTqmR6cdAOb_duKpTeuTZm326UdpmDoU3TwcGBcszNkNfcNjeq-3z2bvS3GXvJIR9A-6j17xAXVVifUwjPIcNs21ajJVhNFcM9cM90OG9mivbE645KkqMT-WQxFPEQ-DqjijmXnLed9QabcfXcZdkT96O7x_zxHTgF46FlVyJtswuLRUO-h-48ai3-6NmfnR0tOCoVYhHJxndcQ0L_-uqFXkw" +} diff --git a/selfservice/strategy/oidc/stub/jwks_public.json b/selfservice/strategy/oidc/stub/jwks_public.json new file mode 100644 index 0000000..48ada38 --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwks_public.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "RTSkc-Jk-iD1lx2TK9WtYDtBtWs0MSyCG0nQSR1mglU", + "alg": "RS256", + "n": "1BqatHWJWPTN4mnkrhns2pSk4LRe7W0cyjs8vp7INp3PtgzQ0-KuUibnE0v0k-6DQcu-3hkP88fUOGlYm2z0x9y2urMoAepBwiWybs1v0xqaQx_eLxtkCiElF3i9zYTqmR6cdAOb_duKpTeuTZm326UdpmDoU3TwcGBcszNkNfcNjeq-3z2bvS3GXvJIR9A-6j17xAXVVifUwjPIcNs21ajJVhNFcM9cM90OG9mivbE645KkqMT-WQxFPEQ-DqjijmXnLed9QabcfXcZdkT96O7x_zxHTgF46FlVyJtswuLRUO-h-48ai3-6NmfnR0tOCoVYhHJxndcQ0L_-uqFXkw" + } + ] +} diff --git a/selfservice/strategy/oidc/stub/jwks_public2.json b/selfservice/strategy/oidc/stub/jwks_public2.json new file mode 100644 index 0000000..6abd169 --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwks_public2.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "swo5qZbZECb_alwFmwoleMN7nFw6Us-TP6f-sKIPDF0", + "alg": "RS256", + "n": "kOB5UX-fhCEesMn7BBRKCwkV33blQrD4xZhRK3rQDySNGwf9Uoeemm6SsO5E3WBnYQHWyH4X4jlwVNmkqBHyijKs3v2DBhIZTXa0dU2qp6dGJXQObSHKN51RzPX6yE3DiuzhKcl0ORlvjZk2nzPDl3l9y_Fl6opjFsnnCdHUovqdTBi9HcdocF7E5QeFvQG0QHs8zDC1myjs73m1F18IlTF6peXgFhgsiPKrXrgugh8vItpzr4dfA8fK3ND-NBLloNaZbtCAGmW6vxJnRae2PVBzSf8GamMBCuCNOJBRaQMRkZnZvkAIMGc9WngaHFhGBnW3IC__B0e-_fCyAVr4KQ" + } + ] +} diff --git a/selfservice/strategy/oidc/stub/merge/0.schema.json b/selfservice/strategy/oidc/stub/merge/0.schema.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/selfservice/strategy/oidc/stub/merge/0.schema.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/selfservice/strategy/oidc/stub/merge/1.schema.json b/selfservice/strategy/oidc/stub/merge/1.schema.json new file mode 100644 index 0000000..d31ab71 --- /dev/null +++ b/selfservice/strategy/oidc/stub/merge/1.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "baz": { + "type": "string" + }, + "bool": { + "type": "boolean" + }, + "opv": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/strategy/oidc/stub/merge/2.schema.json b/selfservice/strategy/oidc/stub/merge/2.schema.json new file mode 100644 index 0000000..d31ab71 --- /dev/null +++ b/selfservice/strategy/oidc/stub/merge/2.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "baz": { + "type": "string" + }, + "bool": { + "type": "boolean" + }, + "opv": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/strategy/oidc/stub/merge/3.schema.json b/selfservice/strategy/oidc/stub/merge/3.schema.json new file mode 100644 index 0000000..d31ab71 --- /dev/null +++ b/selfservice/strategy/oidc/stub/merge/3.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "baz": { + "type": "string" + }, + "bool": { + "type": "boolean" + }, + "opv": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet b/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet new file mode 100644 index 0000000..4a1c102 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if "email" in claims then "email" else null]: claims.email, + }, + }, + } diff --git a/selfservice/strategy/oidc/stub/oidc.hydra.jsonnet b/selfservice/strategy/oidc/stub/oidc.hydra.jsonnet new file mode 100644 index 0000000..0cebce2 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.hydra.jsonnet @@ -0,0 +1,27 @@ +local claims = std.extVar('claims'); +local identity = std.extVar('identity'); + +// Keep the website if the user has set one. +local website = std.get(identity.traits, "website", std.get(claims, "website", "")); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if website != "" then "website" else null]: website, + [if "groups" in claims.raw_claims then "groups" else null]: claims.raw_claims.groups, + }, + metadata_public: { + [if "picture" in claims then "picture" else null]: claims.picture, + }, + metadata_admin: { + [if "phone_number" in claims then "phone_number" else null]: claims.phone_number, + }, + verified_addresses: [ + { via: "email", value: claims.sub }, + ], + }, + } diff --git a/selfservice/strategy/oidc/stub/oidc.invalid-mapper.jsonnet b/selfservice/strategy/oidc/stub/oidc.invalid-mapper.jsonnet new file mode 100644 index 0000000..2f57565 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.invalid-mapper.jsonnet @@ -0,0 +1,7 @@ +local claims = std.extVar('claims'); + +{ + identity: { + traits: "this is not an object", + }, +} diff --git a/selfservice/strategy/oidc/stub/oidc.linkedin.jsonnet b/selfservice/strategy/oidc/stub/oidc.linkedin.jsonnet new file mode 100644 index 0000000..a8cd974 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.linkedin.jsonnet @@ -0,0 +1,19 @@ +local claims = +{ + email_verified: false +} + std.extVar('claims'); +{ + identity: + { + traits: + { + email: claims.email, + [if "picture" in claims then "picture" else null]: claims.picture, + name: + { + first: claims.name, + last: claims.last_name, + } + }, + }, +} diff --git a/selfservice/strategy/oidc/stub/oidc.missing-subject.jsonnet b/selfservice/strategy/oidc/stub/oidc.missing-subject.jsonnet new file mode 100644 index 0000000..3b2b278 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.missing-subject.jsonnet @@ -0,0 +1,9 @@ +local claims = std.extVar('claims'); + +{ + identity: { + traits: { + website: claims.website, + }, + }, +} diff --git a/selfservice/strategy/oidc/stub/oidc.subject-only.jsonnet b/selfservice/strategy/oidc/stub/oidc.subject-only.jsonnet new file mode 100644 index 0000000..23626ba --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.subject-only.jsonnet @@ -0,0 +1,12 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + }, + }, + } diff --git a/selfservice/strategy/oidc/stub/oidc.traits-only.jsonnet b/selfservice/strategy/oidc/stub/oidc.traits-only.jsonnet new file mode 100644 index 0000000..ced6a02 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.traits-only.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if "website" in claims then "website" else null]: claims.website, + }, + }, + } diff --git a/selfservice/strategy/oidc/stub/oidc.uaepass.jsonnet b/selfservice/strategy/oidc/stub/oidc.uaepass.jsonnet new file mode 100644 index 0000000..55f520e --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.uaepass.jsonnet @@ -0,0 +1,32 @@ +local claims = std.extVar('claims'); + +local raw = claims.raw_claims; + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if "email" in claims && claims.email != '' then "email" else null]: claims.email, + [if "fullnameEN" in raw && raw.fullnameEN != '' then "name" else null]: { + full: raw.fullnameEN, + [if "firstnameEN" in raw && raw.firstnameEN != '' then "first" else null]: raw.firstnameEN, + [if "lastnameEN" in raw && raw.lastnameEN != '' then "last" else null]: raw.lastnameEN, + }, + [if "fullnameAR" in raw && raw.fullnameAR != '' then "name_ar" else null]: { + full: raw.fullnameAR, + [if "firstnameAR" in raw && raw.firstnameAR != '' then "first" else null]: raw.firstnameAR, + [if "lastnameAR" in raw && raw.lastnameAR != '' then "last" else null]: raw.lastnameAR, + }, + [if "uuid" in raw && raw.uuid != '' then "uuid" else null]: raw.uuid, + [if "unifiedId" in raw && raw.unifiedId != '' then "unified_id" else null]: raw.unifiedId, + [if "idn" in raw && raw.idn != '' then "idn" else null]: raw.idn, + [if "userType" in raw && raw.userType != '' then "user_type" else null]: raw.userType, + [if "nationalityEN" in raw && raw.nationalityEN != '' then "nationality" else null]: raw.nationalityEN, + [if "gender" in raw && raw.gender != '' then "gender" else null]: raw.gender, + [if "mobile" in raw && raw.mobile != '' then "phone" else null]: raw.mobile, + }, + }, + } diff --git a/selfservice/strategy/oidc/stub/oidc.update-identity.jsonnet b/selfservice/strategy/oidc/stub/oidc.update-identity.jsonnet new file mode 100644 index 0000000..5fedccb --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.update-identity.jsonnet @@ -0,0 +1,26 @@ +local claims = std.extVar('claims'); +local identity = std.extVar('identity'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if "website" in claims then "website" else null]: claims.website, + // Keep existing groups if the claims don't provide them. + [if "groups" in claims.raw_claims then "groups" else null]: + claims.raw_claims.groups, + }, + metadata_public: { + [if "picture" in claims then "picture" else null]: claims.picture, + // Preserve existing metadata that isn't in the claims. + [if "existing_field" in identity.metadata_public then "existing_field" else null]: + identity.metadata_public.existing_field, + }, + metadata_admin: { + [if "phone_number" in claims then "phone_number" else null]: claims.phone_number, + }, + }, + } diff --git a/selfservice/strategy/oidc/stub/registration-aal.schema.json b/selfservice/strategy/oidc/stub/registration-aal.schema.json new file mode 100644 index 0000000..a6387b3 --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration-aal.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": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/registration-multi-schema-extra-fields.schema.json b/selfservice/strategy/oidc/stub/registration-multi-schema-extra-fields.schema.json new file mode 100644 index 0000000..766da84 --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration-multi-schema-extra-fields.schema.json @@ -0,0 +1,60 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "extra_data": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 2 + }, + "website": { + "type": "string", + "format": "uri" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["subject", "extra_data"] + }, + "metadata_public": { + "type": "object", + "properties": { + "picture": { + "type": "string" + } + } + }, + "metadata_admin": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/registration-phone.schema.json b/selfservice/strategy/oidc/stub/registration-phone.schema.json new file mode 100644 index 0000000..5d623ed --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration-phone.schema.json @@ -0,0 +1,58 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "name": { + "type": "string", + "minLength": 2 + }, + "website": { + "type": "string", + "format": "uri" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "subject" + ] + }, + "metadata_public": { + "type": "object", + "properties": { + "picture": { + "type": "string" + } + } + }, + "metadata_admin": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/registration-verifiable-email.schema.json b/selfservice/strategy/oidc/stub/registration-verifiable-email.schema.json new file mode 100644 index 0000000..b405c24 --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration-verifiable-email.schema.json @@ -0,0 +1,61 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + }, + "name": { + "type": "string", + "minLength": 2 + }, + "website": { + "type": "string", + "format": "uri" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "subject" + ] + }, + "metadata_public": { + "type": "object", + "properties": { + "picture": { + "type": "string" + } + } + }, + "metadata_admin": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/registration-with-verification.schema.json b/selfservice/strategy/oidc/stub/registration-with-verification.schema.json new file mode 100644 index 0000000..8ce3e09 --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration-with-verification.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "https://example.com/person-with-verification.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "website": { + "type": "string", + "format": "uri" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "subject" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/registration.schema.json b/selfservice/strategy/oidc/stub/registration.schema.json new file mode 100644 index 0000000..5d623ed --- /dev/null +++ b/selfservice/strategy/oidc/stub/registration.schema.json @@ -0,0 +1,58 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "name": { + "type": "string", + "minLength": 2 + }, + "website": { + "type": "string", + "format": "uri" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "subject" + ] + }, + "metadata_public": { + "type": "object", + "properties": { + "picture": { + "type": "string" + } + } + }, + "metadata_admin": { + "type": "object", + "properties": { + "phone_number": { + "type": "string" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/settings.schema.json b/selfservice/strategy/oidc/stub/settings.schema.json new file mode 100644 index 0000000..9d0752b --- /dev/null +++ b/selfservice/strategy/oidc/stub/settings.schema.json @@ -0,0 +1,32 @@ +{ + "$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": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "name": { + "type": "string", + "minLength": 2 + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/stub/stub.schema.json b/selfservice/strategy/oidc/stub/stub.schema.json new file mode 100644 index 0000000..f9e46b7 --- /dev/null +++ b/selfservice/strategy/oidc/stub/stub.schema.json @@ -0,0 +1,10 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": {} + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/oidc/token_verifier.go b/selfservice/strategy/oidc/token_verifier.go new file mode 100644 index 0000000..408d28c --- /dev/null +++ b/selfservice/strategy/oidc/token_verifier.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + + "github.com/ory/x/reqlog" +) + +func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, rawIDToken, issuerURL string) (*Claims, error) { + tokenAudiences := append([]string{config.ClientID}, config.AdditionalIDTokenAudiences...) + var token *oidc.IDToken + err := fmt.Errorf("no audience matched the token's audience") + for _, aud := range tokenAudiences { + verifier := oidc.NewVerifier(issuerURL, keySet, &oidc.Config{ + ClientID: aud, + }) + t0 := time.Now() + token, err = verifier.Verify(ctx, rawIDToken) + reqlog.AccumulateExternalLatency(ctx, time.Since(t0)) + if err != nil && strings.Contains(err.Error(), "oidc: expected audience") { + // The audience is not the one we expect, try the next one + continue + } else if err != nil { + // Something else went wrong + return nil, err + } + // The token was verified successfully + break + } + if err != nil { + // None of the allowed audiences matched the audience in the token + return nil, fmt.Errorf("token audience didn't match allowed audiences: %+v %w", tokenAudiences, err) + } + claims := &Claims{} + var rawClaims map[string]any + + if token == nil { + return nil, fmt.Errorf("token is nil") + } + + if err := token.Claims(claims); err != nil { + return nil, err + } + if err = token.Claims(&rawClaims); err != nil { + return nil, err + } + claims.RawClaims = rawClaims + + return claims, nil +} diff --git a/selfservice/strategy/oidc/types.go b/selfservice/strategy/oidc/types.go new file mode 100644 index 0000000..69fe4c2 --- /dev/null +++ b/selfservice/strategy/oidc/types.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "cmp" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/text" + + "github.com/ory/kratos/ui/container" + + "github.com/ory/kratos/ui/node" + + "github.com/gofrs/uuid" +) + +type FlowMethod struct { + *container.Container +} + +func AddProviders(c *container.Container, providers []Configuration, message func(provider string, providerId string) *text.Message, credentialsType identity.CredentialsType) { + for _, p := range providers { + if len(p.OrganizationID) > 0 { + continue + } + AddProvider(c, p.ID, message(cmp.Or(p.Label, p.ID), p.ID), credentialsType) + } +} + +func AddProvider(c *container.Container, providerID string, message *text.Message, credentialsType identity.CredentialsType) { + group := node.OpenIDConnectGroup + if credentialsType == identity.CredentialsTypeSAML { + group = node.SAMLGroup + } + field := node.NewInputField("provider", providerID, group, node.InputAttributeTypeSubmit).WithMetaLabel(message) + c.GetNodes().RemoveMatching(field) + c.GetNodes().Append(field) +} + +func NewFlowMethod(f *container.Container) *FlowMethod { + return &FlowMethod{Container: f} +} + +type ider interface { + GetID() uuid.UUID +} diff --git a/selfservice/strategy/passkey/.schema/login.schema.json b/selfservice/strategy/passkey/.schema/login.schema.json new file mode 100644 index 0000000..af01cb9 --- /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 0000000..a2ac044 --- /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 0000000..58bf997 --- /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 0000000..9765040 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=api.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=api.json new file mode 100644 index 0000000..d827c9c --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=api.json @@ -0,0 +1,81 @@ +[ + { + "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": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=browser.json new file mode 100644 index 0000000..9765040 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=browser.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=spa.json new file mode 100644 index 0000000..9765040 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-passkey_display_name-type=spa.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=api.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=api.json new file mode 100644 index 0000000..d827c9c --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=api.json @@ -0,0 +1,81 @@ +[ + { + "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": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=browser.json new file mode 100644 index 0000000..9765040 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=browser.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=spa.json new file mode 100644 index 0000000..9765040 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=succeeds_with_passwordless_login-webauthn_identifier-type=spa.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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 0000000..324f08d --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 0000000..324f08d --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 0000000..2b87ba4 --- /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": "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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_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" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=api-response.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=api-response.json new file mode 100644 index 0000000..fa44203 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=api-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=api.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=api.json new file mode 100644 index 0000000..cccc13f --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=api.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=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 0000000..fa44203 --- /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 0000000..cccc13f --- /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 0000000..fa44203 --- /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 0000000..cccc13f --- /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 0000000..164cac6 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.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": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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" + } +] 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 0000000..ec7d7be --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json @@ -0,0 +1,98 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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", + "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 0000000..af8d4c0 --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 0000000..fe51488 --- /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 0000000..94263a4 --- /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 0000000..fe51488 --- /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 0000000..94263a4 --- /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 0000000..94263a4 --- /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 0000000..fe51488 --- /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 0000000..94263a4 --- /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 0000000..c96d9c6 --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 0000000..fe51488 --- /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 0000000..fe51488 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=api.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=api.json new file mode 100644 index 0000000..c846132 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=api.json @@ -0,0 +1,58 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=browser.json new file mode 100644 index 0000000..9a2c843 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod-type=browser.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..49ae712 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-Dg0gN3fy+JoKxRp9Zda/4KYn3SlMdaKjs3fK5g6nDVQ/CVakD1dfMQyvRtJeiAtzSMEFviJbBLcVSrsBPGsFBA==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=api.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=api.json new file mode 100644 index 0000000..c846132 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=api.json @@ -0,0 +1,58 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=browser.json new file mode 100644 index 0000000..9a2c843 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials-type=browser.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..49ae712 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-Dg0gN3fy+JoKxRp9Zda/4KYn3SlMdaKjs3fK5g6nDVQ/CVakD1dfMQyvRtJeiAtzSMEFviJbBLcVSrsBPGsFBA==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.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/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.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/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..9a2c843 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.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/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..9a2c843 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=api.json b/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=api.json new file mode 100644 index 0000000..27bf405 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=api.json @@ -0,0 +1,59 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclickTrigger": "oryPasskeySettingsRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_settings_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=browser.json new file mode 100644 index 0000000..2e21520 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestPopulateSettingsMethod-method=PopulateSettingsMethod-type=browser.json @@ -0,0 +1,75 @@ +[ + { + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_settings_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] 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 0000000..f445826 --- /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": "default", + "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": "default", + "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 0000000..f445826 --- /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": "default", + "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": "default", + "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 0000000..9c34e53 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -0,0 +1,139 @@ +[ + { + "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": "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": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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_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": { + "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 0000000..9c34e53 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -0,0 +1,139 @@ +[ + { + "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": "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": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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_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": { + "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 0000000..1fdf7ca --- /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 0000000..2a6ad69 --- /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 0000000..fcbca88 --- /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 0000000..3a4ff0a --- /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 0000000..2e83f5a --- /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 0000000..50d686c --- /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 0000000..cd9949c --- /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 0000000..8b480b3 --- /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 0000000..5aa6d11 --- /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 0000000..c4c2e93 --- /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 0000000..7d2b381 --- /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 0000000..5aa6d11 --- /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 0000000..c4c2e93 --- /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 0000000..7d2b381 --- /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/nodes.go b/selfservice/strategy/passkey/nodes.go new file mode 100644 index 0000000..c086e93 --- /dev/null +++ b/selfservice/strategy/passkey/nodes.go @@ -0,0 +1,49 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/webauthnx/js" +) + +func injectOptions(o []byte) *node.Node { + return &node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyCreateData, + Type: node.InputAttributeTypeHidden, + FieldValue: string(o), + }, + } +} + +func passkeyRegister() *node.Node { + return &node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyRegister, + Type: node.InputAttributeTypeHidden, + }, + } +} + +func passkeyRegisterTrigger() *node.Node { + return &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, + }, + } +} diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go new file mode 100644 index 0000000..99de1a2 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login.go @@ -0,0 +1,481 @@ +// 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.AAL1FormHydrator = new(Strategy) + +func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error { + ctx := r.Context() + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := loginFlow.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + 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 = node.InputAttributeAutocompleteUsernameWebauthn + }, + ).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), + }, + }) + + // Inject the WebAuthn JS script only for non-API flows. API flows must not include script nodes. + if loginFlow.Type == flow.TypeBrowser { + loginFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + } + + passkeyLoginAttr := &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + OnLoadTrigger: js.WebAuthnTriggersPasskeyLoginAutocompleteInit, + } + // Only attach raw JS onLoad for browser flows; keep onLoadTrigger as a semantic + // enum for all flow types so SPA/native apps can key off of it. + if loginFlow.Type == flow.TypeBrowser { + passkeyLoginAttr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" + } + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: passkeyLoginAttr, + }) + + 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) + + var p updateLoginFlowWithPasskeyMethod + if err := decoderx.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, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError(). + WithReason("Could not load identity credentials"). + WithWrap(err)), i.ID)) + } + + 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, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError(). + WithReason("The WebAuthn credentials could not be decoded properly"). + WithDebug(err.Error()). + WithWrap(err)), i.ID)) + } + + webAuthCreds := o.Credentials.PasswordlessOnly(&webAuthnResponse.Response.AuthenticatorData.Flags) + validatedCredential, 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, x.WrapWithIdentityIDError(errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/")), i.ID)) + } + + // Persist the updated signature counter and clone warning for W3C WebAuthn + // clone detection. This must not block the login: the user already proved + // possession of the authenticator. Use credentialType, which may be webauthn + // (legacy migration path) rather than passkey. + webauthnx.UpdateSignCountAfterLogin(ctx, s.d, i, credentialType, validatedCredential) + + // 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, x.WrapWithIdentityIDError(errors.WithStack(err), i.ID)) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("Could not update flow").WithDebug(err.Error())), i.ID)) + } + + return i, nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow, _ *session.Session) error { + 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), + }, + }) + + // Inject the WebAuthn JS script only for non-API flows. API flows must not include script nodes. + if f.Type == flow.TypeBrowser { + 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) { + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + // Only attach raw JS handler for browser flows; API flows must stay script-free. + if f.Type == flow.TypeBrowser { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + } + }), + ).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 err := s.populateLoginMethodForPasskeys(r, f); err != nil { + return err + } + + trigger := node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + // Only attach raw JS handler for browser flows; API flows must stay script-free. + if f.Type == flow.TypeBrowser { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + } + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()) + + f.UI.Nodes.Append(trigger) + + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + 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) { + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + // Only attach raw JS handler for browser flows; API flows must stay script-free. + if sr.Type == flow.TypeBrowser { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + } + }), + ).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 err := s.populateLoginMethodForPasskeys(r, sr); err != nil { + return err + } + + return nil +} diff --git a/selfservice/strategy/passkey/passkey_login_signcount_test.go b/selfservice/strategy/passkey/passkey_login_signcount_test.go new file mode 100644 index 0000000..ba9241e --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login_signcount_test.go @@ -0,0 +1,78 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "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/pkg/testhelpers" + "github.com/ory/kratos/ui/node" +) + +// TestLoginUpdatesSignCount is a regression test for HackerOne #3780149 on the +// passkey strategy (which uses ValidateDiscoverableLogin). +// +// The stored passkey credential starts at sign_count = 3 and the assertion +// response reports counter = 10. After a successful login, the stored +// SignCount must be updated to 10 so that W3C clone detection works. +func TestLoginUpdatesSignCount(t *testing.T) { + t.Parallel() + + fix := newLoginFixture(t) + fix.conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + + storedCredential := func(t *testing.T, idID uuid.UUID) (*identity.Credentials, *identity.AuthenticatorWebAuthn) { + stored, err := fix.reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), idID) + require.NoError(t, err) + c, ok := stored.GetCredentials(identity.CredentialsTypePasskey) + require.True(t, ok) + var conf identity.CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(c.Config, &conf)) + require.NotEmpty(t, conf.Credentials) + require.NotNil(t, conf.Credentials[0].Authenticator) + return c, conf.Credentials[0].Authenticator + } + + id := fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + // Sanity: the registered credential starts at the fixture's sign_count = 3. + credBefore, authBefore := storedCredential(t, id.ID) + require.EqualValues(t, 3, authBefore.SignCount) + + browserClient := testhelpers.NewClientWithCookies(t) + body, _, _ := fix.submitWebAuthnLoginWithClient(t, true, loginPasswordlessContext, browserClient, func(values url.Values) { + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + require.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + + credAfter, auth := storedCredential(t, id.ID) + assert.EqualValues(t, 10, auth.SignCount, + "stored SignCount must be updated from the login assertion for clone detection to work") + + // The assertion counter advanced (3 -> 10), so this is not a clone: the + // persisted clone warning must round-trip as false rather than being set + // spuriously on a normal login. + assert.False(t, auth.CloneWarning, + "a normally advancing counter must not set the clone warning") + + // The credential row must be updated in place, not deleted and recreated: + // its id and created_at must be stable across the login. + assert.Equal(t, credBefore.ID, credAfter.ID, + "the credential row must be updated in place (stable id), not recreated") + assert.Equal(t, credBefore.CreatedAt, credAfter.CreatedAt, + "persisting the sign counter must not reset the credential's created_at") +} diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go new file mode 100644 index 0000000..7b4605a --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -0,0 +1,679 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "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/contextx" + "github.com/ory/x/ioutilx" + "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=API flow builds standard nodes and skips JS", func(t *testing.T) { + // Build a properly initialized API flow to avoid nil UI/container issues. + r := httptest.NewRequest("GET", "/self-service/login/api", nil).WithContext(t.Context()) + f, err := login.NewFlow(fix.reg, r, flow.TypeAPI) + require.NoError(t, err) + f.UI.Nodes = make(node.Nodes, 0) + + // For API flows, the strategy should build standard nodes and return before adding JS nodes. + assert.Nil(t, s.PopulateLoginMethodFirstFactor(r, f)) + + // Assert that the passkey challenge input exists (standard node built for API as well). + require.NotNil(t, f.UI.Nodes.Find("passkey_challenge")) + + // Assert no script nodes are present for API flows (JS injection is skipped). + for _, n := range f.UI.Nodes { + assert.NotEqual(t, node.Script, n.Type, "API flow must not include script nodes") + } + }) +} + +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", + }) + }) + + // Assert API-specific CSRF/header failures (Cookie/Origin) similar to registration tests + t.Run("AssertCSRFFailuresAPI", func(t *testing.T) { + fix := newLoginFixture(t) + // Create identity with passkey credentials so the test setup is valid + fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + apiClient := testhelpers.NewDebugClient(t) + + // Now test the specific CSRF error causes for API flows: adding Cookie and Origin headers + for _, tc := range []struct { + mod func(http.Header) + expKey string + }{ + { + mod: func(h http.Header) { h.Add("Cookie", "name=bar") }, + expKey: "Cookie", + }, + { + mod: func(h http.Header) { h.Add("Origin", "www.bar.com") }, + expKey: "Origin", + }, + } { + t.Run(fmt.Sprintf("case=should_fail_with_correct_CSRF_error_cause_local/type=api/%s", tc.expKey), func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + + // Attach internal context to the flow + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = loginPasswordlessContext + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := url.Values{ + "csrf_token": {"invalid_token"}, + } + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + values.Del("method") + + req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + bodyBytes := ioutilx.MustReadAll(res.Body) + actual := string(bodyBytes) + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + + msg := gjson.Get(actual, "ui.messages.0.text").String() + assert.Contains(t, msg, tc.expKey, "actual payload: %s", actual) + }) + } + }) + + 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("type=api", func(t *testing.T) { + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + // Inject internal context required for replaying the WebAuthn response + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = loginPasswordlessContext + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + payload(values) + body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + 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("type=api", func(t *testing.T) { + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + // Inject internal context required for replaying the WebAuthn response + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = loginPasswordlessContext + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + payload(values) + body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + 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("type=api", func(t *testing.T) { + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + payload(values) + body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + 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(t.Context(), 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("type=api", func(t *testing.T) { + fix.conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + + // Inject invalid internal context + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = []byte("invalid context") + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + 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, + ) + }) + }) + + t.Run("case=succeeds with passwordless login", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + fix.conf.MustSet(t.Context(), 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)) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "2.attributes.nonce", + "2.attributes.src", + "5.attributes.value", + }) + + 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(t.Context()).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } + } + + runAPI := func(t *testing.T) { + fix.conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + id := fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false) + + // Inject internal context + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = loginPasswordlessContext + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "4.attributes.value", // passkey_challenge value for API (no script node) + }) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow) + + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, gjson.Get(body, "session.authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, "session.authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, "session.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))) + // API flows don't have continue_with redirect + 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("type=api", func(t *testing.T) { runAPI(t) }) + }) + 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("type=api", func(t *testing.T) { runAPI(t) }) + }) + }) + }) + + t.Run("flow=refresh", func(t *testing.T) { + fix := newLoginFixture(t) + fix.conf.MustSet(t.Context(), 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", + "api", + } { + t.Run(f, func(t *testing.T) { + if f == "api" { + // API refresh flow + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, fix.reg, id) + // Pass true for forced to enable refresh in API flow + apiFlow := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, true) + + // Inject internal context + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(apiFlow.Id)) + require.NoError(t, err) + interim.InternalContext = tc.context + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(apiFlow.Ui.Nodes) + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.PasskeyLogin, string(tc.response)) + body, res := testhelpers.LoginMakeRequest(t, true, false, apiFlow, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow, "%s", body) + + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, expectedAAL, gjson.Get(body, "session.authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, "session.authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.Len(t, gjson.Get(body, "session.authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, "session.identity.id").String(), "%s", body) + } else { + run(t, t.Context(), 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() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true) + ctx = contextx.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.AAL1FormHydrator) + 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(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, 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(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + 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 := contextx.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 := contextx.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 := contextx.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 := contextx.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 := contextx.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 := contextx.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))) + // Verify passkey button is present + assert.NotNil(t, f.UI.Nodes.Find(node.PasskeyLoginTrigger), "passkey button should be present when user has credentials") + 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) + // Verify passkey button is NOT present + assert.Nil(t, f.UI.Nodes.Find(node.PasskeyLoginTrigger), "passkey button should not be present when user has no credentials") + 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 0000000..18d33ab --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -0,0 +1,378 @@ +// 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/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/registration" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/randx" +) + +var _ registration.FormHydrator = new(Strategy) + +// 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) 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, ds *url.URL) (*updateRegistrationFlowWithPasskeyMethod, error) { + var p updateRegistrationFlowWithPasskeyMethod + err := registration.DecodeBody(&p, r, registrationSchema, ds) + 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) + + ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + params, err := s.decode(r, ds) + 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 +} + +// passkeyCreateData carries the WebAuthn registration ceremony options to +// the bundled JS. Both display-name fields are populated from the identity +// schema's flagged traits: DisplayNameFieldName is the alphabetically-first +// candidate kept for backward compatibility with custom UIs that only read +// the singular field; DisplayNameFieldNames is the full sorted list. Newer +// clients should consume DisplayNameFieldNames and pick the first form +// field with a non-empty value. +type passkeyCreateData struct { + CredentialOptions *protocol.CredentialCreation `json:"credentialOptions"` + DisplayNameFieldName string `json:"displayNameFieldName"` + DisplayNameFieldNames []string `json:"displayNameFieldNames"` +} + +// skipMethodOnMissingDisplayName downgrades errNoDisplayNameTrait to a +// warning so that a passkey method without a usable display-name trait drops +// its UI nodes instead of failing the whole registration flow: all other +// enabled methods must keep working even when the passkey method is +// misconfigured. Any other error is returned unchanged. +func (s *Strategy) skipMethodOnMissingDisplayName(r *http.Request, err error) error { + if !errors.Is(err, errNoDisplayNameTrait) { + return err + } + s.d.Logger().WithRequest(r).Warn("The passkey method is enabled, but the identity schema does not mark any trait as a passkey display name (passkey.display_name) or WebAuthn identifier (webauthn.identifier). The passkey method will not be offered on registration flows. Update the identity schema or disable the passkey method.") + return nil +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + ctx := r.Context() + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + opts, err := s.hydratePassKeyRegistrationOptions(ctx, f) + if err != nil { + return s.skipMethodOnMissingDisplayName(r, err) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Upsert(injectOptions(opts)) + + trigger := passkeyRegisterTrigger() + if f.Type == flow.TypeAPI { + // API flows should not have raw JS onClick handlers, but keep onClickTrigger + // as a semantic enum for SPA/native apps to key off of. + if attr, ok := trigger.Attributes.(*node.InputAttributes); ok { + attr.OnClick = "" + } + } + f.UI.Nodes.Upsert(trigger) + + if f.Type == flow.TypeBrowser { + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + } + + f.UI.Nodes.Upsert(passkeyRegister()) + return 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 +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + ctx := r.Context() + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + opts, err := s.hydratePassKeyRegistrationOptions(ctx, f) + if err != nil { + return s.skipMethodOnMissingDisplayName(r, err) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Upsert(injectOptions(opts)) + + trigger := passkeyRegisterTrigger() + if f.Type == flow.TypeAPI { + // API flows should not have raw JS onClick handlers, but keep onClickTrigger + // as a semantic enum for SPA/native apps to key off of. + if attr, ok := trigger.Attributes.(*node.InputAttributes); ok { + attr.OnClick = "" + } + } + f.UI.Nodes.Upsert(trigger) + + if f.Type == flow.TypeBrowser { + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + } + + f.UI.Nodes.Upsert(passkeyRegister()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + ctx := r.Context() + + opts, err := s.hydratePassKeyRegistrationOptions(ctx, f) + if err != nil { + // Without hydrated options no passkey nodes were added by + // PopulateRegistrationMethodCredentials either, so there is nothing + // to remove here. + return s.skipMethodOnMissingDisplayName(r, err) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.RemoveMatching(injectOptions(opts)) + f.UI.Nodes.RemoveMatching(passkeyRegisterTrigger()) + f.UI.Nodes.RemoveMatching(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.RemoveMatching(passkeyRegister()) + return nil +} + +func (s *Strategy) hydratePassKeyRegistrationOptions(ctx context.Context, f *registration.Flow) ([]byte, error) { + defaultSchemaURL, err := f.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return nil, err + } + + if options := gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionOptions)); options.IsObject() { + return []byte(options.Raw), nil + } + + createData := new(passkeyCreateData) + fieldNames, err := s.PasskeyDisplayNameFromSchema(ctx, defaultSchemaURL.String()) + if err != nil { + return nil, err + } + // fieldNames is guaranteed non-empty here: PasskeyDisplayNameFromSchema + // returns at least one candidate or an error. The singular field keeps the + // alphabetically-first candidate for backward compatibility with custom UIs + // that only read it. + createData.DisplayNameFieldNames = fieldNames + createData.DisplayNameFieldName = fieldNames[0] + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return nil, 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 nil, errors.WithStack(err) + } + + createData.CredentialOptions = option + injectWebAuthnOptions, err := json.Marshal(createData) + if err != nil { + return nil, errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetBytes( + f.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return nil, errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetRawBytes( + f.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionOptions), + injectWebAuthnOptions, + ) + if err != nil { + return nil, errors.WithStack(err) + } + + return injectWebAuthnOptions, nil +} diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go new file mode 100644 index 0000000..a436063 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -0,0 +1,965 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/registrationhelpers" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/assertx" + "github.com/ory/x/contextx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/randx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +var ( + flows = []string{"spa", "browser", "api"} + + //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() + // Use the shared helper for browser and spa flows (they already provide + // the WebAuthn internal context via the UI flow initialization). + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, []string{"spa", "browser"}, func(v url.Values) { + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + + // Prepare API flow once and persist the internal context so the + // subsequent header-mangling subtests can reuse it. + fix := newRegistrationFixture(t) + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, fix.publicTS) + + interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = registrationFixtureSuccessBrowserInternalContext + require.NoError(t, fix.reg.RegistrationFlowPersister().UpdateRegistrationFlow(t.Context(), interim)) + + values := url.Values{ + "csrf_token": {"invalid_token"}, + "traits.username": {testhelpers.RandomEmail()}, + "traits.foobar": {"bar"}, + } + values.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + values.Del("method") + + // Check that missing CSRF passes for API (registration happens) + actual, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + require.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, gjson.Get(actual, "identity.id").Raw, "%s", actual) + + // Now test the specific CSRF error causes for API flows: adding Cookie and Origin headers + for _, tc := range []struct { + mod func(http.Header) + exp string + expKey string + }{ + { + mod: func(h http.Header) { h.Add("Cookie", "name=bar") }, + exp: "The HTTP Request Header included the \"Cookie\" key", + expKey: "Cookie", + }, + { + mod: func(h http.Header) { h.Add("Origin", "www.bar.com") }, + exp: "The HTTP Request Header included the \"Origin\" key", + expKey: "Origin", + }, + } { + // Name this test to match the project's existing case naming and mark it as a local variant. + t.Run(fmt.Sprintf("case=should_fail_with_correct_CSRF_error_cause_local/type=api/%s", tc.expKey), func(t *testing.T) { + f2 := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, fix.publicTS) + + // attach internal context to the new flow + interim2, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(t.Context(), uuid.FromStringOrNil(f2.Id)) + require.NoError(t, err) + interim2.InternalContext = registrationFixtureSuccessBrowserInternalContext + require.NoError(t, fix.reg.RegistrationFlowPersister().UpdateRegistrationFlow(t.Context(), interim2)) + + req, err := http.NewRequest("POST", f2.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + bodyBytes := ioutilx.MustReadAll(res.Body) + actual := string(bodyBytes) + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + + // Extract the first ui message text (API error payload) + msg := gjson.Get(actual, "ui.messages.0.text").String() + // Ensure the API error mentions the header key we added (Cookie/Origin) + assert.Contains(t, msg, tc.expKey, "actual payload: %s", actual) + }) + } + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemaDoesNotExist(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, configx.WithValue(config.ViperKeyPasskeyEnabled, false)) + uiFlows := []string{"spa", "browser"} + for _, flowType := range uiFlows { + 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) + uiFlows := []string{"spa", "browser"} + for _, flowType := range uiFlows { + 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{ + "0.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() + + 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.Context(), 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() + + 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.Context(), t, fix.publicTS, fix.conf, f, values) + + assert.NotEmptyf(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Containsf(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.Equalf(t, "bar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual) + assert.Equalf(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Equalf(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() + + 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.submitPasskeyRegistration(t.Context(), 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) { + 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.submitPasskeyRegistration(t.Context(), t, f, testhelpers.NewClientWithCookies(t), values, + withInternalContext(sqlxx.JSONRawMessage(tc.internalContext))) + if flowIsSPA(f) || f == "api" { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual) + } else { + // Browser uses top-level status + 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() + + uiFlows := []string{"spa", "browser"} + for _, f := range uiFlows { + 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) + }) + } + }) + + t.Run("case=registration flow with multiple flagged traits returns all candidate field names", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration-multi-identifier.schema.json") + + uiFlows := []string{"spa", "browser"} + for _, f := range uiFlows { + t.Run("type="+f, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + isSPA := f == "spa" + + // Before the multi-trait fix this would 500 inside InitializeRegistrationFlowViaBrowser + // with "no identifier found". + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false) + + // Find the passkey_create_data hidden input node and extract its value. + var createDataValue string + for _, n := range regFlow.Ui.Nodes { + attrs := n.Attributes.UiNodeInputAttributes + if attrs == nil { + continue + } + if attrs.Name != node.PasskeyCreateData { + continue + } + if v, ok := attrs.Value.(string); ok { + createDataValue = v + } + break + } + + require.NotEmpty(t, createDataValue, "passkey_create_data node must be present and have a non-empty value") + + // Both flagged trait paths must appear in displayNameFieldNames. + fieldNames := gjson.Get(createDataValue, "displayNameFieldNames") + require.True(t, fieldNames.IsArray(), "displayNameFieldNames must be a JSON array, got: %s", createDataValue) + + names := make([]string, 0, len(fieldNames.Array())) + for _, r := range fieldNames.Array() { + names = append(names, r.String()) + } + assert.True(t, slices.Contains(names, "traits.email"), + "traits.email must be in displayNameFieldNames, got: %v", names) + assert.True(t, slices.Contains(names, "traits.phone"), + "traits.phone must be in displayNameFieldNames, got: %v", names) + + // Backward compatibility: the singular field stays populated with + // the alphabetically-first candidate so custom UIs that only read + // it continue to work. + assert.Equal(t, "traits.email", gjson.Get(createDataValue, "displayNameFieldName").String(), + "displayNameFieldName must be the first sorted candidate") + }) + } + }) + + getPrefix := func(f string) (prefix string) { + if f == "spa" || f == "api" { + prefix = "session." + } + return + } + + t.Run("successful registration", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + t.Cleanup(fix.disableSessionAfterRegistration) + + 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" || f == "api" { + 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(t.Context(), 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 []string{"spa", "browser", "api"} { + 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 { + if f == "api" { + 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(t.Context(), 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(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + fix.conf.MustSet(t.Context(), 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 []string{"spa", "browser", "api"} { + 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(t.Context(), 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 []string{"spa", "browser", "api"} { + 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 []string{"spa", "browser", "api"} { + 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(t.Context()).String(config.ViperKeyPasskeyRPID) + prevOrigins := fix.conf.GetProvider(t.Context()).String(config.ViperKeyPasskeyRPOrigins) + + fix.conf.MustSet(t.Context(), config.ViperKeyPasskeyRPID, "www.troweprice.com") + fix.conf.MustSet(t.Context(), config.ViperKeyPasskeyRPOrigins, []string{"android:apk-key-hash:S2RfNYgJmQiKgd6-sdbjW7phcL_OTP4vGE8L51Q2GB0"}) + t.Cleanup(func() { + fix.conf.MustSet(t.Context(), config.ViperKeyPasskeyRPID, prevRPID) + fix.conf.MustSet(t.Context(), 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" || f == "api" { + 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) + } + + if f != "api" { + assert.Containsf(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + } + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, "aal1", i.InternalAvailableAAL.String) + assert.Equalf(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + }) + + t.Run("case=multi-schema registration", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + fix.enableSessionAfterRegistration() + + 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") + } + } + invalidValues := func(email string) func(v url.Values) { + return func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "b") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + } + + fix.conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "does-not-exist") + fix.conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "does-not-exist", URL: "file://./stub/profile.schema.json"}, + {ID: "advanced-user", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true}, + }) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + t.Run("should create the identity and a session and use the correct schema", 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), withInitFlowWithOption([]testhelpers.InitFlowWithOption{testhelpers.InitFlowWithIdentitySchema("advanced-user")})) + + 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(t.Context(), identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + assert.Equal(t, "advanced-user", i.SchemaID, "%s", actual) + }) + + t.Run("registration should fail with invalid form data using the correct schema", func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual, res := fix.makeUnsuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", invalidValues(email), withUserID(userID), withInitFlowWithOption([]testhelpers.InitFlowWithOption{testhelpers.InitFlowWithIdentitySchema("advanced-user")})) + + if f == "browser" { + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", actual) + } else { + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + } + assert.Equal(t, int64(4000003), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int(), "%s", actual) + assert.Equal(t, "length must be \u003e= 2, but got 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String(), "%s", actual) + }) + }) + } + }) + + t.Run("case=profile_first registration flow", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + fix.enableSessionAfterRegistration() + fix.conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first") + + 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=successful registration", func(t *testing.T) { + for _, f := range []string{"browser", "spa"} { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-profile-first-" + 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(t.Context(), identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + }) + + t.Run("case=unified registration flow", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + fix.enableSessionAfterRegistration() + fix.conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationFlowStyle, "unified") + + 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=successful registration", func(t *testing.T) { + for _, f := range []string{"browser", "spa"} { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-unified-" + 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(t.Context(), identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + }) +} + +func TestPopulateRegistrationMethod(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPID, "localhost") + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypePasskey) + require.NoError(t, err) + + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes, except ...snapshotx.Opt) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, append(except, snapshotx.ExceptNestedKeys("nonce", "src"))...) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + newFlowWithType := func(ctx context.Context, t *testing.T, flowType flow.Type) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flowType) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + r, f := newFlowWithType(ctx, t, flow.TypeBrowser) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + t.Run("type=api", func(t *testing.T) { + r, f := newFlowWithType(ctx, t, flow.TypeAPI) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + r, f := newFlowWithType(ctx, t, flow.TypeBrowser) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + t.Run("type=api", func(t *testing.T) { + r, f := newFlowWithType(ctx, t, flow.TypeAPI) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(ctx, t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("1.attributes.value")) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSONExcept(t, snapshots[1], snapshots[3], []string{"3.attributes.nonce"}) + }) + }) +} + +// TestPopulateRegistrationMethodWithoutDisplayNameTrait is a regression test: +// when the passkey method is enabled but the identity schema flags no trait as +// a passkey display name or WebAuthn identifier and offers no untitled +// fallback trait, the passkey form hydrators must skip their UI nodes instead +// of failing the whole registration flow. +func TestPopulateRegistrationMethodWithoutDisplayNameTrait(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration-no-passkey-titled.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPID, "localhost") + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypePasskey) + require.NoError(t, err) + + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + newFlow := func(t *testing.T, flowType flow.Type) (*http.Request, *registration.Flow) { + t.Helper() + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + f, err := registration.NewFlow(reg, r, flowType) + require.NoError(t, err) + f.UI.Nodes = make(node.Nodes, 0) + return r, f + } + + assertNoPasskeyNodes := func(t *testing.T, nodes node.Nodes) { + t.Helper() + for _, n := range nodes { + assert.NotEqual(t, node.PasskeyGroup, n.Group, "unexpected passkey node: %+v", n) + assert.NotEqual(t, node.WebAuthnScript, n.ID(), "unexpected WebAuthn script node: %+v", n) + } + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + for _, ft := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} { + t.Run("type="+string(ft), func(t *testing.T) { + r, f := newFlow(t, ft) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + assertNoPasskeyNodes(t, f.UI.Nodes) + }) + } + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(t, flow.TypeBrowser) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + assertNoPasskeyNodes(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(t, flow.TypeBrowser) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + assertNoPasskeyNodes(t, f.UI.Nodes) + }) +} + +// TestRegistrationWithoutDisplayNameTrait initializes registration flows +// end-to-end with the passkey method enabled and an identity schema that has +// no display-name candidate trait. Flow creation must succeed and offer the +// remaining methods; only the passkey UI nodes are dropped. +func TestRegistrationWithoutDisplayNameTrait(t *testing.T) { + t.Parallel() + + fix := newRegistrationFixture(t, configx.WithValues( + testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration-no-passkey-titled.schema.json"))) + + assertFlow := func(t *testing.T, f *kratos.RegistrationFlow) { + t.Helper() + var groups []string + for _, n := range f.Ui.Nodes { + groups = append(groups, n.Group) + assert.NotEqual(t, "passkey", n.Group, "unexpected passkey node: %+v", n) + } + assert.Contains(t, groups, "password") + } + + t.Run("type=browser", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, false, false, false) + assertFlow(t, f) + }) + + t.Run("type=spa", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, true, false, false) + assertFlow(t, f) + }) + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPI(t, testhelpers.NewDebugClient(t), fix.publicTS) + assertFlow(t, f) + }) +} diff --git a/selfservice/strategy/passkey/passkey_schema_extension.go b/selfservice/strategy/passkey/passkey_schema_extension.go new file mode 100644 index 0000000..ff1e23e --- /dev/null +++ b/selfservice/strategy/passkey/passkey_schema_extension.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "cmp" + "context" + "fmt" + "slices" + "strings" + "sync" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/x/jsonschemax" +) + +// errNoDisplayNameTrait is returned by PasskeyDisplayNameFromSchema when the +// identity schema flags no trait as a passkey display name or WebAuthn +// identifier and has no untitled trait to fall back to. +var errNoDisplayNameTrait = errors.New("no identifier found") + +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() + + // When a schema flags multiple traits, the validator visits them in an + // unspecified order. Keep the first non-empty value so an empty flagged + // trait never clobbers a populated one. This mirrors the client, which + // names the passkey after the first non-empty candidate field. + if s.Credentials.WebAuthn.Identifier && e.WebauthnIdentifier == "" { + e.WebauthnIdentifier = strings.ToLower(fmt.Sprintf("%s", value)) + } + + if s.Credentials.Passkey.DisplayName && e.PasskeyDisplayName == "" { + 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 cmp.Or(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) +} + +// PasskeyDisplayNameFromSchema returns every trait path (e.g. ["traits.email", "traits.phone"]) +// whose schema flags `passkey.display_name: true` or `webauthn.identifier: true`. The slice +// is sorted alphabetically so server and client agree on precedence. When no +// trait is flagged, it preserves the legacy behavior: fall back to the first +// untitled trait, and return an error only when there is none. +func (s *Strategy) PasskeyDisplayNameFromSchema(ctx context.Context, schemaURL string) ([]string, error) { + runner, err := schema.NewExtensionRunner(ctx) + if err != nil { + return nil, err + } + c, err := schema.NewCompilerWithURL(ctx, schemaURL, s.d.Config().SecurityDisallowRefInIdentitySchemas(ctx)) + if err != nil { + return nil, err + } + c.ExtractAnnotations = true + runner.Register(c) + + paths, err := jsonschemax.ListPaths(ctx, schemaURL, c) + if err != nil { + return nil, err + } + + var result []string + for _, p := range paths { + ext, ok := p.CustomProperties[schema.ExtensionName].(*schema.ExtensionConfig) + if !ok { + continue + } + if ext.Credentials.WebAuthn.Identifier || ext.Credentials.Passkey.DisplayName { + result = append(result, p.Name) + } + } + if len(result) > 0 { + slices.Sort(result) + return result, nil + } + + for _, p := range paths { + if strings.HasPrefix(p.Name, "traits.") && p.Title == "" { + return []string{p.Name}, nil + } + } + return nil, errors.WithStack(errNoDisplayNameTrait) +} diff --git a/selfservice/strategy/passkey/passkey_schema_extension_test.go b/selfservice/strategy/passkey/passkey_schema_extension_test.go new file mode 100644 index 0000000..6aa4f2c --- /dev/null +++ b/selfservice/strategy/passkey/passkey_schema_extension_test.go @@ -0,0 +1,98 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + "net/url" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/strategy/passkey" +) + +func TestPasskeyDisplayNameFromSchema(t *testing.T) { + t.Parallel() + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + strategy := passkey.NewStrategy(reg) + + abs := func(t *testing.T, p string) string { + t.Helper() + a, err := filepath.Abs(p) + require.NoError(t, err) + return (&url.URL{Scheme: "file", Path: a}).String() + } + + t.Run("case=single trait flagged returns single path", func(t *testing.T) { + t.Parallel() + paths, err := strategy.PasskeyDisplayNameFromSchema(ctx, abs(t, "stub/registration-single-identifier.schema.json")) + require.NoError(t, err) + assert.Equal(t, []string{"traits.username"}, paths) + }) + + t.Run("case=multiple traits flagged returns sorted paths", func(t *testing.T) { + t.Parallel() + paths, err := strategy.PasskeyDisplayNameFromSchema(ctx, abs(t, "stub/registration-multi-identifier.schema.json")) + require.NoError(t, err) + assert.Equal(t, []string{"traits.email", "traits.phone"}, paths) + }) + + t.Run("case=webauthn.identifier-only flagged trait returns its path", func(t *testing.T) { + t.Parallel() + paths, err := strategy.PasskeyDisplayNameFromSchema(ctx, abs(t, "stub/registration-webauthn-only.schema.json")) + require.NoError(t, err) + assert.Equal(t, []string{"traits.username"}, paths) + }) + + t.Run("case=schema without flagged traits falls back to the untitled trait", func(t *testing.T) { + t.Parallel() + // Preserves the legacy behavior: with no flagged trait, the first + // untitled trait is used as the display-name source. + paths, err := strategy.PasskeyDisplayNameFromSchema(ctx, abs(t, "stub/noid.schema.json")) + require.NoError(t, err) + assert.Equal(t, []string{"traits.email"}, paths) + }) + + t.Run("case=schema with only titled, unflagged traits returns an error", func(t *testing.T) { + t.Parallel() + paths, err := strategy.PasskeyDisplayNameFromSchema(ctx, abs(t, "stub/registration-no-passkey-titled.schema.json")) + require.ErrorContains(t, err, "no identifier found") + assert.Nil(t, paths) + }) +} + +// TestPasskeyDisplayNameFromTraits_MultipleFlaggedTraits guards the resolution +// order when an identity schema flags more than one trait as a passkey +// display-name source. A flagged trait that the user left empty must not clobber +// a flagged trait that has a value, regardless of the order the validator visits +// the traits. This mirrors the client-side behavior, which picks the first +// non-empty candidate field. +func TestPasskeyDisplayNameFromTraits_MultipleFlaggedTraits(t *testing.T) { + t.Parallel() + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration-multi-identifier.schema.json") + + strategy := passkey.NewStrategy(reg) + + t.Run("case=keeps the non-empty candidate when a later flagged trait is empty", func(t *testing.T) { + t.Parallel() + name := strategy.PasskeyDisplayNameFromTraits(ctx, identity.Traits(`{"email":"alice@example.com","phone":""}`)) + assert.Equal(t, "alice@example.com", name) + }) + + t.Run("case=keeps the non-empty candidate when an earlier flagged trait is empty", func(t *testing.T) { + t.Parallel() + name := strategy.PasskeyDisplayNameFromTraits(ctx, identity.Traits(`{"email":"","phone":"+15555550123"}`)) + assert.Equal(t, "+15555550123", name) + }) +} diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go new file mode 100644 index 0000000..39e94ec --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -0,0 +1,444 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "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/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "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/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) SettingsStrategyID() string { return s.ID().String() } + +const ( + InternalContextKeySessionData = "session_data" + InternalContextKeySessionOptions = "session_options" +) + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.passkey.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, id) + if err != nil { + return err + } + + webAuthns, err := s.identityListWebAuthn(id) + if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return err + } + + var registrationOptions []webauthn.RegistrationOption + if webAuthns != nil { + exclude := make([]protocol.CredentialDescriptor, 0, len(webAuthns.Credentials)) + 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 + })) + exclude = append(exclude, cred.ToWebAuthn().Descriptor()) + } + // Exclude every registered passkey: platform authenticators overwrite their stored entry on + // re-enrollment while Kratos keeps both server-side (WebAuthn Level 2, section 5.4.3). + registrationOptions = append(registrationOptions, webauthn.WithExclusions(exclude)) + } + + web, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + + identifier := s.PasskeyDisplayNameFromIdentity(ctx, 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(ctx), + } + option, sessionData, err := web.BeginRegistration(user, registrationOptions...) + 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) + } + + if f.Type == flow.TypeBrowser { + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + } + + f.UI.Nodes.Upsert(node.NewInputField( + node.PasskeyRegisterTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(a *node.InputAttributes) { + a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration + // Only attach raw JS handler for browser flows; API flows must stay script-free. + if f.Type == flow.TypeBrowser { + //nolint:staticcheck + a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()" + } + }), + ).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(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.passkey.Strategy.Settings") + defer otelx.End(span, &err) + + 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 !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + 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())) + } + + for k := range cc.Credentials { + if bytes.Equal(cc.Credentials[k].ID, credential.ID) { + return errors.WithStack(webauthnx.ErrCredentialAlreadyRegistered("#/" + node.PasskeySettingsRegister)) + } + } + + 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.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()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), 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 0000000..3d4df15 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings_test.go @@ -0,0 +1,679 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + + "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/contextx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/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 + +func TestCompleteSettings(t *testing.T) { + fix := newSettingsFixture(t) + + t.Run("case=invalid passkey config", func(t *testing.T) { + fix := newSettingsFixture(t, configx.WithValue(config.ViperKeyPasskeyRPID, "")) + id := fix.createIdentity(t) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, 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.Context(), t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", // CSRF + "1.attributes.nonce", // script + "1.attributes.src", // script + "6.attributes.value", // passkey_settings_register + }) + }) + + t.Run("case=one activation element is shown", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", // CSRF + "1.attributes.nonce", // script + "1.attributes.src", // script + "4.attributes.value", // passkey_create_data + }) + }) + + t.Run("case=registration options exclude already registered passkeys", func(t *testing.T) { + id := fix.createIdentity(t) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + options := gjson.GetBytes(nodes, `#(attributes.name=="passkey_create_data").attributes.value`).String() + exclude := gjson.Get(options, "publicKey.excludeCredentials").Array() + require.Len(t, exclude, 2, options) + assert.Equal(t, "public-key", exclude[0].Get("type").String()) + assert.Equal(t, "Zm9vZm9v", exclude[0].Get("id").String()) + assert.Equal(t, "YmFyYmFy", exclude[1].Get("id").String()) + }) + + t.Run("case=registration options have no exclusions without passkeys", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + options := gjson.GetBytes(nodes, `#(attributes.name=="passkey_create_data").attributes.value`).String() + require.NotEmpty(t, options) + assert.False(t, gjson.Get(options, "publicKey.excludeCredentials").Exists(), options) + }) + + t.Run("case=passkey nodes exist for API but without browser script", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS) + + // Passkey nodes should exist for API + hasPasskeyGroup := false + hasWebAuthnScript := false + for _, n := range f.Ui.Nodes { + if n.Group == "passkey" { + hasPasskeyGroup = true + } + if n.Type == "script" { + hasWebAuthnScript = true + } + } + assert.True(t, hasPasskeyGroup, "passkey group should be present for API flows") + assert.False(t, hasWebAuthnScript, "WebAuthn script should not be present for API flows") + }) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, 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.Context(), t, 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 invalid passkey payload for API", 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) + // API should process the request but fail on invalid WebAuthn payload + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Parse error for Registration", "%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, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + } else { + assert.Contains(t, res.Request.URL.String(), fix.errTS.URL) + assert.Equal(t, nosurfx.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("type=api", func(t *testing.T) { + // API should work without CSRF token + id := fix.createIdentityWithoutPasskey(t) + body, res := doAPIFlow(t, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + // API should process and fail on invalid payload, not CSRF + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Parse error for Registration", "%s", body) + }) + }) + + t.Run("case=fails when 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("type=api", 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.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Parse error for Registration", "%s", body) + }) + }) + + t.Run("case=requires privileged session for register", func(t *testing.T) { + fix.conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + fix.conf.MustSet(t.Context(), 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("type=api", 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) + // API flow should fail with 403 Forbidden due to insufficient privileges + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSON(t, settings.NewFlowNeedsReAuth().DefaultError, json.RawMessage(gjson.Get(body, "error").Raw)) + }) + }) + + t.Run("case=add a passkey", func(t *testing.T) { + run := func(t *testing.T, flowType string) { + // 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(t.Context(), id.ID) + + var f *kratos.SettingsFlow + var client *http.Client + var body string + var res *http.Response + + if flowType == "api" { + client = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, fix.reg, &id) + f = testhelpers.InitializeSettingsFlowViaAPI(t, client, fix.publicTS) + } else { + client = testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, fix.reg, &id) + f = testhelpers.InitializeSettingsFlowViaBrowser(t, client, flowType == "spa", fix.publicTS) + } + + // We inject the session to replay + interim, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, fix.reg.SettingsFlowPersister().UpdateSettingsFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + // We use the response replay + values.Set("method", "passkey") + values.Set(node.PasskeySettingsRegister, string(settingsFixtureSuccessResponse)) + + if flowType == "api" { + body, res = testhelpers.SettingsMakeRequest(t, true, false, f, client, testhelpers.EncodeFormAsJSON(t, true, values)) + } else { + body, res = testhelpers.SettingsMakeRequest(t, false, flowType == "spa", f, client, testhelpers.EncodeFormAsJSON(t, flowType == "spa", values)) + } + require.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + + switch flowType { + case "spa": + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + case "browser": + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + // For API, no redirect check needed + + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + actual, err := fix.reg.Persister().GetIdentityConfidential(t.Context(), 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(t.Context(), 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, client, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey)) + + if flowType == "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, "browser") + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, "spa") + }) + + t.Run("type=api", func(t *testing.T) { + run(t, "api") + }) + }) + + t.Run("case=fails to add a passkey that is already registered", func(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 = x.NewUUID() + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(t.Context(), id.ID) + client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, fix.reg, &id) + + // Seed the identity with the same credential ID the replayed response will try to register. + credID, err := base64.RawURLEncoding.DecodeString(gjson.GetBytes(settingsFixtureSuccessResponse, "rawId").String()) + require.NoError(t, err) + conf, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{{ID: credID, DisplayName: "existing passkey"}}, + }) + require.NoError(t, err) + confidential, err := fix.reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + confidential.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{ + Type: identity.CredentialsTypePasskey, + Config: conf, + }) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), confidential)) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, client, true, fix.publicTS) + + // We inject the session to replay + interim, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, fix.reg.SettingsFlowPersister().UpdateSettingsFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + // The existing passkey's remove button is disabled (it is the identity's only first-factor + // credential), so a real browser would not submit its value. Unlike a browser, the test + // helper copies every node's value regardless of the disabled attribute, so drop it here to + // avoid accidentally triggering the remove path instead of the add path under test. + values.Del(node.PasskeyRemove) + values.Set("method", "passkey") + values.Set(node.PasskeySettingsRegister, string(settingsFixtureSuccessResponse)) + body, res := testhelpers.SettingsMakeRequest(t, false, true, f, client, testhelpers.EncodeFormAsJSON(t, true, values)) + + assert.Equal(t, http.StatusBadRequest, res.StatusCode, body) + assert.Contains(t, body, "already registered", body) + + actual, err := fix.reg.Persister().GetIdentityConfidential(t.Context(), 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, "the duplicate credential must not be persisted") + }) + + t.Run("case=fails to remove passkey if it is the last credential available", func(t *testing.T) { + run := func(t *testing.T, flowType string) { + 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(t.Context(), id, identity.ManagerAllowWriteProtectedTraits)) + + var body string + var res *http.Response + if flowType == "api" { + body, res = doAPIFlow(t, 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) + } else { + body, res = doBrowserFlow(t, flowType == "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) + } + + switch flowType { + case "spa": + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + case "browser": + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + // For API, no redirect check needed + + t.Run("response", func(t *testing.T) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(body, "state").String(), body) + snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(body, "ui.nodes.#(attributes.name==passkey_remove)").String())) + + actual, err := fix.reg.Persister().GetIdentityConfidential(t.Context(), 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, "browser") + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, "spa") + }) + + t.Run("type=api", func(t *testing.T) { + run(t, "api") + }) + }) + + t.Run("case=remove all passkeys", func(t *testing.T) { + run := func(t *testing.T, flowType string) { + 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 { + var body string + var res *http.Response + if flowType == "api" { + body, res = doAPIFlow(t, func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", cred.ID)) + }, id) + } else { + body, res = doBrowserFlow(t, flowType == "spa", func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", cred.ID)) + }, id) + } + + switch flowType { + case "spa": + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + case "browser": + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + // For API, no redirect check needed + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + } + + actual, err := fix.reg.Persister().GetIdentityConfidential(t.Context(), 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, "browser") + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, "spa") + }) + + t.Run("type=api", func(t *testing.T) { + run(t, "api") + }) + }) + + 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(t.Context(), 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(t.Context(), id.ID) + id.NID = x.NewUUID() + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, 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 func() { _ = res.Body.Close() }() + + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.GetBytes(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} + +func TestPopulateSettingsMethod(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/settings.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyPasskeyRPID, "localhost") + + s, err := reg.AllSettingsStrategies().Strategy(string(identity.CredentialsTypePasskey)) + require.NoError(t, err) + + toSnapshot := func(t *testing.T, f node.Nodes, except ...snapshotx.Opt) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, append(except, snapshotx.ExceptNestedKeys("nonce", "src"))...) + } + + newFlowWithType := func(ctx context.Context, t *testing.T, flowType flow.Type) (*http.Request, *settings.Flow, *identity.Identity) { + r := httptest.NewRequest("GET", "/self-service/settings/browser", nil) + r = r.WithContext(ctx) + t.Helper() + id := identity.NewIdentity("default") + id.Traits = identity.Traits(`{"email":"testuser@ory.sh"}`) + f, err := settings.NewFlow(reg, r, id, flowType) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f, id + } + + t.Run("method=PopulateSettingsMethod", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + r, f, id := newFlowWithType(ctx, t, flow.TypeBrowser) + require.NoError(t, s.PopulateSettingsMethod(ctx, r, id, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("4.attributes.value")) + }) + t.Run("type=api", func(t *testing.T) { + r, f, id := newFlowWithType(ctx, t, flow.TypeAPI) + require.NoError(t, s.PopulateSettingsMethod(ctx, r, id, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("3.attributes.value")) + }) + }) +} diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go new file mode 100644 index 0000000..9e7be72 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + "encoding/json" + "strings" + + "github.com/ory/kratos/x/nosurfx" + + "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/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + otelx.Provider + + config.Provider + + continuity.ManagementProvider + + x.CookieProvider + + 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 = (*Strategy)(nil) + _ registration.Strategy = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) +) + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +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 0000000..8be1150 --- /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 0000000..d6e72ec --- /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 0000000..a94dfcc --- /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 0000000..4356579 --- /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 0000000..d1dcaa7 --- /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 0000000..2503b33 --- /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-multi-identifier.schema.json b/selfservice/strategy/passkey/stub/registration-multi-identifier.schema.json new file mode 100644 index 0000000..aae3da2 --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration-multi-identifier.schema.json @@ -0,0 +1,37 @@ +{ + "$id": "https://example.com/multi-identifier.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", + "ory.sh/kratos": { + "credentials": { + "password": { "identifier": true }, + "passkey": { "display_name": true } + } + } + }, + "phone": { + "type": "string", + "format": "tel", + "title": "Phone number", + "ory.sh/kratos": { + "credentials": { + "password": { "identifier": true }, + "passkey": { "display_name": true } + } + } + } + }, + "required": ["email"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration-no-passkey-titled.schema.json b/selfservice/strategy/passkey/stub/registration-no-passkey-titled.schema.json new file mode 100644 index 0000000..05382f3 --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration-no-passkey-titled.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://example.com/no-passkey-titled.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Username" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration-single-identifier.schema.json b/selfservice/strategy/passkey/stub/registration-single-identifier.schema.json new file mode 100644 index 0000000..8648c3c --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration-single-identifier.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/single-identifier.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Username", + "ory.sh/kratos": { + "credentials": { + "password": { "identifier": true }, + "passkey": { "display_name": true }, + "webauthn": { "identifier": true } + } + } + } + }, + "required": ["username"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration-webauthn-only.schema.json b/selfservice/strategy/passkey/stub/registration-webauthn-only.schema.json new file mode 100644 index 0000000..7d902ae --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration-webauthn-only.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://example.com/webauthn-only.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Username", + "ory.sh/kratos": { + "credentials": { + "password": { "identifier": true }, + "webauthn": { "identifier": true } + } + } + } + }, + "required": ["username"] + } + }, + "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 0000000..441a72e --- /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 0000000..d6e72ec --- /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 0000000..9bf8aae --- /dev/null +++ b/selfservice/strategy/passkey/testfixture_test.go @@ -0,0 +1,415 @@ +// 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/x/configx" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/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, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault { + _, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + config.ViperKeySelfServiceRegistrationLoginHints: true, + }), + withEnabledPasskeyStrategy, + }, cfgOpts...)...) + return reg +} + +func newLoginRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault { + _, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + config.ViperKeySelfServiceRegistrationLoginHints: true, + }), + withEnabledPasskeyStrategy, + }, cfgOpts...)...) + return reg +} + +var withEnabledPasskeyStrategy = func() configx.OptionModifier { + key := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePasskey) + return configx.WithValues(map[string]any{ + key + ".enabled": true, + key + ".config.rp.display_name": "Ory Corp", + key + ".config.rp.id": "localhost", + key + ".config.rp.origins": []string{"http://localhost:4455"}, + }) +}() + +type fixture struct { + 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, cfgOpts ...configx.OptionModifier) *fixture { + fix := new(fixture) + fix.reg = newRegistrationRegistry(t, append([]configx.OptionModifier{ + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration.schema.json")), + configx.WithValue(config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}), + }, cfgOpts...)...) + fix.conf = fix.reg.Config() + + fix.publicTS, _ = testhelpers.NewKratosServer(t, fix.reg) + + _ = testhelpers.NewErrorTestServer(t, fix.reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, fix.reg) + _ = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + + 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, cfgOpts ...configx.OptionModifier) *fixture { + fix := new(fixture) + fix.reg = newLoginRegistry(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": false, + config.ViperKeySecretsDefault: []string{"not-a-secure-session-key"}, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + }, cfgOpts...)...) + fix.conf = fix.reg.Config() + + fix.publicTS, _ = testhelpers.NewKratosServer(t, fix.reg) + + 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(t.Context(), config.ViperKeySelfServiceLoginUI, fix.uiTS.URL+"/login-ts") + + 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, cfgOpts ...configx.OptionModifier) *fixture { + fix := newLoginFixture(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m", + config.ViperKeySelfServiceSettingsRequiredAAL: "aal1", + config.ViperKeySessionWhoAmIAAL: "aal1", + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")), + }, cfgOpts...)...) + fix.uiTS = testhelpers.NewSettingsUIFlowEchoServer(t, fix.reg) + + 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) 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(t.Context(), id.ID) + + require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &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(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), 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(ctx, t, 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(context.TODO(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + fix.conf.MustSet(context.TODO(), 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(context.TODO(), config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), nil) +} + +func (fix *fixture) enableSessionAfterRegistration() { + fix.conf.MustSet(context.TODO(), 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 withInitFlowWithOption(ifo []testhelpers.InitFlowWithOption) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.initFlowOpts = ifo + } +} + +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.Context(), 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.Context(), t, flowType, client, cb, + append([]submitPasskeyOption{withInternalContext( + registrationFixtureSuccessAndroidInternalContext, + )}, opts...)...) +} + +func (fix *fixture) submitPasskeyRegistration( + ctx context.Context, + 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" + isAPI := flowType == "api" + + var regFlow *kratos.RegistrationFlow + if isAPI { + regFlow = testhelpers.InitializeRegistrationFlowViaAPI(t, client, fix.publicTS, o.initFlowOpts...) + } else { + regFlow = testhelpers.InitializeRegistrationFlowViaBrowserCtx(ctx, 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") + + // Encode payload based on flow type + var payload string + if isAPI { + payload = testhelpers.EncodeFormAsJSON(t, true, values) + } else { + payload = values.Encode() + } + _, _ = testhelpers.RegistrationMakeRequest(t, isAPI, isSPA, regFlow, client, payload) + + // We inject the session to replay + interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(ctx, uuid.FromStringOrNil(regFlow.Id)) + require.NoError(t, err) + if len(o.internalContext) > 0 { + 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(ctx, interim)) + + // Second step: fill out passkey response + values.Set(node.PasskeyRegister, passkeyRegisterVal) + if isAPI { + payload = testhelpers.EncodeFormAsJSON(t, true, values) + } else { + payload = values.Encode() + } + body, res := testhelpers.RegistrationMakeRequest(t, isAPI, isSPA, regFlow, client, payload) + + 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(t.Context(), 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 + } + if flowType == "api" { + // API does not redirect; response body should contain JSON with identity/session fields. + // Leave detailed assertions to the calling test. + return actual + } + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + return actual +} + +func (fix *fixture) makeUnsuccessfulRegistration(t *testing.T, flowType string, expectReturnTo string, values func(v url.Values), opts ...submitPasskeyOption) (actual string, res *http.Response) { + actual, res, _ = fix.makeRegistration(t, flowType, values, opts...) + return actual, res +} + +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(t.Context(), 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(t.Context()).Generate(t.Context(), []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(t.Context(), 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(t.Context(), 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 new file mode 100644 index 0000000..67b1252 --- /dev/null +++ b/selfservice/strategy/password/.schema/login.schema.json @@ -0,0 +1,67 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 1 + }, + "identifier": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "allOf": [ + { + "if": { + "properties": { + "method": { + "const": "password" + } + }, + "required": [ + "method" + ] + }, + "then": { + "required": [ + "password" + ] + } + }, + { + "if": { + "properties": { + "password_identifier": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "password_identifier" + ] + }, + "then": { + "required": [ + "password_identifier" + ] + }, + "else": { + "required": [ + "identifier" + ] + } + } + ] +} diff --git a/selfservice/strategy/password/.schema/registration.schema.json b/selfservice/strategy/password/.schema/registration.schema.json new file mode 100644 index 0000000..9a4a11b --- /dev/null +++ b/selfservice/strategy/password/.schema/registration.schema.json @@ -0,0 +1,39 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 1 + }, + "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" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "if": { + "properties": { + "method": { + "const": "password" + } + }, + "required": [ + "method" + ] + }, + "then": { + "required": [ + "password" + ] + } +} diff --git a/selfservice/strategy/password/.schema/settings.schema.json b/selfservice/strategy/password/.schema/settings.schema.json new file mode 100644 index 0000000..5e7b667 --- /dev/null +++ b/selfservice/strategy/password/.schema/settings.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "password" + ], + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "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 0000000..4b13f40 --- /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 0000000..eb9d0e2 --- /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 0000000..19765bd --- /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 0000000..831d9f0 --- /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 0000000..19765bd --- /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 0000000..831d9f0 --- /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 0000000..831d9f0 --- /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 0000000..19765bd --- /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 0000000..831d9f0 --- /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 0000000..19765bd --- /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 0000000..19765bd --- /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 0000000..19765bd --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..ff4715f --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.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": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..ff4715f --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.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": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.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/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.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/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..ff4715f --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.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": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.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/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..ff4715f --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.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": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/helpers.go b/selfservice/strategy/password/helpers.go new file mode 100644 index 0000000..dec644a --- /dev/null +++ b/selfservice/strategy/password/helpers.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import "net/url" + +func tidyForm(vv url.Values) url.Values { + for _, k := range []string{"password", "csrf_token", "flow"} { + vv.Del(k) + } + + return vv +} diff --git a/selfservice/strategy/password/helpers_test.go b/selfservice/strategy/password/helpers_test.go new file mode 100644 index 0000000..6cc7b2a --- /dev/null +++ b/selfservice/strategy/password/helpers_test.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTidyForm(t *testing.T) { + assert.EqualValues(t, url.Values{"foobar": {"foo"}}, tidyForm(url.Values{ + "password": {"some-value"}, + "csrf_token": {"some-value"}, + "flow": {"some-value"}, + "foobar": {"foo"}, + })) +} diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go new file mode 100644 index 0000000..16dd5a0 --- /dev/null +++ b/selfservice/strategy/password/login.go @@ -0,0 +1,244 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "bytes" + "cmp" + "context" + "encoding/json" + "net/http" + "time" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "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" +) + +var _ login.AAL1FormHydrator = new(Strategy) + +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", cmp.Or(payload.Identifier, payload.LegacyIdentifier)) + 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) (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 + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { + return nil, err + } + + var p updateLoginFlowWithPasswordMethod + if err := decoderx.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) + } + + identifier := cmp.Or(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(ctx).ExpectedDuration, s.d.Config().HasherArgon2(ctx).ExpectedDeviation)) + return nil, s.handleLoginError(r, f, p, errors.WithStack(schema.NewInvalidCredentialsError())) + } + + var o identity.CredentialsPassword + d := json.NewDecoder(bytes.NewBuffer(c.Config)) + if err := d.Decode(&o); err != nil { + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) + } + + if o.ShouldUsePasswordMigrationHook() { + pwHook := s.d.Config().PasswordMigrationHook(ctx) + if !pwHook.Enabled { + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Password migration hook is not enabled but password migration is requested.")), i.ID) + } + + migrationHook := hook.NewPasswordMigrationHook(s.d, &pwHook.Config) + err = migrationHook.Execute(ctx, r, f, &hook.PasswordMigrationRequest{ + Identifier: identifier, + Password: p.Password, + Identity: i, + }) + if err != nil { + return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID)) + } + + if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { + return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID)) + } + } else { + if err := hash.Compare(ctx, []byte(p.Password), []byte(o.HashedPassword)); err != nil { + return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(schema.NewInvalidCredentialsError(), i.ID))) + } + + if !s.d.Hasher(ctx).Understands([]byte(o.HashedPassword)) { + if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { + s.d.Logger().Warnf("Unable to migrate password hash for identity %s: %s Keeping existing password hash and continuing.", i.ID, x.WrapWithIdentityIDError(err, i.ID)) + } + } + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(herodot.ErrInternalServerError().WithReason("Could not update flow").WithDebug(err.Error()), i.ID))) + } + + return i, nil +} + +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 + } + co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(hpw)}) + if err != nil { + return errors.Wrap(err, "unable to encode password configuration to JSON") + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identifier) + if err != nil { + return err + } + + c, ok := i.GetCredentials(s.ID()) + if !ok { + return errors.New("expected to find password credential but could not") + } + + c.Config = co + i.SetCredentials(s.ID(), *c) + + return s.d.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits) +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow, _ *session.Session) (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 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 + } + + 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) addIdentifierNode(r *http.Request, sr *login.Flow) error { + ds, err := sr.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + // node.WithInputAttributes(func(a *node.InputAttributes) { // TODO: https://github.com/ory-corp/cloud/issues/12416 + // // Hint password managers to autofill the saved username for this site. + // a.Autocomplete = node.InputAttributeAutocompleteUsername + // })).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 + } + } + + if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + 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())) + } + + 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_autocomplete_test.go b/selfservice/strategy/password/login_autocomplete_test.go new file mode 100644 index 0000000..3d4f6d7 --- /dev/null +++ b/selfservice/strategy/password/login_autocomplete_test.go @@ -0,0 +1,45 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/configx" +) + +func TestLoginIdentifierAutocomplete(t *testing.T) { + t.Skip() // TODO: https://github.com/ory-corp/cloud/issues/12416 + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/login.schema.json")), + ) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePassword) + require.NoError(t, err) + fh, ok := s.(login.AAL1FormHydrator) + require.True(t, ok) + + r := httptest.NewRequest("GET", "/self-service/login/browser", nil).WithContext(t.Context()) + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + require.NoError(t, err) + + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + + identifier := f.UI.Nodes.Find("identifier") + require.NotNil(t, identifier, "identifier node must exist") + attr, ok := identifier.Attributes.(*node.InputAttributes) + require.True(t, ok) + require.Equal(t, node.InputAttributeAutocompleteUsername, attr.Autocomplete) +} diff --git a/selfservice/strategy/password/login_race_test.go b/selfservice/strategy/password/login_race_test.go new file mode 100644 index 0000000..11f5234 --- /dev/null +++ b/selfservice/strategy/password/login_race_test.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/pkg" +) + +// TestLoginConfigMutationIsolation guards against Race B: a config-mutating +// subtest must not share its *configx.Provider with requests that read config +// concurrently. The reader path (Bcrypt.Generate -> Config().HasherBcrypt -> +// IsInsecureDevMode -> Provider.Bool) and the writer path (Config.MustSet -> +// Provider.Set -> replaceKoanf, swapping p.Koanf) operate on the same +// *configx.Provider when a registry is shared, which the race detector flags. +// +// The fix is that a config-mutating subtest owns its own registry+config, so +// reads on one registry never race writes on another. This test verifies that +// the per-registry isolation pattern is race-free: a reader keeps hashing on +// one registry while a writer mutates config on its own, separate registry. +func TestLoginConfigMutationIsolation(t *testing.T) { + t.Parallel() + + // Reader owns its registry, mirroring an in-flight request that hashes a + // password and reads IsInsecureDevMode on every call. + _, readerReg := pkg.NewFastRegistryWithMocks(t) + hasher := hash.NewHasherBcrypt(readerReg) + + // Writer owns a separate registry+config, mirroring a config-mutating + // subtest that calls MustSet (and its cleanup), as required by + // .claude/rules/go.md: "subtests that need parallelization must own their + // resources". + writerConf, _ := pkg.NewFastRegistryWithMocks(t) + + var eg errgroup.Group + eg.Go(func() error { + for range 200 { + if _, err := hasher.Generate(t.Context(), []byte("a-long-enough-password")); err != nil { + return err + } + } + return nil + }) + eg.Go(func() error { + for range 200 { + writerConf.MustSet(t.Context(), config.ViperKeyUseLegacyRequireVerifiedLoginError, true) + writerConf.MustSet(t.Context(), config.ViperKeyUseLegacyRequireVerifiedLoginError, false) + } + return nil + }) + require.NoError(t, eg.Wait()) +} diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go new file mode 100644 index 0000000..5ebc833 --- /dev/null +++ b/selfservice/strategy/password/login_test.go @@ -0,0 +1,1515 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "bytes" + "context" + "crypto/sha256" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "slices" + "strings" + "testing" + "time" + + "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/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/registrationhelpers" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/errorsx" + "github.com/ory/x/httprouterx" + "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" +) + +func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity { + p, _ := reg.Hasher(ctx).Generate(t.Context(), []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(ctx, id)) + return id +} + +func TestCompleteLogin(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "migration": "file://./stub/migration.schema.json", + "default": "file://./stub/login.schema.json", + "phone": "file://./stub/phone.schema.json", + })), + ) + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + 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.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequest(t, 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) + + body, res := testhelpers.LoginMakeRequest(t, 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) + + body, res := testhelpers.LoginMakeRequest(t, 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 password can not handle AAL2", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + update, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), update)) + + req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"password"}`)) + 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, 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.LoginMakeRequest(t, 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.LoginMakeRequest(t, 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.LoginMakeRequest(t, 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) { + // Force expiry via the persister so the test is not racy on slow CI runners. + // The previous approach set lifespan to 100ms and slept 101ms, but the setup + // helper itself takes longer than 100ms under load, expiring the flow before + // the GetLoginFlow call inside the helper could fetch it. + expireFlow := func(t *testing.T, id string) { + flowID, err := uuid.FromString(id) + require.NoError(t, err) + fl, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), flowID) + require.NoError(t, err) + fl.ExpiresAt = time.Now().Add(-time.Second) + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), fl)) + } + + values := url.Values{ + "csrf_token": {nosurfx.FakeCSRFToken}, + "identifier": {"identifier"}, + "password": {"password"}, + } + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + expireFlow(t, f.Id) + + actual, res := testhelpers.LoginMakeRequest(t, 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").Str) + assert.NotZero(t, gjson.Get(actual, "use_flow_id").Str) + assert.Regexpf(t, regexp.MustCompile(`The self-service flow expired 0\.0\d minutes ago, initialize a new one\.`), gjson.Get(actual, "error.reason").Str, "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + expireFlow(t, f.Id) + + actual, res := testhelpers.LoginMakeRequest(t, 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.Regexpf(t, regexp.MustCompile(`The login flow expired 0\.0\d minutes ago, please try again\.`), gjson.Get(actual, "ui.messages.0.text").Str, "%s", actual) + }) + + t.Run("type=SPA", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + expireFlow(t, f.Id) + + actual, res := testhelpers.LoginMakeRequest(t, 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()) + assert.NotZero(t, gjson.Get(actual, "use_flow_id").String()) + assert.Regexpf(t, regexp.MustCompile(`The self-service flow expired 0\.0\d minutes ago, initialize a new one\.`), gjson.Get(actual, "error.reason").Str, "%s", actual) + }) + }) + + t.Run("case=should have correct CSRF behavior", func(t *testing.T) { + values := url.Values{ + "method": {"password"}, + "csrf_token": {"invalid_token"}, + "identifier": {"login-identifier-csrf-browser"}, + "password": {x.NewUUID().String()}, + } + + t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + 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, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), + 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) + browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + 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, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), + 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.InitializeLoginFlowViaAPICtx(t.Context(), 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, "provided credentials are invalid") + }) + + 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.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer func() { _ = 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(t.Context()).String())) + } + + t.Run("should return an error because the credentials are invalid (user does not exist)", func(t *testing.T) { + check := func(t *testing.T, body string, start time.Time) { + delay := time.Since(start) + minConfiguredDelay := conf.HasherArgon2(t.Context()).ExpectedDuration - conf.HasherArgon2(t.Context()).ExpectedDeviation + assert.GreaterOrEqual(t, delay, minConfiguredDelay) + 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.Equal(t, text.NewErrorValidationInvalidCredentials().Text, gjson.Get(body, "ui.messages.0.text").String(), body) + } + + values := func(v url.Values) { + v.Set("identifier", "identifier") + v.Set("password", "password") + } + + t.Run("type=browser", func(t *testing.T) { + start := time.Now() + check(t, expectValidationError(t, false, false, false, values), start) + }) + + t.Run("type=SPA", func(t *testing.T) { + start := time.Now() + check(t, expectValidationError(t, false, false, true, values), start) + }) + + t.Run("type=api", func(t *testing.T) { + start := time.Now() + check(t, expectValidationError(t, true, false, false, values), start) + }) + }) + + t.Run("should return an error because no identifier is set", func(t *testing.T) { + check := func(t *testing.T, body string) { + 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) + + ensureFieldsExist(t, []byte(body)) + assert.Equal(t, "Property identifier is missing.", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).messages.0.text").String(), "%s", body) + assert.Len(t, gjson.Get(body, "ui.nodes").Array(), 4) + + // The password value should not be returned! + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) + } + + values := func(v url.Values) { + v.Del("identifier") + v.Set("method", identity.CredentialsTypePassword.String()) + v.Set("password", "password") + } + + 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 return an error because no password is set", func(t *testing.T) { + check := func(t *testing.T, body string) { + 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) + + ensureFieldsExist(t, []byte(body)) + assert.Equal(t, "Property password is missing.", gjson.Get(body, "ui.nodes.#(attributes.name==password).messages.0.text").String(), "%s", body) + assert.Equal(t, "identifier", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.value").String(), "%s", body) + assert.Len(t, gjson.Get(body, "ui.nodes").Array(), 4) + + // This must not include the password! + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) + } + + values := func(v url.Values) { + v.Set("identifier", "identifier") + v.Del("password") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, false, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values)) + }) + }) + + t.Run("should return an error both identifier and password are missing", func(t *testing.T) { + check := func(t *testing.T, body string) { + 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) + + ensureFieldsExist(t, []byte(body)) + assert.Equal(t, "Property password is missing.", gjson.Get(body, "ui.nodes.#(attributes.name==password).messages.0.text").String(), "%s", body) + assert.Equal(t, "Property identifier is missing.", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).messages.0.text").String(), "%s", body) + assert.Len(t, gjson.Get(body, "ui.nodes").Array(), 4) + + // This must not include the password! + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) + } + + values := func(v url.Values) { + v.Set("password", "") + v.Set("identifier", "") + } + + 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, true, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values)) + }) + }) + + t.Run("should return an error because the credentials are invalid (password not correct)", func(t *testing.T) { + check := func(t *testing.T, body string) { + 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) + + ensureFieldsExist(t, []byte(body)) + assert.Equal(t, + errorsx.Cause(schema.NewInvalidCredentialsError()).(*schema.ValidationError).Messages[0].Text, + gjson.Get(body, "ui.messages.0.text").String(), + "%s", body, + ) + + // This must not include the password! + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) + } + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("password", "not-password") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, false, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values)) + }) + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, true, false, true, values)) + }) + }) + + t.Run("should pass with real request", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("password", pwd) + } + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(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) + + t.Run("retry with different refresh", func(t *testing.T) { + t.Run("redirect to returnTS if refresh is missing", func(t *testing.T) { + res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("show UI and hint at username", func(t *testing.T) { + res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow + "?refresh=true") + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + + rid := res.Request.URL.Query().Get("flow") + assert.NotEmpty(t, rid, "%s", res.Request.URL) + + res, err = browserClient.Get(publicTS.URL + login.RouteGetFlow + "?id=" + rid) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.Equal(t, identifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).attributes.value").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password).attributes.value").String(), "%s", body) + assert.True(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password)").Exists(), "%s", body) + }) + }) + + t.Run("do not show password method if identity has no password set", func(t *testing.T) { + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + + res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow + "?refresh=true") + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + + rid := res.Request.URL.Query().Get("flow") + assert.NotEmpty(t, rid, "%s", res.Request.URL) + + res, err = browserClient.Get(publicTS.URL + login.RouteGetFlow + "?id=" + rid) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password)").Exists(), "%s", body) + }) + }) + + t.Run("type=spa", func(t *testing.T) { + hc := testhelpers.NewClientWithCookies(t) + + body := testhelpers.SubmitLoginForm(t, false, hc, publicTS, values, + 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("retry with different refresh", func(t *testing.T) { + t.Run("redirect to returnTS if refresh is missing", func(t *testing.T) { + res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow)) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode, "%s", body) + assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("show UI and hint at username", func(t *testing.T) { + res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow+"?refresh=true")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.Equal(t, identifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).attributes.value").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password).attributes.value").String(), "%s", body) + }) + }) + + t.Run("do not show password method if identity has no password set", func(t *testing.T) { + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + + res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow+"?refresh=true")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password)").Exists(), "%s", body) + }) + }) + + t.Run("type=api", func(t *testing.T) { + body := testhelpers.SubmitLoginForm(t, true, nil, publicTS, values, + 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) + + t.Run("retry with different refresh", func(t *testing.T) { + c := &http.Client{Transport: testhelpers.NewTransportWithHeader(t, http.Header{"Authorization": {"Bearer " + st}})} + + t.Run("redirect to returnTS if refresh is missing", func(t *testing.T) { + res, err := c.Do(testhelpers.NewHTTPGetJSONRequest(t, publicTS.URL+login.RouteInitAPIFlow)) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body) + }) + + t.Run("show UI and hint at username", func(t *testing.T) { + res, err := c.Do(testhelpers.NewHTTPGetJSONRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.Equal(t, identifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).attributes.value").String(), "%s", body) + assert.Empty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password).attributes.value").String(), "%s", body) + }) + + t.Run("show verification confirmation when refresh is set to true", func(t *testing.T) { + res, err := c.Do(testhelpers.NewHTTPGetJSONRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "verifying that", "%s", body) + }) + }) + + t.Run("do not show password method if identity has no password set", func(t *testing.T) { + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + + res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body := ioutilx.MustReadAll(res.Body) + + assert.True(t, gjson.GetBytes(body, "refresh").Bool()) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.False(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password)").Exists(), "%s", body) + }) + }) + }) + + t.Run("should pass with phone number and test normalization", func(t *testing.T) { + // Test backward compatibility: login with a legacy identity whose credential + // identifier is stored in non-normalized form in the database (as it would be + // for records created before identifier normalization was introduced). + nonNormalizedPhone := "+49 176 671 11 638" + normalizedPhone := "+4917667111638" + pwd := "password" + + p, _ := reg.Hasher(t.Context()).Generate(t.Context(), []byte(pwd)) + iId := x.NewUUID() + id := &identity.Identity{ + ID: iId, + SchemaID: "phone", + Traits: identity.Traits(fmt.Sprintf(`{"phone_number":"%s"}`, nonNormalizedPhone)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{nonNormalizedPhone}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: nonNormalizedPhone, + Verified: false, + CreatedAt: time.Now(), + IdentityID: iId, + }, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), id)) + + // CreateIdentity normalizes the credential identifier to E.164 format, + // rewrites the matching trait value, and sets credential version to + // the latest. To simulate a legacy database record created before + // normalization was introduced, revert all three via raw SQL. + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery( + "UPDATE identity_credential_identifiers SET identifier = ? WHERE identity_id = ? AND identifier = ?", + nonNormalizedPhone, iId, normalizedPhone, + ).Exec()) + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery( + "UPDATE identity_credentials SET version = 0 WHERE identity_id = ?", + iId, + ).Exec()) + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery( + "UPDATE identities SET traits = ? WHERE id = ?", + fmt.Sprintf(`{"phone_number":"%s"}`, nonNormalizedPhone), iId, + ).Exec()) + + t.Run("type=browser/login with non-normalized phone", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + + values := func(v url.Values) { + v.Set("identifier", nonNormalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, false, browserClient, publicTS, values, + false, false, http.StatusOK, redirTS.URL) + + assert.Equal(t, nonNormalizedPhone, gjson.Get(body, "identity.traits.phone_number").String(), "%s", body) + }) + + t.Run("type=browser/login with normalized phone", func(t *testing.T) { + // Login with the normalized (E.164) phone number fails against a legacy + // record that only stores the non-normalized identifier. The lookup query + // searches for IN(normalizedInput, rawInput), but both resolve to the same + // E.164 value, so neither matches the non-normalized DB record. + browserClient := testhelpers.NewClientWithCookies(t) + + values := func(v url.Values) { + v.Set("identifier", normalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, false, browserClient, publicTS, values, + false, false, http.StatusOK, conf.SelfServiceFlowLoginUI(t.Context()).String()) + + assert.Equal(t, text.NewErrorValidationInvalidCredentials().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("type=spa/login with non-normalized phone", func(t *testing.T) { + hc := testhelpers.NewClientWithCookies(t) + + values := func(v url.Values) { + v.Set("identifier", nonNormalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, false, hc, publicTS, values, + true, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, nonNormalizedPhone, gjson.Get(body, "session.identity.traits.phone_number").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) + + require.NotEmpty(t, hc.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL)), "%+v", hc.Jar) + }) + + t.Run("type=spa/login with normalized phone", func(t *testing.T) { + hc := testhelpers.NewClientWithCookies(t) + + values := func(v url.Values) { + v.Set("identifier", normalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, false, hc, publicTS, values, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, text.NewErrorValidationInvalidCredentials().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("type=api/login with non-normalized phone", func(t *testing.T) { + values := func(v url.Values) { + v.Set("identifier", nonNormalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, true, nil, publicTS, values, + false, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, nonNormalizedPhone, gjson.Get(body, "session.identity.traits.phone_number").String(), "%s", body) + st := gjson.Get(body, "session_token").String() + assert.NotEmpty(t, st, "%s", body) + }) + + t.Run("type=api/login with normalized phone", func(t *testing.T) { + values := func(v url.Values) { + v.Set("identifier", normalizedPhone) + v.Set("password", pwd) + } + + body := testhelpers.SubmitLoginFormCtx(t.Context(), t, true, nil, publicTS, values, + false, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, text.NewErrorValidationInvalidCredentials().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + }) + + t.Run("case=should return an error because not passing validation and reset previous errors and values", func(t *testing.T) { + 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) + } + + checkFirst := func(t *testing.T, actual string) { + check(t, actual) + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==identifier).messages.0").String(), "Property identifier is missing.", "%s", actual) + } + + checkSecond := func(t *testing.T, actual string) { + check(t, actual) + + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==identifier).attributes.error")) + assert.EqualValues(t, "identifier", gjson.Get(actual, "ui.nodes.#(attributes.name==identifier).attributes.value").String(), actual) + assert.EqualValues(t, "password", gjson.Get(actual, "ui.nodes.#(attributes.name==method).attributes.value").String(), actual) + assert.Empty(t, gjson.Get(actual, "ui.error")) + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0").String(), "Property password is missing.", "%s", actual) + } + + valuesFirst := func(v url.Values) url.Values { + v.Del("identifier") + v.Set("password", x.NewUUID().String()) + return v + } + + valuesSecond := func(v url.Values) url.Values { + v.Set("identifier", "identifier") + v.Del("password") + return v + } + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + actual, _ := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesFirst(testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)))) + checkFirst(t, actual) + actual, _ = testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesSecond(testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)))) + checkSecond(t, actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + actual, _ := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, valuesFirst(testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)).Encode()) + checkFirst(t, actual) + actual, _ = testhelpers.LoginMakeRequest(t, false, false, f, browserClient, valuesSecond(testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)).Encode()) + checkSecond(t, actual) + }) + }) + + t.Run("should be a new session with refresh flag", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + values := url.Values{ + "method": {"password"}, "identifier": {identifier}, + "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}, + }.Encode() + + body1, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + f = testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, true, false, false, false) + body2, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + + require.Contains(t, res.Request.URL.Path, "return-ts", "%s", res.Request.URL.String()) + assert.Equal(t, identifier, gjson.Get(body2, "identity.traits.subject").String(), "%s", body2) + assert.Equal(t, gjson.Get(body1, "id").String(), gjson.Get(body2, "id").String(), "%s\n\n%s\n", body1, body2) + }) + + t.Run("should login same identity regardless of identifier capitalization", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}}.Encode() + + body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + 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(t.Context(), 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": {nosurfx.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(t.Context()).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(t.Context(), reg, t, identifier, pwd) + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), 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(t.Context(), reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + values := url.Values{"method": {"password"}, "password_identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}}.Encode() + + body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + }) + + t.Run("should login same identity regardless of leading or trailing whitespace", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + values := url.Values{"method": {"password"}, "identifier": {" " + identifier + " "}, "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}}.Encode() + + body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + }) + + t.Run("should fail as email is not yet verified", func(t *testing.T) { + // This subtest mutates config (the require_verified_address login hook + // and the legacy-error flag) and registers a cleanup that mutates it + // again. Sharing the parent's registry would let that mutation race + // in-flight requests of sibling subtests reading the same config (e.g. + // IsInsecureDevMode via the password hasher). It therefore owns its own + // registry, config, and server stack. See .claude/rules/go.md: + // "subtests that need parallelization must own their resources". + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true), + configx.WithValues(testhelpers.IdentitySchemasConfig(map[string]string{ + "default": "file://./stub/login.schema.json", + })), + configx.WithValue(config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + {"hook": "require_verified_address"}, + }), + configx.WithValue(config.ViperKeyUseLegacyRequireVerifiedLoginError, true), + ) + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix()) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + 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(t.Context()).String())) + } + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(t.Context(), reg, t, identifier, pwd) + + values := func(v url.Values) { + v.Set("method", "password") + v.Set("identifier", identifier) + v.Set("password", pwd) + } + + check := func(t *testing.T, body string) { + 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) + + ensureFieldsExist(t, []byte(body)) + assert.Equal(t, + errorsx.Cause(schema.NewAddressNotVerifiedError()).(*schema.ValidationError).Messages[0].Text, + gjson.Get(body, "ui.messages.0.text").String(), + "%s", body, + ) + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, false, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values)) + }) + }) + + t.Run("should upgrade password not primary hashing algorithm", func(t *testing.T) { + identifier, pwd := x.NewUUID().String()+"@google.com", "password" + h := &hash.Pbkdf2{ + Algorithm: "sha256", + Iterations: 100000, + SaltLength: 32, + KeyLength: 32, + } + p, _ := h.Generate(t.Context(), []byte(pwd)) + + iId := x.NewUUID() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &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(`{"hashed_password":"` + string(p) + `"}`), + }, + }, + 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", pwd) + } + + browserClient := testhelpers.NewClientWithCookies(t) + + 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(t.Context(), 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(t.Context()).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) + }) + + t.Run("suite=password rehashing degrades gracefully during login", func(t *testing.T) { + identifier := x.NewUUID().String() + "@google.com" + // pwd := "Kd9hUV4Xkcq87VSca6A4fq1iBijrMScBFhkpIPEwBtvTDsBwfqJCqXPPr4TkhOhsd9wFGeB3MzS4bJuesLCAjJc5s1GKJ51zW7F" + pwd := randx.MustString(100, randx.AlphaNum) // longer than bcrypt max length + require.Greater(t, len(pwd), 72) // bcrypt max length + salt := randx.MustString(32, randx.AlphaNum) + sha := sha256.Sum256([]byte(pwd + salt)) + hashed := "{SSHA256}" + base64.StdEncoding.EncodeToString(slices.Concat(sha[:], []byte(salt))) + iId := x.NewUUID() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &identity.Identity{ + ID: iId, + SchemaID: "migration", + Traits: identity.Traits(fmt.Sprintf(`{"email":%q}`, identifier)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + hashed + `"}`), + }, + }, + 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", pwd) + } + + browserClient := testhelpers.NewClientWithCookies(t) + + 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 that the password hash algorithm is unchanged + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + var o identity.CredentialsPassword + require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o)) + assert.Equal(t, hashed, o.HashedPassword) + + // login still works + 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) + }) + + t.Run("suite=password migration hook", func(t *testing.T) { + ctx := t.Context() + + 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(t.Context(), 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 + } + }) + } + + t.Run("case=custom hook payload", func(t *testing.T) { + var rawBody []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + rawBody, err = io.ReadAll(r.Body) + require.NoError(t, err) + _ = r.Body.Close() + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"password_match"}`)) + })) + + t.Cleanup(ts.Close) + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook, map[string]any{ + "config": map[string]any{ + "url": ts.URL, + "body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)), + }, + })) + + identifier := x.NewUUID().String() + "@google.com" + identityID := x.NewUUID() + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("method", identity.CredentialsTypePassword.String()) + v.Set("password", x.NewUUID().String()) + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &identity.Identity{ + ID: identityID, + 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(`{"use_password_migration_hook": true}`), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: identifier, + Verified: true, + IdentityID: identityID, + }, + }, + })) + + browserClient := testhelpers.NewClientWithCookies(t) + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, redirTS.URL) + assert.Equalf(t, identifier, gjson.Get(body, "identity.traits.email").String(), "%s", body) + + for _, path := range []string{ + "identifier", "password", + "identity", "identity.traits", + "flow", "flow.id", + "request_headers", "request_cookies", "request_method", "request_url", + } { + assert.Truef(t, gjson.GetBytes(rawBody, path).Exists(), "%s does not exist in %s", path, rawBody) + } + }) + }) +} + +func TestFormHydration(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/login.schema.json")), + ) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePassword) + require.NoError(t, err) + fh, ok := s.(login.AAL1FormHydrator) + 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(reg, r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + id := createIdentity(t.Context(), reg, t, "some@user.com", "password") + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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 := contextx.WithConfigValue(t.Context(), 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(t.Context(), t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/password/nodes.go b/selfservice/strategy/password/nodes.go new file mode 100644 index 0000000..f45a9a5 --- /dev/null +++ b/selfservice/strategy/password/nodes.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func NewPasswordNode(name string, autocomplete node.UiNodeInputAttributeAutocomplete) *node.Node { + return node.NewInputField(name, nil, node.PasswordGroup, + node.InputAttributeTypePassword, + node.WithRequiredInputAttribute, + node.WithInputAttributes(func(a *node.InputAttributes) { + a.Autocomplete = autocomplete + })). + WithMetaLabel(text.NewInfoNodeInputPassword()) +} diff --git a/selfservice/strategy/password/op_helpers_test.go b/selfservice/strategy/password/op_helpers_test.go new file mode 100644 index 0000000..557c916 --- /dev/null +++ b/selfservice/strategy/password/op_helpers_test.go @@ -0,0 +1,224 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + + dockertest "github.com/ory/dockertest/v4" + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/x/urlx" +) + +type clientAppConfig struct { + client *oauth2.Config + expectToken bool + state *clientAppState +} + +type clientAppState struct { + visits int64 + tokens int64 +} + +type callTrace string + +const ( + RegistrationUI callTrace = "registration-ui" + RegistrationWithOAuth2LoginChallenge callTrace = "registration-with-oauth2-login-challenge" + RegistrationWithFlowID callTrace = "registration-with-flow-id" + LoginUI callTrace = "login-ui" + LoginWithOAuth2LoginChallenge callTrace = "login-with-oauth2-login-challenge" + LoginWithFlowID callTrace = "login-with-flow-id" + Consent callTrace = "consent" + ConsentWithChallenge callTrace = "consent-with-challenge" + ConsentAccept callTrace = "consent-accept" + ConsentSkip callTrace = "consent-skip" + ConsentClientSkip callTrace = "consent-client-skip" + CodeExchange callTrace = "code-exchange" + CodeExchangeWithToken callTrace = "code-exchange-with-token" +) + +type testContextKey string + +const ( + TestUIConfig testContextKey = "test-ui-config" + TestOAuthClientState testContextKey = "test-oauth-client-state" +) + +type testConfig struct { + identifier string + password string + browserClient *http.Client + kratosPublicTS *httptest.Server + clientAppTS *httptest.Server + hydraAdminClient hydraclientgo.OAuth2API + consentRemember bool + requestedScope []string + callTrace *[]callTrace +} + +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 +} + +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" + tokenEndpointAuthMethod := "client_secret_post" + clientSecret := "client-secret" + + c, r, err := hydraAdmin.CreateOAuth2Client(ctx).OAuth2Client( + hydraclientgo.OAuth2Client{ + ClientName: &clientName, + RedirectUris: redirectURIs, + Scope: &scope, + TokenEndpointAuthMethod: &tokenEndpointAuthMethod, + ClientSecret: &clientSecret, + SkipConsent: &skipConsent, + }, + ).Execute() + require.NoError(t, err) + require.Equal(t, r.StatusCode, http.StatusCreated) + return *c.ClientId +} + +func makeAuthCodeURL(t *testing.T, c *oauth2.Config, requestedClaims string, isForced bool) string { + t.Helper() + + var options []oauth2.AuthCodeOption + + if isForced { + options = append(options, oauth2.SetAuthURLParam("prompt", "login")) + } + if requestedClaims != "" { + options = append(options, oauth2.SetAuthURLParam("claims", requestedClaims)) + } + + state := fmt.Sprintf("%x", uuid.Must(uuid.NewV4())) + return c.AuthCodeURL(state, options...) +} + +func newHydra(t *testing.T, loginUI string, consentUI string) (hydraAdmin string, hydraPublic string) { + pool := dockertest.NewPoolT(t, "") + + // The public host port must be known before the container starts because it + // is part of URLS_SELF_ISSUER. freeport releases the port before Docker binds + // it, so another process can grab it in between — retry on that race. + var hydraResource dockertest.ClosableResource + ctx := t.Context() + require.EventuallyWithT(t, func(t *assert.CollectT) { + publicPort, err := freeport.GetFreePort() + require.NoError(t, err) + + hydraResource, err = pool.Run(ctx, "oryd/hydra", + // Keep tag in sync with the version in ci.yaml + dockertest.WithTag("v2.2.0"), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{ + "DSN=memory", + fmt.Sprintf("URLS_SELF_ISSUER=http://127.0.0.1:%d/", publicPort), + "URLS_LOGIN=" + loginUI, + "URLS_CONSENT=" + consentUI, + "LOG_LEAK_SENSITIVE_VALUES=true", + "SECRETS_SYSTEM=someverylongsecretthatis32byteslong", + }), + dockertest.WithCmd([]string{"serve", "all", "--dev"}), + dockertest.WithContainerConfig(func(cc *container.Config) { + cc.ExposedPorts = network.PortSet{ + network.MustParsePort("4444/tcp"): struct{}{}, + network.MustParsePort("4445/tcp"): struct{}{}, + } + }), + dockertest.WithHostConfig(func(hc *container.HostConfig) { + hc.PortBindings = network.PortMap{ + network.MustParsePort("4444/tcp"): {{HostPort: strconv.Itoa(publicPort)}}, + network.MustParsePort("4445/tcp"): {{HostPort: ""}}, + } + }), + ) + require.NoError(t, err) + }, 2*time.Minute, 100*time.Millisecond) + hydraResource.Cleanup(t) + require.NotEmpty(t, hydraResource.GetPort("4444/tcp"), "%+v", hydraResource.Container().NetworkSettings.Ports) + require.NotEmpty(t, hydraResource.GetPort("4445/tcp"), "%+v", hydraResource.Container) + + hydraPublic = "http://127.0.0.1:" + hydraResource.GetPort("4444/tcp") + hydraAdmin = "http://127.0.0.1:" + hydraResource.GetPort("4445/tcp") + + go func() { + stdout, stderr, err := hydraResource.Logs(context.Background()) + if err == nil { + if stdout != "" { + t.Logf("[hydra stdout]:\n%s\n", stdout) + } + if stderr != "" { + t.Logf("[hydra stderr]:\n%s\n", stderr) + } + } + }() + client := testhelpers.NewTestClient(t) + require.EventuallyWithT(t, func(t *assert.CollectT) { + res, err := client.Get(hydraPublic + "/health/ready") + require.NoError(t, err) + assert.Equal(t, 200, res.StatusCode) + + res, err = client.Get(hydraAdmin + "/health/ready") + require.NoError(t, err) + assert.Equal(t, 200, res.StatusCode) + }, 30*time.Second, time.Second) + + t.Logf("Ory Hydra running at: %s %s", hydraPublic, hydraAdmin) + + return hydraAdmin, hydraPublic +} + +type TestLogWriter struct { + streamName string + *testing.T +} + +func (t TestLogWriter) Write(p []byte) (int, error) { + t.Logf("[%d bytes @ %s]:\n\n%s\n", len(p), t.streamName, string(p)) + return len(p), nil +} + +func doOAuthFlow(t *testing.T, ctx context.Context, oauthClient *oauth2.Config, browserClient *http.Client) { + t.Helper() + + authCodeURL := makeAuthCodeURL(t, oauthClient, "", false) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authCodeURL, nil) + require.NoError(t, err) + res, err := browserClient.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.NoError(t, res.Body.Close()) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, res.StatusCode) +} diff --git a/selfservice/strategy/password/op_login_test.go b/selfservice/strategy/password/op_login_test.go new file mode 100644 index 0000000..6f88ea1 --- /dev/null +++ b/selfservice/strategy/password/op_login_test.go @@ -0,0 +1,850 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + _ "embed" + "fmt" + "net/http" + "net/url" + "strings" + "sync/atomic" + "testing" + + "github.com/gofrs/uuid" + "github.com/tidwall/gjson" + "github.com/urfave/negroni" + "golang.org/x/oauth2" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" +) + +func TestOAuth2Provider(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)), + ) + + var testRequireLogin atomic.Bool + testRequireLogin.Store(true) + + router := httprouterx.NewRouterPublic() + kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + errTS := testhelpers.NewErrorTestServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + router.GET("/login-ts", func(w http.ResponseWriter, r *http.Request) { + t.Log("[loginTS] navigated to the login ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, LoginUI) + + q := r.URL.Query() + hlc := r.URL.Query().Get("login_challenge") + if hlc != "" { + *c.callTrace = append(*c.callTrace, LoginWithOAuth2LoginChallenge) + + loginUrl, err := url.Parse(c.kratosPublicTS.URL + login.RouteInitBrowserFlow) + require.NoError(t, err) + + q := loginUrl.Query() + q.Set("login_challenge", hlc) + loginUrl.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", loginUrl.String(), nil) + require.NoError(t, err) + + resp, err := c.browserClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + + // if the registration page redirects us to the login page + // we will sign in which means we might have a session + var oryCookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + + for _, c := range c.browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + oryCookie = c + break + } + } + + // in some cases the initialize login flow already navigated to the consent page + // in which case no flow exists, but a session cookie does + if oryCookie != nil && !resp.Request.URL.Query().Has("flow") { + t.Logf("[loginTS] found a session cookie: %s", oryCookie.String()) + return + } + + flowID := resp.Request.URL.Query().Get("flow") + lf := testhelpers.GetLoginFlow(t, c.browserClient, c.kratosPublicTS, flowID) + require.NotNil(t, lf) + + values := url.Values{"method": {"password"}, "identifier": {c.identifier}, "password": {c.password}, "csrf_token": {nosurfx.FakeCSRFToken}}.Encode() + _, res := testhelpers.LoginMakeRequest(t, false, false, lf, c.browserClient, values) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + return + } + + if q.Has("flow") { + *c.callTrace = append(*c.callTrace, LoginWithFlowID) + t.Log("[loginTS] login flow is ignored here since it will be handled by the code above, we just need to return") + return + } + }) + + router.GET("/consent", func(w http.ResponseWriter, r *http.Request) { + t.Log("[consentTS] navigated to the consent ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, Consent) + + q := r.URL.Query() + consentChallenge := q.Get("consent_challenge") + assert.NotEmpty(t, consentChallenge) + + if consentChallenge != "" { + *c.callTrace = append(*c.callTrace, ConsentWithChallenge) + } + + cr, resp, err := c.hydraAdminClient.GetOAuth2ConsentRequest(ctx).ConsentChallenge(q.Get("consent_challenge")).Execute() + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.ElementsMatch(t, cr.RequestedScope, c.requestedScope) + + if cr.GetSkip() { + *c.callTrace = append(*c.callTrace, ConsentSkip) + } + + if cr.Client.GetSkipConsent() { + *c.callTrace = append(*c.callTrace, ConsentClientSkip) + } + + completedAcceptRequest, resp, err := c.hydraAdminClient.AcceptOAuth2ConsentRequest(r.Context()).AcceptOAuth2ConsentRequest(hydraclientgo.AcceptOAuth2ConsentRequest{ + Remember: &c.consentRemember, + GrantScope: c.requestedScope, + }).ConsentChallenge(q.Get("consent_challenge")).Execute() + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + require.NotNil(t, completedAcceptRequest) + *c.callTrace = append(*c.callTrace, ConsentAccept) + + t.Logf("[consentTS] navigating to %s", completedAcceptRequest.RedirectTo) + resp, err = c.browserClient.Get(completedAcceptRequest.RedirectTo) + require.NoError(t, err) + require.Equal(t, c.clientAppTS.URL, fmt.Sprintf("%s://%s", resp.Request.URL.Scheme, resp.Request.URL.Host)) + }) + + kratosUIMiddleware := negroni.New() + kratosUIMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + kratosUIMiddleware.UseHandler(router) + + kratosUITS := testhelpers.NewHTTPTestServer(t, kratosUIMiddleware) + + clientAppTSMiddleware := negroni.New() + clientAppTSMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + clientAppTSMiddleware.UseHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c := ctx.Value(TestOAuthClientState).(*clientAppConfig) + kc := ctx.Value(TestUIConfig).(*testConfig) + *kc.callTrace = append(*kc.callTrace, CodeExchange) + + c.state.visits += 1 + t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) + if r.URL.Query().Has("code") { + token, err := c.client.Exchange(r.Context(), r.URL.Query().Get("code")) + require.NoError(t, err) + + if token != nil && token.AccessToken != "" { + t.Log("[clientAppTS] successfully exchanged code for token") + *kc.callTrace = append(*kc.callTrace, CodeExchangeWithToken) + c.state.tokens += 1 + } else { + t.Log("[clientAppTS] did not receive a token") + } + } else { + t.Error("[clientAppTS] code query parameter is missing") + } + }) + // A new OAuth client which will also function as the callback for the code exchange + clientAppTS := testhelpers.NewHTTPTestServer(t, clientAppTSMiddleware) + + conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, kratosUITS.URL+"/login-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/return-ts") + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/login.schema.json") + + hydraAdmin, hydraPublic := newHydra(t, kratosUITS.URL+"/login-ts", kratosUITS.URL+"/consent") + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, hydraAdmin) + + hydraAdminClient := createHydraOAuth2ApiClient(hydraAdmin) + + loginToAccount := func(t *testing.T, browserClient *http.Client, identifier, pwd string) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false, false) + + values := url.Values{"method": {"password"}, "identifier": {identifier}, "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}}.Encode() + + body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + } + + t.Run("should prompt the user for login and consent", func(t *testing.T) { + // This is the default case, where the user is prompted for login and consent. + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + }) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + hydraAdminClient: hydraAdminClient, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: true, + identifier: identifier, + password: pwd, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + }) + + t.Run("should prompt the user for login and consent again", func(t *testing.T) { + // This test verifies that when Kratos is set + // 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, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + password: pwd, + identifier: identifier, + hydraAdminClient: hydraAdminClient, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: false, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + + // Reset the call trace + ct = []callTrace{} + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 2, + tokens: 2, + }, clientAS) + + expected = []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + }) + + t.Run("should prompt the user for consent, but not for login", func(t *testing.T) { + // This test verifies that when Kratos is set + // to SessionPersistentCookie=true, the user is + // remembered from the previous OAuth2 flow. + // The user must then only re-consent. + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier, pwd := x.NewUUID().String(), "password" + + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + hydraAdminClient: hydraAdminClient, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: false, + password: pwd, + identifier: identifier, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + + // reset the call trace + ct = []callTrace{} + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 2, + tokens: 2, + }, clientAS) + + expected = []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + require.ElementsMatch(t, expected, ct) + }) + + t.Run("should prompt login even with session with OAuth flow", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + loginToAccount(t, browserClient, identifier, pwd) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithFlowID, + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + }) + + t.Run("first party clients can skip consent", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + clientSkipConsent := true + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentClientSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + }) + + t.Run("oauth flow with consent remember, skips consent", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: true, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + + // reset the call trace + ct = []callTrace{} + clientAS = clientAppState{ + visits: 0, + tokens: 0, + } + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + }) + + 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, nil) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: false, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithFlowID, + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + + // reset the call trace + ct = []callTrace{} + clientAS = clientAppState{ + visits: 0, + tokens: 0, + } + + reg.SetHydra(&AcceptWrongSubject{h: reg.Hydra().(*hydra.DefaultHydra)}) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + // In Hydra v2.2.0-rc.3 this would have failed outright: Hydra returned an error + // when AcceptLoginRequest arrived with a subject that did not match the subject + // stored in the existing login session (skip=true flow). + // + // In Hydra v2.2.0 final the mismatch is handled gracefully instead of with an + // error. When Hydra detects that the accepted subject differs from the session + // subject (consent/handler.go, acceptOAuth2LoginRequest), it redirects the + // browser back to the original authorization URL with prompt=login appended. + // That restart produces a fresh flow with f.Subject="" (no prior session + // context), so the mismatch guard (f.Subject != "" && payload.Subject != + // f.Subject) does not fire on the second attempt — and AcceptWrongSubject's + // random UUID is accepted, eventually completing the flow. + // + // Security note: this is not exploitable in production. AcceptLoginRequest is a + // trusted admin API reachable only by the login provider (Kratos), which always + // sends the correctly authenticated identity ID. AcceptWrongSubject is a test + // construct only. The change removes a hard early-failure signal for accidental + // subject substitution but does not introduce a new attack surface. + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, // skip=true, wrong subject → prompt=login redirect + LoginUI, + LoginWithOAuth2LoginChallenge, // skip=false, f.Subject="" → guard bypassed, wrong subject accepted + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + }) +} + +var _ hydra.Hydra = &AcceptWrongSubject{} + +type AcceptWrongSubject struct { + h *hydra.DefaultHydra +} + +func (h *AcceptWrongSubject) AcceptLoginRequest(ctx context.Context, params hydra.AcceptLoginRequestParams) (string, error) { + params.IdentityID = uuid.Must(uuid.NewV4()).String() + return h.h.AcceptLoginRequest(ctx, params) +} + +func (h *AcceptWrongSubject) GetLoginRequest(ctx context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) { + return h.h.GetLoginRequest(ctx, loginChallenge) +} diff --git a/selfservice/strategy/password/op_registration_test.go b/selfservice/strategy/password/op_registration_test.go new file mode 100644 index 0000000..5e5e8a1 --- /dev/null +++ b/selfservice/strategy/password/op_registration_test.go @@ -0,0 +1,905 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + _ "embed" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/negroni" + "golang.org/x/oauth2" + + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" +) + +func TestOAuth2ProviderRegistration(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true)) + + kratosPublicTS, _ := testhelpers.NewKratosServer(t, reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + var hydraAdminClient hydraclientgo.OAuth2API + + router := http.NewServeMux() + + type contextKey string + const ( + TestUIConfig contextKey = "test-ui-config" + TestOAuthClientState contextKey = "test-oauth-client-state" + ) + + router.HandleFunc("GET /login-ts", func(w http.ResponseWriter, r *http.Request) { + t.Log("[loginTS] navigated to the login ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, LoginUI) + + q := r.URL.Query() + hlc := r.URL.Query().Get("login_challenge") + + if hlc != "" { + *c.callTrace = append(*c.callTrace, LoginWithOAuth2LoginChallenge) + return + } + + if q.Has("flow") { + *c.callTrace = append(*c.callTrace, LoginWithFlowID) + lf := testhelpers.GetLoginFlow(t, c.browserClient, c.kratosPublicTS, q.Get("flow")) + require.NotNil(t, lf) + values := testhelpers.SDKFormFieldsToURLValues(lf.Ui.Nodes) + values.Set("password", c.password) + + _, _ = testhelpers.LoginMakeRequest(t, false, false, lf, c.browserClient, values.Encode()) + t.Log("[loginTS] login flow is ignored here since it will be handled by the code above, we just need to return") + return + } + }) + + router.HandleFunc("GET /registration-ts", func(w http.ResponseWriter, r *http.Request) { + t.Log("[registrationTS] navigated to the registration ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, RegistrationUI) + + q := r.URL.Query() + hlc := q.Get("login_challenge") + + if hlc != "" { + *c.callTrace = append(*c.callTrace, RegistrationWithOAuth2LoginChallenge) + t.Log("[registrationTS] initializing a new OpenID Provider flow through the registration endpoint") + registrationUrl, err := url.Parse(c.kratosPublicTS.URL + registration.RouteInitBrowserFlow) + require.NoError(t, err) + + q := registrationUrl.Query() + q.Set("login_challenge", hlc) + registrationUrl.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", registrationUrl.String(), nil) + require.NoError(t, err) + + resp, err := c.browserClient.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + + // if the registration page redirects us to the login page + // we will sign in which means we might have a session + var oryCookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + + for _, c := range c.browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + oryCookie = c + break + } + } + + if oryCookie != nil { + t.Log("[registrationTS] we expect to have been at the login screen and got an active flow. This means we have a session now") + return + } + + flowID := resp.Request.URL.Query().Get("flow") + assert.NotEmpty(t, flowID) + + f := testhelpers.GetRegistrationFlow(t, c.browserClient, c.kratosPublicTS, flowID) + require.NotNil(t, f) + + // continue the registration flow here + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.foobar", c.identifier) + values.Set("traits.username", c.identifier) + values.Set("password", c.password) + + _, res := testhelpers.RegistrationMakeRequest(t, false, false, f, c.browserClient, values.Encode()) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + return + } + + if q.Has("flow") { + *c.callTrace = append(*c.callTrace, RegistrationWithFlowID) + t.Log("[registrationTS] registration flow is ignored here since it will be handled by the code above, we just need to return") + return + } + }) + + router.HandleFunc("GET /consent", func(w http.ResponseWriter, r *http.Request) { + t.Log("[consentTS] navigated to the consent ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, Consent) + + q := r.URL.Query() + consentChallenge := q.Get("consent_challenge") + assert.NotEmpty(t, consentChallenge) + + if consentChallenge != "" { + *c.callTrace = append(*c.callTrace, ConsentWithChallenge) + } + + cr, resp, err := hydraAdminClient.GetOAuth2ConsentRequest(ctx).ConsentChallenge(q.Get("consent_challenge")).Execute() + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.ElementsMatch(t, cr.RequestedScope, c.requestedScope) + + if cr.GetSkip() { + *c.callTrace = append(*c.callTrace, ConsentSkip) + } + + if cr.Client.GetSkipConsent() { + *c.callTrace = append(*c.callTrace, ConsentClientSkip) + } + + completedAcceptRequest, resp, err := hydraAdminClient.AcceptOAuth2ConsentRequest(r.Context()).AcceptOAuth2ConsentRequest(hydraclientgo.AcceptOAuth2ConsentRequest{ + Remember: &c.consentRemember, + GrantScope: c.requestedScope, + }).ConsentChallenge(q.Get("consent_challenge")).Execute() + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + require.NotNil(t, completedAcceptRequest) + *c.callTrace = append(*c.callTrace, ConsentAccept) + + t.Logf("[consentTS] navigating to %s", completedAcceptRequest.RedirectTo) + resp, err = c.browserClient.Get(completedAcceptRequest.RedirectTo) + require.NoError(t, err) + require.Equal(t, c.clientAppTS.URL, fmt.Sprintf("%s://%s", resp.Request.URL.Scheme, resp.Request.URL.Host)) + }) + + kratosUIMiddleware := negroni.New() + kratosUIMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + kratosUIMiddleware.UseHandler(router) + + kratosUITS := testhelpers.NewHTTPTestServer(t, kratosUIMiddleware) + + clientAppTSMiddleware := negroni.New() + clientAppTSMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + clientAppTSMiddleware.UseHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c := ctx.Value(TestOAuthClientState).(*clientAppConfig) + kc := ctx.Value(TestUIConfig).(*testConfig) + *kc.callTrace = append(*kc.callTrace, CodeExchange) + + c.state.visits += 1 + t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) + if r.URL.Query().Has("code") { + token, err := c.client.Exchange(r.Context(), r.URL.Query().Get("code")) + require.NoError(t, err) + + if token != nil && token.AccessToken != "" { + t.Log("[clientAppTS] successfully exchanged code for token") + *kc.callTrace = append(*kc.callTrace, CodeExchangeWithToken) + c.state.tokens += 1 + } else { + t.Log("[clientAppTS] did not receive a token") + } + } else { + t.Error("[clientAppTS] code query parameter is missing") + } + }) + // A new OAuth client which will also function as the callback for the code exchange + clientAppTS := testhelpers.NewHTTPTestServer(t, clientAppTSMiddleware) + + // we want to test if the registration ui is used if the flow contains an oauth2 login challenge + // so we will have Hydra redirect to the base path of the test kratos ui server which + // will then initiate the registration flow + hydraAdmin, hydraPublic := newHydra(t, kratosUITS.URL+"/registration-ts", kratosUITS.URL+"/consent") + + hydraAdminClient = createHydraOAuth2ApiClient(hydraAdmin) + + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, hydraAdmin+"/") + conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, kratosUITS.URL+"/login-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, kratosUITS.URL+"/registration-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/return-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + + doOAuthFlow := func(t *testing.T, ctx context.Context, oauthClient *oauth2.Config, browserClient *http.Client) { + t.Helper() + + authCodeURL := makeAuthCodeURL(t, oauthClient, "", false) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authCodeURL, nil) + require.NoError(t, err) + res, err := browserClient.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.NoError(t, res.Body.Close()) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, res.StatusCode) + } + + 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) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.foobar", identifier) + values.Set("traits.username", identifier) + values.Set("password", password) + + _, resp := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var cookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + + for _, c := range browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + cookie = c + break + } + } + + require.NotNil(t, cookie, "expected exactly one session cookie to be set but got none") + } + + // important, we will set the persistent cookie to false for most of the tests here + // once this is true, the behavior of the consent flow changes + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + t.Run("case=should accept oauth login request on registration", func(t *testing.T) { + // this test initiates a new OAuth2 flow which goes directly to the registration page + // we then create a new account through the registration flow + // and expect the OAuth2 flow to succeed + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauth2Client := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + ctx = context.WithValue(ctx, TestUIConfig, &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + callTrace: &ct, + requestedScope: scopes, + }) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauth2Client, + expectToken: true, + state: &clientAS, + }) + + doOAuthFlow(t, ctx, + oauth2Client, + browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=registration with session should redirect to login to re-authenticate and to consent", func(t *testing.T) { + // this test registers a new account which sets a session + // we then initiate a new OAuth2 flow which should redirect us to the registration page + // the registration page does a session validation and retrieves the loginRequest from Hydra + // which in this case will indicate we cannot skip the login flow (since the there is no previous OAuth flow associated) + // we then get redirected to the the login page with refresh=true + // we then sign in and expect to be redirected to the consent page + // and then back to the client app + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + requestedScope: scopes, + callTrace: &ct, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + expectToken: true, + state: &clientAS, + }) + + registerNewAccount(t, browserClient, identifier, password) + + require.ElementsMatch(t, []callTrace{ + RegistrationUI, + RegistrationWithFlowID, + }, ct, "expected the call trace to match") + + // reset the call trace + ct = []callTrace{} + tc.callTrace = &ct + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=registration should redirect to login if session exists and skip=false", func(t *testing.T) { + // we dont want to skip the consent page here + // we want the registration page to redirect to the login page + // since we have a session but do not skip the consent page + clientSkipConsent := false + consentRemember := false + + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: consentRemember, + requestedScope: scopes, + callTrace: &ct, + } + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, tc) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + // reset our state on the client app + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + clientAppConfig.state = &clientAS + + // we should now have a session, but not skip the consent page + doOAuthFlow(t, ctx, oauthClient, browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected = append(expected, + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + ) + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=consent should be skipped if client is configured to skip", func(t *testing.T) { + clientSkipConsent := true + scopes := []string{"profile", "email", "offline_access"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: false, + requestedScope: scopes, + callTrace: &ct, + } + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentClientSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, tc) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=consent should be skipped if user has a session and has already consented", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + }) + + consentRemember := true + + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + kratosUIConfig := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: consentRemember, + requestedScope: scopes, + callTrace: &ct, + } + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, kratosUIConfig) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + // reset our state on the client app + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + clientAppConfig.state = &clientAS + + // reset the call trace + ct = []callTrace{} + kratosUIConfig.callTrace = &ct + + // we should now have a session, but not skip the consent page + doOAuthFlow(t, ctx, oauthClient, browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected = []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=should fail because the persistent Hydra session doesn't match the new Kratos session subject", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + }) + + // this test re-uses the previous oauthClient + // but creates a new user account through the registration flow + // since the session with the new user does not match the hydra session it should fail + scopes := []string{"profile", "email", "offline_access"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) + + ct := make([]callTrace, 0) + + kratosUIConfig := &testConfig{ + identifier: x.NewUUID().String(), + password: x.NewUUID().String(), + kratosPublicTS: kratosPublicTS, + browserClient: browserClient, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + callTrace: &ct, + requestedScope: scopes, + } + + ctx = context.WithValue(ctx, TestUIConfig, kratosUIConfig) + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: false, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + cookies := browserClient.Jar.Cookies(currentURL) + + // remove the kratos session so we can register a new account + for _, c := range cookies { + if c.Name == config.DefaultSessionCookieName { + c.MaxAge = -1 + c.Value = "" + break + } + } + browserClient.Jar.SetCookies(currentURL, cookies) + + kratosUIConfig.identifier = x.NewUUID().String() + kratosUIConfig.password = x.NewUUID().String() + + // reset the call trace + ct = []callTrace{} + kratosUIConfig.callTrace = &ct + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + + // In Hydra v2.2.0-rc.3 this would have failed outright: Hydra returned an error + // when AcceptLoginRequest arrived with a subject (new user B) that did not match + // the subject in the existing login session (user A, skip=true flow). + // + // In Hydra v2.2.0 final the mismatch is handled with a prompt=login redirect + // instead of an error. The restart produces a fresh flow with f.Subject="" so + // the mismatch guard (f.Subject != "" && payload.Subject != f.Subject) in + // consent/handler.go does not fire on the second attempt, and the new user's + // subject is accepted. The flow completes successfully for user B. + // + // Security note: this is not exploitable in production. AcceptLoginRequest is a + // trusted admin API reachable only by the login provider (Kratos), which always + // sends the correctly authenticated identity ID. The change removes a hard + // early-failure signal for accidental subject substitution but does not + // introduce a new attack surface. + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected = []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, // skip=true, subject mismatch → prompt=login redirect + RegistrationUI, + RegistrationWithFlowID, + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, // skip=false, f.Subject="" → guard bypassed, new subject accepted + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) +} diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go new file mode 100644 index 0000000..b8587b7 --- /dev/null +++ b/selfservice/strategy/password/registration.go @@ -0,0 +1,250 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/ory/x/otelx/semconv" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/text" + + "github.com/pkg/errors" + + "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/registration" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +func nodePasswordInput() *node.Node { + return NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword) +} + +func nodeSubmit() *node.Node { + return node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()) +} + +var _ registration.FormHydrator = new(Strategy) + +// Update Registration Flow with Password Method +// +// swagger:model updateRegistrationFlowWithPasswordMethod +type UpdateRegistrationFlowWithPasswordMethod struct { + // Password to sign the user up with + // + // required: true + Password string `json:"password"` + + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // Method to use + // + // This field must be set to `password` when using the password method. + // + // required: true + Method string `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) handleRegistrationError(r *http.Request, f *registration.Flow, p UpdateRegistrationFlowWithPasswordMethod, err error) error { + if f != 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(p *UpdateRegistrationFlowWithPasswordMethod, r *http.Request, ds *url.URL) (err error) { + return registration.DecodeBody(p, r, registrationSchema, ds) +} + +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 + } + + ds, err := f.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + var p UpdateRegistrationFlowWithPasswordMethod + if err := s.decode(&p, r, ds); err != nil { + return s.handleRegistrationError(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 s.handleRegistrationError(r, f, p, err) + } + + if len(p.Password) == 0 { + return s.handleRegistrationError(r, f, p, schema.NewRequiredError("#/password", "password")) + } + + if len(p.Traits) == 0 { + p.Traits = json.RawMessage("{}") + } + + hpw := make(chan []byte) + errC := make(chan error) + go func() { + defer close(hpw) + defer close(errC) + + h, err := s.d.Hasher(ctx).Generate(ctx, []byte(p.Password)) + if err != nil { + errC <- err + return + } + hpw <- h + }() + + if err != nil { + 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(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(r, f, p, err) + case h := <-hpw: + co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(h)}) + if err != nil { + 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) + } + + return nil +} + +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 + } + + c, ok := i.GetCredentials(identity.CredentialsTypePassword) + if !ok { + // This should never happen + return errors.WithStack(x.PseudoPanic.WithReasonf("identity object did not provide the %s CredentialType unexpectedly", identity.CredentialsTypePassword)) + } else if len(c.Identifiers) == 0 { + return schema.NewMissingIdentifierError() + } + + for _, id := range c.Identifiers { + if err := s.d.PasswordValidator().Validate(ctx, id, pw); err != nil { + if herodotErr := new(herodot.DefaultError); errors.As(err, &herodotErr) { + return err + } + if message := new(text.Message); errors.As(err, &message) { + return schema.NewPasswordPolicyViolationError("#/password", message) + } + return schema.NewPasswordPolicyViolationError("#/password", text.NewErrorValidationPasswordPolicyViolationGeneric(err.Error())) + } + } + + return nil +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.Strategy.PopulateRegistrationMethod") + defer otelx.End(span, &err) + + ds, err := f.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + // The group used to be `password`, but to make it consistent with other methods and two-step registration, + // it is now `default`. To make this switch backwards compatible, this feature flag is used. + // + // Previously, the behavior would be that the group of NodesFromJSONSchema would be `password`, but as soon + // as any other method (code, passkeys, two-step) would be enabled, the group would be `default`. + // + // Going forward, the default is that the group is `default` and the feature flag is not set. + // + // TODO remove me when everyone has migrated. + if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) && node.UiNodeGroup(s.d.Config().SelfServiceFlowRegistrationPasswordMethodProfileGroup(r.Context())) == node.PasswordGroup { + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "password_profile_registration_node_group=password")) + + // This is the legacy code path. In the new code path, the profile method is responsible for hydrating the form + // nodes. In the old code path, the password method is responsible for hydrating the form nodes if it is + // the only method enabled. + nodes, err := container.NodesFromJSONSchema(r.Context(), node.PasswordGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + + for _, n := range nodes { + f.UI.SetNode(n) + } + } + // TODO end + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Upsert(nodePasswordInput()) + f.UI.Nodes.Upsert(nodeSubmit()) + + return nil +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Upsert(nodePasswordInput()) + f.UI.Nodes.Upsert(nodeSubmit()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + // The profile method is responsible for rendering the profile form fields. + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.RemoveMatching(nodePasswordInput()) + f.UI.Nodes.RemoveMatching(nodeSubmit()) + return nil +} diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go new file mode 100644 index 0000000..020d47c --- /dev/null +++ b/selfservice/strategy/password/registration_test.go @@ -0,0 +1,985 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + _ "embed" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/registrationhelpers" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "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/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlcon" +) + +var flows = []string{"spa", "api", "browser"} + +func newRegistrationRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault { + _, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + config.ViperKeySelfServiceRegistrationLoginHints: true, + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + }), + }, cfgOpts...)...) + + return reg +} + +func TestRegistration(t *testing.T) { + t.Parallel() + + t.Run("case=registration", func(t *testing.T) { + t.Parallel() + + reg := newRegistrationRegistry(t, configx.WithValues(map[string]any{ + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: "file://stub/registration.schema.json", SelfserviceSelectable: true}, + {ID: "other", URL: "file://stub/registration.schema.json", SelfserviceSelectable: true}, + }, + })) + conf := reg.Config() + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg) + + // set the "return to" server, which will assert the session state + // (redirTS: enforce that a session exists, redirNoSessionTS: enforce that no session exists) + useReturnToFromTS := func(ts *httptest.Server) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts") + } + + useReturnToFromTS(redirTS) + + apiClient := testhelpers.NewDebugClient(t) + + t.Run("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) + }) + + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { + v.Del("traits.foobar") + }) + }) + + t.Run("AssertCSRFFailures", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { + v.Set("password", x.NewUUID().String()) + v.Set("method", identity.CredentialsTypePassword.String()) + }) + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemaDoesNotExist(t, reg, flows, func(v url.Values) { + v.Set("password", x.NewUUID().String()) + v.Set("method", identity.CredentialsTypePassword.String()) + }) + }) + + 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, + publicTS.URL+registration.RouteSubmitFlow) + } + + if hc == nil { + hc = testhelpers.NewClientWithCookies(t) + } + + expectReturnTo := browserRedirTS.URL + "/registration-return-ts" + if isSPA { + expectReturnTo = publicTS.URL + } + + return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, + isSPA, http.StatusOK, expectReturnTo) + } + + expectSuccessfulRegistration := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + useReturnToFromTS(redirTS) + return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values) + } + + expectNoRegistration := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + useReturnToFromTS(redirNoSessionTS) + t.Cleanup(func() { + useReturnToFromTS(redirTS) + }) + return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) + } + + t.Run("case=should reject invalid transient payload", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + username := x.NewUUID().String() + body := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, func(v url.Values) { + v.Set("traits.username", username) + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + v.Set("transient_payload", "42") + }) + assert.Equal(t, "bar", gjson.Get(body, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", body) + assert.Equal(t, username, gjson.Get(body, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", body) + assert.Equal(t, int64(4000026), gjson.Get(body, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", body) + }) + } + }) + + t.Run("case=should accept valid transient payload", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + setValues := func(username string, v url.Values) { + v.Set("traits.username", username) + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + v.Set("transient_payload.stuff", "42") + } + + t.Run("type=api", func(t *testing.T) { + username := x.NewUUID().String() + 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) + assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + username := x.NewUUID().String() + 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) + assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + }) + + t.Run("type=browser", func(t *testing.T) { + username := x.NewUUID().String() + 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) + }) + }) + + t.Run("case=should pass and set up a session", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + t.Run("type=api", func(t *testing.T) { + 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") + }) + 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 := 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") + }) + 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 := 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) + }) + }) + + t.Run("case=should not set up a session if hook is not configured", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + + t.Run("type=api", func(t *testing.T) { + 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") + }) + assert.Equal(t, `registration-identifier-8-api-nosession`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session.id").String(), "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + 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") + }) + }) + + t.Run("type=browser", func(t *testing.T) { + 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") + }) + }) + }) + + t.Run("case=should fail to register the same user again", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + applyTransform := func(values, transform func(v url.Values)) func(v url.Values) { + return func(v url.Values) { + values(v) + transform(v) + } + } + + // test duplicate registration on all client types, where the values can be transformed before + // they are sent the second time + testWithTransform := func(t *testing.T, suffix string, transform func(v url.Values)) { + t.Run("type=api", func(t *testing.T) { + values := func(v url.Values) { + v.Set("traits.username", "registration-identifier-8-api-duplicate-"+suffix) + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + } + + _ = expectSuccessfulRegistration(t, true, false, apiClient, values) + body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, + applyTransform(values, transform), false, http.StatusBadRequest, + publicTS.URL+registration.RouteSubmitFlow) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-api-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + values := func(v url.Values) { + v.Set("traits.username", "registration-identifier-8-spa-duplicate-"+suffix) + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + } + + _ = expectSuccessfulRegistration(t, false, true, nil, values) + body := registrationhelpers.ExpectValidationError(t.Context(), 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) + }) + + t.Run("type=browser", func(t *testing.T) { + values := func(v url.Values) { + v.Set("traits.username", "registration-identifier-8-browser-duplicate-"+suffix) + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + } + + _ = expectSuccessfulRegistration(t, false, false, nil, values) + body := registrationhelpers.ExpectValidationError(t.Context(), 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) + }) + } + + t.Run("case=identical input", func(t *testing.T) { + testWithTransform(t, "identical", func(v url.Values) { + // base case + }) + }) + + t.Run("case=different capitalization", func(t *testing.T) { + testWithTransform(t, "caps", func(v url.Values) { + v.Set("traits.username", strings.ToUpper(v.Get("traits.username"))) + }) + }) + + t.Run("case=leading whitespace", func(t *testing.T) { + testWithTransform(t, "leading", func(v url.Values) { + v.Set("traits.username", " "+v.Get("traits.username")) + }) + }) + + t.Run("case=trailing whitespace", func(t *testing.T) { + testWithTransform(t, "trailing", func(v url.Values) { + v.Set("traits.username", v.Get("traits.username")+" ") + }) + }) + }) + + t.Run("case=should return correct error ids from validation failures", func(t *testing.T) { + test := func(t *testing.T, constraint string, setValues func(url.Values), expectedId text.ID, expectedMesage string) { + template := `{ + "$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": { + %s + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "required": [ + "foobar", + "username" + ] + } + }, + "additionalProperties": false + }` + + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, []byte(fmt.Sprintf(template, constraint))) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) + c := f.Ui + + values := testhelpers.SDKFormFieldsToURLValues(c.Nodes) + setValues(values) + values.Set("traits.username", "registration-identifier-9") + values.Set("password", x.NewUUID().String()) + actual, _ := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username") + assert.EqualValues(t, "registration-identifier-9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array()) + assert.Len(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array(), 0) + assert.Equal(t, int64(expectedId), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int()) + assert.Equal(t, expectedMesage, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String()) + assert.Equal(t, "error", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.type").String()) + } + + const key = "traits.foobar" + t.Run("case=string violating minLength", func(t *testing.T) { + test(t, `"type": "string", "minLength": 5`, func(v url.Values) { v.Set(key, "bar") }, text.ErrorValidationMinLength, "length must be >= 5, but got 3") + }) + + t.Run("case=string violating maxLength", func(t *testing.T) { + test(t, `"type": "string", "maxLength": 5`, func(v url.Values) { v.Set(key, "qwerty") }, text.ErrorValidationMaxLength, "length must be <= 5, but got 6") + }) + + t.Run("case=string violating pattern", func(t *testing.T) { + test(t, `"type": "string", "pattern": "^[a-z]*$"`, func(v url.Values) { v.Set(key, "FUBAR") }, text.ErrorValidationInvalidFormat, "does not match pattern \"^[a-z]*$\"") + }) + + t.Run("case=number violating minimum", func(t *testing.T) { + test(t, `"type": "number", "minimum": 5`, func(v url.Values) { v.Set(key, "3") }, text.ErrorValidationMinimum, "must be >= 5 but found 3") + }) + + t.Run("case=number violating exclusiveMinimum", func(t *testing.T) { + test(t, `"type": "number", "exclusiveMinimum": 5`, func(v url.Values) { v.Set(key, "5") }, text.ErrorValidationExclusiveMinimum, "must be > 5 but found 5") + }) + + t.Run("case=number violating maximum", func(t *testing.T) { + test(t, `"type": "number", "maximum": 5`, func(v url.Values) { v.Set(key, "6") }, text.ErrorValidationMaximum, "must be <= 5 but found 6") + }) + + t.Run("case=number violating exclusiveMaximum", func(t *testing.T) { + test(t, `"type": "number", "exclusiveMaximum": 5`, func(v url.Values) { v.Set(key, "5") }, text.ErrorValidationExclusiveMaximum, "must be < 5 but found 5") + }) + + t.Run("case=number violating multipleOf", func(t *testing.T) { + test(t, `"type": "number", "multipleOf": 3`, func(v url.Values) { v.Set(key, "7") }, text.ErrorValidationMultipleOf, "7 not multipleOf 3") + }) + + t.Run("case=array violating maxItems", func(t *testing.T) { + test(t, `"type": "array", "items": { "type": "string" }, "maxItems": 3`, func(v url.Values) { v.Add(key, "a"); v.Add(key, "b"); v.Add(key, "c"); v.Add(key, "d") }, text.ErrorValidationMaxItems, "maximum 3 items allowed, but found 4 items") + }) + + t.Run("case=array violating minItems", func(t *testing.T) { + test(t, `"type": "array", "items": { "type": "string" }, "minItems": 3`, func(v url.Values) { v.Add(key, "a"); v.Add(key, "b") }, text.ErrorValidationMinItems, "minimum 3 items allowed, but found 2 items") + }) + + t.Run("case=array violating uniqueItems", func(t *testing.T) { + test(t, `"type": "array", "items": { "type": "string" }, "uniqueItems": true`, func(v url.Values) { v.Add(key, "abc"); v.Add(key, "XYZ"); v.Add(key, "abc") }, text.ErrorValidationUniqueItems, "items at index 0 and 2 are equal") + }) + + t.Run("case=wrong type", func(t *testing.T) { + test(t, `"type": "number"`, func(v url.Values) { v.Set(key, "blabla") }, text.ErrorValidationWrongType, "expected number, but got string") + }) + }) + + 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/registration.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+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username") + } + + checkFirst := func(t *testing.T, actual string) { + check(t, actual) + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual) + } + + checkSecond := func(t *testing.T, actual string) { + check(t, actual) + assert.EqualValues(t, "registration-identifier-9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%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()) + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) + } + + valuesFirst := func(v url.Values) url.Values { + v.Del("traits.username") + v.Set("password", x.NewUUID().String()) + v.Set("traits.foobar", "bar") + return v + } + + valuesSecond := func(v url.Values) url.Values { + v.Set("traits.username", "registration-identifier-9") + v.Set("password", x.NewUUID().String()) + v.Del("traits.foobar") + return v + } + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) + c := f.Ui + + actual, _ := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) + checkFirst(t, actual) + actual, _ = testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) + checkSecond(t, actual) + }) + + t.Run("type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) + c := f.Ui + + actual, _ := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, testhelpers.EncodeFormAsJSON(t, false, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) + checkFirst(t, actual) + actual, _ = testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, testhelpers.EncodeFormAsJSON(t, false, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) + checkSecond(t, actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) + c := f.Ui + + actual, _ := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)).Encode()) + checkFirst(t, actual) + actual, _ = testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)).Encode()) + checkSecond(t, actual) + }) + }) + + t.Run("case=should work even if password is just numbers", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + t.Run("type=api", func(t *testing.T) { + 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") + }) + assert.Equal(t, `registration-identifier-10-api`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + }) + + t.Run("type=spa", func(t *testing.T) { + 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") + }) + assert.Equal(t, `registration-identifier-10-spa`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + 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") + }) + assert.Equal(t, `registration-identifier-10-browser`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + }) + }) + + t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json") + + for _, f := range []string{"spa", "api", "browser"} { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, func(v url.Values) { + v.Set("traits.email", testhelpers.RandomEmail()) + v.Set("password", x.NewUUID().String()) + v.Set("method", "password") + }) + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + t.Run("case=should work with regular JSON", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/registration.schema.json") + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + hc := testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(hc.Transport, t) + payload := testhelpers.InitializeRegistrationFlowViaBrowser(t, hc, publicTS, false, false, false) + values := testhelpers.SDKFormFieldsToURLValues(payload.Ui.Nodes) + time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. + + username := x.NewUUID() + actual, res := testhelpers.RegistrationMakeRequest(t, true, false, payload, hc, fmt.Sprintf(`{ + "method": "password", + "csrf_token": "%s", + "password": "%s", + "traits": { + "foobar": "bar", + "username": "%s" + } +}`, values.Get("csrf_token"), x.NewUUID(), username)) + assert.EqualValues(t, http.StatusOK, res.StatusCode, assertx.PrettifyJSONPayload(t, actual)) + assert.Equal(t, username.String(), gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + }) + + t.Run("case=should choose the correct identity schema", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "does-not-exist", URL: "file://./stub/not-exists.schema.json"}, + {ID: "advanced-user", URL: "file://./stub/registration.secondary.schema.json"}, + }) + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + + username := "registration-custom-schema" + for _, tc := range []struct { + name string + isAPI bool + isSPA bool + }{ + { + name: "api", + isAPI: true, + }, { + name: "spa", + isSPA: true, + }, { + name: "browser", + }, + } { + t.Run("type="+tc.name, func(t *testing.T) { + body := expectNoRegistration(t, tc.isAPI, tc.isSPA, nil, func(v url.Values) { + v.Set("traits.username", username+"-"+tc.name) + v.Set("password", x.NewUUID().String()) + v.Set("traits.baz", "bar") + }) + + if tc.isAPI || tc.isSPA { // body is empty for browser flow + assert.Equal(t, username+"-"+tc.name, gjson.Get(body, "identity.traits.username").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session.id").String(), "%s", body) + } + }) + } + }) + + t.Run("case=multi-schema select", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "username", URL: "file://stub/sort.schema.json", SelfserviceSelectable: true}, + {ID: "email", URL: "file://stub/email.schema.json", SelfserviceSelectable: true}, + {ID: "phone", URL: "file://stub/phone.schema.json"}, + {ID: "not-allowed", URL: "file://stub/login.schema.json"}, + }) + // We set default to phone and select email in InitializeRegistrationFlowViaBrowser + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") + + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, "https://www.ory.com") + + browserClient := testhelpers.NewClientWithCookies(t) + + expected := container.Container{ + Method: "POST", + Nodes: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + node.NewInputField("traits.email", nil, node.DefaultGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Autocomplete = node.InputAttributeAutocompleteEmail + })).WithMetaLabel(text.NewInfoNodeLabelGenerated("E-Mail", "traits.email")), + node.NewInputField("password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Autocomplete = node.InputAttributeAutocompleteNewPassword + })).WithMetaLabel(text.NewInfoNodeInputPassword()), + node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()), + }, + } + + for _, tc := range []struct { + name string + isAPI bool + isSPA bool + }{ + { + name: "api", + isAPI: true, + }, + { + name: "spa", + isSPA: true, + }, + { + name: "browser", + }, + } { + t.Run("type="+tc.name+" registration success", func(t *testing.T) { + var f *kratos.RegistrationFlow + var hc *http.Client + var payload string + if tc.isAPI { + f = testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("email")) + hc = apiClient + } else { + f = testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, tc.isSPA, false, false, testhelpers.InitFlowWithIdentitySchema("email")) + hc = browserClient + } + + expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id + assertx.EqualAsJSON(t, expected, f.Ui) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.email", testhelpers.RandomEmail()) + values.Set("password", x.NewUUID().String()) + + var expectedURL string + if tc.isAPI { + payload = testhelpers.EncodeFormAsJSON(t, true, values) + } else { + payload = values.Encode() + } + if tc.name == "browser" { + expectedURL = redirTS.URL + } else { + expectedURL = conf.SelfPublicURL(t.Context()).String() + login.RouteSubmitFlow + } + + actual, resp := testhelpers.RegistrationMakeRequest(t, tc.isAPI, tc.isSPA, f, hc, payload) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + if tc.isAPI { + assert.NotEmpty(t, gjson.Get(actual, "identity.id").String(), "%s", actual) + assert.Equal(t, gjson.Get(actual, "identity.traits.email").String(), values.Get("traits.email"), "%s", actual) + assert.Equal(t, gjson.Get(actual, "identity.schema_id").String(), "email", "%s", actual) + } + + identity, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypePassword, values.Get("traits.email")) + require.NoError(t, err, sqlcon.ErrNoRows()) + + assert.NotEmpty(t, identity.ID) + assert.Equalf(t, values.Get("traits.email"), gjson.GetBytes(identity.Traits, "email").String(), "%s", identity.Traits) + assert.Equal(t, "email", identity.SchemaID) + + // login + loginValues := func(v url.Values) { + v.Set("identifier", values.Get("traits.email")) + v.Set("password", values.Get("password")) + } + + body := testhelpers.SubmitLoginForm(t, tc.isAPI, nil, publicTS, loginValues, + tc.isSPA, false, http.StatusOK, expectedURL, testhelpers.InitFlowWithIdentitySchema("email")) + + if tc.name == "browser" { + assert.Equalf(t, identity.ID.String(), gjson.Get(body, "identity.id").String(), "%s", body) + } else { + assert.Equalf(t, identity.ID.String(), gjson.Get(body, "session.identity.id").String(), "%s", body) + } + }) + + t.Run("type="+tc.name+" registration fail due to invalid form data", func(t *testing.T) { + var f *kratos.RegistrationFlow + var hc *http.Client + var payload string + if tc.isAPI { + f = testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("email")) + hc = apiClient + } else { + f = testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, tc.isSPA, false, false, testhelpers.InitFlowWithIdentitySchema("email")) + hc = browserClient + } + + expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id + assertx.EqualAsJSON(t, expected, f.Ui) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.email", "invalidemail") + values.Set("password", x.NewUUID().String()) + + if tc.isAPI { + payload = testhelpers.EncodeFormAsJSON(t, true, values) + } else { + payload = values.Encode() + } + + actual, resp := testhelpers.RegistrationMakeRequest(t, tc.isAPI, tc.isSPA, f, hc, payload) + if tc.name == "browser" { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } + + assert.EqualValuesf(t, 4000040, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", actual) + assert.Equalf(t, "Enter a valid email address", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", actual) + }) + } + + t.Run("type=browser schema=default", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") + }) + // We set default to email + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "email") + + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + + expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id + assertx.EqualAsJSON(t, expected, f.Ui) + }) + + t.Run("type=api schema=default", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") + }) + // We set default to email + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "email") + + f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) + + expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id + assertx.EqualAsJSON(t, expected, f.Ui) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.email", testhelpers.RandomEmail()) + values.Set("password", x.NewUUID().String()) + + actual, _ := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + assert.NotEmptyf(t, gjson.Get(actual, "identity.id").String(), "%s", actual) + assert.Containsf(t, gjson.Get(actual, "identity.traits.email").String(), values.Get("email"), "%s", actual) + assert.Containsf(t, gjson.Get(actual, "identity.schema_id").String(), "email", "%s", actual) + }) + + t.Run("type=browser schema=does-not-exist", func(t *testing.T) { + testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, true, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) + + testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, true, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) + }) + + t.Run("type=api schema=does-not-exist", func(t *testing.T) { + testhelpers.InitializeRegistrationFlowViaAPIExpectError(t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) + + testhelpers.InitializeLoginFlowViaAPIExpectError(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) + }) + + t.Run("type=browser schema=not-allowed", func(t *testing.T) { + testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, true, testhelpers.InitFlowWithIdentitySchema("not-allowed")) + + testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, true, testhelpers.InitFlowWithIdentitySchema("not-allowed")) + }) + + t.Run("type=api schema=not-allowed", func(t *testing.T) { + testhelpers.InitializeRegistrationFlowViaAPIExpectError(t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("not-allowed")) + + testhelpers.InitializeLoginFlowViaAPIExpectError(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithIdentitySchema("not-allowed")) + }) + }) + }) + + t.Run("method=PopulateSignUpMethod", func(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeyPublicBaseURL: "https://foo/", + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + }), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/sort.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) + + assertx.EqualAsJSON(t, container.Container{ + Action: conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id, + Method: "POST", + Nodes: node.Nodes{ + node.NewCSRFNode(nosurfx.FakeCSRFToken), + node.NewInputField("traits.username", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Autocomplete = node.InputAttributeAutocompleteNewPassword + })).WithMetaLabel(text.NewInfoNodeInputPassword()), + node.NewInputField("traits.bar", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()), + }, + }, f.Ui) + }) +} + +func TestPopulateRegistrationMethod(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/identity.schema.json")), + ) + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypePassword) + require.NoError(t, err) + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src")) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(t.Context(), t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSON(t, snapshots[1], snapshots[3]) + }) + }) +} diff --git a/selfservice/strategy/password/schema.go b/selfservice/strategy/password/schema.go new file mode 100644 index 0000000..35a7169 --- /dev/null +++ b/selfservice/strategy/password/schema.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +//go:embed .schema/settings.schema.json +var settingsSchema []byte diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go new file mode 100644 index 0000000..f8104c5 --- /dev/null +++ b/selfservice/strategy/password/settings.go @@ -0,0 +1,242 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/hash" + "github.com/ory/kratos/text" + + "github.com/ory/kratos/ui/node" + + "github.com/ory/kratos/session" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +func (s *Strategy) SettingsStrategyID() string { + return identity.CredentialsTypePassword.String() +} + +// Update Settings Flow with Password Method +// +// swagger:model updateSettingsFlowWithPasswordMethod +type updateSettingsFlowWithPasswordMethod struct { + // Password is the updated password + // + // required: true + Password string `json:"password"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to password when trying to update a password. + // + // 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" form:"transient_payload"` +} + +func (p *updateSettingsFlowWithPasswordMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithPasswordMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "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(ctx, r, ctxUpdate, p) + } else if err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); 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) + } + + // This does not come from the payload! + p.Flow = ctxUpdate.Flow.ID.String() + if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return ctxUpdate, 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.Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +// Try to find a password hash in the credentials. Returns it if found, otherwise return an empty string. +func getPasswordHashFromCredential(creds map[identity.CredentialsType]identity.Credentials) string { + if creds == nil { + return "" + } + + cred, ok := creds[identity.CredentialsTypePassword] + if !ok { + return "" + } + + var hashedPassword identity.CredentialsPassword + if err := json.Unmarshal(cred.Config, &hashedPassword); err != nil { + return "" + } + return hashedPassword.HashedPassword +} + +// Detect whether the new password is the same as the old password. +// This is helpful to a user, e.g. in the case of a password leak: they want to change their password, +// and unknowingly set the new password to be the same as the old one (that leaked). We force them to +// set a different password in that case. +func isNewPasswordSameAsOld(ctx context.Context, oldHashedPassword string, newPassword string) bool { + if oldHashedPassword == "" { + return false + } + + // `hash.Compare` returns `nil` on 'success' i.e. old and new are the same. + return hash.Compare(ctx, []byte(newPassword), []byte(oldHashedPassword)) == 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(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()) + } + + if len(p.Password) == 0 { + return schema.NewRequiredError("#/password", "password") + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return err + } + + g, ctx := errgroup.WithContext(ctx) + var newPasswordHash []byte + // Extract an immutable value to avoid data races between goroutines. + oldHashedPassword := getPasswordHashFromCredential(i.Credentials) + + // Do in parallel due to limitations of the `bcrypt` library and for performance: + // - `hash(newPassword)` (expensive). + // - Check that the new password is not the same as the old password, + // which internally computes `hash(newPassword)` (expensive). + // - `validateCredentials` which may call the HaveIBeenPawned external API. + g.Go(func() error { + var err error + newPasswordHash, err = s.d.Hasher(ctx).Generate(ctx, []byte(p.Password)) + return err + }) + g.Go(func() error { + if isNewPasswordSameAsOld(ctx, oldHashedPassword, p.Password) { + return schema.NewPasswordPolicyViolationError("#/password", text.NewErrorValidationPasswordNewSameAsOld()) + } + return nil + }) + g.Go(func() error { + // Note: this goroutine mutates `i` so careful not to share it with other goroutines! + + // The credentials could have been modified in many ways possible. To keep it simple, we reset, and the validators + // will populate it correctly. + i.UpsertCredentialsConfig(s.ID(), []byte("{}"), 0) + return s.validateCredentials(ctx, i, p.Password) + }) + if err := g.Wait(); err != nil { + return err + } + + co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(newPasswordHash)}) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode password options to JSON: %s", err)) + } + i.UpsertCredentialsConfig(s.ID(), co, 0) + ctxUpdate.UpdateIdentity(i) + + return nil +} + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, _ *identity.Identity, f *settings.Flow) (err error) { + _, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword).WithMetaLabel(text.NewInfoNodeInputPassword())) + f.UI.Nodes.Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave())) + + return nil +} + +func (s *Strategy) handleSettingsError(ctx context.Context, 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(ctx, w, r, settings.ContinuityKey(s.SettingsStrategyID()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), 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/password/settings_test.go b/selfservice/strategy/password/settings_test.go new file mode 100644 index 0000000..bec5191 --- /dev/null +++ b/selfservice/strategy/password/settings_test.go @@ -0,0 +1,684 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + + "github.com/ory/client-go" + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/settingshelpers" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/httpx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/randx" +) + +func init() { + corpx.RegisterFakes() +} + +func newIdentityWithPassword(email string) *identity.Identity { + return &identity.Identity{ + ID: x.NewUUID(), + NID: x.NewUUID(), + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": { + Type: "password", + Identifiers: []string{email}, + Config: []byte(`{"hashed_password":"foo"}`), + }, + }, + State: identity.StateActive, + Traits: identity.Traits(`{"email":"` + email + `"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } +} + +func newEmptyIdentity() *identity.Identity { + return &identity.Identity{ + ID: x.NewUUID(), + NID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } +} + +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, + } +} + +func TestSettings(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/profile.schema.json")), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)), + configx.WithValues(testhelpers.MethodEnableConfig(settings.StrategyProfile, true)), + configx.WithValue(config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m"), + ) + + settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewLoginUIWith401Response(t, conf) + + browserIdentity1 := newIdentityWithPassword("john-browser@doe.com") + apiIdentity1 := newIdentityWithPassword("john-api@doe.com") + browserIdentity2 := newEmptyIdentity() + apiIdentity2 := newEmptyIdentity() + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, browserIdentity1) + browserUser1.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, browserIdentity2) + browserUser2.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, apiIdentity1) + apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, apiIdentity2) + + t.Run("case=should reject a new password if it is the same as the old one", func(t *testing.T) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + }) + + cfg := client.NewConfiguration() + u, err := url.Parse(publicTS.URL) + require.NoError(t, err) + cfg.Scheme = u.Scheme + cfg.Host = u.Host + cl := client.NewAPIClient(cfg) + api := cl.FrontendAPI + + t.Run("type=api", func(t *testing.T) { + // Create a new account. + password := uuid.Must(uuid.NewV4()).String() + var sessionToken string + { + registrationFlow, _, err := api.CreateNativeRegistrationFlow(t.Context()).Execute() + require.NoError(t, err) + require.NotNil(t, registrationFlow) + + registrationBody := client.UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasswordMethod: &client.UpdateRegistrationFlowWithPasswordMethod{ + Method: "password", + Password: password, + Traits: map[string]any{ + "email": uuid.Must(uuid.NewV4()).String() + "@ory.dev", + }, + }, + } + registration, _, err := api.UpdateRegistrationFlow(t.Context()).Flow(registrationFlow.Id).UpdateRegistrationFlowBody(registrationBody).Execute() + require.NoError(t, err) + require.NotNil(t, registration) + require.NotNil(t, registration.SessionToken) + + sessionToken = *registration.SessionToken + require.NotEmpty(t, sessionToken) + } + + // Create a settings flow. + var settingsFlow *client.SettingsFlow + { + + var err error + settingsFlow, _, err = api.CreateNativeSettingsFlow(t.Context()).XSessionToken(sessionToken).Execute() + require.NoError(t, err) + require.NotNil(t, settingsFlow) + } + + // Try to set the same password: fails. + { + update := client.UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{ + Method: "password", + Password: password, + }, + } + req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).XSessionToken(sessionToken) + settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req) + require.Error(t, err) + require.Nil(t, settingsFlow) + require.NotNil(t, httpResp) + require.Equal(t, http.StatusBadRequest, httpResp.StatusCode) + } + + // Try to set a different password: succeeds. + { + update := client.UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{ + Method: "password", + Password: uuid.Must(uuid.NewV4()).String(), + }, + } + req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).XSessionToken(sessionToken) + settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req) + require.NoError(t, err) + require.NotNil(t, settingsFlow) + require.NotNil(t, httpResp) + require.Equal(t, http.StatusOK, httpResp.StatusCode) + } + }) + + t.Run("type=browser", func(t *testing.T) { + // Create a new account. + password := uuid.Must(uuid.NewV4()).String() + var cookie string + { + registrationFlow, _, err := api.CreateBrowserRegistrationFlow(t.Context()).Execute() + require.NoError(t, err) + require.NotNil(t, registrationFlow) + + csrfToken := registrationFlow.Ui.Nodes[0].Attributes.UiNodeInputAttributes.Value.(string) + require.NotEmpty(t, csrfToken) + + registrationBody := client.UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasswordMethod: &client.UpdateRegistrationFlowWithPasswordMethod{ + Method: "password", + Password: password, + Traits: map[string]any{ + "email": uuid.Must(uuid.NewV4()).String() + "@ory.dev", + }, + CsrfToken: &csrfToken, + }, + } + + registration, httpResp, err := api.UpdateRegistrationFlow(t.Context()).Flow(registrationFlow.Id).UpdateRegistrationFlowBody(registrationBody).Execute() + require.NoError(t, err) + require.NotNil(t, httpResp) + require.NotNil(t, registration) + cookie = httpResp.Header.Get("Set-Cookie") + require.NotEmpty(t, cookie) + } + + // Create a settings flow. + var settingsFlow *client.SettingsFlow + var csrfToken string + { + + var err error + settingsFlow, _, err = api.CreateBrowserSettingsFlow(t.Context()).Cookie(cookie).Execute() + require.NoError(t, err) + require.NotNil(t, settingsFlow) + + csrfToken = settingsFlow.Ui.Nodes[0].Attributes.UiNodeInputAttributes.Value.(string) + require.NotEmpty(t, csrfToken) + } + + // Try to set the same password: fails. + { + update := client.UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{ + Method: "password", + Password: password, + CsrfToken: &csrfToken, + }, + } + req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).Cookie(cookie) + settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req) + require.Error(t, err) + require.Nil(t, settingsFlow) + require.NotNil(t, httpResp) + require.Equal(t, http.StatusBadRequest, httpResp.StatusCode) + } + + // Try to set a different password: succeeds. + { + update := client.UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{ + Method: "password", + Password: uuid.Must(uuid.NewV4()).String(), + CsrfToken: &csrfToken, + }, + } + req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).Cookie(cookie) + settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req) + require.NoError(t, err) + require.NotNil(t, settingsFlow) + require.NotNil(t, httpResp) + require.Equal(t, http.StatusOK, httpResp.StatusCode) + } + }) + }) + + t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) { + c := testhelpers.NewDebugClient(t) + t.Run("type=browser", func(t *testing.T) { + res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceLoginUI)) + }) + + t.Run("type=spa", func(t *testing.T) { + res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/json")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), settings.RouteSubmitFlow) + }) + + t.Run("type=api", func(t *testing.T) { + res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(`{"foo":"bar"}`), "application/json")) + require.NoError(t, err) + assert.Len(t, res.Cookies(), 0) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + }) + }) + + expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(t.Context()).String())) + } + + t.Run("description=should fail if password violates policy", func(t *testing.T) { + check := func(t *testing.T, reason, actual string) { + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).attributes.value").String(), "%s", actual) + assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual) + assert.Equal(t, reason, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0.text").String(), "%s", actual) + } + + t.Run("session=with privileged session", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + + payload := func(v url.Values) { + v.Set("password", "123456") + v.Set("method", "password") + } + + t.Run("type=api", func(t *testing.T) { + check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, true, false, apiUser1, payload)) + }) + + t.Run("spa=spa", func(t *testing.T) { + check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, true, browserUser1, payload)) + }) + + t.Run("type=browser", func(t *testing.T) { + check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + + t.Run("session=needs reauthentication", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + defer testhelpers.NewLoginUIWith401Response(t, conf) + t.Cleanup(func() { + conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + payload := func(v url.Values) { + v.Set("method", "password") + v.Set("password", "123456") + } + + t.Run("type=api/expected=an error because reauth can not be initialized for API clients", func(t *testing.T) { + _ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, apiUser1), conf) + actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, + http.StatusForbidden, publicTS.URL+settings.RouteSubmitFlow) + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + assert.NotEmpty(t, json.RawMessage(gjson.Get(actual, "redirect_browser_to").String())) + }) + + t.Run("type=spa", func(t *testing.T) { + _ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf) + actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, + http.StatusForbidden, publicTS.URL+settings.RouteSubmitFlow) + assertx.EqualAsJSON(t, settings.NewFlowNeedsReAuth().DefaultError, json.RawMessage(gjson.Get(actual, "error").Raw)) + assert.NotEmpty(t, json.RawMessage(gjson.Get(actual, "redirect_browser_to").String())) + }) + + t.Run("type=browser", func(t *testing.T) { + _ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf) + check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + }) + + t.Run("description=should not be able to make requests for another user", func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + 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.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) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + 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.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) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "password") + 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, "reason").String(), "initiated by someone else", "%s", actual) + }) + }) + + t.Run("description=should update the password and clear errors if everything is ok", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).value").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0.text").String(), actual) + } + + payload := func(v url.Values) { + v.Set("method", "password") + v.Set("password", x.NewUUID().String()) + } + + 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) { + actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(t.Context()).String()) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + }) + }) + + t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "password") + values.Set("password", x.NewUUID().String()) + values.Set("csrf_token", "invalid_token") + + actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser1, values.Encode()) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceErrorUI)) + + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual) + }) + + t.Run("case=should pass even without CSRF token/type=spa", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "password") + values.Set("password", x.NewUUID().String()) + values.Set("csrf_token", "invalid_token") + actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser1, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), 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.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "password") + values.Set("password", x.NewUUID().String()) + values.Set("csrf_token", "invalid_token") + actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser1, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.Equal(t, http.StatusOK, res.StatusCode) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.NotEmpty(t, gjson.Get(actual, "identity.id").String(), "%s", actual) + }) + + 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.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("password", x.NewUUID().String()) + + req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + tc.mod(req.Header) + + res, err := apiUser1.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + actual := string(ioutilx.MustReadAll(res.Body)) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, actual, tc.exp) + }) + } + }) + + expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, http.StatusOK, + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(t.Context()).String())) + } + + t.Run("description=should update the password even if no password was set before", func(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.Context(), t, reg, bi) + spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, si) + apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, ai) + + check := func(t *testing.T, actual string, id *identity.Identity) { + assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(name==password).attributes.value").String(), "%s", actual) + + actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config) + assert.Contains(t, cfg, "hashed_password", "%+v", actualIdentity.Credentials) + require.Len(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers, 1) + assert.Contains(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0], "-4") + } + + payload := func(v url.Values) { + v.Set("method", "password") + v.Set("password", randx.MustString(16, randx.AlphaNum)) + } + + t.Run("type=api", func(t *testing.T) { + actual := expectSuccess(t, true, false, apiUser, payload) + check(t, actual, ai) + }) + + t.Run("type=spa", func(t *testing.T) { + actual := expectSuccess(t, false, true, spaUser, payload) + check(t, actual, si) + }) + + t.Run("type=browser", func(t *testing.T) { + actual := expectSuccess(t, false, false, browserUser, payload) + check(t, actual, bi) + }) + }) + + t.Run("description=should update the password and perform the correct redirection", func(t *testing.T) { + rts := testhelpers.NewRedirTS(t, "", conf) + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsAfter+"."+config.DefaultBrowserReturnURL, rts.URL+"/return-ts") + t.Cleanup(func() { + conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsAfter, nil) + }) + + run := func(t *testing.T, f *kratos.SettingsFlow, isAPI bool, c *http.Client, _ *identity.Identity) { + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "password") + values.Set("password", randx.MustString(16, randx.AlphaNum)) + _, res := testhelpers.SettingsMakeRequest(t, isAPI, false, f, c, testhelpers.EncodeFormAsJSON(t, isAPI, values)) + require.EqualValues(t, rts.URL+"/return-ts", res.Request.URL.String()) + + actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), browserIdentity1.ID) + require.NoError(t, err) + + cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config) + assert.NotContains(t, cfg, "foo") + assert.NotEqual(t, `{"hashed_password":"foo"}`, cfg) + } + + t.Run("type=api", func(t *testing.T) { + t.Skip("Post-registration redirects do not work for API flows and are thus not tested here.") + }) + + t.Run("type=spa", func(t *testing.T) { + t.Skip("Post-registration redirects do not work for API flows and are thus not tested here.") + }) + + t.Run("type=browser", func(t *testing.T) { + rs := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + run(t, rs, false, browserUser1, browserIdentity1) + }) + }) + + t.Run("description=should update the password and revoke other user sessions", func(t *testing.T) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, "password"), []config.SelfServiceHook{{Name: "revoke_active_sessions"}}) + t.Cleanup(func() { + conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsAfter, nil) + }) + + check := func(t *testing.T, actual string, id *identity.Identity) { + assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(name==password).attributes.value").String(), "%s", actual) + + actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config) + assert.Contains(t, cfg, "hashed_password", "%+v", actualIdentity.Credentials) + require.Len(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers, 1) + assert.Contains(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0], "-4") + } + + initClients := func(isAPI bool, id *identity.Identity) (client1, client2 *http.Client) { + if isAPI { + client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + return client1, client2 + } + client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + + return client1, client2 + } + + run := func(t *testing.T, isAPI, isSPA bool, id *identity.Identity) { + payload := func(v url.Values) { + v.Set("method", "password") + v.Set("password", randx.MustString(16, randx.AlphaNum)) + } + + user1, user2 := initClients(isAPI, id) + + actual := expectSuccess(t, isAPI, isSPA, user1, payload) + check(t, actual, id) + + // second client should be logged out + res, err := user2.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/json")) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request) + + // again change password via first client + actual = expectSuccess(t, isAPI, isSPA, user1, payload) + check(t, actual, id) + } + + t.Run("type=api", func(t *testing.T) { + id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") + run(t, true, false, id) + }) + + t.Run("type=spa", func(t *testing.T) { + id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") + run(t, false, true, id) + }) + + t.Run("type=browser", func(t *testing.T) { + id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") + run(t, false, false, id) + }) + }) + + t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json") + + id := newIdentityWithoutCredentials(testhelpers.RandomEmail()) + browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + api := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + + for _, f := range []string{"spa", "api", "browser"} { + t.Run("type="+f, func(t *testing.T) { + hc := browser + if f == "api" { + hc = api + } + actual := settingshelpers.ExpectValidationError(t, publicTS, hc, conf, f, func(v url.Values) { + v.Set("password", x.NewUUID().String()) + v.Set("method", "password") + }) + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go new file mode 100644 index 0000000..afdce4a --- /dev/null +++ b/selfservice/strategy/password/strategy.go @@ -0,0 +1,120 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "context" + "encoding/json" + "strings" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/pkg/errors" + + "github.com/ory/kratos/ui/node" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "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/x" +) + +var ( + _ login.Strategy = (*Strategy)(nil) + _ registration.Strategy = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + httpx.ClientProvider + otelx.Provider + jsonnetsecure.VMProvider + config.Provider + continuity.ManagementProvider + + x.CookieProvider + + errorx.ManagementProvider + ValidationProvider + 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.ManagementProvider + + session.HandlerProvider + session.ManagementProvider +} + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +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 + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + if len(strings.Join(c.Identifiers, "")) > 0 && + ((s.d.Config().PasswordMigrationHook(ctx).Enabled && conf.UsePasswordMigrationHook) || + len(conf.HashedPassword) > 0) { + count++ + } + } + } + return +} + +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypePassword +} + +func (s *Strategy) CompletedAuthenticationMethod(_ context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.PasswordGroup +} diff --git a/selfservice/strategy/password/strategy_disabled_test.go b/selfservice/strategy/password/strategy_disabled_test.go new file mode 100644 index 0000000..248c366 --- /dev/null +++ b/selfservice/strategy/password/strategy_disabled_test.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + "io" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/x/configx" +) + +func TestDisabledEndpoint(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/sort.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + c := testhelpers.NewClientWithCookies(t) + t.Run("case=should not login when password method is disabled", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPICtx(context.Background(), t, c, publicTS, false) + + res, err := c.PostForm(f.Ui.Action, url.Values{"method": {"password"}, "password_identifier": []string{"identifier"}, "password": []string{"password"}}) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator", "%s", b) + }) + + t.Run("case=should not registration when password method is disabled", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, c, publicTS) + + res, err := c.PostForm(f.Ui.Action, url.Values{"method": {"password"}, "password_identifier": []string{"identifier"}, "password": []string{"password"}}) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator", "%s", b) + }) + + 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(context.Background(), t, reg) + + t.Run("method=GET", func(t *testing.T) { + t.Skip("GET is currently not supported for this endpoint.") + }) + + t.Run("method=POST", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaAPI(t, c, publicTS) + res, err := c.PostForm(f.Ui.Action, url.Values{ + "method": {"password"}, + "password": {"bar"}, + }) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(b), "This endpoint was disabled by system administrator", "%s", b) + }) + }) +} diff --git a/selfservice/strategy/password/strategy_test.go b/selfservice/strategy/password/strategy_test.go new file mode 100644 index 0000000..4b81a26 --- /dev/null +++ b/selfservice/strategy/password/strategy_test.go @@ -0,0 +1,168 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "cmp" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/password" + "github.com/ory/x/contextx" +) + +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) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := password.NewStrategy(reg) + + h1, err := hash.NewHasherBcrypt(reg).Generate(t.Context(), []byte("a password")) + require.NoError(t, err) + h2, err := reg.Hasher(t.Context()).Generate(t.Context(), []byte("a password")) + require.NoError(t, err) + + 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 := range 100 { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + cred, c := generateRandomConfig(t) + actual, err := strategy.CountActiveFirstFactorCredentials(t.Context(), 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: contextx.WithConfigValue(t.Context(), 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: contextx.WithConfigValue(t.Context(), 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, t.Context()), tc.in) + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + + actual, err = strategy.CountActiveMultiFactorCredentials(cmp.Or(tc.ctx, t.Context()), tc.in) + assert.NoError(t, err) + assert.Equal(t, 0, actual) + }) + } + }) +} diff --git a/selfservice/strategy/password/stub/email.schema.json b/selfservice/strategy/password/stub/email.schema.json new file mode 100644 index 0000000..571df3d --- /dev/null +++ b/selfservice/strategy/password/stub/email.schema.json @@ -0,0 +1,50 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.email.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", + "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" + }, + "organizations": { + "matcher": "email_domain" + } + }, + "maxLength": 320 + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/selfservice/strategy/password/stub/login.schema.json b/selfservice/strategy/password/stub/login.schema.json new file mode 100644 index 0000000..82f8581 --- /dev/null +++ b/selfservice/strategy/password/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/strategy/password/stub/migration.schema.json b/selfservice/strategy/password/stub/migration.schema.json new file mode 100644 index 0000000..5ac7314 --- /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/stub/missing-identifier.schema.json b/selfservice/strategy/password/stub/missing-identifier.schema.json new file mode 100644 index 0000000..4356579 --- /dev/null +++ b/selfservice/strategy/password/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/password/stub/phone.schema.json b/selfservice/strategy/password/stub/phone.schema.json new file mode 100644 index 0000000..f14f2a3 --- /dev/null +++ b/selfservice/strategy/password/stub/phone.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.sms.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone_number": { + "type": "string", + "format": "tel", + "title": "Phone Number", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + }, + "code": { + "identifier": true, + "via": "sms" + }, + "passkey": { + "display_name": true + } + }, + "verification": { + "via": "sms" + } + }, + "maxLength": 320 + } + }, + "required": ["phone_number"], + "additionalProperties": false + } + } +} diff --git a/selfservice/strategy/password/stub/profile.schema.json b/selfservice/strategy/password/stub/profile.schema.json new file mode 100644 index 0000000..7a7d716 --- /dev/null +++ b/selfservice/strategy/password/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": { + "password": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/password/stub/registration.schema.json b/selfservice/strategy/password/stub/registration.schema.json new file mode 100644 index 0000000..deb697c --- /dev/null +++ b/selfservice/strategy/password/stub/registration.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": { + "foobar": { + "type": "string", + "minLength": 2 + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "required": ["foobar", "username"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/password/stub/registration.secondary.schema.json b/selfservice/strategy/password/stub/registration.secondary.schema.json new file mode 100644 index 0000000..a2f502e --- /dev/null +++ b/selfservice/strategy/password/stub/registration.secondary.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "baz": { + "type": "string", + "minLength": 2 + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "required": [ + "baz", + "username" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/password/stub/sort.schema.json b/selfservice/strategy/password/stub/sort.schema.json new file mode 100644 index 0000000..889e166 --- /dev/null +++ b/selfservice/strategy/password/stub/sort.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/strategy/password/types.go b/selfservice/strategy/password/types.go new file mode 100644 index 0000000..6a3515c --- /dev/null +++ b/selfservice/strategy/password/types.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import "encoding/json" + +// Update Login Flow with Password Method +// +// swagger:model updateLoginFlowWithPasswordMethod +type updateLoginFlowWithPasswordMethod 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"` + + // The user's password. + // + // required: true + Password string `json:"password"` + + // Identifier is the email or username of the user trying to log in. + // This field is deprecated! + LegacyIdentifier string `json:"password_identifier"` + + // 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/password/validator.go b/selfservice/strategy/password/validator.go new file mode 100644 index 0000000..170ff96 --- /dev/null +++ b/selfservice/strategy/password/validator.go @@ -0,0 +1,238 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "bufio" + "context" + "crypto/sha1" //#nosec G505 -- sha1 is used for k-anonymity + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/ory/kratos/text" + + "github.com/arbovm/levenshtein" + "github.com/dgraph-io/ristretto/v2" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httpx" + "github.com/ory/x/otelx" +) + +const hashCacheItemTTL = time.Hour + +// maxIdentifierSimilarityCheckLength bounds the operands of the O(n·m) +// identifier-similarity check to keep its worst-case cost constant. Real +// identifiers (emails, usernames) and passwords are far shorter than this, so +// legitimate flows are unaffected, while oversized operands are skipped to keep +// the check's cost bounded. The bound is checked on the raw byte length as a +// cheap pre-filter and again on the case-folded operands, since lowercasing can +// expand an operand (e.g. Turkish dotted I). +const maxIdentifierSimilarityCheckLength = 256 + +// Validator implements a validation strategy for passwords. One example is that the password +// has to have at least 6 characters and at least one lower and one uppercase password. +type Validator interface { + // Validate returns nil if the password is passing the validation strategy and an error otherwise. If a validation error + // occurs, a regular error will be returned. If some other type of error occurs (e.g. HTTP request failed), an error + // of type *herodot.DefaultError will be returned. + Validate(ctx context.Context, identifier, password string) error +} + +type ValidationProvider interface { + PasswordValidator() Validator +} + +var ( + _ Validator = new(DefaultPasswordValidator) + ErrNetworkFailure = herodot.ErrUpstreamError().WithError("Leaked password server unavailable").WithReasonf("Unable to check if password has been leaked because an unexpected network error occurred") + ErrUnexpectedStatusCode = herodot.ErrUpstreamError().WithError("Leaked password server unavailable").WithReasonf("Unexpected status code from haveibeenpwned.com") +) + +// DefaultPasswordValidator implements Validator. It is based on best +// practices as defined in the following blog posts: +// +// - https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/ +// - https://www.microsoft.com/en-us/research/wp-content/uploads/2016/06/Microsoft_Password_Guidance-1.pdf +// +// Additionally passwords are being checked against Troy Hunt's +// [haveibeenpwnd](https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange) service to check if the +// password has been breached in a previous data leak using k-anonymity. +type DefaultPasswordValidator struct { + reg validatorDependencies + hashes *ristretto.Cache[string, int64] + + minIdentifierPasswordDist int + maxIdentifierPasswordSubstrThreshold float32 +} + +type validatorDependencies interface { + config.Provider + httpx.ClientProvider +} + +func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) (*DefaultPasswordValidator, error) { + cache, err := ristretto.NewCache(&ristretto.Config[string, int64]{ + NumCounters: 10 * 10000, + MaxCost: 60 * 10000, // BCrypt hash size is 60 bytes + BufferItems: 64, + IgnoreInternalCost: true, + }) + // sanity check - this should never happen unless above configuration variables are invalid + if err != nil { + return nil, errors.Wrap(err, "error while setting up validator cache") + } + return &DefaultPasswordValidator{ + reg: reg, + hashes: cache, + minIdentifierPasswordDist: 5, maxIdentifierPasswordSubstrThreshold: 0.5, + }, nil +} + +func b20(src []byte) string { + return fmt.Sprintf("%X", src) +} + +// code inspired by https://rosettacode.org/wiki/Longest_Common_Substring#Go +func lcsLength(a, b string) int { + lengths := make([]int, len(a)*len(b)) + greatestLength := 0 + for i, x := range a { + for j, y := range b { + if x == y { + curr := 1 + if i != 0 && j != 0 { + curr = lengths[(i-1)*len(b)+j-1] + 1 + } + + if curr > greatestLength { + greatestLength = curr + } + lengths[i*len(b)+j] = curr + } + } + } + return greatestLength +} + +func (s *DefaultPasswordValidator) fetch(ctx context.Context, hpw []byte, apiDNSName string) (int64, error) { + prefix := fmt.Sprintf("%X", hpw)[0:5] + loc := fmt.Sprintf("https://%s/range/%s", apiDNSName, prefix) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", loc, nil) + if err != nil { + return 0, err + } + res, err := s.reg.HTTPClient(ctx, httpx.ResilientClientWithConnectionTimeout(time.Second)).Do(req) + if err != nil { + return 0, errors.Wrapf(ErrNetworkFailure, "%s", err) + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusOK { + return 0, errors.Wrapf(ErrUnexpectedStatusCode, "%d", res.StatusCode) + } + + var thisCount int64 + + sc := bufio.NewScanner(res.Body) + for sc.Scan() { + row := sc.Text() + result := strings.Split(strings.TrimSpace(row), ":") + + // We assume a count of 1. HIBP API sometimes responds without the + // colon, so we just assume that the leak count is one. + // + // See https://github.com/ory/kratos/issues/2145 + count := int64(1) + if len(result) == 2 { + count, err = strconv.ParseInt(strings.ReplaceAll(result[1], ",", ""), 10, 64) + if err != nil { + return 0, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Expected password hash to contain a count formatted as int but got: %s", result[1])) + } + } + + s.hashes.SetWithTTL(prefix+result[0], count, 1, hashCacheItemTTL) + if prefix+result[0] == b20(hpw) { + thisCount = count + } + } + + // A read that fails after the response headers arrived (connection reset, + // client timeout, truncated body) is as much a network failure as a failed + // request, so it has to be classified as one to stay subject to + // ignore_network_errors. + if err := sc.Err(); err != nil { + return 0, errors.Wrapf(ErrNetworkFailure, "unable to read the response body: %s", err) + } + + s.hashes.SetWithTTL(b20(hpw), thisCount, 1, hashCacheItemTTL) + return thisCount, nil +} + +func (s *DefaultPasswordValidator) Validate(ctx context.Context, identifier, password string) error { + return otelx.WithSpan(ctx, "password.DefaultPasswordValidator.Validate", func(ctx context.Context) error { + return s.validate(ctx, identifier, password) + }) +} + +func (s *DefaultPasswordValidator) validate(ctx context.Context, identifier, password string) error { + passwordPolicyConfig := s.reg.Config().PasswordPolicyConfig(ctx) + + //nolint:gosec // disable G115 + if len(password) < int(passwordPolicyConfig.MinPasswordLength) { + //nolint:gosec // disable G115 + return text.NewErrorValidationPasswordMinLength(int(passwordPolicyConfig.MinPasswordLength), len(password)) + } + + if passwordPolicyConfig.IdentifierSimilarityCheckEnabled && len(identifier) > 0 && + len(identifier) <= maxIdentifierSimilarityCheckLength && + len(password) <= maxIdentifierSimilarityCheckLength { + compIdentifier, compPassword := strings.ToLower(identifier), strings.ToLower(password) + // Case-folding can expand an operand, so re-check the folded lengths + // before running the O(n·m) comparison. + if len(compIdentifier) <= maxIdentifierSimilarityCheckLength && + len(compPassword) <= maxIdentifierSimilarityCheckLength { + dist := levenshtein.Distance(compIdentifier, compPassword) + lcs := float32(lcsLength(compIdentifier, compPassword)) / float32(len(compPassword)) + if dist < s.minIdentifierPasswordDist || lcs > s.maxIdentifierPasswordSubstrThreshold { + return text.NewErrorValidationPasswordIdentifierTooSimilar() + } + } + } + + if !passwordPolicyConfig.HaveIBeenPwnedEnabled { + return nil + } + + //#nosec G401 -- sha1 is used for k-anonymity + h := sha1.New() + if _, err := h.Write([]byte(password)); err != nil { + return err + } + hpw := h.Sum(nil) + + c, ok := s.hashes.Get(b20(hpw)) + if !ok { + var err error + c, err = s.fetch(ctx, hpw, passwordPolicyConfig.HaveIBeenPwnedHost) + if (errors.Is(err, ErrNetworkFailure) || errors.Is(err, ErrUnexpectedStatusCode)) && passwordPolicyConfig.IgnoreNetworkErrors { + return nil + } else if err != nil { + return err + } + } + + //nolint:gosec // disable G115 + if c > int64(s.reg.Config().PasswordPolicyConfig(ctx).MaxBreaches) { + return text.NewErrorValidationPasswordTooManyBreaches(c) + } + + return nil +} diff --git a/selfservice/strategy/password/validator_lcs_test.go b/selfservice/strategy/password/validator_lcs_test.go new file mode 100644 index 0000000..72887a2 --- /dev/null +++ b/selfservice/strategy/password/validator_lcs_test.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLCSLength(t *testing.T) { + for k, tc := range []struct { + a string + b string + l int + }{ + {a: "foo", b: "foo", l: 3}, + {a: "fo", b: "foo", l: 2}, + {a: "bar", b: "foo", l: 0}, + {a: "foobar", b: "foo", l: 3}, + {a: "foobar", b: "oo", l: 2}, + {a: "foobar", b: "a", l: 1}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + require.Equal(t, tc.l, lcsLength(tc.a, tc.b)) + require.Equal(t, tc.l, lcsLength(tc.b, tc.a)) + }) + } +} diff --git a/selfservice/strategy/password/validator_test.go b/selfservice/strategy/password/validator_test.go new file mode 100644 index 0000000..e1b3c48 --- /dev/null +++ b/selfservice/strategy/password/validator_test.go @@ -0,0 +1,469 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" //#nosec G505 -- compatibility for imported passwords + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/password" + "github.com/ory/kratos/text" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/httpx" +) + +// testRegistry embeds RegistryDefault and overrides HTTPClient to inject a +// controllable HTTP client for HIBP-related tests. +type testRegistry struct { + *driver.RegistryDefault + cl *retryablehttp.Client +} + +func (r *testRegistry) HTTPClient(_ context.Context, _ ...httpx.ResilientOptions) *retryablehttp.Client { + return r.cl +} + +// newTestValidator wires base with a fake HIBP HTTP client and returns the +// validator along with the fake client for request assertions and stubbing. +func newTestValidator(t *testing.T, base *driver.RegistryDefault, opts ...httpx.ResilientOptions) (*password.DefaultPasswordValidator, *fakeHttpClient) { + t.Helper() + fakeClient := NewFakeHTTPClient() + cl := httpx.NewResilientClient(append([]httpx.ResilientOptions{ + httpx.ResilientClientWithMaxRetry(1), + httpx.ResilientClientWithConnectionTimeout(time.Millisecond), + }, opts...)...) + cl.HTTPClient = &fakeClient.Client + s, err := password.NewDefaultPasswordValidatorStrategy(&testRegistry{RegistryDefault: base, cl: cl}) + require.NoError(t, err) + return s, fakeClient +} + +func TestDefaultPasswordValidationStrategy(t *testing.T) { + t.Parallel() + + // Tests are based on: + // - https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/ + // - https://www.microsoft.com/en-us/research/wp-content/uploads/2016/06/Microsoft_Password_Guidance-1.pdf + + t.Run("default strategy", func(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + s, err := password.NewDefaultPasswordValidatorStrategy(reg) + require.NoError(t, err) + + for k, tc := range []struct { + id, pw string + expectedErr error + }{ + {pw: "", expectedErr: text.NewErrorValidationPasswordMinLength(0, 0)}, + {pw: "12", expectedErr: text.NewErrorValidationPasswordMinLength(0, 0)}, + {pw: "1234", expectedErr: text.NewErrorValidationPasswordMinLength(0, 0)}, + {pw: "123456", expectedErr: text.NewErrorValidationPasswordMinLength(0, 0)}, + {pw: "12345678", expectedErr: text.NewErrorValidationPasswordTooManyBreaches(0)}, + {pw: "password", expectedErr: text.NewErrorValidationPasswordTooManyBreaches(0)}, + {pw: "1234567890", expectedErr: text.NewErrorValidationPasswordTooManyBreaches(0)}, + {pw: "qwertyui", expectedErr: text.NewErrorValidationPasswordTooManyBreaches(0)}, + {pw: "l3f9to", expectedErr: text.NewErrorValidationPasswordMinLength(0, 0)}, + {pw: "l3f9toh1uaf81n21"}, + {pw: "l3f9toh1uaf81n21", id: "l3f9toh1uaf81n21", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {pw: "l3f9toh1"}, + {pw: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, + // simple permutation tests + {id: "hello@example.com", pw: "hello@example.com12345", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "hello@example.com", pw: "123hello@example.com123", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "hello@example.com", pw: "hello@exam", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "hello@example.com", pw: "HELLO@EXAMPLE.COM", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "hello@example.com", pw: "HELLO@example.com", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {pw: "hello@example.com", id: "hello@exam", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "hello@example.com", pw: "h3ll0@example", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {pw: "hello@example.com", id: "hello@exam", expectedErr: text.NewErrorValidationPasswordIdentifierTooSimilar()}, + {id: "abcd", pw: "9d3c8a1b"}, + {id: "a", pw: "kjOklafe"}, + {id: "ab", pw: "0000ab0000123"}, + // longest common substring with long password + {id: "d4f6090b-5a84", pw: "d4f6090b-5a84-2184-4404-8d1b-8da3eb00ebbe"}, + {id: "asdflasdflasdf", pw: "asdflasdflpiuhefnciluaksdzuföfhg"}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + t.Parallel() + + err := s.Validate(t.Context(), tc.id, tc.pw) + if tc.expectedErr == nil { + require.NoErrorf(t, err, "id: %s, pw: %s", tc.id, tc.pw) + } else { + require.ErrorIsf(t, err, tc.expectedErr, "id: %s, pw: %s", tc.id, tc.pw) + } + }) + } + }) + + t.Run("failure cases", func(t *testing.T) { + t.Parallel() + + _, base := pkg.NewFastRegistryWithMocks(t) + s, fakeClient := newTestValidator(t, base, httpx.ResilientClientWithMaxRetryWait(time.Millisecond)) + + t.Run("case=should send request to pwnedpasswords.com", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false) + require.Error(t, s.Validate(ctx, "mohutdesub", "damrumukuh")) + require.Contains(t, fakeClient.RequestedURLs(), "https://api.pwnedpasswords.com/range/BCBA9") + }) + + t.Run("case=should fail if request fails and ignoreNetworkErrors is not set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false) + fakeClient.RespondWithError("Network request failed") + require.Error(t, s.Validate(ctx, "", "sumdarmetp")) + }) + + t.Run("case=should not fail if request fails and ignoreNetworkErrors is set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, true) + fakeClient.RespondWithError("Network request failed") + require.NoError(t, s.Validate(ctx, "", "pepegtawni")) + }) + + t.Run("case=should fail if response has non 200 code and ignoreNetworkErrors is not set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false) + fakeClient.RespondWith(http.StatusForbidden, "") + require.Error(t, s.Validate(ctx, "", "jolhakowef")) + }) + + t.Run("case=should not fail if response has non 200 code code and ignoreNetworkErrors is set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, true) + fakeClient.RespondWith(http.StatusInternalServerError, "") + require.NoError(t, s.Validate(ctx, "", "jenuzuhjoj")) + }) + + t.Run("case=should fail with a network error if reading the response body fails and ignoreNetworkErrors is not set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false) + fakeClient.RespondWithBodyError("003D68EB55068C33ACE09247EE4C639306B:3\n", errBodyRead) + require.ErrorIs(t, s.Validate(ctx, "", "vebjihwoct"), password.ErrNetworkFailure) + }) + + t.Run("case=should not fail if reading the response body fails and ignoreNetworkErrors is set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, true) + fakeClient.RespondWithBodyError("003D68EB55068C33ACE09247EE4C639306B:3\n", errBodyRead) + require.NoError(t, s.Validate(ctx, "", "hufzeqmalt")) + }) + }) + + t.Run("max breaches", func(t *testing.T) { + t.Parallel() + + const maxBreaches = 5 + _, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordMaxBreaches, maxBreaches)) + s, fakeClient := newTestValidator(t, base) + + hibpResp := make(chan string, 1) + fakeClient.responder = func(req *http.Request) (*http.Response, error) { + buffer := bytes.NewBufferString(<-hibpResp) + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(buffer), + ContentLength: int64(buffer.Len()), + Request: req, + }, nil + } + + hashPw := func(t *testing.T, pw string) string { + //#nosec G401 -- sha1 is used for k-anonymity + h := sha1.New() + _, err := h.Write([]byte(pw)) + require.NoError(t, err) + hpw := h.Sum(nil) + return fmt.Sprintf("%X", hpw)[5:] + } + randomPassword := func(t *testing.T) string { + pw := make([]byte, 10) + _, err := rand.Read(pw) + require.NoError(t, err) + return fmt.Sprintf("%x", pw) + } + + for _, tc := range []struct { + name string + res func(t *testing.T, hash string) string + expectErr error + }{ + { + name: "contains invalid data which is ignored", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf("%s:2\ninvalid", hash) + }, + }, + { + name: "is missing a colon", + res: func(t *testing.T, hash string) string { + return hash + }, + }, + { + name: "contains invalid hash count", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf("%s:text\n%s:2", hashPw(t, randomPassword(t)), hash) + }, + expectErr: herodot.ErrUpstreamError(), + }, + { + name: "is missing hash count", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf("%s\n%s:2", hash, hashPw(t, randomPassword(t))) + }, + }, + { + name: "response contains no matches", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf("%s:57", hashPw(t, randomPassword(t))) + }, + }, + { + name: "contains less than maxBreachesThreshold", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf( + "%s:%d\n%s:%d", + hash, + maxBreaches, + hashPw(t, randomPassword(t)), + maxBreaches+1, + ) + }, + }, + { + name: "contains less than maxBreachesThreshold with a leading comma", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf( + "%s:%d\n%s:0,%d", + hash, + maxBreaches, + hashPw(t, randomPassword(t)), + maxBreaches+1, + ) + }, + }, + { + name: "contains more than maxBreachesThreshold", + res: func(t *testing.T, hash string) string { + return fmt.Sprintf("%s:%d", hash, maxBreaches+1) + }, + expectErr: text.NewErrorValidationPasswordTooManyBreaches( + int64(maxBreaches) + 1, // #nosec G115 + ), + }, + } { + t.Run(fmt.Sprintf("case=%s/expected err=%s", tc.name, tc.expectErr), func(t *testing.T) { + pw := randomPassword(t) + hash := hashPw(t, pw) + hibpResp <- tc.res(t, hash) + + err := s.Validate(context.Background(), "", pw) + assert.ErrorIs(t, err, tc.expectErr) + }) + + // verify the fetch was done, i.e. channel is empty + select { + case r := <-hibpResp: + t.Logf("expected the validate step to fetch the response, but I still got %s", r) + t.FailNow() + default: + // continue + } + } + }) +} + +func TestChangeHaveIBeenPwnedValidationHost(t *testing.T) { + t.Parallel() + + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) + t.Cleanup(testServer.Close) + testServerURL, err := url.Parse(testServer.URL) + require.NoError(t, err) + + _, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedHost, testServerURL.Host)) + s, fakeClient := newTestValidator(t, base) + + testServerExpectedCallURL := fmt.Sprintf("https://%s/range/BCBA9", testServerURL.Host) + + t.Run("case=should send request to test server", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false) + require.Error(t, s.Validate(ctx, "mohutdesub", "damrumukuh")) + require.Contains(t, fakeClient.RequestedURLs(), testServerExpectedCallURL) + }) +} + +func TestDisableHaveIBeenPwnedValidationHost(t *testing.T) { + t.Parallel() + + _, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedEnabled, false)) + s, fakeClient := newTestValidator(t, base) + + t.Run("case=should not send request to test server", func(t *testing.T) { + require.NoError(t, s.Validate(t.Context(), "mohutdesub", "damrumukuh")) + require.Empty(t, fakeClient.RequestedURLs()) + }) +} + +func TestChangeMinPasswordLength(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordMinLength, 10)) + s, err := password.NewDefaultPasswordValidatorStrategy(reg) + require.NoError(t, err) + + t.Run("case=should not fail if password is longer than min length", func(t *testing.T) { + require.NoError(t, s.Validate(t.Context(), "", "kuobahcaas")) + }) + + t.Run("case=should fail if password is shorter than min length", func(t *testing.T) { + require.Error(t, s.Validate(t.Context(), "", "rfqyfjied")) + }) +} + +func TestChangeIdentifierSimilarityCheckEnabled(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + s, err := password.NewDefaultPasswordValidatorStrategy(reg) + require.NoError(t, err) + + t.Run("case=should not fail if password is similar to identifier", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyPasswordIdentifierSimilarityCheckEnabled, false) + require.NoError(t, s.Validate(ctx, "bosqwfaxee", "bosqwfaxee")) + }) + + t.Run("case=should fail if password is similar to identifier", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyPasswordIdentifierSimilarityCheckEnabled, true) + require.Error(t, s.Validate(ctx, "bosqwfaxee", "bosqwfaxee")) + }) +} + +// TestIdentifierSimilarityCheckMaxLength guards against an algorithmic-complexity +// DoS (H1 #3866310): the O(n·m) identifier-similarity check must skip oversized +// operands, which a short identifier can never be similar to anyway. +func TestIdentifierSimilarityCheckMaxLength(t *testing.T) { + t.Parallel() + + // Disable HaveIBeenPwned so validation is deterministic and offline. + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedEnabled, false)) + s, err := password.NewDefaultPasswordValidatorStrategy(reg) + require.NoError(t, err) + + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyPasswordIdentifierSimilarityCheckEnabled, true) + + t.Run("case=skips the similarity check for oversized operands", func(t *testing.T) { + // Two identical strings one char past the 256-char cap would trip the + // too-similar check if it ran, so a nil result proves the length guard + // skipped it. Staying just over the cap keeps the test cheap even if the + // guard regresses (the check allocates an O(n·m) matrix). + large := strings.Repeat("a", 257) + require.NoError(t, s.Validate(ctx, large, large)) + }) + + t.Run("case=still detects similarity for short operands", func(t *testing.T) { + require.ErrorIs(t, s.Validate(ctx, "bosqwfaxee", "bosqwfaxee"), text.NewErrorValidationPasswordIdentifierTooSimilar()) + }) +} + +type fakeHttpClient struct { + http.Client + + requestedURLs []string + responder func(*http.Request) (*http.Response, error) +} + +func NewFakeHTTPClient() *fakeHttpClient { + client := fakeHttpClient{ + responder: func(*http.Request) (*http.Response, error) { + return nil, errors.New("No responder defined in fake HTTP client") + }, + } + client.Client = http.Client{ + Transport: &fakeRoundTripper{&client}, + Timeout: time.Second, + } + return &client +} + +func (c *fakeHttpClient) RespondWith(status int, body string) { + c.responder = func(request *http.Request) (*http.Response, error) { + buffer := bytes.NewBufferString(body) + return &http.Response{ + StatusCode: status, + Body: io.NopCloser(buffer), + ContentLength: int64(buffer.Len()), + Request: request, + }, nil + } +} + +func (c *fakeHttpClient) RespondWithError(err string) { + c.responder = func(*http.Request) (*http.Response, error) { + return nil, errors.New(err) + } +} + +// RespondWithBodyError responds with a status 200 whose body yields prefix and +// then fails, mimicking a connection reset or a client timeout that only hits +// once the response is already being read. +func (c *fakeHttpClient) RespondWithBodyError(prefix string, err error) { + c.responder = func(request *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(io.MultiReader(strings.NewReader(prefix), errorReader{err})), + Request: request, + }, nil + } +} + +func (c *fakeHttpClient) Reset() { + c.requestedURLs = nil +} + +func (c *fakeHttpClient) RequestedURLs() []string { + return c.requestedURLs +} + +func (c *fakeHttpClient) handle(request *http.Request) (*http.Response, error) { + c.requestedURLs = append(c.requestedURLs, request.URL.String()) + if request.Body != nil { + _ = request.Body.Close() + } + return c.responder(request) +} + +type fakeRoundTripper struct { + client *fakeHttpClient +} + +func (rt *fakeRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + return rt.client.handle(request) +} + +// errBodyRead is the error net/http returns from a response body read when the +// client's timeout fires after the response headers have been received. +var errBodyRead = errors.New("net/http: request canceled (Client.Timeout or context cancellation while reading body)") + +type errorReader struct{ err error } + +func (r errorReader) Read([]byte) (int, error) { return 0, r.err } diff --git a/selfservice/strategy/profile/.schema/registration.schema.json b/selfservice/strategy/profile/.schema/registration.schema.json new file mode 100644 index 0000000..727b8f8 --- /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 new file mode 100644 index 0000000..90246cb --- /dev/null +++ b/selfservice/strategy/profile/.schema/settings.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "traits" + ], + "properties": { + "method": { + "type": "string" + }, + "traits": {}, + "csrf_token": { + "type": "string" + }, + "action": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json b/selfservice/strategy/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json new file mode 100644 index 0000000..9b846a2 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json @@ -0,0 +1,126 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "default", + "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.stringy", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text" + }, + "group": "default", + "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/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json b/selfservice/strategy/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json new file mode 100644 index 0000000..9b846a2 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestOneStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json @@ -0,0 +1,126 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "default", + "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.stringy", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text" + }, + "group": "default", + "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/profile/.snapshots/TestPopulateRegistrationMethod-case=multi-schema-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-case=multi-schema-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..9364651 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-case=multi-schema-method=PopulateRegistrationMethodProfile.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..d789a59 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json @@ -0,0 +1,87 @@ +[ + { + "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": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..ee40fd4 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json @@ -0,0 +1,21 @@ +[ + { + "type": "input", + "group": "profile", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040008, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..9364651 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..9364651 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..5ea71db --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040008, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..9364651 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "checkbox", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..5ea71db --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json @@ -0,0 +1,106 @@ +[ + { + "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": "traits.booly", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040008, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api.json new file mode 100644 index 0000000..d6665e7 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api.json @@ -0,0 +1,153 @@ +{ + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text", + "value": "foobar" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number", + "value": 2.5 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox", + "value": false + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number", + "value": 2048 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text", + "value": "asdfasdfasdfasdfasfdasdfasdfasdf" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + } + ] +} diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=browser.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=browser.json new file mode 100644 index 0000000..d6665e7 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=browser.json @@ -0,0 +1,153 @@ +{ + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text", + "value": "foobar" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number", + "value": 2.5 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox", + "value": false + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number", + "value": 2048 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text", + "value": "asdfasdfasdfasdfasfdasdfasdfasdf" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + } + ] +} diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json new file mode 100644 index 0000000..d6665e7 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json @@ -0,0 +1,153 @@ +{ + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text", + "value": "foobar" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number", + "value": 2.5 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox", + "value": false + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number", + "value": 2048 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text", + "value": "asdfasdfasdfasdfasfdasdfasdfasdf" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "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": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + } + ] +} diff --git a/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json new file mode 100644 index 0000000..1005b3e --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-case=multi-schema-empty_flow.json @@ -0,0 +1,129 @@ +[ + { + "attributes": { + "disabled": false, + "name": "provider", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google", + "provider_id": "google" + }, + "id": 1040002, + "text": "Sign up with google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json new file mode 100644 index 0000000..1005b3e --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-empty_flow.json @@ -0,0 +1,129 @@ +[ + { + "attributes": { + "disabled": false, + "name": "provider", + "node_type": "input", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google", + "provider_id": "google" + }, + "id": 1040002, + "text": "Sign up with google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-return_to_profile.json b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-return_to_profile.json new file mode 100644 index 0000000..b5f8d19 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-return_to_profile.json @@ -0,0 +1,154 @@ +[ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with google", + "type": "info", + "context": { + "provider": "google", + "provider_id": "google" + } + } + } + }, + { + "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": "traits.email", + "type": "text", + "value": "browser-1-1@example.org", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "text", + "value": "string", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "number", + "value": 1, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.booly", + "type": "checkbox", + "value": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "number", + "value": 1000000, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "text", + "value": "1111111111111111111111111111111111111111111111111111111111", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials.json b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials.json new file mode 100644 index 0000000..ffe50a1 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials.json @@ -0,0 +1,252 @@ +[ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with google", + "type": "info", + "context": { + "provider": "google", + "provider_id": "google" + } + } + } + }, + { + "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": "traits.email", + "type": "hidden", + "value": "browser-1@example.org", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "hidden", + "value": "string", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "hidden", + "value": 1, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.booly", + "type": "hidden", + "value": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "hidden", + "value": 1000000, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "hidden", + "value": "1111111111111111111111111111111111111111111111111111111111", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040008, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials_again.json b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials_again.json new file mode 100644 index 0000000..098a914 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestTwoStepRegistration-initial_form_is_populated_with_identity_traits-type=browser-select_credentials_again.json @@ -0,0 +1,247 @@ +[ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with google", + "type": "info", + "context": { + "provider": "google", + "provider_id": "google" + } + } + } + }, + { + "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": "traits.email", + "type": "hidden", + "value": "browser-1-1@example.org", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.stringy", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.numby", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.booly", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_big_number", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.should_long_string", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register_trigger", + "type": "button", + "disabled": false, + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_register", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_create_data", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040006, + "text": "Send sign up code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "new-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": 1040001, + "text": "Sign up", + "type": "info" + } + } + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "screen", + "type": "submit", + "value": "previous", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040008, + "text": "Back", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/profile/nodes.go b/selfservice/strategy/profile/nodes.go new file mode 100644 index 0000000..766e255 --- /dev/null +++ b/selfservice/strategy/profile/nodes.go @@ -0,0 +1,27 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func nodePreviousScreen() *node.Node { + return node.NewInputField( + "screen", + "previous", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack()) +} + +func nodeSubmitProfile() *node.Node { + return node.NewInputField( + "method", + "profile", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistration()) +} diff --git a/selfservice/strategy/profile/registration.go b/selfservice/strategy/profile/registration.go new file mode 100644 index 0000000..259f0ad --- /dev/null +++ b/selfservice/strategy/profile/registration.go @@ -0,0 +1,341 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "net/url" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "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/x/decoderx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" +) + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +// The RegistrationScreen +// swagger:enum RegistrationScreen +type RegistrationScreen string + +const ( + //nolint:gosec // not a credential + RegistrationScreenCredentialSelection RegistrationScreen = "credential-selection" + RegistrationScreenPrevious RegistrationScreen = "previous" +) + +var _ registration.Strategy = new(Strategy) +var _ registration.FormHydrator = new(Strategy) + +// 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) ID() identity.CredentialsType { + return identity.CredentialsTypeProfile +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + f.UI.Nodes.Append(nodePreviousScreen()) + f.UI.Nodes.RemoveMatching(nodeSubmitProfile()) + + for _, n := range f.UI.Nodes { + if n.Group != node.DefaultGroup || n.Type != node.Input { + continue + } + if attr, ok := n.Attributes.(*node.InputAttributes); ok { + attr.Type = node.InputAttributeTypeHidden + } + } + + f.UI.Messages.Add(text.NewInfoSelfServiceChooseCredentials()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + conf := new(registration.FormHydratorOptions) + for _, o := range options { + o(conf) + } + + f.UI.Nodes.RemoveMatching(nodePreviousScreen()) + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + for _, n := range nodes { + f.UI.Nodes.Upsert(n) + } + + if len(conf.WithTraits) > 0 { + f.UI.UpdateNodeValuesFromJSON(conf.WithTraits, "traits", node.DefaultGroup) + } + + f.UI.Nodes.Append(nodeSubmitProfile()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + + for _, n := range nodes { + f.UI.SetNode(n) + } + + return nil +} + +func (s *Strategy) decode(p *updateRegistrationFlowWithProfileMethod, r *http.Request, ds *url.URL) error { + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(registrationSchema) + if err != nil { + return errors.WithStack(err) + } + + if err := decoderx.Decode(r, p, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return err + } + + if p.Method != "profile" && p.Method != "profile:back" && len(p.Screen) == 0 { + return errors.WithStack(flow.ErrStrategyNotResponsible) + } + + return registration.DecodeBody(p, r, registrationSchema, ds) +} + +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) { + return flow.ErrStrategyNotResponsible + } + + var params updateRegistrationFlowWithProfileMethod + + ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + if err = s.decode(¶ms, r, ds); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + if params.Method == "profile" || len(params.Screen) > 0 { + switch params.Screen { + case RegistrationScreenCredentialSelection: + return s.showCredentialsSelection(ctx, w, r, regFlow, i, params) + case RegistrationScreenPrevious: + return s.returnToProfileForm(ctx, w, r, regFlow, params) + default: + // FIXME In this scenario we are on the first step of the registration flow and the user clicked on "continue". + // FIXME The appropriate solution would be to also have `screen=credential-selection` available, but that + // FIXME is not the case right now. So instead, we fall back. + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:missing_screen_parameter")) + return s.showCredentialsSelection(ctx, w, r, regFlow, i, params) + } + } else if params.Method == "profile:back" { + // "profile:back" is kept for backwards compatibility. + // FIXME remove this at some point. + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:back")) + return s.returnToProfileForm(ctx, w, r, regFlow, params) + } + + return flow.ErrStrategyNotResponsible +} + +func (s *Strategy) returnToProfileForm(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod) error { + regFlow.UI.ResetMessages() + regFlow.OrganizationID = uuid.NullUUID{} + regFlow.UI.UpdateNodeValuesFromJSON(params.Traits, "traits", node.DefaultGroup) + + for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) { + populator, ok := ls.(registration.FormHydrator) + if !ok { + continue + } + + if err := populator.PopulateRegistrationMethodProfile(r, regFlow, registration.WithTraits(params.Traits)); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + } + + ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + 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) showCredentialsSelection(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) + } + + var didPopulate bool + for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) { + populator, ok := ls.(registration.FormHydrator) + if !ok { + continue + } + + if err := populator.PopulateRegistrationMethodCredentials(r, regFlow, registration.WithTraits([]byte(i.Traits))); errors.Is(err, registration.ErrBreakRegistrationPopulate) { + didPopulate = true + break + } else if err != nil { + return s.handleRegistrationError(r, regFlow, params, 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 s.handleRegistrationError(r, regFlow, params, errors.WithStack(schema.NewNoRegistrationStrategyResponsible())) + } + + regFlow.UI.UpdateNodeValuesFromJSON(json.RawMessage(i.Traits), "traits", node.DefaultGroup) + + ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + 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.DefaultGroup, 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()) + } + + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + return err +} diff --git a/selfservice/strategy/profile/registration_test.go b/selfservice/strategy/profile/registration_test.go new file mode 100644 index 0000000..54ebf2e --- /dev/null +++ b/selfservice/strategy/profile/registration_test.go @@ -0,0 +1,355 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/assertx" + "github.com/ory/x/contextx" + "github.com/ory/x/snapshotx" +) + +func TestTwoStepRegistration(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true) + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{ + { + ID: "google", + Provider: "google", + ClientID: "1234", + ClientSecret: "1234", + }, + }}) + + conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) + conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "localhost") + conf.MustSet(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost") + conf.MustSet(ctx, config.ViperKeyWebAuthnRPID, "localhost") + conf.MustSet(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true) + + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first") + + _ = testhelpers.NewErrorTestServer(t, reg) + publicTS, _ := testhelpers.NewKratosServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + + t.Run("initial form is populated with identity traits", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + + t.Run("empty_flow", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths( + "1.attributes.value", + "8.attributes.nonce", + "8.attributes.src", + "10.attributes.value", + )) + }) + + t.Run("case=multi-schema-empty_flow", func(t *testing.T) { + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"}, + {ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + }) + + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default")) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths( + "1.attributes.value", + "8.attributes.nonce", + "8.attributes.src", + "10.attributes.value", + )) + }) + + t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) { + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"}, + {ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + }) + + body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) { + v.Set("traits.email", "invalidemail") + v.Set("traits.booly", "true") + v.Set("traits.numby", "1") + v.Set("traits.stringy", "string") + v.Set("traits.should_big_number", "1000000") + v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111") + + v.Set("method", "profile") + }, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default")) + + fmt.Println(body) + + require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body) + require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body) + }) + + t.Run("select_credentials", func(t *testing.T) { + res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) { + v.Set("traits.email", "browser-1@example.org") + v.Set("traits.booly", "true") + v.Set("traits.numby", "1") + v.Set("traits.stringy", "string") + v.Set("traits.should_big_number", "1000000") + v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111") + + v.Set("method", "profile") + }, false, http.StatusOK, ui.URL) + snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths( + "1.attributes.value", + "8.attributes.nonce", + "8.attributes.src", + "11.attributes.value", + )) + }) + + t.Run("return_to_profile", func(t *testing.T) { + res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) { + v.Set("traits.email", "browser-1-1@example.org") + v.Set("traits.booly", "true") + v.Set("traits.numby", "1") + v.Set("traits.stringy", "string") + v.Set("traits.should_big_number", "1000000") + v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111") + + v.Set("screen", "previous") + }, false, http.StatusOK, ui.URL) + snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths( + "1.attributes.value", + "8.attributes.nonce", + "8.attributes.src", + "10.attributes.value", + )) + }) + + t.Run("select_credentials_again", func(t *testing.T) { + res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) { + v.Set("traits.email", "browser-1-1@example.org") + v.Set("method", "profile") + }, false, http.StatusOK, ui.URL) + snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths( + "1.attributes.value", + "8.attributes.nonce", + "8.attributes.src", + "11.attributes.value", + )) + }) + }) + }) +} + +func TestOneStepRegistration(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/") + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "unified") + + _ = testhelpers.NewErrorTestServer(t, reg) + publicTS, _ := testhelpers.NewKratosServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + + t.Run("initial form is populated with identity traits", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + + t.Run("empty_flow", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + + t.Run("case=multi-schema-empty_flow", func(t *testing.T) { + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"}, + {ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + }) + + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default")) + snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths( + "0.attributes.value", + )) + }) + + t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) { + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + }) + conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"}, + {ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + }) + + body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) { + v.Set("traits.email", "invalidemail") + v.Set("traits.booly", "true") + v.Set("traits.numby", "1") + v.Set("traits.stringy", "string") + v.Set("traits.should_big_number", "1000000") + v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111") + v.Set("password", "password") + + v.Set("method", "password") + }, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default")) + + fmt.Println(body) + + require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body) + require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body) + }) + }) + }) +} + +func TestPopulateRegistrationMethod(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/identity.schema.json") + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeProfile) + require.NoError(t, err) + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + f.ResetNodes("passkey_challenge") + snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src")) + } + + newFlowInternal := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) { + query := "" + if identitySchema != "" { + query = "?identity_schema=" + identitySchema + } + + r := httptest.NewRequest("GET", "/self-service/registration/browser"+query, nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + return newFlowInternal(ctx, t, "") + } + newFlowWithIdentitySchema := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) { + return newFlowInternal(ctx, t, identitySchema) + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(ctx, t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSON(t, snapshots[1], snapshots[3]) + }) + }) + + t.Run("case=multi-schema-method=PopulateRegistrationMethodProfile", func(t *testing.T) { + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + }) + multiSchema := contextx.WithConfigValue(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + multiSchema = contextx.WithConfigValue(multiSchema, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"}, + {ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true}, + }) + + r, f := newFlowWithIdentitySchema(multiSchema, t, "not-default") + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) +} diff --git a/selfservice/strategy/profile/schema.go b/selfservice/strategy/profile/schema.go new file mode 100644 index 0000000..3ea7aa0 --- /dev/null +++ b/selfservice/strategy/profile/schema.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + _ "embed" +) + +//go:embed .schema/settings.schema.json +var settingsSchema []byte diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go new file mode 100644 index 0000000..06ef68a --- /dev/null +++ b/selfservice/strategy/profile/strategy.go @@ -0,0 +1,322 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +var _ settings.Strategy = new(Strategy) + +type ( + strategyDependencies interface { + nosurfx.CSRFProvider + nosurfx.CSRFTokenGeneratorProvider + httpx.WriterProvider + logrusx.Provider + otelx.Provider + + config.Provider + + continuity.ManagementProvider + + x.CookieProvider + + session.HandlerProvider + session.ManagementProvider + + identity.ValidationProvider + identity.ManagementProvider + identity.PrivilegedPoolProvider + + errorx.ManagementProvider + + settings.HookExecutorProvider + settings.ErrorHandlerProvider + settings.FlowPersistenceProvider + settings.StrategyProvider + settings.HooksProvider + + registration.FlowPersistenceProvider + registration.StrategyProvider + + schema.IdentitySchemaProvider + } + Strategy struct{ d strategyDependencies } +) + +func NewStrategy(d strategyDependencies) *Strategy { return &Strategy{d: d} } + +func (s *Strategy) SettingsStrategyID() string { + return settings.StrategyProfile +} + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.profile.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + schemas, err := s.d.IdentityTraitsSchemas(ctx) + if err != nil { + return err + } + + traitsSchema, err := schemas.GetByID(id.SchemaID) + if err != nil { + return err + } + + // Use a schema compiler that disables identifiers. NewCompilerWithURL + // pre-registers the top-level schema via the trusted global loader; the + // `security.disallow_ref_in_identity_schemas` flag controls whether + // `$ref` resolution rejects the `file` scheme. + disallowRefs := s.d.Config().SecurityDisallowRefInIdentitySchemas(ctx) + schemaCompiler, err := schema.NewCompilerWithURL(ctx, traitsSchema.URL.String(), disallowRefs) + if err != nil { + return err + } + nodes, err := container.NodesFromJSONSchema(ctx, node.ProfileGroup, traitsSchema.URL.String(), "", schemaCompiler, disallowRefs) + if err != nil { + return err + } + + for _, n := range nodes { + f.UI.SetNode(n) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.UpdateNodeValuesFromJSON(json.RawMessage(id.Traits), "traits", node.ProfileGroup) + f.UI.Nodes.Append(node.NewInputField("method", "profile", node.ProfileGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave())) + + return nil +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "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(ctx, r, ctxUpdate, p) + } else if err != nil { + return ctxUpdate, s.handleSettingsError(ctx, 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(ctx, ctxUpdate.GetSessionIdentity()) + if err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err) + } + + if err := decoderx.Decode(r, &p, option, + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err) + } + + // Reset after decoding form + p.SetFlowID(ctxUpdate.Flow.ID) + + if err := s.continueFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err) + } + + return ctxUpdate, 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(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return err + } + + if len(p.Traits) == 0 { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Did not receive any value changes.")) + } + + if err := s.hydrateForm(r, ctxUpdate.Flow, p.Traits); err != nil { + return err + } + + options := []identity.ManagerOption{identity.ManagerExposeValidationErrorsForInternalTypeAssertion} + if s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + options = append(options, identity.ManagerAllowWriteProtectedTraits) + } + + 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() + } + return err + } + + ctxUpdate.UpdateIdentity(update) + return nil +} + +// Update Settings Flow with Profile Method +// +// swagger:model updateSettingsFlowWithProfileMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateSettingsFlowWithProfileMethod 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"` + + // 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" form:"transient_payload"` +} + +func (p *updateSettingsFlowWithProfileMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.FlowID) +} + +func (p *updateSettingsFlowWithProfileMethod) SetFlowID(rid uuid.UUID) { + p.FlowID = rid.String() +} + +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) + } + ar.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + return nil +} + +// 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(ctx context.Context, 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(ctx, w, r, + settings.ContinuityKey(s.SettingsStrategyID()), + continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), + settings.ContinuityOptions(p, puc.GetSessionIdentity())...); err != nil { + return err + } + } + + if puc.Flow != nil { + if traits == nil { + if len(p.Traits) >= 0 { + traits = p.Traits + } else { + traits = json.RawMessage(puc.GetSessionIdentity().Traits) + } + } + + if err := s.hydrateForm(r, puc.Flow, traits); err != nil { + return err + } + } + + return err +} + +// newSettingsProfileDecoder returns a decoderx.HTTPDecoderOption with a JSON Schema for type assertion and +// validation. +func (s *Strategy) newSettingsProfileDecoder(ctx context.Context, i *identity.Identity) (decoderx.HTTPDecoderOption, error) { + schemas, err := s.d.IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + ss, err := schemas.GetByID(i.SchemaID) + if err != nil { + return nil, err + } + raw, err := sjson.SetBytes(settingsSchema, + "properties.traits.$ref", ss.URL.String()+"#/properties/traits") + if err != nil { + return nil, errors.WithStack(err) + } + + o, err := decoderx.HTTPRawJSONSchemaCompiler(raw) + if err != nil { + return nil, errors.WithStack(err) + } + + return o, nil +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.ProfileGroup +} + +// SortForHydration sorts the strategies so that the profile strategy is always first. +func SortForHydration(strats registration.Strategies) registration.Strategies { + sorted := make(registration.Strategies, len(strats)) + copy(sorted, strats) + + for i, strat := range sorted { + if strat.ID() == identity.CredentialsTypeProfile { + sorted = append([]registration.Strategy{strat}, append(sorted[:i], sorted[i+1:]...)...) + break + } + } + + return sorted +} diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go new file mode 100644 index 0000000..a9c8b80 --- /dev/null +++ b/selfservice/strategy/profile/strategy_test.go @@ -0,0 +1,684 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "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/webauthn" + + "github.com/ory/kratos/selfservice/strategy/profile" + + "github.com/ory/x/jsonx" + + kratos "github.com/ory/kratos/pkg/httpclient" + + "github.com/ory/kratos/corpx" + + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/httpx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" +) + +func init() { + corpx.RegisterFakes() +} + +func newIdentityWithPassword(email string) *identity.Identity { + return &identity.Identity{ + ID: x.NewUUID(), + Credentials: map[identity.CredentialsType]identity.Credentials{ + "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, + Traits: identity.Traits(`{"email":"` + email + `","stringy":"foobar","booly":false,"numby":2.5,"should_long_string":"asdfasdfasdfasdfasfdasdfasdfasdf","should_big_number":2048}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + State: identity.StateActive, + VerifiableAddresses: []identity.VerifiableAddress{{Value: email, Via: identity.AddressTypeEmail}}, + // TO ADD - RECOVERY EMAIL, + } +} + +func TestStrategyTraits(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/") + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true) + + setPrivilegedTime := func(t *testing.T, duration time.Duration) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, duration.String()) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + }) + } + + setPrivileged := func(t *testing.T) { + setPrivilegedTime(t, time.Minute*10) + } + + setUnprivileged := func(t *testing.T) { + setPrivilegedTime(t, time.Nanosecond) + } + + ui := testhelpers.NewSettingsUIEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + browserIdentity1 := newIdentityWithPassword("john-browser@doe.com") + apiIdentity1 := newIdentityWithPassword("john-api@doe.com") + 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(ctx, t, reg, browserIdentity1) + browserUser1.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, browserIdentity2) + browserUser2.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) + apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity1) + apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity2) + + t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) { + setUnprivileged(t) + + t.Run("type=browser", func(t *testing.T) { + res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) + }) + + t.Run("type=api/spa", func(t *testing.T) { + res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(`{"foo":"bar"}`), "application/json")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + }) + }) + + t.Run("description=should fail to post data if CSRF is invalid/type=browser", func(t *testing.T) { + setUnprivileged(t) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + + actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser1, + url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}}.Encode()) + assert.EqualValues(t, http.StatusOK, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual) + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual) + }) + + t.Run("description=should fail to post data if CSRF is invalid/type=spa", func(t *testing.T) { + setUnprivileged(t) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS) + + actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser1, + testhelpers.EncodeFormAsJSON(t, true, url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}})) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual) + assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual) + }) + + t.Run("description=should not fail because of CSRF token but because of unprivileged/type=api", func(t *testing.T) { + setUnprivileged(t) + + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + + actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser1, `{"traits.booly":true,"method":"profile","csrf_token":"`+nosurfx.FakeCSRFToken+`"}`) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 403 error because the session is unprivileged\n\t%s", actual) + require.Len(t, res.Cookies(), 1) + assert.Equal(t, "ory_kratos_continuity", res.Cookies()[0].Name) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "login session is too old", actual) + }) + + t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) { + setPrivileged(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.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + + req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(`{"traits.booly":true,"method":"profile","csrf_token":"invalid"}`)) + tc.mod(req.Header) + + res, err := apiUser1.Do(req) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + actual := string(ioutilx.MustReadAll(res.Body)) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, actual, tc.exp, "%s", actual) + }) + } + }) + + t.Run("description=hydrate the proper fields", func(t *testing.T) { + setPrivileged(t) + + run := func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) { + assert.NotEmpty(t, payload.Identity) + assert.Equal(t, id.ID.String(), string(payload.Identity.Id)) + assert.JSONEq(t, string(id.Traits), x.MustEncodeJSON(t, payload.Identity.Traits)) + assert.Equal(t, id.SchemaID, payload.Identity.SchemaId) + assert.Equal(t, publicTS.URL+route, payload.RequestUrl) + + actual := jsonx.TestMarshalJSONString(t, payload.Ui) + assert.EqualValues(t, payload.Identity.Traits.(map[string]interface{})["email"], gjson.Get(actual, "nodes.#(attributes.name==traits.email).attributes.value").String()) + assert.NotEmpty(t, gjson.Get(actual, "nodes.#(attributes.name==csrf_token).attributes.value").String(), "csrf token missing") + + testhelpers.SnapshotTExcept(t, payload.Ui, []string{"action", "nodes.0.attributes.value", "nodes.1.attributes.value"}) + } + + t.Run("type=api", func(t *testing.T) { + pr, _, err := testhelpers.NewSDKCustomClient(publicTS, apiUser1).FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute() + require.NoError(t, err) + run(t, apiIdentity1, pr, settings.RouteInitAPIFlow) + }) + + 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) + }) + + t.Run("type=browser", func(t *testing.T) { + res, err := browserUser1.Get(publicTS.URL + settings.RouteInitBrowserFlow) + require.NoError(t, err) + assert.Contains(t, res.Request.URL.String(), ui.URL+"/settings?flow") + + 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() + require.NoError(t, err, "%s", rid) + + run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow) + }) + }) + + expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) + } + + t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, actual string) { + assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual) + assert.Equal(t, "too-short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual) + assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) + assert.Equal(t, "2.5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual) + assert.Equal(t, "length must be >= 25, but got 9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual) + } + + payload := func(v url.Values) { + v.Set("method", "profile") + v.Set("traits.should_long_string", "too-short") + v.Set("traits.stringy", "bazbar") + } + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, apiUser1, payload)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, false, true, browserUser1, payload)) + }) + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + + t.Run("description=should not be able to make requests for another user", func(t *testing.T) { + setUnprivileged(t) + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values)) + 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) { + f := testhelpers.InitializeSettingsFlowViaAPI(t, browserUser1, publicTS) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, testhelpers.EncodeFormAsJSON(t, true, values)) + 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) { + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + + 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, "reason").String(), "initiated by someone else", "%s", actual) + }) + }) + + t.Run("description=should end up at the login endpoint if trying to update protected field without sudo mode", func(t *testing.T) { + run := func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response { + time.Sleep(time.Millisecond) + + values := testhelpers.SDKFormFieldsToURLValues(config.Ui.Nodes) + values.Set("method", "profile") + values.Set("traits.email", "not-john-doe@foo.bar") + res, err := c.PostForm(config.Ui.Action, values) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + return res + } + + t.Run("type=api", func(t *testing.T) { + setUnprivileged(t) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS) + res := run(t, f, true, apiUser1) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + }) + + t.Run("type=sqa", func(t *testing.T) { + setUnprivileged(t) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS) + res := run(t, f, true, browserUser1) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) + }) + + t.Run("type=browser", func(t *testing.T) { + setUnprivileged(t) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + res := run(t, f, false, browserUser1) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) + + t.Run("should update when signed back in", func(t *testing.T) { + setPrivileged(t) + res, err := browserUser1.Get(f.Ui.Action) + require.NoError(t, err) + + body := ioutilx.MustReadAll(res.Body) + defer func() { _ = res.Body.Close() }() + + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) + assert.EqualValues(t, flow.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body) + }) + }) + }) + + t.Run("flow=fail first update", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + assert.Equal(t, "1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) + assert.Equal(t, "must be >= 1200 but found 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) + assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here + } + + payload := func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Set("traits.should_big_number", "1") + } + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, apiUser1, payload)) + }) + + t.Run("type=sqa", func(t *testing.T) { + check(t, expectValidationError(t, false, true, browserUser1, payload)) + }) + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + + t.Run("flow=fail second update", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) + + assert.Equal(t, "short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual) + assert.Equal(t, "length must be >= 25, but got 5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual) + + assert.Equal(t, "this-is-not-a-number", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual) + assert.Equal(t, "expected number, but got string", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).messages.0.text").String(), "%s", actual) + + assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here + } + + payload := func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Del("traits.should_big_number") + v.Set("traits.should_long_string", "short") + v.Set("traits.numby", "this-is-not-a-number") + } + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, apiUser1, payload)) + }) + + t.Run("type=spa", func(t *testing.T) { + check(t, expectValidationError(t, false, true, browserUser1, payload)) + }) + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + + expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, + http.StatusOK, + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) + } + + t.Run("flow=succeed with final request", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual) + + assert.Equal(t, 15.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").Value(), "%s", actual) + assert.Equal(t, 9001.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").Value(), "%s", actual) + assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").Value(), "%s", actual) + } + + payload := func(newEmail string) func(v url.Values) { + return func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Set("traits.email", newEmail) + v.Set("traits.numby", "15") + v.Set("traits.should_big_number", "9001") + v.Set("traits.should_long_string", "this is such a long string, amazing stuff!") + } + } + + 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) { + 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) + }) + }) + + t.Run("flow=try another update with invalid data", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + } + + payload := func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Set("traits.should_long_string", "short") + } + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, apiUser1, payload)) + }) + + t.Run("type=sqa", func(t *testing.T) { + check(t, expectValidationError(t, false, true, browserUser1, payload)) + }) + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, browserUser1, payload)) + }) + }) + + t.Run("description=ensure that hooks are running", func(t *testing.T) { + setPrivileged(t) + + var returned bool + router := http.NewServeMux() + router.HandleFunc("GET /return-ts", func(w http.ResponseWriter, r *http.Request) { + returned = true + }) + rts := httptest.NewServer(router) + t.Cleanup(rts.Close) + + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, rts.URL+"/return-ts") + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", settings.StrategyProfile) + values.Set("traits.should_big_number", "9001") + res, err := browserUser1.PostForm(f.Ui.Action, values) + + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.True(t, returned, "%d - %s", res.StatusCode, body) + }) + + // Update the login endpoint to auto-accept any incoming login request! + _ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf) + + t.Run("description=should send email with verifiable address", func(t *testing.T) { + setPrivileged(t) + + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo:bar@irrelevant.com/") + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil) + }) + + check := func(t *testing.T, actual, newEmail string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual) + + m, err := reg.CourierPersister().LatestQueuedMessage(context.Background()) + require.NoError(t, err) + assert.Contains(t, m.Subject, "Use code") + } + + payload := func(newEmail string) func(v url.Values) { + return func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Set("traits.email", newEmail) + } + } + + t.Run("type=api", func(t *testing.T) { + newEmail := "update-verify-api@mail.com" + actual := expectSuccess(t, true, false, apiUser1, payload(newEmail)) + check(t, actual, newEmail) + }) + + t.Run("type=spa", func(t *testing.T) { + newEmail := "update-verify-browser@mail.com" + actual := expectSuccess(t, false, true, browserUser1, payload(newEmail)) + check(t, actual, newEmail) + }) + + t.Run("type=browser", func(t *testing.T) { + newEmail := "update-verify-browser@mail.com" + actual := expectSuccess(t, false, false, browserUser1, payload(newEmail)) + check(t, actual, newEmail) + }) + }) + + t.Run("description=should update protected field with sudo mode", func(t *testing.T) { + setPrivileged(t) + + check := func(t *testing.T, newEmail string, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual) + assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual) + assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here + } + + payload := func(email string) func(v url.Values) { + return func(v url.Values) { + v.Set("method", settings.StrategyProfile) + v.Set("traits.email", email) + } + } + + t.Run("type=api", func(t *testing.T) { + email := "not-john-doe-api@mail.com" + actual := expectSuccess(t, true, false, apiUser1, payload(email)) + check(t, email, actual) + }) + + t.Run("type=sqa", func(t *testing.T) { + email := "not-john-doe-browser@mail.com" + actual := expectSuccess(t, false, true, browserUser1, payload(email)) + check(t, email, actual) + }) + + t.Run("type=browser", func(t *testing.T) { + email := "not-john-doe-browser@mail.com" + actual := expectSuccess(t, false, false, browserUser1, payload(email)) + check(t, email, actual) + }) + }) +} + +func TestDisabledEndpoint(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, false) + errTS := testhelpers.NewErrorTestServer(t, reg) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + browserIdentity1 := newIdentityWithPassword("john-browser@doe.com") + browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(context.Background(), t, 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) { + t.Skip("GET is not supported here") + }) + + t.Run("method=POST", func(t *testing.T) { + b := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, func(v url.Values) { + v.Set("method", settings.StrategyProfile) + }, + http.StatusOK, + testhelpers.ExpectURL(false, publicTS.URL+settings.RouteSubmitFlow, errTS.URL)) + + assert.Contains(t, string(b), "404") + assert.Contains(t, string(b), "This endpoint was disabled by system administrator") + }) + }) +} + +func TestSortedForHydration(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + // Get a reference to all registration strategies + allStrategies := []registration.Strategy{ + password.NewStrategy(reg), + code.NewStrategy(reg), + oidc.NewStrategy(reg), + code.NewStrategy(reg), + passkey.NewStrategy(reg), + passkey.NewStrategy(reg), + profile.NewStrategy(reg), + webauthn.NewStrategy(reg), + } + + var originalOrder []string + for _, s := range allStrategies { + if s.ID().String() == "profile" { + continue + } + originalOrder = append(originalOrder, s.ID().String()) + } + + var actual []string + for _, s := range profile.SortForHydration(allStrategies) { + actual = append(actual, s.ID().String()) + } + + assert.EqualValues(t, append([]string{"profile"}, originalOrder...), actual) +} diff --git a/selfservice/strategy/profile/stub/identity.schema.json b/selfservice/strategy/profile/stub/identity.schema.json new file mode 100644 index 0000000..6bdf636 --- /dev/null +++ b/selfservice/strategy/profile/stub/identity.schema.json @@ -0,0 +1,43 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + } + } +} diff --git a/selfservice/strategy/saml/login.go b/selfservice/strategy/saml/login.go new file mode 100644 index 0000000..683e591 --- /dev/null +++ b/selfservice/strategy/saml/login.go @@ -0,0 +1,31 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package saml + +import "encoding/json" + +// Update login flow using SAML +// +// swagger:model updateLoginFlowWithSamlMethod +type _ struct { + // The provider to register with + // + // required: true + Provider string `json:"provider"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // Method to use + // + // This field must be set to `saml` when using the saml method. + // + // required: true + Method string `json:"method"` + + // 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/saml/registration.go b/selfservice/strategy/saml/registration.go new file mode 100644 index 0000000..2ab91b4 --- /dev/null +++ b/selfservice/strategy/saml/registration.go @@ -0,0 +1,34 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package saml + +import "encoding/json" + +// Update registration flow using SAML +// +// swagger:model updateRegistrationFlowWithSamlMethod +type _ struct { + // The provider to register with + // + // required: true + Provider string `json:"provider"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // The identity traits + Traits json.RawMessage `json:"traits"` + + // Method to use + // + // This field must be set to `saml` when using the saml method. + // + // required: true + Method string `json:"method"` + + // 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/saml/settings.go b/selfservice/strategy/saml/settings.go new file mode 100644 index 0000000..c3a5354 --- /dev/null +++ b/selfservice/strategy/saml/settings.go @@ -0,0 +1,52 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package saml + +import "encoding/json" + +// Update settings flow using SAML +// +// swagger:model updateSettingsFlowWithSamlMethod +type _ struct { + // Method + // + // Should be set to saml when trying to update a profile. + // + // required: true + Method string `json:"method"` + + // Link this provider + // + // Either this or `unlink` must be set. + // + // type: string + // in: body + Link string `json:"link"` + + // Unlink this provider + // + // Either this or `link` must be set. + // + // type: string + // in: body + Unlink string `json:"unlink"` + + // Flow ID is the flow's ID. + // + // in: query + FlowID string `json:"flow"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // The identity's traits + // + // in: body + Traits json.RawMessage `json:"traits"` + + // 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/totp/.schema/login.schema.json b/selfservice/strategy/totp/.schema/login.schema.json new file mode 100644 index 0000000..fb32f6a --- /dev/null +++ b/selfservice/strategy/totp/.schema/login.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/totp/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "totp_code", + "method" + ], + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "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 new file mode 100644 index 0000000..8767f5a --- /dev/null +++ b/selfservice/strategy/totp/.schema/settings.schema.json @@ -0,0 +1,37 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/totp/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "totp_code": { + "type": "string" + }, + "totp_unlink": { + "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "if": { + "properties": { + "method": { + "const": "totp" + } + } + }, + "then": { + "totp_code": { + "type": "string", + "maxLength": 6, + "minLength": 6 + } + } +} diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json new file mode 100644 index 0000000..afae3de --- /dev/null +++ b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json @@ -0,0 +1,54 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "totp_code", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "totp" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_setup_is_available_when_identity_has_no_totp_yet.json b/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_setup_is_available_when_identity_has_no_totp_yet.json new file mode 100644 index 0000000..29d17cc --- /dev/null +++ b/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_setup_is_available_when_identity_has_no_totp_yet.json @@ -0,0 +1,93 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "height": 256, + "id": "totp_qr", + "node_type": "img", + "width": 256 + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1050005, + "text": "Authenticator app QR code", + "type": "info" + } + }, + "type": "img" + }, + { + "attributes": { + "id": "totp_secret_key", + "node_type": "text", + "text": { + "context": { + }, + "id": 1050006, + "type": "info" + } + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1050017, + "text": "This is your authenticator app secret. Use it if you can not scan the QR code.", + "type": "info" + } + }, + "type": "text" + }, + { + "attributes": { + "disabled": false, + "name": "totp_code", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1070006, + "text": "Verify code", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "totp" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_unlinking_is_available_when_identity_has_totp.json b/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_unlinking_is_available_when_identity_has_totp.json new file mode 100644 index 0000000..d94bc19 --- /dev/null +++ b/selfservice/strategy/totp/.snapshots/TestCompleteSettings-case=device_unlinking_is_available_when_identity_has_totp.json @@ -0,0 +1,35 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "totp_unlink", + "node_type": "input", + "required": true, + "type": "submit", + "value": "true" + }, + "group": "totp", + "messages": [], + "meta": { + "label": { + "id": 1050004, + "text": "Unlink TOTP Authenticator App", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 0000000..a32d8ed --- /dev/null +++ b/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.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": "totp", + "attributes": { + "name": "totp_code", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "totp", + "attributes": { + "name": "method", + "type": "submit", + "value": "totp", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json new file mode 100644 index 0000000..a32d8ed --- /dev/null +++ b/selfservice/strategy/totp/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.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": "totp", + "attributes": { + "name": "totp_code", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010006, + "text": "Authentication code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "totp", + "attributes": { + "name": "method", + "type": "submit", + "value": "totp", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010009, + "text": "Use Authenticator", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go new file mode 100644 index 0000000..9846506 --- /dev/null +++ b/selfservice/strategy/totp/generator.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "bytes" + "context" + "encoding/base64" + "image/png" + + "github.com/pkg/errors" + "github.com/pquerna/otp" + stdtotp "github.com/pquerna/otp/totp" + + "github.com/ory/kratos/driver/config" +) + +// rfc4226 recommends: +// +// The algorithm MUST use a strong shared secret. The length of +// the shared secret MUST be at least 128 bits. This document +// RECOMMENDs a shared secret length of 160 bits. +// +// 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 +}) (*otp.Key, error) { + key, err := stdtotp.Generate(stdtotp.GenerateOpts{ + Issuer: d.Config().TOTPIssuer(ctx), + AccountName: accountName, + SecretSize: secretSize, + Digits: digits, + Period: 30, + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return key, err +} + +func KeyToHTMLImage(key *otp.Key) (string, error) { + var buf bytes.Buffer + img, err := key.Image(256, 256) + if err != nil { + return "", errors.WithStack(err) + } + + if err := png.Encode(&buf, img); err != nil { + return "", errors.WithStack(err) + } + + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} diff --git a/selfservice/strategy/totp/generator_test.go b/selfservice/strategy/totp/generator_test.go new file mode 100644 index 0000000..7524e92 --- /dev/null +++ b/selfservice/strategy/totp/generator_test.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/contextx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/totp" +) + +func TestGenerator(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + t.Run("no issuer set", func(t *testing.T) { + key, err := totp.NewKey(t.Context(), "foo", reg) + require.NoError(t, err) + assert.Equal(t, reg.Config().SelfPublicURL(t.Context()).Hostname(), key.Issuer(), "if issuer is not set explicitly it should be the public URL") + }) + + t.Run("custom issuer set", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyTOTPIssuer, "foobar.com") + + key, err := totp.NewKey(ctx, "foo", reg) + require.NoError(t, err) + assert.Equal(t, "foobar.com", key.Issuer(), "if issuer is set explicitly it should be the correct value") + assert.Equal(t, "foo", key.AccountName()) + }) + + t.Run("generate HTML image", func(t *testing.T) { + key, err := totp.NewKey(t.Context(), "foo", reg) + require.NoError(t, err) + + img, err := totp.KeyToHTMLImage(key) + require.NoError(t, err) + assert.Truef(t, strings.HasPrefix(img, "data:image/png;base64,"), "image is a base64 encoded png: %s", img) + }) +} diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go new file mode 100644 index 0000000..bf50098 --- /dev/null +++ b/selfservice/strategy/totp/login.go @@ -0,0 +1,145 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "encoding/json" + "net/http" + + "github.com/pkg/errors" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "go.opentelemetry.io/otel/attribute" + + "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/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/otelx" +) + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { + // This strategy can only solve AAL2 + if requestedAAL != identity.AuthenticatorAssuranceLevel2 { + return nil + } + + // We have done proper validation before so this should never error. + // Only the identity ID is read below; the identity is re-fetched with its confidential + // fields anyway, so skip expanding anything here. + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing) + if err != nil { + return err + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), sess.IdentityID) + if err != nil { + return err + } + + _, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no TOTP + return nil + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoLoginTOTPLabel())) + sr.UI.GetNodes().Append(node.NewInputField("method", s.ID(), node.TOTPGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginTOTP())) + + return nil +} + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error { + if f != nil { + f.UI.Nodes.ResetNodes("totp_code") + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +// Update Login Flow with TOTP Method +// +// swagger:model updateLoginFlowWithTotpMethod +type updateLoginFlowWithTotpMethod struct { + // Method should be set to "totp" when logging in using the TOTP strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // The TOTP code. + // + // 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(_ 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 + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { + return nil, err + } + + var p updateLoginFlowWithTotpMethod + if err := decoderx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + 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(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + 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())) + } + + var o identity.CredentialsTOTPConfig + if err := json.Unmarshal(c.Config, &o); err != nil { + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("The TOTP credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) + } + + key, err := otp.NewKeyFromURL(o.TOTPURL) + if err != nil { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(err), i.ID)) + } + + if !totp.Validate(p.TOTPCode, key.Secret()) { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewTOTPVerifierWrongError("#/")), i.ID)) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("Could not update flow").WithDebug(err.Error())), i.ID)) + } + + return i, nil +} diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go new file mode 100644 index 0000000..f3cd5fa --- /dev/null +++ b/selfservice/strategy/totp/login_test.go @@ -0,0 +1,650 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp_test + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/pquerna/otp" + stdtotp "github.com/pquerna/otp/totp" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/totp" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" +) + +const totpCodeGJSONQuery = "ui.nodes.#(attributes.name==totp_code)" + +func createIdentityWithoutTOTP(ctx context.Context, t *testing.T, reg driver.Registry) *identity.Identity { + id, _, _ := createIdentity(ctx, t, reg) + delete(id.Credentials, identity.CredentialsTypeTOTP) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, id)) + return id +} + +func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, string, *otp.Key) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + key, err := totp.NewKey(ctx, "foo", reg) + require.NoError(t, err) + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: identifier, + Verified: false, + CreatedAt: time.Now(), + }, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(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.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + Identifiers: []string{i.ID.String()}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`), + }, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, i)) + return i, password, key +} + +// createIdentityWithImportedTOTPViaCreate mimics the admin identity import +// path (`POST /admin/identities` with `credentials.totp`): it builds an +// Identity with TOTP and password credentials populated upfront, leaves +// `Identifiers` empty on the TOTP credential exactly like +// `Handler.importTOTPCredentials` does, and persists it via +// `Manager.Create`. The persister is expected to default the TOTP +// identifier to the persisted identity ID. See +// https://github.com/ory/kratos/issues/4561. +func createIdentityWithImportedTOTPViaCreate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, *otp.Key) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + key, err := totp.NewKey(ctx, "foo", reg) + require.NoError(t, err) + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: identifier, Verified: false, CreatedAt: time.Now()}, + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + identity.CredentialsTypeTOTP: { + // Identifiers intentionally empty; the import path does not + // know the persisted identity ID yet. + Type: identity.CredentialsTypeTOTP, + Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`), + }, + }, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + return i, key +} + +// createIdentityWithImportedTOTPViaUpdate mimics the admin identity import +// path on an existing identity (`PUT /admin/identities/{id}` with +// `credentials.totp`): it first creates an identity with a password +// credential, then attaches a TOTP credential via `Manager.Update` with +// `Identifiers` left empty. The persister is expected to default the TOTP +// identifier to the existing identity ID. See +// https://github.com/ory/kratos/issues/4561. +func createIdentityWithImportedTOTPViaUpdate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, *otp.Key) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + {Value: identifier, Verified: false, CreatedAt: time.Now()}, + }, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + }, + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + key, err := totp.NewKey(ctx, "foo", reg) + require.NoError(t, err) + // Import a TOTP credential via update with no identifier set; the + // persister must default it to the (already persisted) identity ID. + i.Credentials[identity.CredentialsTypeTOTP] = identity.Credentials{ + Type: identity.CredentialsTypeTOTP, + Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`), + } + require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits)) + return i, key +} + +func TestCompleteLogin(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeTOTP, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{redirTS.URL + "/return-to-wherever"}) + + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + t.Run("case=totp payload is set when identity has totp", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(context.Background(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + + t.Run("case=totp payload is not set when identity has no totp", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(context.Background(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + assertx.EqualAsJSON(t, nil, f.Ui.Nodes) + }) + + t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + + t.Run("type=api", func(t *testing.T) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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.NewHTTPClientWithIdentitySessionCookie(ctx, t, 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Ö") + 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.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, 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) + }) + }) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "totp") + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.LoginMakeRequestCtx(t.Context(), t, true, false, f, apiClient, payload) + } + + doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity, returnTo string) (string, *http.Response) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id) + + opts := []testhelpers.InitFlowWithOption{testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)} + if len(returnTo) > 0 { + opts = append(opts, testhelpers.InitFlowWithReturnTo(returnTo)) + } + + f := testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, spa, false, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "totp") + v(values) + return testhelpers.LoginMakeRequestCtx(t.Context(), t, false, spa, f, browserClient, values.Encode()) + } + + checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) { + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts") + } else { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + } + } + + t.Run("case=should fail if code is empty", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set("totp_code", "") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "Property totp_code is missing.", gjson.Get(body, totpCodeGJSONQuery+".messages.0.text").String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id, "") + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id, "") + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if code is invalid", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set("totp_code", "111111") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id, "") + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id, "") + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if code is too long", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set("totp_code", "1111111111") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id, "") + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id, "") + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if TOTP was not set up for identity", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + payload := func(v url.Values) { + v.Set("totp_code", "111111") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationNoTOTPDevice().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + check(t, false, body, res) + }) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, payload, id, "") + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, id, "") + check(t, false, body, res) + }) + }) + + t.Run("case=should pass when TOTP is supplied correctly", func(t *testing.T) { + id, _, key := createIdentity(t.Context(), t, reg) + code, err := stdtotp.GenerateCode(key.Secret(), time.Now()) + require.NoError(t, err) + payload := func(v url.Values) { + v.Set("totp_code", code) + } + + startAt := time.Now() + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + prefix := "session." + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), redirTS.URL+"/return-ts") + prefix = "" + } else { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + } + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String()) + require.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), 2) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, prefix+"authentication_methods.0.method").String(), 2) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time().After(startAt), 2) + assert.EqualValues(t, identity.CredentialsTypeTOTP, gjson.Get(body, prefix+"authentication_methods.1.method").String(), 2) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(startAt), 2) + assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time()), 2) + assert.Equal(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().Unix(), gjson.Get(body, prefix+"authenticated_at").Time().Unix(), "%s", body) + } + + 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 := redirTS.URL + "/return-to-wherever" + 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 := redirTS.URL + "/return-to-wherever" + 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) + }) + }) + + // Regression coverage for https://github.com/ory/kratos/issues/4561. + // When a TOTP credential is imported through the admin identity API, + // AAL2 login must be able to resolve it via FindByCredentialsIdentifier, + // which means an identity_credential_identifiers row keyed by the + // identity ID has to be persisted. Both the create and update entry + // points are covered here. + t.Run("case=imported TOTP via create identity should succeed at AAL2 login", func(t *testing.T) { + id, key := createIdentityWithImportedTOTPViaCreate(t.Context(), t, reg) + + // The persister must have defaulted the TOTP identifier to the + // identity ID, otherwise the login lookup below would fail with + // "You have no TOTP device set up.". + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + assert.Equal(t, []string{actual.ID.String()}, actual.Credentials[identity.CredentialsTypeTOTP].Identifiers) + + code, err := stdtotp.GenerateCode(key.Secret(), time.Now()) + require.NoError(t, err) + payload := func(v url.Values) { + v.Set("totp_code", code) + } + + startAt := time.Now() + check := func(t *testing.T, body string) { + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, "session.authenticator_assurance_level").String()) + // The seeded session records password at AAL1; submitting the + // TOTP code adds a second authentication method. + require.Len(t, gjson.Get(body, "session.authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, "session.authentication_methods.0.method").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypeTOTP, gjson.Get(body, "session.authentication_methods.1.method").String(), "%s", body) + assert.True(t, gjson.Get(body, "session.authentication_methods.1.completed_at").Time().After(startAt), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, body) + }) + }) + + t.Run("case=imported TOTP via update identity should succeed at AAL2 login", func(t *testing.T) { + id, key := createIdentityWithImportedTOTPViaUpdate(t.Context(), t, reg) + + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + assert.Equal(t, []string{actual.ID.String()}, actual.Credentials[identity.CredentialsTypeTOTP].Identifiers) + + code, err := stdtotp.GenerateCode(key.Secret(), time.Now()) + require.NoError(t, err) + payload := func(v url.Values) { + v.Set("totp_code", code) + } + + startAt := time.Now() + check := func(t *testing.T, body string) { + assert.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, "session.authenticator_assurance_level").String()) + // The seeded session records password at AAL1; submitting the + // TOTP code adds a second authentication method. + require.Len(t, gjson.Get(body, "session.authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, "session.authentication_methods.0.method").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypeTOTP, gjson.Get(body, "session.authentication_methods.1.method").String(), "%s", body) + assert.True(t, gjson.Get(body, "session.authentication_methods.1.completed_at").Time().After(startAt), "%s", body) + } + + t.Run("type=api", func(t *testing.T) { + body, res := doAPIFlow(t, payload, id) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, body) + }) + }) + + t.Run("case=should fail because totp can not handle AAL1", func(t *testing.T) { + apiClient := testhelpers.NewDebugClient(t) + f := testhelpers.InitializeLoginFlowViaAPICtx(t.Context(), t, apiClient, publicTS, false) + + update, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), update)) + + req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"totp"}`)) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + res, err := testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(body, "ui.messages.0.text").String()) + }) + + t.Run("case=should pass without csrf if API flow", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + body, res := doAPIFlow(t, func(v url.Values) { + v.Del("csrf_token") + v.Set("totp_code", "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, func(v url.Values) { + v.Del("csrf_token") + v.Set("totp_code", "111111") + }, id, "") + + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "reason").String(), body) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, func(v url.Values) { + v.Del("csrf_token") + v.Set("totp_code", "111111") + }, id, "") + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + }) + }) + + t.Run("case=should pass return_to URL after login", func(t *testing.T) { + id, pwd, _ := createIdentity(t.Context(), t, reg) + + t.Run("type=browser", func(t *testing.T) { + returnTo := redirTS.URL + "/return-to-wherever" + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithReturnTo(returnTo)) + + cred, ok := id.GetCredentials(identity.CredentialsTypePassword) + require.True(t, ok) + values := url.Values{ + "method": {"password"}, "password_identifier": {cred.Identifiers[0]}, + "password": {pwd}, "csrf_token": {nosurfx.FakeCSRFToken}, + }.Encode() + + body, res := testhelpers.LoginMakeRequestCtx(t.Context(), t, false, false, f, browserClient, values) + require.Contains(t, res.Request.URL.Path, "login", "%s", res.Request.URL.String()) + assert.Equal(t, gjson.Get(body, "requested_aal").String(), "aal2", "%s", body) + assert.Equal(t, gjson.Get(body, "return_to").String(), returnTo, "%s", body) + }) + }) +} + +func TestFormHydration(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeTOTP, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeTOTP) + require.NoError(t, err) + fh, ok := s.(login.AAL2FormHydrator) + 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(reg, 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) { + id, _, _ := createIdentity(t.Context(), t, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + r, f := newFlow(t.Context(), t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + r, f := newFlow(t.Context(), t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/totp/nodes.go b/selfservice/strategy/totp/nodes.go new file mode 100644 index 0000000..f7ba6ba --- /dev/null +++ b/selfservice/strategy/totp/nodes.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "github.com/pquerna/otp" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func NewVerifyTOTPNode() *node.Node { + return node.NewInputField(node.TOTPCode, nil, node.TOTPGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeLabelVerifyOTP()) +} + +func NewTOTPImageQRNode(key *otp.Key) (*node.Node, error) { + src, err := KeyToHTMLImage(key) + if err != nil { + return nil, err + } + + return node.NewImageField(node.TOTPQR, src, node.TOTPGroup, node.WithImageAttributes(func(a *node.ImageAttributes) { + a.Height = 256 + a.Width = 256 + })).WithMetaLabel(text.NewInfoSelfServiceSettingsTOTPQRCode()), nil +} + +func NewTOTPSourceURLNode(key *otp.Key) *node.Node { + return node.NewTextField(node.TOTPSecretKey, + text.NewInfoSelfServiceSettingsTOTPSecret(key.Secret()), node.TOTPGroup). + WithMetaLabel(text.NewInfoSelfServiceSettingsTOTPSecretLabel()) +} + +func NewUnlinkTOTPNode() *node.Node { + return node.NewInputField(node.TOTPUnlink, "true", node.TOTPGroup, + node.InputAttributeTypeSubmit, + node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkTOTP()) +} diff --git a/selfservice/strategy/totp/schema.go b/selfservice/strategy/totp/schema.go new file mode 100644 index 0000000..5055700 --- /dev/null +++ b/selfservice/strategy/totp/schema.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + _ "embed" +) + +//go:embed .schema/settings.schema.json +var settingsSchema []byte + +//go:embed .schema/login.schema.json +var loginSchema []byte diff --git a/selfservice/strategy/totp/schema_extension.go b/selfservice/strategy/totp/schema_extension.go new file mode 100644 index 0000000..8cd2dde --- /dev/null +++ b/selfservice/strategy/totp/schema_extension.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "fmt" + "sync" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" +) + +type SchemaExtension struct { + AccountName string + l sync.Mutex +} + +func NewSchemaExtension(fallback string) *SchemaExtension { + return &SchemaExtension{AccountName: fallback} +} + +func (r *SchemaExtension) Run(_ jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { + r.l.Lock() + defer r.l.Unlock() + if s.Credentials.TOTP.AccountName { + r.AccountName = fmt.Sprintf("%s", value) + } + return nil +} + +func (r *SchemaExtension) Finish() error { + return nil +} diff --git a/selfservice/strategy/totp/settings.go b/selfservice/strategy/totp/settings.go new file mode 100644 index 0000000..83a83fd --- /dev/null +++ b/selfservice/strategy/totp/settings.go @@ -0,0 +1,316 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/ory/x/otelx" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/schema" + + "github.com/ory/kratos/text" + + "github.com/ory/kratos/ui/node" + + "github.com/ory/kratos/session" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "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/x" + "github.com/ory/x/decoderx" +) + +const InternalContextKeyURL = "url" + +func (s *Strategy) SettingsStrategyID() string { + return identity.CredentialsTypeTOTP.String() +} + +// Update Settings Flow with TOTP Method +// +// swagger:model updateSettingsFlowWithTotpMethod +type updateSettingsFlowWithTotpMethod struct { + // ValidationTOTP must contain a valid TOTP based on the + ValidationTOTP string `json:"totp_code"` + + // UnlinkTOTP if true will remove the TOTP pairing, + // effectively removing the credential. This can be used + // to set up a new TOTP device. + UnlinkTOTP bool `json:"totp_unlink"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to "totp" when trying to add, update, or remove a totp pairing. + // + // 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" form:"transient_payload"` +} + +func (p *updateSettingsFlowWithTotpMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithTotpMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.totp.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(ctx, 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 p.UnlinkTOTP { + // This is a submit so we need to manually set the type to TOTP + 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 if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + // This does not come from the payload! + p.Flow = ctxUpdate.Flow.ID.String() + if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return ctxUpdate, 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.Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +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(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return err + } + + if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + + hasTOTP, err := s.identityHasTOTP(ctx, ctxUpdate.Session.Identity) + if err != nil { + return err + } + + // We have now two cases: + // + // 1. TOTP should be removed -> we have it already + // 2. TOTP should be added -> we do not have it yet + var i *identity.Identity + if hasTOTP { + i, err = s.continueSettingsFlowRemoveTOTP(ctx, ctxUpdate, p) + } else { + i, err = s.continueSettingsFlowAddTOTP(ctx, ctxUpdate, p) + } + + if err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + return nil +} + +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 the TOTP key in the internal context. This is a code bug and should be reported to https://github.com/ory/kratos/.")) + } + + key, err := otp.NewKeyFromURL(keyURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithTrace(err).WithReasonf("Could not decode TOTP key from the internal context. This is a code bug and should be reported to https://github.com/ory/kratos/.")) + } + + if len(key.Secret()) == 0 { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("TOTP secret is not set. This is a code bug and should be reported to https://github.com/ory/kratos/.")) + } + + if p.ValidationTOTP == "" { + return nil, schema.NewRequiredError("#/totp_code", "totp_code") + } + + if !totp.Validate(p.ValidationTOTP, key.Secret()) { + return nil, schema.NewTOTPVerifierWrongError("#/totp_code") + } + + co, err := json.Marshal(&identity.CredentialsTOTPConfig{TOTPURL: key.URL()}) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode totp options to JSON: %s", err)) + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return nil, err + } + + // We do not really need the identifier, so we add the identity's ID + c := &identity.Credentials{Type: s.ID(), Identifiers: []string{i.ID.String()}, Config: co} + c.Config = co + i.SetCredentials(s.ID(), *c) + + // Remove the TOTP URL from the internal context now that it is set! + ctxUpdate.Flow.InternalContext, err = sjson.DeleteBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyURL)) + if err != nil { + return nil, err + } + + 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(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel2, + }); err != nil { + return nil, err + } + + return i, nil +} + +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(ctx, ctxUpdate.Session.Identity.ID) + if err != nil { + return nil, err + } + + i.DeleteCredentialsType(identity.CredentialsTypeTOTP) + return i, nil +} + +func (s *Strategy) identityHasTOTP(ctx context.Context, id *identity.Identity) (bool, error) { + if len(id.Credentials) == 0 { + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, id, identity.ExpandCredentials); err != nil { + return false, err + } + } + + count, err := s.CountActiveMultiFactorCredentials(ctx, id.Credentials) + if err != nil { + return false, err + } + + return count > 0, nil +} + +func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.totp.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + hasTOTP, err := s.identityHasTOTP(ctx, id) + if err != nil { + return err + } + + // OTP already set up, just add an unlink option + if hasTOTP { + f.UI.Nodes.Upsert(NewUnlinkTOTPNode()) + } else { + e := NewSchemaExtension(id.ID.String()) + _ = s.d.IdentityValidator().ValidateWithRunner(ctx, id, e) + + // No TOTP set up yet, add nodes allowing us to add it. + key, err := NewKey(ctx, e.AccountName, s.d) + if err != nil { + return err + } + + f.InternalContext, err = sjson.SetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyURL), key.URL()) + if err != nil { + return err + } + + qr, err := NewTOTPImageQRNode(key) + if err != nil { + return err + } + + f.UI.Nodes.Upsert(NewTOTPSourceURLNode(key)) + f.UI.Nodes.Upsert(qr) + f.UI.Nodes.Upsert(NewVerifyTOTPNode()) + f.UI.Nodes.Append(node.NewInputField("method", "totp", node.TOTPGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave())) + } + + return nil +} + +func (s *Strategy) handleSettingsError(ctx context.Context, 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(ctx, w, r, settings.ContinuityKey(s.SettingsStrategyID()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), 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/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go new file mode 100644 index 0000000..61379a2 --- /dev/null +++ b/selfservice/strategy/totp/settings_test.go @@ -0,0 +1,425 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp_test + +import ( + _ "embed" + "encoding/json" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/pquerna/otp" + stdtotp "github.com/pquerna/otp/totp" + "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/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/totp" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/assertx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/sqlcon" +) + +func TestCompleteSettings(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)), + configx.WithValues(testhelpers.MethodEnableConfig("profile", false)), + configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeTOTP, true)), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceSettingsRequiredAAL: "aal1", + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m", + }), + ) + + router := httprouterx.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, httprouterx.NewRouterAdminWithPrefix()) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + t.Run("case=device unlinking is available when identity has totp", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + + t.Run("case=device setup is available when identity has no totp yet", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + id.Credentials = nil + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "1.attributes.src", + "2.attributes.text.context.secret", + "2.attributes.text.text", + }) + }) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "totp") + 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.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("method", "totp") + v(values) + return testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + } + + t.Run("case=should pass without csrf if API flow", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + body, res := doAPIFlow(t, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.TOTPCode, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, totpCodeGJSONQuery+".messages.0.text").String(), "%s", body) + }) + + t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlow(t, false, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.TOTPCode, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "reason").String(), body) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.TOTPCode, "111111") + }, id) + + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + }) + }) + + t.Run("type=can not unlink without privileged session", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + id, _, key := createIdentity(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set("totp_unlink", "true") + } + + checkIdentity := func(t *testing.T) { + _, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypeTOTP, id.ID.String()) + require.NoError(t, err) + assert.Equal(t, key.URL(), gjson.GetBytes(cred.Config, "totp_url").String()) + } + + t.Run("type=api", func(t *testing.T) { + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=spa", func(t *testing.T) { + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=browser", func(t *testing.T) { + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), loginTS.URL+"/login-ts") + assertx.EqualAsJSON(t, text.NewInfoLoginReAuth().Text, json.RawMessage(gjson.Get(actual, "ui.messages.0.text").Raw), actual) + checkIdentity(t) + }) + }) + + t.Run("type=can not set up new totp device without privileged session", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + id := createIdentityWithoutTOTP(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set(node.TOTPCode, "111111") + } + + checkIdentity := func(t *testing.T) { + _, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypeTOTP, id.ID.String()) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + } + + t.Run("type=api", func(t *testing.T) { + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=spa", func(t *testing.T) { + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=") + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"}) + checkIdentity(t) + }) + + t.Run("type=browser", func(t *testing.T) { + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), loginTS.URL+"/login-ts") + assertx.EqualAsJSON(t, text.NewInfoLoginReAuth().Text, json.RawMessage(gjson.Get(actual, "ui.messages.0.text").Raw), actual) + checkIdentity(t) + }) + }) + + t.Run("type=unlink TOTP device", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("totp_unlink", "true") + } + + checkIdentity := func(t *testing.T, id *identity.Identity) { + _, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypeTOTP, id.ID.String()) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + } + + t.Run("type=api", func(t *testing.T) { + id, _, _ := createIdentity(t.Context(), t, reg) + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + 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) { + id, _, _ := createIdentity(t.Context(), t, reg) + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + 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) { + id, _, _ := createIdentity(t.Context(), t, reg) + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + 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) + }) + }) + + t.Run("type=set up TOTP device but code is incorrect", func(t *testing.T) { + payload := func(v url.Values) { + v.Set(node.TOTPCode, "111111") + } + + checkIdentity := func(t *testing.T, id *identity.Identity) { + _, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypeTOTP, id.ID.String()) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + } + + t.Run("type=api", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + actual, res := doAPIFlow(t, payload, id) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(actual, totpCodeGJSONQuery+".messages.0.text").String(), actual) + checkIdentity(t, id) + }) + + t.Run("type=spa", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + actual, res := doBrowserFlow(t, true, payload, id) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(actual, totpCodeGJSONQuery+".messages.0.text").String(), actual) + checkIdentity(t, id) + }) + + t.Run("type=browser", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + actual, res := doBrowserFlow(t, false, payload, id) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(actual, totpCodeGJSONQuery+".messages.0.text").String(), actual) + checkIdentity(t, id) + }) + }) + + t.Run("type=set up TOTP device", func(t *testing.T) { + checkIdentity := func(t *testing.T, id *identity.Identity, key string) { + i, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypeTOTP, id.ID.String()) + require.NoError(t, err) + var c identity.CredentialsTOTPConfig + require.NoError(t, json.Unmarshal(cred.Config, &c)) + actual, err := otp.NewKeyFromURL(c.TOTPURL) + require.NoError(t, err) + assert.Equal(t, key, actual.Secret()) + assert.Contains(t, c.TOTPURL, gjson.GetBytes(i.Traits, "subject").String()) + } + + run := func(t *testing.T, isAPI, isSPA bool, id *identity.Identity, hc *http.Client, f *kratos.SettingsFlow) { + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + + key := gjson.GetBytes(nodes, "#(attributes.id==totp_secret_key).attributes.text.context.secret").String() + require.NotEmpty(t, key, nodes) + + code, err := stdtotp.GenerateCode(key, time.Now()) + require.NoError(t, err) + values.Set("method", "totp") + values.Set(node.TOTPCode, code) + + actual, res := testhelpers.SettingsMakeRequest(t, isAPI, isSPA, f, hc, testhelpers.EncodeFormAsJSON(t, isAPI || isSPA, values)) + require.NotEmpty(t, key) + assert.Equal(t, http.StatusOK, res.StatusCode) + + if isAPI || isSPA { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) + } else { + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) + } + + actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeTOTP, totp.InternalContextKeyURL))) + + 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.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) + + run(t, true, false, id, apiClient, f) + }) + + t.Run("type=spa", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, true, publicTS) + + run(t, false, true, id, user, f) + }) + + t.Run("type=browser", func(t *testing.T) { + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, false, publicTS) + + run(t, false, false, id, user, f) + }) + + t.Run("type=refreshes authenticated_at of the enrolling session", func(t *testing.T) { + // Pin the privileged window explicitly so this subtest does not + // depend on sibling tests' config mutations. The session is + // backdated but stays well inside the 5m window, while the + // refreshed timestamp is still observable. + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") + }) + + id := createIdentityWithoutTOTP(t.Context(), t, reg) + + staleAuthenticatedAt := time.Now().Add(-30 * time.Second).UTC() + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + sess, err := testhelpers.NewActiveSession(req, reg, id, staleAuthenticatedAt, + identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + browserClient := testhelpers.NewHTTPClientWithSessionCookie(t.Context(), t, reg, sess) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, false, publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + key := gjson.GetBytes(nodes, "#(attributes.id==totp_secret_key).attributes.text.context.secret").String() + require.NotEmpty(t, key, "%s", nodes) + code, err := stdtotp.GenerateCode(key, time.Now()) + require.NoError(t, err) + values.Set("method", "totp") + values.Set(node.TOTPCode, code) + + _, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserClient, testhelpers.EncodeFormAsJSON(t, false, values)) + require.Equal(t, http.StatusOK, res.StatusCode) + + actual, err := reg.SessionPersister().GetSession(t.Context(), sess.ID, session.ExpandNothing) + require.NoError(t, err) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, actual.AuthenticatorAssuranceLevel) + assert.True(t, actual.AuthenticatedAt.After(staleAuthenticatedAt.Add(10*time.Second)), + "authenticated_at must be refreshed by TOTP enrollment; got %s, seeded %s", actual.AuthenticatedAt, staleAuthenticatedAt) + }) + }) +} diff --git a/selfservice/strategy/totp/strategy.go b/selfservice/strategy/totp/strategy.go new file mode 100644 index 0000000..cb190a0 --- /dev/null +++ b/selfservice/strategy/totp/strategy.go @@ -0,0 +1,126 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/pkg/errors" + "github.com/pquerna/otp" + + "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/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +var ( + _ login.Strategy = (*Strategy)(nil) + _ login.AAL2FormHydrator = (*Strategy)(nil) + _ settings.Strategy = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + otelx.Provider + + config.Provider + + continuity.ManagementProvider + + x.CookieProvider + + 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 + + session.HandlerProvider + session.ManagementProvider + session.PersistenceProvider +} + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +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 + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + _, err := otp.NewKeyFromURL(conf.TOTPURL) + if len(conf.TOTPURL) > 0 && err == nil { + count++ + } + } + } + return +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeTOTP +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.TOTPGroup +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel2, + } +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error { + return s.PopulateLoginMethod(r, identity.AuthenticatorAssuranceLevel2, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateLoginMethod(r, identity.AuthenticatorAssuranceLevel2, f) +} diff --git a/selfservice/strategy/totp/strategy_test.go b/selfservice/strategy/totp/strategy_test.go new file mode 100644 index 0000000..801dd58 --- /dev/null +++ b/selfservice/strategy/totp/strategy_test.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package totp_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/totp" +) + +func TestCountActiveCredentials(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := totp.NewStrategy(reg) + + key, err := totp.NewKey(t.Context(), "foo", reg) + require.NoError(t, err) + + t.Run("first factor", func(t *testing.T) { + actual, err := strategy.CountActiveFirstFactorCredentials(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, 0, actual) + }) + + t.Run("multi factor", func(t *testing.T) { + 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(`{"totp_url": ""}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"totp_url": "` + key.URL() + `"}`), + }}, + expected: 1, + }, + { + 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) { + cc := map[identity.CredentialsType]identity.Credentials{} + for _, c := range tc.in { + cc[c.Type] = c + } + + actual, err := strategy.CountActiveMultiFactorCredentials(t.Context(), cc) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } + }) +} diff --git a/selfservice/strategy/totp/stub/login.schema.json b/selfservice/strategy/totp/stub/login.schema.json new file mode 100644 index 0000000..82f8581 --- /dev/null +++ b/selfservice/strategy/totp/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/strategy/totp/stub/settings.schema.json b/selfservice/strategy/totp/stub/settings.schema.json new file mode 100644 index 0000000..7b381a3 --- /dev/null +++ b/selfservice/strategy/totp/stub/settings.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "totp": { + "account_name": true + } + } + } + } + } + } + } +} diff --git a/selfservice/strategy/webauthn/.schema/login.schema.json b/selfservice/strategy/webauthn/.schema/login.schema.json new file mode 100644 index 0000000..3029101 --- /dev/null +++ b/selfservice/strategy/webauthn/.schema/login.schema.json @@ -0,0 +1,54 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/webauthn/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "webauthn_login": { + "type": "string" + }, + "method": { + "type": "string" + }, + "identifier": { + "type": "string", + "minLength": 1 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "if": { + "anyOf": [ + { + "properties": { + "method": { + "const": "webauthn" + } + }, + "required": [ + "method" + ] + }, + { + "properties": { + "webauthn_login": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "webauthn_login" + ] + } + ] + }, + "then": { + "required": [ + "identifier" + ] + } +} diff --git a/selfservice/strategy/webauthn/.schema/registration.schema.json b/selfservice/strategy/webauthn/.schema/registration.schema.json new file mode 100644 index 0000000..2751e94 --- /dev/null +++ b/selfservice/strategy/webauthn/.schema/registration.schema.json @@ -0,0 +1,42 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/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" + }, + "webauthn_register": { + "type": "string" + }, + "webauthn_register_displayname": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "oneOf": [ + { + "required": [ + "webauthn_register" + ] + }, + { + "method": { + "const": "webauthn" + }, + "required": [ + "identifier", + "method" + ] + } + ] +} diff --git a/selfservice/strategy/webauthn/.schema/settings.schema.json b/selfservice/strategy/webauthn/.schema/settings.schema.json new file mode 100644 index 0000000..f89ec98 --- /dev/null +++ b/selfservice/strategy/webauthn/.schema/settings.schema.json @@ -0,0 +1,47 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/webauthn/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "webauthn_register": { + "type": "string" + }, + "webauthn_register_displayname": { + "type": "string" + }, + "webauthn_remove": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + }, + "if": { + "properties": { + "webauthn_register": { + "minLength": 1 + } + }, + "required": [ + "webauthn_register" + ] + }, + "then": { + "properties": { + "webauthn_register_displayname": { + "minLength": 1 + } + }, + "required": [ + "webauthn_register_displayname" + ] + } +} + diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=can_not_use_security_key_for_passwordless_in_mfa_flow.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=can_not_use_security_key_for_passwordless_in_mfa_flow.json new file mode 100644 index 0000000..7472fbd --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=can_not_use_security_key_for_passwordless_in_mfa_flow.json @@ -0,0 +1,7 @@ +[ + { + "id": 4000043, + "text": "The provided web authn login is invalid, please try again.", + "type": "error" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_not_set_when_identity_has_no_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_not_set_when_identity_has_no_webauthn.json new file mode 100644 index 0000000..d601ae1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_not_set_when_identity_has_no_webauthn.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=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 new file mode 100644 index 0000000..e08248c --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -0,0 +1,74 @@ +[ + { + "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" + }, + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 new file mode 100644 index 0000000..408d417 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -0,0 +1,88 @@ +{ + "organization_id": null, + "type": "browser", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "required": true, + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "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": { + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } + ], + "messages": [ + { + "text": "Prepare your WebAuthn device (e.g. security key, biometrics scanner, ...) and press continue.", + "type": "info" + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} 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 new file mode 100644 index 0000000..408d417 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -0,0 +1,88 @@ +{ + "organization_id": null, + "type": "browser", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "required": true, + "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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "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": { + "text": "Continue", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } + ], + "messages": [ + { + "text": "Prepare your WebAuthn device (e.g. security key, biometrics scanner, ...) and press continue.", + "type": "info" + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} 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 new file mode 100644 index 0000000..f38fb92 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json @@ -0,0 +1,55 @@ +[ + { + "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": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "webauthn" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with a hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=false-browser.json new file mode 100644 index 0000000..d601ae1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=false-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=no_webauth_credentials-passwordless=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=false-spa.json new file mode 100644 index 0000000..d601ae1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=false-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=no_webauth_credentials-passwordless=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=true-browser.json new file mode 100644 index 0000000..d601ae1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=true-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=no_webauth_credentials-passwordless=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=true-spa.json new file mode 100644 index 0000000..d601ae1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=no_webauth_credentials-passwordless=true-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=false-case=mfa_v0_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json new file mode 100644 index 0000000..9d9a0f7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json new file mode 100644 index 0000000..9d9a0f7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json new file mode 100644 index 0000000..9d9a0f7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json new file mode 100644 index 0000000..9d9a0f7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 0000000..d601ae1 --- /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 0000000..d601ae1 --- /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 0000000..d601ae1 --- /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 0000000..d601ae1 --- /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 0000000..d601ae1 --- /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 0000000..d601ae1 --- /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 0000000..9d9a0f7 --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 0000000..9d9a0f7 --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 new file mode 100644 index 0000000..5da84d4 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -0,0 +1,129 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_remove", + "node_type": "input", + "type": "submit", + "value": "626172626172" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "bar" + }, + "id": 1050018, + "text": "Remove security key \"bar\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_remove", + "node_type": "input", + "type": "submit", + "value": "666f6f666f6f" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + }, + "id": 1050018, + "text": "Remove security key \"foo\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_displayname", + "node_type": "input", + "type": "text", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] 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 new file mode 100644 index 0000000..2fdeed6 --- /dev/null +++ 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 @@ -0,0 +1,33 @@ +{ + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [ + { + "id": 4000001, + "text": "unable to remove this security key because it would lock you out of your account", + "type": "error", + "context": { + "reason": "unable to remove this security key because it would lock you out of your account" + } + } + ], + "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 new file mode 100644 index 0000000..9bd36e7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json @@ -0,0 +1,11 @@ +{ + "webauthn_register": [ + "" + ], + "webauthn_register_displayname": [ + "" + ], + "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 new file mode 100644 index 0000000..2fdeed6 --- /dev/null +++ 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 @@ -0,0 +1,33 @@ +{ + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [ + { + "id": 4000001, + "text": "unable to remove this security key because it would lock you out of your account", + "type": "error", + "context": { + "reason": "unable to remove this security key because it would lock you out of your account" + } + } + ], + "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 new file mode 100644 index 0000000..9bd36e7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json @@ -0,0 +1,11 @@ +{ + "webauthn_register": [ + "" + ], + "webauthn_register_displayname": [ + "" + ], + "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 new file mode 100644 index 0000000..216ace1 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -0,0 +1,81 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_displayname", + "node_type": "input", + "type": "text", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] 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 new file mode 100644 index 0000000..9bd36e7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json @@ -0,0 +1,11 @@ +{ + "webauthn_register": [ + "" + ], + "webauthn_register_displayname": [ + "" + ], + "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 new file mode 100644 index 0000000..9bd36e7 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json @@ -0,0 +1,11 @@ +{ + "webauthn_register": [ + "" + ], + "webauthn_register_displayname": [ + "" + ], + "webauthn_remove": [ + "666f6f666f6f" + ] +} diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json new file mode 100644 index 0000000..d321084 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-case=Multi-Schema-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 a hardware key", + "type": "info" + } + } + } +] 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 0000000..fe51488 --- /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 0000000..d321084 --- /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 a 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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..ca78a76 --- /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 a 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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..ca78a76 --- /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 a 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 0000000..fe51488 --- /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 0000000..ca78a76 --- /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 a 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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..ca78a76 --- /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 a 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 0000000..fe51488 --- /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 0000000..fe51488 --- /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 0000000..f6490bc --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 0000000..fe51488 --- /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 0000000..f6490bc --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 0000000..fe51488 --- /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 0000000..f6490bc --- /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-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "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 a 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 0000000..fe51488 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json new file mode 100644 index 0000000..d33b69d --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethod.json @@ -0,0 +1,81 @@ +[ + { + "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": "webauthn_register_displayname", + "type": "text", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json new file mode 100644 index 0000000..d33b69d --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodCredentials.json @@ -0,0 +1,81 @@ +[ + { + "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": "webauthn_register_displayname", + "type": "text", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=PopulateRegistrationMethodProfile.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/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=1.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/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json new file mode 100644 index 0000000..d33b69d --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=2.json @@ -0,0 +1,81 @@ +[ + { + "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": "webauthn_register_displayname", + "type": "text", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.json new file mode 100644 index 0000000..364b8ab --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=3.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/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json new file mode 100644 index 0000000..d33b69d --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestPopulateRegistrationMethod-method=idempotency-case=4.json @@ -0,0 +1,81 @@ +[ + { + "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": "webauthn_register_displayname", + "type": "text", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnRegistration", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + } + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_register", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_does_not_exist_when_passwordless_is_disabled-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_does_not_exist_when_passwordless_is_disabled-browser.json new file mode 100644 index 0000000..f445826 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_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": "default", + "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": "default", + "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/webauthn/.snapshots/TestRegistration-case=webauthn_button_does_not_exist_when_passwordless_is_disabled-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_does_not_exist_when_passwordless_is_disabled-spa.json new file mode 100644 index 0000000..f445826 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_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": "default", + "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": "default", + "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/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json new file mode 100644 index 0000000..5fab38a --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -0,0 +1,146 @@ +[ + { + "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": "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": "webauthn_register_displayname", + "node_type": "input", + "type": "text", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json new file mode 100644 index 0000000..5fab38a --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -0,0 +1,146 @@ +[ + { + "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": "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": "webauthn_register_displayname", + "node_type": "input", + "type": "text", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1050013, + "text": "Name of the security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_register_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1040004, + "text": "Sign up with security key", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "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/webauthn/fixtures/login/success/mfa/identity.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/identity.json new file mode 100644 index 0000000..2a6ad69 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/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/webauthn/fixtures/login/success/mfa/response.invalid.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/response.invalid.json new file mode 100644 index 0000000..a5253c2 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/response.invalid.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": "AAaaaaaaTD6lazfHs42XPg" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/credentials.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/credentials.json new file mode 100644 index 0000000..19dd817 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/credentials.json @@ -0,0 +1,16 @@ +{ + "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" + } + ] +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/internal_context.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/internal_context.json new file mode 100644 index 0000000..7a4c59d --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/internal_context.json @@ -0,0 +1,10 @@ +{ + "webauthn_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "9dG2o6S7RPeRYfT4d+/prQ==", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/response.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/response.json new file mode 100644 index 0000000..cb30f3a --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v0/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": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/credentials.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/credentials.json new file mode 100644 index 0000000..68087d5 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/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": false + } + ], + "user_handle": "9dG2o6S7RPeRYfT4d+/prQ==" +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/internal_context.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/internal_context.json new file mode 100644 index 0000000..7a4c59d --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/internal_context.json @@ -0,0 +1,10 @@ +{ + "webauthn_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "9dG2o6S7RPeRYfT4d+/prQ==", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/response.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/response.json new file mode 100644 index 0000000..cb30f3a --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1/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": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/credentials.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/credentials.json new file mode 100644 index 0000000..352d21e --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/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": false + } + ], + "user_handle": "RPwiyauuTD6lazfHs42XPg==" +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/internal_context.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/internal_context.json new file mode 100644 index 0000000..170cbb2 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/internal_context.json @@ -0,0 +1,10 @@ +{ + "webauthn_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "RPwiyauuTD6lazfHs42XPg==", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/response.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/response.json new file mode 100644 index 0000000..ce1ad4d --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_handle/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": "RPwiyauuTD6lazfHs42XPg" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/credentials.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/credentials.json new file mode 100644 index 0000000..b975759 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/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": "9dG2o6S7RPeRYfT4d+/prQ==" +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/internal_context.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/internal_context.json new file mode 100644 index 0000000..7a4c59d --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/internal_context.json @@ -0,0 +1,10 @@ +{ + "webauthn_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "9dG2o6S7RPeRYfT4d+/prQ==", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/response.json b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/response.json new file mode 100644 index 0000000..cb30f3a --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/login/success/mfa/v1_passwordless/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": "" + } +} 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 0000000..1f4913b --- /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 0000000..6eaa968 --- /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/fixtures/registration/success/identity.json b/selfservice/strategy/webauthn/fixtures/registration/success/identity.json new file mode 100644 index 0000000..5aa6d11 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/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/webauthn/fixtures/registration/success/internal_context.json b/selfservice/strategy/webauthn/fixtures/registration/success/internal_context.json new file mode 100644 index 0000000..b986cda --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/success/internal_context.json @@ -0,0 +1,8 @@ +{ + "totp_url": "otpauth://totp/issuer.ory.sh:6e11a9a7-62fd-4c88-871a-097f18f0306f?algorithm=SHA1&digits=6&issuer=issuer.ory.sh&period=30&secret=2F43HRJNMUW67EDMRR7AKQYRZP3AI6IG", + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/registration/success/response.json b/selfservice/strategy/webauthn/fixtures/registration/success/response.json new file mode 100644 index 0000000..7d2b381 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/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/webauthn/fixtures/settings/success/identity.json b/selfservice/strategy/webauthn/fixtures/settings/success/identity.json new file mode 100644 index 0000000..5aa6d11 --- /dev/null +++ b/selfservice/strategy/webauthn/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/webauthn/fixtures/settings/success/internal_context.json b/selfservice/strategy/webauthn/fixtures/settings/success/internal_context.json new file mode 100644 index 0000000..b986cda --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/settings/success/internal_context.json @@ -0,0 +1,8 @@ +{ + "totp_url": "otpauth://totp/issuer.ory.sh:6e11a9a7-62fd-4c88-871a-097f18f0306f?algorithm=SHA1&digits=6&issuer=issuer.ory.sh&period=30&secret=2F43HRJNMUW67EDMRR7AKQYRZP3AI6IG", + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/settings/success/response.json b/selfservice/strategy/webauthn/fixtures/settings/success/response.json new file mode 100644 index 0000000..7d2b381 --- /dev/null +++ b/selfservice/strategy/webauthn/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/webauthn/login.go b/selfservice/strategy/webauthn/login.go new file mode 100644 index 0000000..92de22b --- /dev/null +++ b/selfservice/strategy/webauthn/login.go @@ -0,0 +1,444 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +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" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/pkg/errors" + + "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/x/decoderx" +) + +var ( + _ login.AAL1FormHydrator = new(Strategy) + _ login.AAL2FormHydrator = new(Strategy) +) + +func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error { + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) + return nil +} + +func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *identity.Identity, label *text.Message, aal identity.AuthenticatorAssuranceLevel) error { + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), i.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no webauth + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthnFiltered(aal, nil) + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return webauthnx.ErrNoCredentials + } + + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to initiate WebAuth.").WithDebug(err.Error())) + } + + 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())) + } + + // Remove the WebAuthn URL from the internal context now that it is set! + sr.InternalContext, err = sjson.SetBytes(sr.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(options) + if err != nil { + return errors.WithStack(err) + } + + if len(cred.Identifiers) > 0 { + sr.UI.SetNode(node.NewInputField("identifier", cred.Identifiers[0], node.DefaultGroup, node.InputAttributeTypeHidden)) + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + 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(webauthnx.NewWebAuthnLoginInput()) + + return nil +} + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error { + if f != nil { + f.UI.Nodes.ResetNodes("webauthn_login") + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +// Update Login Flow with WebAuthn Method +// +// swagger:model updateLoginFlowWithWebAuthnMethod +type updateLoginFlowWithWebAuthnMethod struct { + // Identifier is the email or username of the user trying to log in. + // + // required: true + Identifier string `json:"identifier"` + + // Method should be set to "webAuthn" when logging in using the WebAuthn 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:"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 := decoderx.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(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) + } + + if s.d.Config().WebAuthnForPasswordless(ctx) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { + return s.loginPasswordless(ctx, w, r, f, &p) + } + + return s.loginMultiFactor(ctx, r, f, sess.IdentityID, &p) +} + +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(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if p.Identifier == "" { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest().WithReason("identifier is required"))) + } + + i, _, err = s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), p.Identifier) + if err != nil { + 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())) + } + + if len(p.Login) == 0 { + // Reset all nodes to not confuse users. + // This is kinda hacky and will probably need to be updated at some point. + previousNodes := f.UI.Nodes + f.UI.Nodes = node.Nodes{} + + 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 { + return nil, s.handleLoginError(r, f, err) + } + + // Adds the "Continue" button + 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(ctx, 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, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } + + return nil, errors.WithStack(flow.ErrCompletedByStrategy) + } + + return s.loginAuthenticate(ctx, r, f, i.ID, p, identity.AuthenticatorAssuranceLevel1) +} + +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())) + } + + c, ok := i.GetCredentials(s.ID()) + 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))) + } + + 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()))) + } + + 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))) + } + + webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal, &webAuthnResponse.Response.AuthenticatorData.Flags) + if f.IsRefresh() { + webAuthCreds = o.Credentials.ToWebAuthn() + } + + validatedCredential, err := web.ValidateLogin(webauthnx.NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) + } + + // Persist the updated signature counter and clone warning for W3C WebAuthn + // clone detection. This must not block the login: the user already proved + // possession of the authenticator. + webauthnx.UpdateSignCountAfterLogin(ctx, s.d, i, identity.CredentialsTypeWebAuthn, validatedCredential) + + // 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) 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(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, _ *session.Session) 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 := sr.IdentitySchema.URL(r.Context(), s.d.Config()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context())) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { + attributes.Autocomplete = node.InputAttributeAutocompleteUsernameWebauthn + }, + ).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. + // The full expansion is required: the form hydrator reads the identity's credentials. + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + 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_signcount_test.go b/selfservice/strategy/webauthn/login_signcount_test.go new file mode 100644 index 0000000..52a74de --- /dev/null +++ b/selfservice/strategy/webauthn/login_signcount_test.go @@ -0,0 +1,137 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn_test + +import ( + "encoding/json" + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/ui/node" +) + +// TestLoginUpdatesSignCount is a regression test for HackerOne #3780149. +// +// The go-webauthn library returns a *webauthn.Credential from ValidateLogin +// whose Authenticator.SignCount reflects the signature counter reported by the +// authenticator during the assertion. W3C WebAuthn clone detection relies on +// the relying party persisting this incremented counter so that a replayed or +// cloned authenticator (which reports a stale counter) can be detected. +// +// Before the fix, Kratos discarded the returned credential, so the stored +// SignCount stayed at its registration value forever and clone detection was +// inert. +// +// The MFA v1 fixtures store sign_count = 3 and the assertion response reports +// counter = 10, so a correct implementation persists SignCount = 10. +func TestLoginUpdatesSignCount(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false), + enabledWebauthn, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + + createIdentityWithWebAuthn := func(t *testing.T, c identity.Credentials) *identity.Identity { + var id identity.Identity + require.NoError(t, json.Unmarshal(loginFixtureSuccessIdentity, &id)) + + id.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Identifiers: []string{loginFixtureSuccessEmail}, + Config: c.Config, + Type: identity.CredentialsTypeWebAuthn, + Version: c.Version, + }) + + _ = reg.PrivilegedIdentityPool().DeleteIdentity(t.Context(), id.ID) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &id)) + return &id + } + + 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) { + client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, isSPA, false, false, opts...) + + interim, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode()) + return body, res, f + } + + // Reads the stored first webauthn credential: the outer row (for id and + // created_at) and the decoded authenticator state. + storedCredential := func(t *testing.T, idID uuid.UUID) (*identity.Credentials, *identity.AuthenticatorWebAuthn) { + stored, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), idID) + require.NoError(t, err) + c, ok := stored.GetCredentials(identity.CredentialsTypeWebAuthn) + require.True(t, ok) + var conf identity.CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(c.Config, &conf)) + require.NotEmpty(t, conf.Credentials) + require.NotNil(t, conf.Credentials[0].Authenticator) + return c, conf.Credentials[0].Authenticator + } + + conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + + id := createIdentityWithWebAuthn(t, identity.Credentials{ + Config: loginFixtureSuccessV1Credentials, + Version: 1, + }) + + // Sanity: the registered credential starts at the fixture's sign_count = 3. + credBefore, authBefore := storedCredential(t, id.ID) + require.EqualValues(t, 3, authBefore.SignCount) + + body, _, _ := submitWebAuthnLogin(t, true, id, loginFixtureSuccessV1Context, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1Response)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + // The login must succeed. + require.True(t, gjson.Get(body, "session.active").Bool(), "%s", body) + + // The stored SignCount must be updated to the value reported by the + // authenticator assertion (10), not left at the registration value (3). + credAfter, auth := storedCredential(t, id.ID) + assert.EqualValues(t, 10, auth.SignCount, + "stored SignCount must be updated from the login assertion for clone detection to work") + + // The assertion counter advanced (3 -> 10), so this is not a clone: the + // persisted clone warning must round-trip as false rather than being set + // spuriously on a normal login. + assert.False(t, auth.CloneWarning, + "a normally advancing counter must not set the clone warning") + + // The credential row must be updated in place, not deleted and recreated: + // its id and created_at must be stable across the login. + assert.Equal(t, credBefore.ID, credAfter.ID, + "the credential row must be updated in place (stable id), not recreated") + assert.Equal(t, credBefore.CreatedAt, credAfter.CreatedAt, + "persisting the sign counter must not reset the credential's created_at") +} diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go new file mode 100644 index 0000000..f36fdb0 --- /dev/null +++ b/selfservice/strategy/webauthn/login_test.go @@ -0,0 +1,1029 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/selfservice/strategy/webauthn" + "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/contextx" + "github.com/ory/x/jsonx" + "github.com/ory/x/snapshotx" +) + +var ( + //go:embed fixtures/login/success/mfa/response.invalid.json + loginFixtureSuccessResponseInvalid []byte + //go:embed fixtures/login/success/mfa/identity.json + loginFixtureSuccessIdentity []byte + //go:embed fixtures/login/success/mfa/v0/credentials.json + loginFixtureSuccessV0Credentials []byte + //go:embed fixtures/login/success/mfa/v0/internal_context.json + loginFixtureSuccessV0Context []byte + //go:embed fixtures/login/success/mfa/v0/response.json + loginFixtureSuccessV0Response []byte + //go:embed fixtures/login/success/mfa/v1/credentials.json + loginFixtureSuccessV1Credentials []byte + //go:embed fixtures/login/success/mfa/v1/internal_context.json + loginFixtureSuccessV1Context []byte + //go:embed fixtures/login/success/mfa/v1/response.json + loginFixtureSuccessV1Response []byte + //go:embed fixtures/login/success/mfa/v1_handle/credentials.json + loginFixtureSuccessV1WithHandleCredentials []byte + //go:embed fixtures/login/success/mfa/v1_handle/internal_context.json + loginFixtureSuccessV1WithHandleContext []byte + //go:embed fixtures/login/success/mfa/v1_handle/response.json + loginFixtureSuccessV1WithHandleResponse []byte + //go:embed fixtures/login/success/mfa/v1_passwordless/credentials.json + loginFixtureSuccessV1PasswordlessCredentials []byte + //go:embed fixtures/login/success/mfa/v1_passwordless/internal_context.json + loginFixtureSuccessV1PasswordlessContext []byte + //go:embed fixtures/login/success/mfa/v1_passwordless/response.json + loginFixtureSuccessV1PasswordlessResponse []byte +) + +var loginFixtureSuccessEmail = gjson.GetBytes(loginFixtureSuccessIdentity, "traits.email").String() + +func TestCompleteLogin(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false), + enabledWebauthn, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + _ = testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) { + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts") + } else { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + } + } + + doAPIFlow := func(t *testing.T, v func(url.Values), apiClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, 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) + } + + doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) + } + + createIdentityWithWebAuthn := func(t *testing.T, c identity.Credentials) *identity.Identity { + var id identity.Identity + require.NoError(t, json.Unmarshal(loginFixtureSuccessIdentity, &id)) + + id.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Identifiers: []string{loginFixtureSuccessEmail}, + Config: c.Config, + Type: identity.CredentialsTypeWebAuthn, + Version: c.Version, + }) + + // We clean up the identity in case it has been created before + _ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID) + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &id)) + return &id + } + + submitWebAuthnLoginFlowWithClient := func(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 := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), 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 + } + + submitWebAuthnLoginWithClient := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, isSPA, false, false, opts...) + return submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb) + } + + 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.Context(), t, reg, id) + return submitWebAuthnLoginWithClient(t, isSPA, id, contextFixture, browserClient, cb, opts...) + } + + t.Run("flow=refresh", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, nil) + }) + + 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.InitFlowWithAAL(expectedAAL), + ) + snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "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 := "" + if isSPA { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), 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.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).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) + } + + t.Run("case=passwordless", func(t *testing.T) { + for _, e := range []bool{ + true, + false, + } { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, e) + expectedAAL := identity.AuthenticatorAssuranceLevel1 + if !e { + // If passwordless is disabled, using WebAuthn means that we have a second factor enabled. + // Thus, AAL2 :) + expectedAAL = identity.AuthenticatorAssuranceLevel2 + } + + for _, tc := range []struct { + 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", + expectTriggers: !e, + }, + { + creds: identity.Credentials{ + Config: loginFixtureSuccessV1Credentials, + Version: 1, + }, + context: loginFixtureSuccessV1Context, + response: loginFixtureSuccessV1Response, + descript: "mfa v1 credentials", + expectTriggers: !e, + }, + { + creds: identity.Credentials{ + Config: loginFixtureSuccessV1PasswordlessCredentials, + Version: 1, + }, + context: loginFixtureSuccessV1PasswordlessContext, + response: loginFixtureSuccessV1PasswordlessResponse, + descript: "passwordless credentials", + expectTriggers: e, + }, + } { + 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{ + "browser", + "spa", + } { + t.Run(f, func(t *testing.T) { + run(t, id, tc.context, tc.response, f == "spa", expectedAAL, tc.expectTriggers) + }) + } + }) + } + } + }) + + t.Run("case=no webauth credentials", func(t *testing.T) { + for _, e := range []bool{true, false} { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, e) + t.Run(fmt.Sprintf("passwordless=%v", e), func(t *testing.T) { + for _, f := range []string{"browser", "spa"} { + t.Run(f, func(t *testing.T) { + id := identity.NewIdentity("") + id.NID = x.NewUUID() + client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, true, f == "spa", false, false) + snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + }) + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + assert.False(t, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").Bool(), "%s", nodes) + assert.False(t, gjson.GetBytes(nodes, "#(attributes.name=="+node.WebAuthnLoginTrigger+").attributes.value").Bool(), "%s", nodes) + }) + } + }) + } + }) + }) + + t.Run("flow=passwordless", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false) + }) + + t.Run("case=webauthn button exists", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, true, false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) + }) + + t.Run("case=webauthn 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", identity.CredentialsTypeWebAuthn.String()) + v.Set("identifier", "doesnotexist") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + 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 := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + 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.Context(), t, reg, nil) + id.DeleteCredentialsType(identity.CredentialsTypeWebAuthn) + require.NoError(t, reg.IdentityManager().Update(t.Context(), id, identity.ManagerAllowWriteProtectedTraits)) + + payload := func(v url.Values) { + v.Set("method", identity.CredentialsTypeWebAuthn.String()) + v.Set("identifier", subject) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + 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 := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=webauthn MFA credentials can not be used for passwordless login", func(t *testing.T) { + _, subject := createIdentityAndReturnIdentifier(t.Context(), t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`)) + + payload := func(v url.Values) { + v.Set("method", identity.CredentialsTypeWebAuthn.String()) + v.Set("identifier", subject) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + 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 := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) { + _, subject := createIdentityAndReturnIdentifier(t.Context(), t, 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...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + values.Set("method", identity.CredentialsTypeWebAuthn.String()) + values.Set("identifier", subject) + body, res := testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) + if spa { + assert.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + redir := gjson.Get(body, "redirect_browser_to").String() + assert.NotEmpty(t, redir) + + res, err := browserClient.Get(redir) + require.NoError(t, err) + + defer func() { _ = res.Body.Close() }() + raw, err := io.ReadAll(res.Body) + require.NoError(t, err) + body = string(raw) + } else { + checkURL(t, !spa, res) + } + + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptNestedKeys("value", "src", "nonce", "action", "request_url", "issued_at", "expires_at", "created_at", "updated_at", "id", "onclick")) + assert.Equal(t, text.NewInfoLoginWebAuthnPasswordless().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + + values.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid)) + values.Set("identifier", subject) + body, res = testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) + checkURL(t, !spa, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "The provided web authn login is invalid, please try again.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + doBrowserFlow(t, false, testhelpers.NewClientWithCookies(t)) + }) + + t.Run("type=spa", func(t *testing.T) { + doBrowserFlow(t, true, testhelpers.NewClientWithCookies(t)) + }) + }) + + t.Run("case=succeeds with passwordless login", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1") + // We load our identity which we will use to replay the webauth session + id := createIdentityWithWebAuthn(t, identity.Credentials{ + Config: loginFixtureSuccessV1PasswordlessCredentials, + Version: 1, + }) + + browserClient := testhelpers.NewClientWithCookies(t) + body, res, f := submitWebAuthnLoginWithClient(t, spa, id, loginFixtureSuccessV1PasswordlessContext, browserClient, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1PasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + prefix := "" + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), 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.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).method").String(), "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + + 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(t.Context()).String(), "%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("flow=mfa", func(t *testing.T) { + t.Run("case=webauthn payload is set when identity has webauthn", func(t *testing.T) { + id := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, 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", + "3.attributes.src", + "3.attributes.nonce", + "4.attributes.onclick", + "4.attributes.onload", + "4.attributes.value", + }) + 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.Context(), t, reg, id) + f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + + t.Run("case=webauthn payload is not set for API clients", func(t *testing.T) { + id := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, 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.Context(), t, 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.Context(), t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + } + + t.Run("case=should refuse to execute api flow", func(t *testing.T) { + id := createIdentity(t.Context(), t, reg) + payload := func(v url.Values) { + v.Set(node.WebAuthnLogin, "{}") + } + + body, res := doAPIFlowSignedIn(t, payload, id) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "Could not find a strategy to log you in with. Did you fill out the form correctly?", gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) { + id, sub := createIdentityAndReturnIdentifier(t.Context(), t, reg, nil) + payload := func(v url.Values) { + v.Set("identifier", sub) + v.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid)) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "The provided web authn login is invalid, please try again.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + body, res := doBrowserFlowSignIn(t, false, payload, id) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := doBrowserFlowSignIn(t, true, payload, id) + check(t, false, body, res) + }) + }) + + t.Run("case=can not use security key for passwordless in mfa flow", func(t *testing.T) { + id := createIdentityWithWebAuthn(t, identity.Credentials{ + Config: loginFixtureSuccessV1PasswordlessCredentials, + Version: 1, + }) + + body, res, _ := submitWebAuthnLogin(t, true, id, loginFixtureSuccessV1PasswordlessContext, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1PasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.messages").Raw), []string{}) + }) + + t.Run("case=login with a security key using", func(t *testing.T) { + idd := uuid.FromStringOrNil("44fc22c9-abae-4c3e-a56b-37c7b38d973e") + out, err := json.Marshal(identity.CredentialsWebAuthnConfig{UserHandle: idd[:]}) + require.NoError(t, err) + t.Logf("json: %s", out) + out, err = json.Marshal(protocol.AuthenticatorAssertionResponse{UserHandle: idd[:]}) + require.NoError(t, err) + t.Logf("wa: %s", out) + + for _, tc := range []struct { + d string + v int + cf []byte + sf []byte + ix []byte + }{ + {d: "v0 without userhandle", v: 0, cf: loginFixtureSuccessV0Credentials, sf: loginFixtureSuccessV0Response, ix: loginFixtureSuccessV0Context}, + {d: "v1 without userhandle", v: 1, cf: loginFixtureSuccessV1Credentials, sf: loginFixtureSuccessV1Response, ix: loginFixtureSuccessV1Context}, + {d: "v1 with differing userhandle", v: 1, cf: loginFixtureSuccessV1WithHandleCredentials, sf: loginFixtureSuccessV1WithHandleResponse, ix: loginFixtureSuccessV1WithHandleContext}, + } { + t.Run(tc.d, func(t *testing.T) { + run := func(t *testing.T, spa bool) { + // We load our identity which we will use to replay the webauth session + id := createIdentityWithWebAuthn(t, identity.Credentials{ + Config: tc.cf, + Version: tc.v, + }) + + body, res, f := submitWebAuthnLogin(t, spa, id, tc.ix, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.WebAuthnLogin, string(tc.sf)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + prefix := "" + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), redirTS.URL) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).method").String(), "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + + 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))) + } + + 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 be able to log in with phone number identifier", func(t *testing.T) { + // Test backward compatibility with a legacy identity whose credential + // identifier is stored in non-normalized form in the database. + // Note: AAL2 (MFA) login resolves the identity from the session, not via + // credential identifier lookup, so both normalized and non-normalized + // identifiers succeed regardless of the DB state. + nonNormalizedPhone := "+44 20 7946 0958" + normalizedPhone := "+442079460958" + + phoneID := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, nonNormalizedPhone)), + } + phoneID.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Identifiers: []string{nonNormalizedPhone}, + Config: loginFixtureSuccessV1Credentials, + Type: identity.CredentialsTypeWebAuthn, + Version: 1, + }) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), phoneID)) + t.Cleanup(func() { + _ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), phoneID.ID) + }) + + // CreateIdentity normalizes the credential identifier to E.164 format and + // may update the credential version. To simulate a legacy database record + // created before normalization was introduced, revert both via raw SQL. + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery( + "UPDATE identity_credential_identifiers SET identifier = ? WHERE identity_id = ? AND identifier = ?", + nonNormalizedPhone, phoneID.ID, normalizedPhone, + ).Exec()) + require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery( + "UPDATE identity_credentials SET version = 0 WHERE identity_id = ?", + phoneID.ID, + ).Exec()) + + run := func(t *testing.T, spa bool, identifier string) { + body, res, f := submitWebAuthnLogin(t, spa, phoneID, loginFixtureSuccessV1Context, func(values url.Values) { + values.Set("identifier", identifier) + values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1Response)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + + prefix := "" + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), redirTS.URL) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, phoneID.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + + 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))) + } + + t.Run("identifier=non-normalized", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + run(t, false, nonNormalizedPhone) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true, nonNormalizedPhone) + }) + }) + + t.Run("identifier=normalized", func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + run(t, false, normalizedPhone) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true, normalizedPhone) + }) + }) + }) +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true) + ctx = contextx.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) + fhAAL1, ok := s.(login.AAL1FormHydrator) + require.True(t, ok) + fhAAL2, ok := s.(login.AAL2FormHydrator) + 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")) + } + + newFlowInternal := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *login.Flow) { + query := "" + if identitySchema != "" { + query = "?identity_schema=" + identitySchema + } + + r := httptest.NewRequest("GET", "/self-service/login/browser"+query, nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + return newFlowInternal(ctx, t, "") + } + newFlowWithIdentitySchema := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *login.Flow) { + return newFlowInternal(ctx, t, identitySchema) + } + + passwordlessEnabled := contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true) + mfaEnabled := contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, false) + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + id := createIdentity(ctx, t, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, 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, fhAAL2.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, fhAAL2.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, fhAAL1.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.NoError(t, fhAAL1.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(ctx, t, reg) + r, f := newFlow(passwordlessEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("case=passwordless enabled and user has passwordless credentials", func(t *testing.T) { + id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + r, f := newFlow(passwordlessEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled and user has mfa credentials", func(t *testing.T) { + id := createIdentity(ctx, t, reg) + r, f := newFlow(mfaEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled but user has passwordless credentials", func(t *testing.T) { + id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + r, f := newFlow(mfaEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil)) + 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( + contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fhAAL1.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( + contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fhAAL1.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 := contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true) + passwordlessEnabled := contextx.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, fhAAL1.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, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + mfaEnabled := contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false) + passwordlessEnabled := contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false) + + id, _ := createIdentityAndReturnIdentifier(ctx, t, 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, fhAAL1.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, fhAAL1.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, fhAAL1.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, fhAAL1.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, fhAAL1.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("case=Multi-Schema-method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + multiSchema := contextx.WithConfigValue(ctx, config.ViperKeyDefaultIdentitySchemaID, "default") + multiSchema = contextx.WithConfigValue(multiSchema, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "file://./stub/missing-identifier.schema.json"}, + {ID: "not-default", URL: "file://./stub/login.schema.json", SelfserviceSelectable: true}, + }) + + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlowWithIdentitySchema(contextx.WithConfigValue(multiSchema, config.ViperKeyWebAuthnPasswordless, true), t, "not-default") + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlowWithIdentitySchema(contextx.WithConfigValue(multiSchema, config.ViperKeyWebAuthnPasswordless, false), t, "not-default") + require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + }) +} diff --git a/selfservice/strategy/webauthn/nodes.go b/selfservice/strategy/webauthn/nodes.go new file mode 100644 index 0000000..ed775a2 --- /dev/null +++ b/selfservice/strategy/webauthn/nodes.go @@ -0,0 +1,17 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/webauthnx" +) + +func nodeWebauthnRegistrationOptions(opts []byte) *node.Node { + return webauthnx.NewWebAuthnConnectionTrigger(string(opts)).WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterWebAuthn()) +} + +func nodeDisplayName() *node.Node { return webauthnx.NewWebAuthnConnectionName() } +func nodeConnectionInput() *node.Node { return webauthnx.NewWebAuthnConnectionInput() } diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go new file mode 100644 index 0000000..3204cfa --- /dev/null +++ b/selfservice/strategy/webauthn/registration.go @@ -0,0 +1,292 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "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/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" +) + +var _ registration.FormHydrator = new(Strategy) + +// Update Registration Flow with WebAuthn Method +// +// swagger:model updateRegistrationFlowWithWebAuthnMethod +type updateRegistrationFlowWithWebAuthnMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"webauthn_register"` + + // Name of the WebAuthn Security Key to be Added + // + // A human-readable name for the security key which will be added. + RegisterDisplayName string `json:"webauthn_register_displayname"` + + // 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 "webauthn" 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"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} + +func (s *Strategy) handleRegistrationError(r *http.Request, f *registration.Flow, p updateRegistrationFlowWithWebAuthnMethod, err error) error { + if f != 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()) + } + + f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName) + + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) decode(p *updateRegistrationFlowWithWebAuthnMethod, r *http.Request, ds *url.URL) error { + return registration.DecodeBody(p, r, registrationSchema, ds) +} + +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 + } + + ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config()) + if err != nil { + return err + } + + var p updateRegistrationFlowWithWebAuthnMethod + if err := s.decode(&p, r, ds); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) + } + + regFlow.TransientPayload = p.TransientPayload + + 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(ctx, regFlow.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) + } + + if len(p.Traits) == 0 { + p.Traits = json.RawMessage("{}") + } + i.Traits = identity.Traits(p.Traits) + + webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if !webAuthnSession.IsObject() { + 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(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(r, regFlow, p, errors.WithStack( + herodot.ErrBadRequest().WithReasonf("Unable to parse WebAuthn response: %s", err))) + } + + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) + if err != nil { + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError().WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) + } + + 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(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError().WithReasonf("Unable to create WebAuthn credential: %s", err))) + } + + 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(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError().WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) + } + + 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! + regFlow.InternalContext, err = sjson.DeleteBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if err != nil { + return s.handleRegistrationError(r, regFlow, p, err) + } + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) + } + + return nil +} + +func (s *Strategy) injectWebauthnRegistrationOptions(r *http.Request, f *registration.Flow) ([]byte, error) { + ctx := r.Context() + if options := gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyWebauthnOptions)); options.IsObject() { + return []byte(options.Raw), nil + } + + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) + if err != nil { + return nil, errors.WithStack(err) + } + + webauthID := x.NewUUID() + user := webauthnx.NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(ctx)) + option, sessionData, err := web.BeginRegistration(user) + if err != nil { + return nil, errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return nil, errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData) + if err != nil { + return nil, errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetRawBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyWebauthnOptions), injectWebAuthnOptions) + if err != nil { + return nil, errors.WithStack(err) + } + + return injectWebAuthnOptions, nil +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + ctx := r.Context() + if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + opts, err := s.injectWebauthnRegistrationOptions(r, f) + if err != nil { + return nil + } + + f.UI.Nodes.Upsert(nodeDisplayName()) + f.UI.Nodes.Upsert(nodeWebauthnRegistrationOptions(opts)) + + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.Upsert(nodeConnectionInput()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + ctx := r.Context() + if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + opts, err := s.injectWebauthnRegistrationOptions(r, f) + if err != nil { + return nil + } + + f.UI.Nodes.RemoveMatching(nodeDisplayName()) + f.UI.Nodes.RemoveMatching(nodeWebauthnRegistrationOptions(opts)) + + f.UI.Nodes.RemoveMatching(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.RemoveMatching(nodeConnectionInput()) + return nil +} + +func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error { + ctx := r.Context() + if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + opts, err := s.injectWebauthnRegistrationOptions(r, f) + if err != nil { + return nil + } + + f.UI.Nodes.Upsert(nodeDisplayName()) + f.UI.Nodes.Upsert(nodeWebauthnRegistrationOptions(opts)) + + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.Upsert(nodeConnectionInput()) + return nil +} diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go new file mode 100644 index 0000000..8cbbfda --- /dev/null +++ b/selfservice/strategy/webauthn/registration_test.go @@ -0,0 +1,628 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn_test + +import ( + "context" + _ "embed" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/x/configx" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + kratos "github.com/ory/kratos/pkg/httpclient" + "github.com/ory/kratos/pkg/registrationhelpers" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/webauthn" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/assertx" + "github.com/ory/x/contextx" + "github.com/ory/x/snapshotx" +) + +var ( + flows = []string{"spa", "browser"} + //go:embed fixtures/registration/success/response.json + 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 { + return flow == "spa" +} + +func newRegistrationRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault { + _, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, + config.ViperKeyWebAuthnPasswordless: true, + config.ViperKeySelfServiceRegistrationLoginHints: true, + config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, + }), + enabledWebauthn, + }, cfgOpts...)...) + return reg +} + +func TestRegistration(t *testing.T) { + reg := newRegistrationRegistry(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration.schema.json"))) + conf := reg.Config() + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + _ = testhelpers.NewErrorTestServer(t, reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg) + + // set the "return to" server, which will assert the session state + // (redirTS: enforce that a session exists, redirNoSessionTS: enforce that no session exists) + useReturnToFromTS := func(ts *httptest.Server) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts") + } + useReturnToFromTS(redirTS) + + //checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) { + // if shouldRedirect { + // assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") + // } else { + // assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) + // } + //} + + t.Run("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) + }) + + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { + v.Del("traits.foobar") + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertCSRFFailures", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemaDoesNotExist(t, reg, flows, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + }) + }) + + t.Run("case=webauthn button does not exist when passwordless is disabled", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true) + }) + for _, f := range flows { + t.Run(f, func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + } + }) + + t.Run("case=webauthn button exists", func(t *testing.T) { + for _, f := range flows { + t.Run(f, func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "5.attributes.onclick", + "5.attributes.value", + "6.attributes.nonce", + "6.attributes.src", + }) + }) + } + }) + + t.Run("case=should return an error because not passing validation", func(t *testing.T) { + email := testhelpers.RandomEmail() + + values := func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + 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", "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) { + email := testhelpers.RandomEmail() + + values := func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bar") + v.Set("transient_payload", "42") + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + 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", "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 webauthn response is invalid", func(t *testing.T) { + email := testhelpers.RandomEmail() + values := func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.WebAuthnRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values) + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + 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", "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) + }) + } + }) + + submitWebAuthnRegistrationWithClient := func(t *testing.T, flow string, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.RegistrationFlow) { + isSPA := flow == "spa" + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, isSPA, false, false, opts...) + + // We inject the session to replay + interim, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + + // We use the response replay + body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, f, client, values.Encode()) + 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) { + 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() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + }) + + email := testhelpers.RandomEmail() + + values := func(v url.Values) { + v.Set("traits.email", email) + 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, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values) + + 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.email") + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + makeRegistration := func(t *testing.T, f string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) { + actual, res, actualFlow := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values, opts...) + fetchedFlow, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(actualFlow.Id)) + require.NoError(t, err) + + return actual, res, fetchedFlow + } + + makeSuccessfulRegistration := func(t *testing.T, f string, expectReturnTo string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string) { + actual, res, fetchedFlow := makeRegistration(t, f, values, opts...) + assert.Empty(t, gjson.GetBytes(fetchedFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)), "has cleaned up the internal context after success") + if f == "spa" { + expectReturnTo = publicTS.URL + } + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + return actual + } + + getPrefix := func(f string) (prefix string) { + if f == "spa" { + prefix = "session." + } + return + } + + t.Run("successful registration", func(t *testing.T) { + t.Cleanup(func() { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), nil) + }) + + 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.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + } + + t.Run("case=should create the identity but not a session", func(t *testing.T) { + useReturnToFromTS(redirNoSessionTS) + t.Cleanup(func() { + useReturnToFromTS(redirTS) + }) + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", values(email)) + + 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 := 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) + } + }) + } + }) + + t.Run("case=should accept valid transient payload", func(t *testing.T) { + useReturnToFromTS(redirNoSessionTS) + t.Cleanup(func() { + useReturnToFromTS(redirTS) + }) + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) { + values(email)(v) + v.Set("transient_payload.stuff", "42") + }) + + 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 := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=should create the identity and a session and use the correct schema", func(t *testing.T) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}}) + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + conf.MustSet(t.Context(), 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() + actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email)) + + 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 := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) + 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) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}}) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email)) + assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual) + + 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 passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + t.Run("case=reset previous form errors", func(t *testing.T) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}}) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual, _, _ := makeRegistration(t, f, func(v url.Values) { + v.Del("traits.username") + v.Set("traits.foobar", "bazbar") + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "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, _, _ = makeRegistration(t, f, func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "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=multi-schema should create the identity and a session and use the correct schema", func(t *testing.T) { + conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}}) + conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "does-not-exist") + conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "does-not-exist", URL: "file://./stub/profile.schema.json"}, + {ID: "advanced-user", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true}, + }) + + for _, f := range flows { + t.Run("type="+f+" registration success", func(t *testing.T) { + email := testhelpers.RandomEmail() + actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email), testhelpers.InitFlowWithIdentitySchema("advanced-user")) + + 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 := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + + t.Run("type="+f+" registration failure due to invalid form data", func(t *testing.T) { + invalidValues := func(v url.Values) { + v.Set("traits.username", testhelpers.RandomEmail()) + v.Set("traits.foobar", "b") + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + + actual, res, _ := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), invalidValues, testhelpers.InitFlowWithIdentitySchema("advanced-user")) + + if f == "browser" { + assert.Equal(t, http.StatusOK, res.StatusCode, "%s", actual) + } else { + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual) + } + assert.Equal(t, int64(4000003), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int(), "%s", actual) + assert.Equal(t, "length must be \u003e= 2, but got 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String(), "%s", actual) + }) + } + }) + }) + + t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json") + + for _, f := range []string{"spa", "api", "browser"} { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := makeRegistration(t, f, func(v url.Values) { + v.Set("traits.email", testhelpers.RandomEmail()) + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} + +func TestPopulateRegistrationMethod(t *testing.T) { + ctx := context.Background() + _, reg := pkg.NewFastRegistryWithMocks(t) + + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPID, "localhost") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost") + ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true) + + s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeWebAuthn) + require.NoError(t, err) + + fh, ok := s.(registration.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f node.Nodes, except ...snapshotx.Opt) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f, append(except, snapshotx.ExceptNestedKeys("nonce", "src", "onclick"))...) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { + r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := registration.NewFlow(reg, r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethod(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value")) + }) + + t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value")) + }) + + t.Run("method=idempotency", func(t *testing.T) { + r, f := newFlow(ctx, t) + + var snapshots []node.Nodes + + t.Run("case=1", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=2", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value")) + }) + + t.Run("case=3", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes) + }) + + t.Run("case=4", func(t *testing.T) { + require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) + snapshots = append(snapshots, f.UI.Nodes) + toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value")) + }) + + t.Run("case=evaluate", func(t *testing.T) { + assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) + assertx.EqualAsJSONExcept(t, snapshots[1], snapshots[3], []string{"3.attributes.nonce"}) + }) + }) +} diff --git a/selfservice/strategy/webauthn/schema.go b/selfservice/strategy/webauthn/schema.go new file mode 100644 index 0000000..b3fc7a9 --- /dev/null +++ b/selfservice/strategy/webauthn/schema.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte + +//go:embed .schema/settings.schema.json +var settingsSchema []byte + +//go:embed .schema/registration.schema.json +var registrationSchema []byte diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go new file mode 100644 index 0000000..899925c --- /dev/null +++ b/selfservice/strategy/webauthn/settings.go @@ -0,0 +1,428 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "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" + + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + + "github.com/ory/kratos/session" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/x/decoderx" + + "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/x" +) + +func (s *Strategy) SettingsStrategyID() string { + return identity.CredentialsTypeWebAuthn.String() +} + +const ( + InternalContextKeySessionData = "session_data" + InternalContextKeyWebauthnOptions = "session_options" +) + +// Update Settings Flow with WebAuthn Method +// +// swagger:model updateSettingsFlowWithWebAuthnMethod +type updateSettingsFlowWithWebAuthnMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"webauthn_register"` + + // Name of the WebAuthn Security Key to be Added + // + // A human-readable name for the security key which will be added. + RegisterDisplayName string `json:"webauthn_register_displayname"` + + // Remove a WebAuthn Security Key + // + // This must contain the ID of the WebAuthN connection. + Remove string `json:"webauthn_remove"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to "webauthn" 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"` + + // 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 { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithWebAuthnMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "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(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 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(ctx, w, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return ctxUpdate, 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.Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +func (s *Strategy) continueSettingsFlow( + ctx context.Context, + w http.ResponseWriter, r *http.Request, + ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithWebAuthnMethod, +) 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 !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + } else { + return errors.New("ended up in unexpected state") + } + + if len(p.Register) > 0 { + return s.continueSettingsFlowAdd(ctx, ctxUpdate, p) + } else if len(p.Remove) > 0 { + return s.continueSettingsFlowRemove(ctx, w, r, ctxUpdate, p) + } + + 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 updateSettingsFlowWithWebAuthnMethod) 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())) + } + + var wasPasswordless bool + 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]) + } else if cred.IsPasswordless { + wasPasswordless = true + } + } + + if len(updated) == len(cc.Credentials) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("You tried to remove a WebAuthn credential which does not exist.")) + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i) + if err != nil { + return err + } + + if count < 2 && wasPasswordless { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(webauthnx.ErrNotEnoughCredentials())) + } + + if len(updated) == 0 { + i.DeleteCredentialsType(identity.CredentialsTypeWebAuthn) + 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 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.")) + } + + 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().WebAuthnConfig(ctx)) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) + } + + 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(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())) + } + + for k := range cc.Credentials { + if bytes.Equal(cc.Credentials[k].ID, credential.ID) { + return errors.WithStack(webauthnx.ErrCredentialAlreadyRegistered("#/" + node.WebAuthnRegister)) + } + } + + 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(ctx) + cc.UserHandle = ctxUpdate.Session.IdentityID[:] + + cc.Credentials = append(cc.Credentials, *wc) + co, 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(), co, 1) + 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 + 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(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ + Method: s.ID(), + AAL: aal, + }); err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + 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) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.webauthn.Strategy.PopulateSettingsMethod") + defer otelx.End(span, &err) + + if f.Type != flow.TypeBrowser { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, id) + if err != nil { + return err + } + + webAuthns, err := s.identityListWebAuthn(id) + if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) { + return err + } + + var registrationOptions []webauthn.RegistrationOption + if webAuthns != nil { + exclude := make([]protocol.CredentialDescriptor, 0, len(webAuthns.Credentials)) + 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.NewWebAuthnUnlink(cred, func(a *node.InputAttributes) { + // Do not remove this node because it is the last credential the identity can sign in with. + a.Disabled = cred.IsPasswordless && count < 2 + })) + exclude = append(exclude, cred.ToWebAuthn().Descriptor()) + } + // Exclude every registered credential, passwordless or not: platform authenticators keep one + // credential per relying party and user handle, so re-enrolling would overwrite the entry on + // the device while Kratos keeps both server-side (WebAuthn Level 2, section 5.4.3). + registrationOptions = append(registrationOptions, webauthn.WithExclusions(exclude)) + } + + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + + option, sessionData, err := web.BeginRegistration(webauthnx.NewUser(id.ID.Bytes(), nil, web.Config), registrationOptions...) + 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(ctx))) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionName()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). + WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterWebAuthn())) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionInput()) + return nil +} + +func (s *Strategy) handleSettingsError(ctx context.Context, 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(ctx, w, r, settings.ContinuityKey(s.SettingsStrategyID()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), 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/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go new file mode 100644 index 0000000..4ab5c4b --- /dev/null +++ b/selfservice/strategy/webauthn/settings_test.go @@ -0,0 +1,643 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn_test + +import ( + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/ory/x/snapshotx" + + "github.com/ory/kratos/selfservice/strategy/webauthn" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + kratos "github.com/ory/kratos/pkg/httpclient" + "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" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/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 + +const registerDisplayNameGJSONQuery = "ui.nodes.#(attributes.name==" + node.WebAuthnRegisterDisplayName + ")" + +func createIdentityWithoutWebAuthn(t *testing.T, reg driver.Registry) *identity.Identity { + id := createIdentity(t.Context(), t, reg) + delete(id.Credentials, identity.CredentialsTypeWebAuthn) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) + return id +} + +func createIdentityAndReturnIdentifier(ctx context.Context, t *testing.T, reg driver.Registry, conf []byte) (*identity.Identity, string) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + SchemaID: "default", + NID: uuid.Must(uuid.NewV4()), + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%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, reg.PrivilegedIdentityPool().CreateIdentity(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.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{identifier}, + Config: conf, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, i)) + return i, identifier +} + +func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry) *identity.Identity { + id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, nil) + return id +} + +var enabledWebauthn = func() configx.OptionModifier { + webauthn := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypeWebAuthn) + return configx.WithValues(map[string]any{ + webauthn + ".enabled": true, + webauthn + ".config.rp.display_name": "Ory Corp", + webauthn + ".config.rp.id": "localhost", + webauthn + ".config.rp.origin": "http://localhost:4455", + }) +}() + +func ensureReplacement(t *testing.T, index string, ui kratos.UiContainer, expected string) { + actual, err := json.Marshal(ui.Nodes) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(actual, index+".attributes.onclick").String(), expected, "ensure that the replacement works") +} + +func TestCompleteSettings(t *testing.T) { + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, + config.ViperKeySelfServiceSettingsRequiredAAL: "aal1", + config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m", + }), + enabledWebauthn, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")), + ) + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewSettingsUIFlowEchoServer(t, reg) + _ = testhelpers.NewRedirSessionEchoTS(t, reg) + loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + t.Run("case=a device is shown which can be unlinked", func(t *testing.T) { + id := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "5.attributes.onclick", + "5.attributes.value", + "6.attributes.src", + "6.attributes.nonce", + }) + 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(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "3.attributes.onload", + "3.attributes.onclick", + "3.attributes.value", + "4.attributes.src", + "4.attributes.nonce", + }) + ensureReplacement(t, "3", f.Ui, "Ory Corp") + }) + + t.Run("case=registration options exclude already registered credentials", func(t *testing.T) { + id := createIdentity(t.Context(), t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) + + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + options := gjson.GetBytes(nodes, `#(attributes.name=="webauthn_register_trigger").attributes.value`).String() + exclude := gjson.Get(options, "publicKey.excludeCredentials").Array() + require.Len(t, exclude, 2, options) + assert.Equal(t, "public-key", exclude[0].Get("type").String()) + assert.Equal(t, "Zm9vZm9v", exclude[0].Get("id").String()) + assert.Equal(t, "YmFyYmFy", exclude[1].Get("id").String()) + }) + + t.Run("case=registration options have no exclusions without credentials", func(t *testing.T) { + id := createIdentityWithoutWebAuthn(t, reg) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) + + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + options := gjson.GetBytes(nodes, `#(attributes.name=="webauthn_register_trigger").attributes.value`).String() + require.NotEmpty(t, options) + assert.False(t, gjson.Get(options, "publicKey.excludeCredentials").Exists(), options) + }) + + t.Run("case=webauthn only works for browsers", func(t *testing.T) { + id := createIdentityWithoutWebAuthn(t, reg) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, 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.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, 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.Context(), t, reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, 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 := createIdentityWithoutWebAuthn(t, reg) + body, res := doAPIFlow(t, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + }, id) + + assert.Contains(t, res.Request.URL.String(), 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 if name is missing", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := createIdentityWithoutWebAuthn(t, reg) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + } + assert.Contains(t, gjson.Get(body, registerDisplayNameGJSONQuery+".messages.0.text").String(), "Property webauthn_register_displayname is missing.", "%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 with browser submit because csrf token is missing", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := createIdentityWithoutWebAuthn(t, reg) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.WebAuthnRegister, "{}") + v.Set(node.WebAuthnRegisterDisplayName, "foobar") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body) + } else { + assert.Contains(t, res.Request.URL.String(), errTS.URL) + assert.Equal(t, nosurfx.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 := createIdentityWithoutWebAuthn(t, reg) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + v.Set(node.WebAuthnRegisterDisplayName, "foobar") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), 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) { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + run := func(t *testing.T, spa bool) { + id := createIdentityWithoutWebAuthn(t, reg) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.WebAuthnRegister, "{}") + v.Set(node.WebAuthnRegisterDisplayName, "foobar") + }, 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(), 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 security key", 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 = uuid.Must(uuid.NewV4()) + _ = reg.PrivilegedIdentityPool().DeleteIdentity(t.Context(), id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, &id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) + + // We inject the session to replay + interim, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(context.Background(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + // We use the response replay + 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) + } else { + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) + 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)) + require.NoError(t, err) + // new session data has been generated + assert.NotEqual(t, settingsFixtureSuccessInternalContext, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData))) + + testhelpers.EnsureAAL(t, browserClient, publicTS, "aal2", string(identity.CredentialsTypeWebAuthn)) + } + + 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 add a security key that is already registered", func(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(t.Context(), id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, &id) + + // Seed the identity with the same credential ID the replayed response will try to register. + credID, err := base64.RawURLEncoding.DecodeString(gjson.GetBytes(settingsFixtureSuccessResponse, "rawId").String()) + require.NoError(t, err) + conf, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{{ID: credID, DisplayName: "existing key"}}, + }) + require.NoError(t, err) + confidential, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + confidential.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Config: conf, + }) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), confidential)) + + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS) + + // We inject the session to replay + interim, err := reg.SettingsFlowPersister().GetSettingsFlow(t.Context(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(t.Context(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set(node.WebAuthnRegister, string(settingsFixtureSuccessResponse)) + values.Set(node.WebAuthnRegisterDisplayName, "foobar") + body, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserClient, testhelpers.EncodeFormAsJSON(t, true, values)) + + assert.Equal(t, http.StatusBadRequest, res.StatusCode, body) + assert.Contains(t, body, "already registered", body) + + actual, err := reg.Persister().GetIdentityConfidential(t.Context(), id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) + require.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1, "the duplicate credential must not be persisted") + }) + + t.Run("case=fails to remove security key if it is passwordless and the last credential available", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true) + t.Cleanup(func() { + conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false) + }) + + run := func(t *testing.T, spa bool) { + id := createIdentity(t.Context(), t, reg) + id.DeleteCredentialsType(identity.CredentialsTypePassword) + conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`) + id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, conf, 0) + require.NoError(t, reg.IdentityManager().Update(t.Context(), id, identity.ManagerAllowWriteProtectedTraits)) + + body, res := doBrowserFlow(t, spa, func(v url.Values) { + // The remove key should be empty + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) + + v.Set(node.WebAuthnRemove, "666f6f666f6f") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), 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==webauthn_remove)").String()), nil) + + actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) + 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=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.Context(), t, 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(t.Context(), 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", + "webauthn_register_trigger", + }) + v.Set(node.WebAuthnRemove, "666f6f666f6f") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + } + + t.Run("response", func(t *testing.T) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + _, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) + assert.False(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=remove all security keys", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := createIdentity(t.Context(), t, reg) + allCred, ok := id.GetCredentials(identity.CredentialsTypeWebAuthn) + 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.WebAuthnRemove, fmt.Sprintf("%x", cred.ID)) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + 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) + require.NoError(t, err) + _, ok = actual.GetCredentials(identity.CredentialsTypeWebAuthn) + 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 := createIdentity(t.Context(), t, reg) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.WebAuthnRemove, fmt.Sprintf("%x", []byte("foofoo"))) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) + + actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) + 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=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(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 + id.NID = uuid.Must(uuid.NewV4()) + require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + _ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, &id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, isSPA, publicTS) + + // We inject the session to replay + interim, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, reg.SettingsFlowPersister().UpdateSettingsFlow(context.Background(), interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + // We use the response replay + values.Set(node.WebAuthnRegister, string(settingsFixtureSuccessResponse)) + values.Set(node.WebAuthnRegisterDisplayName, "foobar") + actual, _ := testhelpers.SettingsMakeRequest(t, false, isSPA, f, browserClient, testhelpers.EncodeFormAsJSON(t, isSPA, values)) + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go new file mode 100644 index 0000000..747b60a --- /dev/null +++ b/selfservice/strategy/webauthn/strategy.go @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + "context" + "encoding/json" + "strings" + + "github.com/ory/kratos/x/nosurfx" + + "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/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +var ( + _ login.Strategy = new(Strategy) + _ settings.Strategy = new(Strategy) + _ identity.ActiveCredentialsCounter = new(Strategy) +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider + nosurfx.CSRFTokenGeneratorProvider + nosurfx.CSRFProvider + otelx.Provider + + config.Provider + + continuity.ManagementProvider + + x.CookieProvider + + 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 +} + +type Strategy struct{ d dependencies } + +func NewStrategy(d dependencies) *Strategy { return &Strategy{d: d} } + +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(_ 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, onlyPasswordlessCredentials bool) (count int, err error) { + for _, c := range cc { + 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 _, 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++ + } + } + } + return +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeWebAuthn +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.WebAuthnGroup +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + aal := identity.AuthenticatorAssuranceLevel1 + if !s.d.Config().WebAuthnForPasswordless(ctx) { + aal = identity.AuthenticatorAssuranceLevel2 + } + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: aal, + } +} diff --git a/selfservice/strategy/webauthn/strategy_test.go b/selfservice/strategy/webauthn/strategy_test.go new file mode 100644 index 0000000..a9d0e01 --- /dev/null +++ b/selfservice/strategy/webauthn/strategy_test.go @@ -0,0 +1,135 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/selfservice/strategy/webauthn" + "github.com/ory/kratos/session" + "github.com/ory/x/contextx" +) + +func TestCompletedAuthenticationMethod(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := webauthn.NewStrategy(reg) + + t.Run("default config", func(t *testing.T) { + assert.Equal(t, session.AuthenticationMethod{ + Method: strategy.ID(), + AAL: identity.AuthenticatorAssuranceLevel2, + }, strategy.CompletedAuthenticationMethod(t.Context())) + }) + + t.Run("with passwordless enabled", func(t *testing.T) { + ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyWebAuthnPasswordless, true) + assert.Equal(t, session.AuthenticationMethod{ + Method: strategy.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + }, strategy.CompletedAuthenticationMethod(ctx)) + }) +} + +func TestCountActiveFirstFactorCredentials(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + strategy := webauthn.NewStrategy(reg) + + for k, tc := range []struct { + in map[identity.CredentialsType]identity.Credentials + expectedFirst int + expectedMulti int + }{ + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"credentials": []}`), + }}, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"credentials": [{}]}`), + }}, + 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(), + Identifiers: []string{"foo"}, + Config: []byte(`{"credentials": [{"is_passwordless": true}]}`), + }}, + 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(), + Identifiers: []string{"foo"}, + Config: []byte(`{"credentials": [{"is_passwordless": true}, {"is_passwordless": true}]}`), + }}, + expectedFirst: 2, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"credentials": [{"is_passwordless": true}, {"is_passwordless": false}]}`), + }}, + expectedFirst: 1, + expectedMulti: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{}`), + }}, + }, + { + in: nil, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + cc := map[identity.CredentialsType]identity.Credentials{} + for _, c := range tc.in { + cc[c.Type] = c + } + + actual, err := strategy.CountActiveFirstFactorCredentials(t.Context(), cc) + require.NoError(t, err) + assert.Equal(t, tc.expectedFirst, actual) + + actual, err = strategy.CountActiveMultiFactorCredentials(t.Context(), cc) + require.NoError(t, err) + assert.Equal(t, tc.expectedMulti, actual) + }) + } +} diff --git a/selfservice/strategy/webauthn/stub/login.schema.json b/selfservice/strategy/webauthn/stub/login.schema.json new file mode 100644 index 0000000..82f8581 --- /dev/null +++ b/selfservice/strategy/webauthn/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/strategy/webauthn/stub/missing-identifier.schema.json b/selfservice/strategy/webauthn/stub/missing-identifier.schema.json new file mode 100644 index 0000000..4356579 --- /dev/null +++ b/selfservice/strategy/webauthn/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/webauthn/stub/noid.schema.json b/selfservice/strategy/webauthn/stub/noid.schema.json new file mode 100644 index 0000000..d1dcaa7 --- /dev/null +++ b/selfservice/strategy/webauthn/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/webauthn/stub/profile.schema.json b/selfservice/strategy/webauthn/stub/profile.schema.json new file mode 100644 index 0000000..2eae6b6 --- /dev/null +++ b/selfservice/strategy/webauthn/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": { + "webauthn": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/webauthn/stub/registration.schema.json b/selfservice/strategy/webauthn/stub/registration.schema.json new file mode 100644 index 0000000..2f355fb --- /dev/null +++ b/selfservice/strategy/webauthn/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 + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": [ + "foobar", + "username" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/webauthn/stub/settings.schema.json b/selfservice/strategy/webauthn/stub/settings.schema.json new file mode 100644 index 0000000..ccc216d --- /dev/null +++ b/selfservice/strategy/webauthn/stub/settings.schema.json @@ -0,0 +1,32 @@ +{ + "$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/webauthn/validate.go b/selfservice/strategy/webauthn/validate.go new file mode 100644 index 0000000..1c3509c --- /dev/null +++ b/selfservice/strategy/webauthn/validate.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import ( + "context" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" +) + +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.CredentialsTypeWebAuthn, &identity.Credentials{}) + if len(c.Identifiers) == 0 { + return schema.NewMissingIdentifierError() + } + + return nil +} diff --git a/selfservice/stub/fake-session.schema.json b/selfservice/stub/fake-session.schema.json new file mode 100644 index 0000000..558106d --- /dev/null +++ b/selfservice/stub/fake-session.schema.json @@ -0,0 +1,7 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": {} +} diff --git a/selfservice/stub/identity.schema.json b/selfservice/stub/identity.schema.json new file mode 100644 index 0000000..0337894 --- /dev/null +++ b/selfservice/stub/identity.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } + } + } +} diff --git a/selfservice/stub/new-form.json b/selfservice/stub/new-form.json new file mode 100644 index 0000000..7b6db2a --- /dev/null +++ b/selfservice/stub/new-form.json @@ -0,0 +1,34 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": {} + } + } + } + } + } + } +} diff --git a/selfservice/stub/registration.schema.json b/selfservice/stub/registration.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/stub/registration.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/stub/updated.schema.json b/selfservice/stub/updated.schema.json new file mode 100644 index 0000000..c7005d8 --- /dev/null +++ b/selfservice/stub/updated.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json b/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json new file mode 100644 index 0000000..19ce08b --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json @@ -0,0 +1,8 @@ +{ + "exp": 1675209660, + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json b/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json new file mode 100644 index 0000000..19ce08b --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json @@ -0,0 +1,8 @@ +{ + "exp": 1675209660, + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/.snapshots/TestTokenizer-case=rs512-with-external_id-in-sub.json b/session/.snapshots/TestTokenizer-case=rs512-with-external_id-in-sub.json new file mode 100644 index 0000000..eb8e4dc --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=rs512-with-external_id-in-sub.json @@ -0,0 +1,13 @@ +{ + "aal": "aal1", + "exp": 1675209660, + "external_id": "external-id", + "foo": "bar", + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "schema_id": "default", + "second_claim": 1675209660, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "external-id" +} diff --git a/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json b/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json new file mode 100644 index 0000000..ff4988b --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json @@ -0,0 +1,13 @@ +{ + "aal": "aal1", + "exp": 1675209660, + "external_id": "external-id", + "foo": "bar", + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "schema_id": "default", + "second_claim": 1675209660, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/error.go b/session/error.go new file mode 100644 index 0000000..a03540f --- /dev/null +++ b/session/error.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "net/http" + + "github.com/ory/herodot" +) + +func RespondWithJSONErrorOnAuthenticated(h herodot.Writer, err error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.WriteError(w, r, err) + } +} + +func ErrNoSessionFound() *herodot.DefaultError { + return herodot.ErrUnauthorized().WithReasonf("No valid session credentials found in the request.") +} diff --git a/session/expand.go b/session/expand.go new file mode 100644 index 0000000..33b7547 --- /dev/null +++ b/session/expand.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "strings" + + "github.com/ory/x/sqlxx" +) + +// Expandable controls what fields to expand for sessions. +type Expandable = sqlxx.Expandable + +const ( + // ExpandSessionDevices expands devices related to the session + ExpandSessionDevices Expandable = "Devices" + // ExpandSessionIdentity expands Identity related to the session + ExpandSessionIdentity Expandable = "Identity" + ExpandSessionIdentityRecoveryAddress Expandable = "Identity.RecoveryAddresses" + ExpandSessionIdentityVerifiableAddress Expandable = "Identity.VerifiableAddresses" +) + +var expandablesMap = map[string]Expandable{ + "devices": ExpandSessionDevices, + "identity": ExpandSessionIdentity, +} + +// Expandables is a list of Expandable values. +type Expandables = sqlxx.Expandables + +func ParseExpandable(in string) (Expandable, bool) { + e, ok := expandablesMap[strings.ToLower(in)] + return e, ok +} + +// ExpandNothing expands nothing +var ExpandNothing Expandables + +// ExpandDefault expands the default fields of a session +// - Associated Identity +var ExpandDefault = Expandables{ + ExpandSessionIdentity, +} + +// ExpandEverything expands all the fields of a session. +var ExpandEverything = Expandables{ + ExpandSessionDevices, + ExpandSessionIdentity, +} diff --git a/session/expand_test.go b/session/expand_test.go new file mode 100644 index 0000000..23b47f6 --- /dev/null +++ b/session/expand_test.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExpandableSearch_ExistingExpand(t *testing.T) { + e := ExpandEverything + + assert.True(t, e.Has(ExpandSessionDevices)) +} + +func TestExpandableSearch_NonExistingExpandOption(t *testing.T) { + e := ExpandEverything + + assert.False(t, e.Has("SomeExpand")) +} + +func TestExpandables_ToEager_skips_Identity(t *testing.T) { + e := ExpandEverything + + res := e.ToEager() + assert.ElementsMatch(t, []string{string(ExpandSessionDevices), string(ExpandSessionIdentity)}, res) +} + +func TestExpandables_ExpandNothing_IsEmpty(t *testing.T) { + e := ExpandNothing + + assert.Len(t, e, 0) +} diff --git a/session/handler.go b/session/handler.go new file mode 100644 index 0000000..d66968e --- /dev/null +++ b/session/handler.go @@ -0,0 +1,1436 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/httprouterx" + + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/pagination/migrationpagination" + + "github.com/ory/x/pagination/keysetpagination" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" +) + +type ( + // FlowForTokenExchange looks up a login or registration flow by ID. + // It is used by the token exchange handler to surface flow errors when no + // session was created (e.g. because a before-registration webhook rejected + // the request). + FlowForTokenExchange interface { + GetFlowForTokenExchange(ctx context.Context, flowID uuid.UUID) (any, error) + } + FlowForTokenExchangeProvider interface { + FlowForTokenExchange() FlowForTokenExchange + } + + handlerDependencies interface { + ManagementProvider + PersistenceProvider + httpx.WriterProvider + otelx.Provider + logrusx.Provider + nosurfx.CSRFProvider + config.Provider + sessiontokenexchange.PersistenceProvider + FlowForTokenExchangeProvider + TokenizerProvider + } + HandlerProvider interface { + SessionHandler() *Handler + } + Handler struct{ r handlerDependencies } +) + +func NewHandler(r handlerDependencies) *Handler { return &Handler{r: r} } + +const ( + RouteCollection = "/sessions" + RouteExchangeCodeForSessionToken = RouteCollection + "/token-exchange" // #nosec G101 + RouteWhoami = RouteCollection + "/whoami" + RouteSession = RouteCollection + "/{id}" +) + +const ( + AdminRouteIdentity = "/identities" + AdminRouteIdentitiesSessions = AdminRouteIdentity + "/{id}/sessions" + AdminRouteSessionExtendId = RouteSession + "/extend" + + // ManageSessionsMaxIDs caps the number of explicit IDs accepted per call. + // Picked defensively — not validated against a production dataset. + // Tied to MaxManageSessionsBodySize in cloudlib/ratelimit/inflight: 500 UUIDs + // serialise to ~20 KiB, so 32 KiB has ~40% headroom. Raise both together. + ManageSessionsMaxIDs = 500 + + // manageSessionsWildcardBatchSize is the per-statement chunk size for the + // wildcard ("*") variants. One HTTP call processes at most this many rows; + // the caller drains larger networks by re-issuing the request while the + // response reports `more: true`. + manageSessionsWildcardBatchSize = 5000 +) + +func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { + admin.GET(RouteCollection, h.adminListSessions) + admin.GET(RouteSession, h.getSession) + admin.DELETE(RouteSession, h.disableSession) + + admin.GET(AdminRouteIdentitiesSessions, h.listIdentitySessions) + admin.DELETE(AdminRouteIdentitiesSessions, h.deleteIdentitySessions) + admin.PATCH(AdminRouteSessionExtendId, h.adminSessionExtend) + admin.POST(RouteCollection, h.manageSessions) + + admin.DELETE(RouteCollection, redir.RedirectToPublicRoute(h.r)) +} + +func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { + // We need to completely ignore the whoami/logout path so that we do not accidentally set + // some cookie. + h.r.CSRFHandler().IgnorePath(RouteWhoami) + h.r.CSRFHandler().IgnorePath(RouteCollection) + h.r.CSRFHandler().IgnoreGlob(RouteCollection + "/*") + h.r.CSRFHandler().IgnoreGlob(RouteCollection + "/*/extend") + h.r.CSRFHandler().IgnoreGlob(AdminRouteIdentity + "/*/sessions") + + for _, m := range []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodConnect, http.MethodOptions, http.MethodTrace} { + public.Handle(m+" "+RouteWhoami, http.HandlerFunc(h.whoami)) + } + + public.DELETE(RouteCollection, h.deleteMySessions) + public.DELETE(RouteSession, h.deleteMySession) + public.GET(RouteCollection, h.listMySessions) + + public.GET(RouteExchangeCodeForSessionToken, h.exchangeCode) + + public.DELETE(AdminRouteIdentitiesSessions, redir.RedirectToAdminRoute(h.r)) +} + +// Check Session Request Parameters +// +// swagger:parameters toSession +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type toSession struct { + // Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that + // scenario you must include the HTTP Cookie Header which originally was included in the request to your server. + // An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`. + // + // It is ok if more than one cookie are included here as all other cookies will be ignored. + // + // in: header + Cookie string `json:"Cookie"` + + // Returns the session additionally as a token (such as a JWT) + // + // The value of this parameter has to be a valid, configured Ory Session token template. For more information head over to [the documentation](http://ory.sh/docs/identities/session-to-jwt-cors). + // + // in: query + TokenizeAs string `json:"tokenize_as"` +} + +// swagger:route GET /sessions/whoami frontend toSession +// +// # Check Who the Current HTTP Session Belongs To +// +// Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated. +// Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. +// When the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header +// in the response. +// +// If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: +// +// ```js +// // pseudo-code example +// router.get('/protected-endpoint', async function (req, res) { +// const session = await client.toSession(undefined, req.header('cookie')) +// +// // console.log(session) +// }) +// ``` +// +// When calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token: +// +// ```js +// // pseudo-code example +// // ... +// const session = await client.toSession("the-session-token") +// +// // console.log(session) +// ``` +// +// When using a token template, the token is included in the `tokenized` field of the session. +// +// ```js +// // pseudo-code example +// // ... +// const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) +// +// console.log(session.tokenized) // The JWT +// ``` +// +// Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator +// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn +// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user +// to sign in with the second factor or change the configuration. +// +// This endpoint is useful for: +// +// - AJAX calls. Remember to send credentials and set up CORS correctly! +// - Reverse proxies and API Gateways +// - Server-side calls - use the `X-Session-Token` header! +// +// This endpoint authenticates users by checking: +// +// - if the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie; +// - if the `Authorization: bearer ` HTTP header was set with a valid Ory Kratos Session Token; +// - if the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token. +// +// If none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code. +// +// As explained above, this request may fail due to several reasons. The `error.id` can be one of: +// +// - `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. +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: session +// 401: errorGeneric +// 403: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-high +func (h *Handler) whoami(w http.ResponseWriter, r *http.Request) { + ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.Handler.whoami") + defer span.End() + + // Whoami strips the credentials from the response below (CopyWithoutCredentials), so there is + // no point in loading them from the database: it is by far the most expensive part of fetching + // a session. + s, err := h.r.SessionManager().FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverythingButCredentials) + c := h.r.Config() + if err != nil { + // We cache errors (and set cache header only when configured) where no session was found. + if noSess := new(ErrNoActiveSessionFound); c.SessionWhoAmICaching(ctx) && errors.As(err, &noSess) && noSess.CredentialsMissing { + w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%d", int64(time.Minute.Seconds()))) + } + + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session found.") + h.r.Writer().WriteError(w, r, ErrNoSessionFound().WithWrap(err)) + return + } + + var aalErr *ErrAALNotSatisfied + if err := h.r.SessionManager().DoesSessionSatisfy(ctx, s, c.SessionWhoAmIAAL(ctx), + // For the time being we want to update the AAL in the database if it is unset. + UpsertAAL, + ); errors.As(err, &aalErr) { + h.r.Logger().WithRequest(r).WithError(err).Info("Session was found but AAL is not satisfied for calling this endpoint.") + h.r.Writer().WriteError(w, r, err) + return + } else if err != nil { + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("Unable to determine AAL.")) + return + } + + // s.Devices = nil + s.Identity = s.Identity.CopyWithoutCredentials() + + tokenizeTemplate := r.URL.Query().Get("tokenize_as") + if tokenizeTemplate != "" { + if err := h.r.SessionTokenizer().TokenizeSession(ctx, tokenizeTemplate, s); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + + // Set userId as the X-Kratos-Authenticated-Identity-Id header. + w.Header().Set("X-Kratos-Authenticated-Identity-Id", s.Identity.ID.String()) + + // Set Cache header only when configured, and when no tokenization is requested. + if c.SessionWhoAmICaching(ctx) && len(tokenizeTemplate) == 0 { + 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 { + h.r.Logger().WithRequest(r).WithError(err).Info("Could not re-issue cookie.") + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, s) +} + +// Delete Identity Session Parameters +// +// swagger:parameters deleteIdentitySessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type deleteIdentitySessions struct { + // ID is the identity's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route DELETE /admin/identities/{id}/sessions identity deleteIdentitySessions +// +// # Delete & Invalidate an Identity's Sessions +// +// Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 401: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) deleteIdentitySessions(w http.ResponseWriter, r *http.Request) { + iID, err := uuid.FromString(r.PathValue("id")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + if err := h.r.SessionPersister().DeleteSessionsByIdentity(r.Context(), iID); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Session List Request +// +// The request object for listing sessions in an administrative context. +// +// swagger:parameters listSessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listSessionsRequest struct { + keysetpagination.RequestParameters + + // Active is a boolean flag that filters out sessions based on the state. If no value is provided, all sessions are returned. + // + // required: false + // in: query + Active bool `json:"active"` + + // ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session. + // If no value is provided, the expandable properties are skipped. + // + // required: false + // in: query + 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. +// +// swagger:response listSessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listSessionsResponse struct { + keysetpagination.ResponseHeaders + + // The list of sessions found + // in: body + Sessions []Session +} + +// swagger:route GET /admin/sessions identity listSessions +// +// # List All Sessions +// +// Listing all sessions that exist. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: listSessions +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) adminListSessions(w http.ResponseWriter, r *http.Request) { + activeRaw := r.URL.Query().Get("active") + activeBool, err := strconv.ParseBool(activeRaw) + if activeRaw != "" && err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("could not parse parameter active")) + return + } + + var active *bool + if activeRaw != "" { + active = &activeBool + } + + // Parse request pagination parameters + opts, err := keysetpagination.Parse(r.URL.Query(), keysetpagination.NewMapPageToken) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("could not parse parameter page_size")) + return + } + + var expandables Expandables + if es, ok := r.URL.Query()["expand"]; ok { + for _, e := range es { + expand, ok := ParseExpandable(e) + if !ok { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Could not parse expand option: %s", e))) + return + } + expandables = append(expandables, expand) + } + } + + sess, nextPage, err := h.r.SessionPersister().ListSessions(r.Context(), active, opts, expandables) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + u := *r.URL + keysetpagination.Header(w, &u, nextPage) + h.r.Writer().Write(w, r, AdminSessions(sess)) +} + +// Session Get Request +// +// The request object for getting a session in an administrative context. +// +// swagger:parameters getSession +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type getSession struct { + // ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session. + // Example - ?expand=Identity&expand=Devices + // If no value is provided, the expandable properties are skipped. + // + // required: false + // in: query + ExpandOptions []SessionExpandable `json:"expand"` + + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route GET /admin/sessions/{id} identity getSession +// +// # Get Session +// +// This endpoint is useful for: +// +// - Getting a session object with all specified expandables that exist in an administrative context. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: session +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-high +func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) { + if r.PathValue("id") == "whoami" { + // for /admin/sessions/whoami redirect to the public route + redir.RedirectToPublicRoute(h.r)(w, r) + return + } + + sID, err := uuid.FromString(r.PathValue("id")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + + var expandables Expandables + + urlValues := r.URL.Query() + if es, ok := urlValues["expand"]; ok { + for _, e := range es { + expand, ok := ParseExpandable(e) + if !ok { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Could not parse expand option: %s", e))) + return + } + expandables = append(expandables, expand) + } + } + + sess, err := h.r.SessionPersister().GetSession(r.Context(), sID, expandables) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, AdminSession(*sess)) +} + +// List Identity Sessions Parameters +// +// swagger:parameters listIdentitySessions +// Deactivate Session Parameters +// +// swagger:parameters disableSession +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type disableSession struct { + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route DELETE /admin/sessions/{id} identity disableSession +// +// # Deactivate a Session +// +// Calling this endpoint deactivates the specified session. Session data is not deleted. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 401: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) disableSession(w http.ResponseWriter, r *http.Request) { + sID, err := uuid.FromString(r.PathValue("id")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + + if err := h.r.SessionPersister().RevokeSessionById(r.Context(), sID); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().WriteCode(w, r, http.StatusNoContent, nil) +} + +// List Identity Sessions Parameters +// +// swagger:parameters listIdentitySessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listIdentitySessionsRequest struct { + migrationpagination.RequestParameters + + // ID is the identity's ID. + // + // required: true + // in: path + ID string `json:"id"` + + // Active is a boolean flag that filters out sessions based on the state. If no value is provided, all sessions are returned. + // + // required: false + // in: query + Active bool `json:"active"` +} + +// List Identity Sessions Response +// +// swagger:response listIdentitySessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listIdentitySessionsResponse struct { + migrationpagination.ResponseHeaderAnnotation + + // in: body + Body []Session +} + +// swagger:route GET /admin/identities/{id}/sessions identity listIdentitySessions +// +// # List an Identity's Sessions +// +// This endpoint returns all sessions that belong to the given Identity. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: listIdentitySessions +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-medium +func (h *Handler) listIdentitySessions(w http.ResponseWriter, r *http.Request) { + iID, err := uuid.FromString(r.PathValue("id")) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))) + return + } + + activeRaw := r.URL.Query().Get("active") + activeBool, err := strconv.ParseBool(activeRaw) + if activeRaw != "" && err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError("could not parse parameter active"))) + return + } + + var active *bool + if activeRaw != "" { + active = &activeBool + } + + page, perPage := x.ParsePagination(r) + sess, total, err := h.r.SessionPersister().ListSessionsByIdentity(r.Context(), iID, active, page, perPage, uuid.Nil, ExpandEverything) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + x.PaginationHeader(w, *r.URL, total, page, perPage) + h.r.Writer().Write(w, r, AdminSessions(sess)) +} + +// Deleted Session Count +// +// swagger:model deleteMySessionsCount +type deleteMySessionsCount struct { + // The number of sessions that were revoked. + Count int `json:"count"` +} + +// Disable My Other Session Parameters +// +// swagger:parameters disableMyOtherSessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type disableMyOtherSessions struct { + // Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that + // scenario you must include the HTTP Cookie Header which originally was included in the request to your server. + // An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`. + // + // It is ok if more than one cookie are included here as all other cookies will be ignored. + // + // in: header + Cookie string `json:"Cookie"` +} + +// swagger:route DELETE /sessions frontend 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. +// +// Schemes: http, https +// +// Responses: +// 200: deleteMySessionsCount +// 400: errorGeneric +// 401: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) deleteMySessions(w http.ResponseWriter, r *http.Request) { + // Only session columns are read below, so skip loading the devices and the identity. + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing) + if err != nil { + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("No valid session cookie found.")) + return + } + + n, err := h.r.SessionPersister().RevokeSessionsIdentityExcept(r.Context(), s.IdentityID, s.ID) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().WriteCode(w, r, http.StatusOK, &deleteMySessionsCount{Count: n}) +} + +// Disable My Session Parameters +// +// swagger:parameters disableMySession +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type disableMySession struct { + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` + + // Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that + // scenario you must include the HTTP Cookie Header which originally was included in the request to your server. + // An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`. + // + // It is ok if more than one cookie are included here as all other cookies will be ignored. + // + // in: header + Cookie string `json:"Cookie"` +} + +// swagger:route DELETE /sessions/{id} frontend disableMySession +// +// # Disable one of my sessions +// +// Calling this endpoint invalidates the specified session. The current session cannot be revoked. +// Session data are not deleted. +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 401: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-low +func (h *Handler) deleteMySession(w http.ResponseWriter, r *http.Request) { + sid := r.PathValue("id") + if sid == "whoami" { + // Special case where we actually want to handle the whoami endpoint. + h.whoami(w, r) + return + } + + // Only session columns are read below, so skip loading the devices and the identity. + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing) + if err != nil { + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("No valid session cookie found.")) + return + } + + sessionID, err := uuid.FromString(sid) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + if sessionID == s.ID { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("cannot revoke current session").WithDebug("use the logout flow instead")) + return + } + + if err := h.r.SessionPersister().RevokeSession(r.Context(), s.IdentityID, sessionID); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().WriteCode(w, r, http.StatusNoContent, nil) +} + +// List My Session Parameters +// +// swagger:parameters listMySessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listMySessionsParameters struct { + migrationpagination.RequestParameters + + // Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`. + // + // in: header + SessionToken string `json:"X-Session-Token"` + + // Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that + // scenario you must include the HTTP Cookie Header which originally was included in the request to your server. + // An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`. + // + // It is ok if more than one cookie are included here as all other cookies will be ignored. + // + // in: header + Cookie string `json:"Cookie"` +} + +// List My Session Response +// +// swagger:response listMySessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type listMySessionsResponse struct { + migrationpagination.ResponseHeaderAnnotation + + // in: body + Body []Session +} + +// swagger:route GET /sessions frontend 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. +// +// Schemes: http, https +// +// Responses: +// 200: listMySessions +// 400: errorGeneric +// 401: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) listMySessions(w http.ResponseWriter, r *http.Request) { + // Only session columns are read below; DoesSessionSatisfy loads the identity lazily when the + // AAL check needs it. + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing) + if err != nil { + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithWrap(err).WithReasonf("No valid session cookie found."))) + return + } + + c := h.r.Config() + + var aalErr *ErrAALNotSatisfied + if err := h.r.SessionManager().DoesSessionSatisfy(r.Context(), s, c.SessionWhoAmIAAL(r.Context())); errors.As(err, &aalErr) { + h.r.Logger().WithRequest(r).WithError(err).Info("Session was found but AAL is not satisfied for calling this endpoint.") + h.r.Writer().WriteError(w, r, err) + return + } else if err != nil { + h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("Unable to determine AAL.")) + return + } + + page, perPage := x.ParsePagination(r) + sess, total, err := h.r.SessionPersister().ListSessionsByIdentity(r.Context(), s.IdentityID, new(true), page, perPage, s.ID, ExpandEverything) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + x.PaginationHeader(w, *r.URL, total, page, perPage) + h.r.Writer().Write(w, r, sess) +} + +type sessionInContext int + +const ( + sessionInContextKey sessionInContext = iota +) + +func (h *Handler) IsAuthenticated(wrap http.HandlerFunc, onUnauthenticated http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // The session is stored in the request context for downstream handlers, which may read + // any of its associations - keep the full expansion. + sess, err := h.r.SessionManager().FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverything) + if err != nil { + if onUnauthenticated != nil { + onUnauthenticated(w, r) + return + } + + h.r.Writer().WriteError(w, r, errors.WithStack(NewErrNoActiveSessionFound().WithReason("This endpoint can only be accessed with a valid session. Please log in and try again."))) + return + } + + wrap(w, r.WithContext(context.WithValue(ctx, sessionInContextKey, sess))) + } +} + +// swagger:parameters extendSession +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type extendSession struct { + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route PATCH /admin/sessions/{id}/extend identity 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. +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: session +// 204: emptyResponse +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) adminSessionExtend(w http.ResponseWriter, r *http.Request) { + id, err := uuid.FromString(r.PathValue("id")) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))) + return + } + + c := h.r.Config() + if err := h.r.SessionPersister().ExtendSession(r.Context(), id); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // Default behavior going forward. + if c.FeatureFlagFasterSessionExtend(r.Context()) { + w.WriteHeader(http.StatusNoContent) + return + } + + trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "legacy_slower_session_extend")) + + // 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) +} + +func (h *Handler) IsNotAuthenticated(wrap http.HandlerFunc, onAuthenticated http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := h.r.SessionManager().SessionActiveForRequest(r.Context(), r); err != nil { + if e := new(ErrNoActiveSessionFound); errors.As(err, &e) { + wrap(w, r) + return + } + h.r.Writer().WriteError(w, r, err) + return + } + + if onAuthenticated != nil { + onAuthenticated(w, r) + return + } + + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("This endpoint can only be accessed without a login session. Please log out and try again."))) + } +} + +func RedirectOnAuthenticated(d interface{ config.Provider }) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + returnTo, err := redir.SecureRedirectTo(r, d.Config().SelfServiceBrowserDefaultReturnTo(ctx), redir.SecureRedirectAllowSelfServiceURLs(d.Config().SelfPublicURL(ctx))) + if err != nil { + http.Redirect(w, r, d.Config().SelfServiceBrowserDefaultReturnTo(ctx).String(), http.StatusFound) + return + } + + http.Redirect(w, r, returnTo.String(), http.StatusFound) + } +} + +func RedirectOnUnauthenticated(to string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, to, http.StatusFound) + } +} + +func RespondWitherrorGenericOnAuthenticated(h herodot.Writer, err error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.WriteError(w, r, err) + } +} + +// Exchange Session Token Parameters +// +// swagger:parameters exchangeSessionToken +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type exchangeSessionToken struct { + // The part of the code return when initializing the flow. + // + // required: true + // in: query + InitCode string `json:"init_code"` + + // The part of the code returned by the return_to URL. + // + // required: true + // in: query + ReturnToCode string `json:"return_to_code"` +} + +// The Response for Registration Flows via API +// +// swagger:model successfulCodeExchangeResponse +type CodeExchangeResponse struct { + // 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! + Token string `json:"session_token,omitempty"` + + // The Session + // + // The session contains information about the user, the session device, and so on. + // This is only available for API flows, not for Browser flows! + // + // required: true + Session *Session `json:"session"` +} + +// swagger:route GET /sessions/token-exchange frontend exchangeSessionToken +// +// # Exchange Session Token +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: successfulNativeLogin +// 403: errorGeneric +// 404: errorGeneric +// 410: errorGeneric +// 422: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-public-medium +func (h *Handler) exchangeCode(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + initCode = r.URL.Query().Get("init_code") + returnToCode = r.URL.Query().Get("return_to_code") + ) + + if initCode == "" || returnToCode == "" { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReason(`"init_code" and "return_to_code" query params must be set`)) + return + } + + e, err := h.r.SessionTokenExchangePersister().GetExchangerFromCode(ctx, initCode, returnToCode) + if err != nil { + // The session might not be set because the flow encountered an error (e.g. a + // before-registration webhook rejected the request). Check whether the exchanger + // exists without requiring a session and, if so, return the flow with its error + // messages so that the client can act on them. + pending, pendingErr := h.r.SessionTokenExchangePersister().GetExchangerFromCodeAllowPending(ctx, initCode, returnToCode) + if pendingErr != nil { + h.r.Logger().WithRequest(r).WithError(pendingErr).Info("Could not look up pending session token exchanger.") + h.r.Writer().WriteError(w, r, herodot.ErrNotFound().WithReason(`no session yet for this "code"`)) + return + } + + f, fErr := h.r.FlowForTokenExchange().GetFlowForTokenExchange(ctx, pending.FlowID) + if fErr != nil { + h.r.Logger().WithRequest(r).WithError(fErr).Info("Could not look up flow for pending session token exchange.") + h.r.Writer().WriteError(w, r, herodot.ErrNotFound().WithReason(`no session yet for this "code"`)) + return + } + + h.r.Writer().WriteCode(w, r, http.StatusUnprocessableEntity, f) + return + } + + sess, err := h.r.SessionPersister().GetSession(ctx, e.SessionID.UUID, ExpandDefault) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, &CodeExchangeResponse{ + Token: sess.Token, + Session: sess, + }) +} + +// ManageSessionsAction enumerates the supported actions for the manage-sessions +// endpoint. +// +// swagger:enum ManageSessionsAction +type ManageSessionsAction string + +const ( + ManageSessionsActionDisable ManageSessionsAction = "disable" + ManageSessionsActionDelete ManageSessionsAction = "delete" +) + +// ManageSessionsAllToken is the explicit-consent value that callers must pass +// in `identities` or `sessions` to operate on every row in the network. It +// must be the only element of the array and may not be mixed with explicit +// IDs. +const ManageSessionsAllToken = "*" + +// Manage Sessions Body +// +// Body for the bulk session management endpoint. Exactly one of `identities` +// or `sessions` must be provided. To operate on every session in the network, +// pass `identities: ["*"]` — the wildcard must appear alone, never mixed with +// explicit IDs. +// +// swagger:model manageSessionsBody +type ManageSessionsBody struct { + // Action to perform on the matching sessions. + // + // required: true + // enum: disable,delete + Action ManageSessionsAction `json:"action"` + + // Identity IDs whose sessions should be disabled or deleted, or `["*"]` + // to operate on every session in the network. Mutually exclusive with + // `sessions`. + Identities []string `json:"identities"` + + // Session IDs to disable or delete. Mutually exclusive with `identities`. + // The wildcard `["*"]` is not accepted in this field — pass + // `identities: ["*"]` to scope the operation to every session in the + // network. + Sessions []string `json:"sessions"` +} + +// swagger:parameters manageSessions +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type manageSessionsParameters struct { + // in: body + // required: true + Body ManageSessionsBody +} + +// Manage Sessions Response +// +// Response body for the bulk session management endpoint. Reports how many +// rows the call processed and, for the wildcard variant, whether the network +// still has matching rows left. Explicit-IDs requests always return +// `more: false`. Wildcard callers drain the network by re-issuing the same +// request while `more` is `true`. +// +// swagger:model manageSessionsResponse +type ManageSessionsResponse struct { + // Number of sessions processed in this call. For `disable`, counts only + // sessions that were active before the call (already-inactive sessions + // are skipped). For `delete`, counts every matching row removed. + Processed int `json:"processed"` + + // True when the call reached the per-call batch limit and additional + // matching rows may remain. Always false for explicit-IDs requests. + More bool `json:"more"` +} + +// swagger:route POST /admin/sessions identity manageSessions +// +// # Manage sessions in bulk +// +// Disable or delete sessions for a list of identities or a list of sessions in +// a single call. The `action` field selects the operation: +// +// - `disable` — deactivate matching sessions (sets `active = false`, preserves +// audit data). +// - `delete` — permanently delete matching sessions. +// +// Exactly one of `identities` or `sessions` must be provided. To scope the +// operation to every session in the network, pass `identities: ["*"]`; the +// wildcard is not accepted in the `sessions` field. Up to 500 explicit IDs +// are accepted per call. +// +// All requests return `200 OK` with `{processed, more}`. `processed` reports +// how many rows the call affected; for `disable` it counts only sessions +// that were active before the call. `more` is `true` only when a wildcard +// request reached the per-call batch limit and additional rows may remain; +// callers drain the network by re-issuing the same request while `more` is +// `true`. Explicit-IDs requests always return `more: false`. +// +// Consumes: +// - application/json +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: manageSessionsResponse +// 400: errorGeneric +// 401: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: kratos-admin-low +func (h *Handler) manageSessions(w http.ResponseWriter, r *http.Request) { + var body ManageSessionsBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithReason("Invalid JSON body.")) + return + } + + switch body.Action { + case ManageSessionsActionDisable, ManageSessionsActionDelete: + default: + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReasonf( + "The 'action' field must be one of '%s' or '%s'.", + ManageSessionsActionDisable, ManageSessionsActionDelete)) + return + } + + identitiesSet := len(body.Identities) > 0 + sessionsSet := len(body.Sessions) > 0 + if identitiesSet == sessionsSet { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReason( + "Exactly one of 'identities' or 'sessions' must be a non-empty array.")) + return + } + + var resp *ManageSessionsResponse + var err error + switch { + case identitiesSet: + resp, err = h.manageByIdentities(r.Context(), body.Action, body.Identities) + case sessionsSet: + resp, err = h.manageBySessions(r.Context(), body.Action, body.Sessions) + default: + err = errors.WithStack(herodot.ErrInternalServerError().WithReason( + "neither 'identities' nor 'sessions' set after validation")) + } + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + h.r.Writer().Write(w, r, resp) +} + +// manageByIdentities applies the requested action to all sessions belonging +// to the given identity IDs, or to a single bounded batch of every session in +// the network when raw is ["*"]. `more` is set only on the wildcard variant +// when the call hit the per-call batch limit; explicit-IDs always return +// `more: false`. +func (h *Handler) manageByIdentities(ctx context.Context, action ManageSessionsAction, raw []string) (*ManageSessionsResponse, error) { + ids, wildcard, err := parseManageSessionsIDsOrWildcard(raw) + if err != nil { + return nil, herodot.ErrBadRequest().WithReasonf("Could not parse 'identities' field: %s", err) + } + + p := h.r.SessionPersister() + switch action { + case ManageSessionsActionDisable: + if wildcard { + return wildcardBatch(ctx, p.RevokeAllSessions) + } + n, err := p.RevokeSessionsByIdentities(ctx, ids) + return explicitBatch(n), err + case ManageSessionsActionDelete: + if wildcard { + return wildcardBatch(ctx, p.DeleteAllSessions) + } + n, err := p.DeleteSessionsByIdentities(ctx, ids) + return explicitBatch(n), err + default: + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf( + "unhandled session action %q", action)) + } +} + +// manageBySessions applies the requested action to the given explicit session +// IDs. The wildcard ["*"] is not supported in this field; callers that need +// network-wide scope must use `identities: ["*"]`. See manageByIdentities for +// the response-shape contract. +func (h *Handler) manageBySessions(ctx context.Context, action ManageSessionsAction, raw []string) (*ManageSessionsResponse, error) { + ids, err := parseManageSessionsIDs(raw) + if err != nil { + return nil, herodot.ErrBadRequest().WithReasonf("Could not parse 'sessions' field: %s", err) + } + p := h.r.SessionPersister() + switch action { + case ManageSessionsActionDisable: + n, err := p.RevokeSessionsByIDs(ctx, ids) + return explicitBatch(n), err + case ManageSessionsActionDelete: + n, err := p.DeleteSessionsByIDs(ctx, ids) + return explicitBatch(n), err + default: + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf( + "unhandled session action %q", action)) + } +} + +// parseManageSessionsIDsOrWildcard recognizes the network-wide wildcard form +// ["*"] and otherwise delegates to parseManageSessionsIDs. Use it in fields +// that accept the wildcard (currently `identities`); fields that do not +// accept wildcard should call parseManageSessionsIDs directly so the token is +// rejected. +func parseManageSessionsIDsOrWildcard(raw []string) (ids []uuid.UUID, wildcard bool, err error) { + if len(raw) == 1 && raw[0] == ManageSessionsAllToken { + return nil, true, nil + } + ids, err = parseManageSessionsIDs(raw) + return ids, false, err +} + +// parseManageSessionsIDs interprets a manage-sessions filter array as a list +// of explicit UUIDs and rejects any input containing the wildcard token. +// Callers that accept the wildcard must use parseManageSessionsIDsOrWildcard. +func parseManageSessionsIDs(raw []string) ([]uuid.UUID, error) { + if len(raw) == 0 { + return nil, errors.New("array must not be empty") + } + if len(raw) > ManageSessionsMaxIDs { + return nil, fmt.Errorf("at most %d IDs may be provided per call", ManageSessionsMaxIDs) + } + ids := make([]uuid.UUID, 0, len(raw)) + for _, s := range raw { + if s == ManageSessionsAllToken { + return nil, errors.New("the wildcard '*' is not accepted here") + } + id, err := uuid.FromString(s) + if err != nil { + return nil, fmt.Errorf("could not parse %q as UUID: %w", s, err) + } + ids = append(ids, id) + } + return ids, nil +} + +// wildcardBatch runs a single chunked bulk-session operation and packages the +// row count plus a `more` flag for the response. `more` is true when the call +// reached the per-call batch limit, signaling that the caller should re-issue +// the request to drain the rest. +// +// When the row count is an exact multiple of the batch size, `more` is set +// even though no rows are left; the caller will issue one extra request that +// returns `{processed: 0, more: false}`. This is intentional — distinguishing +// "exactly batch-size" from "batch-size and more" would cost an extra DB +// query on every call to save one round-trip in a rare edge case. +func wildcardBatch(ctx context.Context, op func(context.Context, int) (int, error)) (*ManageSessionsResponse, error) { + n, err := op(ctx, manageSessionsWildcardBatchSize) + if err != nil { + return nil, err + } + return &ManageSessionsResponse{ + Processed: n, + More: n == manageSessionsWildcardBatchSize, + }, nil +} + +// explicitBatch packages the row count from an explicit-IDs bulk call. +// `more` is always false because explicit-IDs requests are bounded by +// ManageSessionsMaxIDs and complete in a single statement. +func explicitBatch(n int) *ManageSessionsResponse { + return &ManageSessionsResponse{Processed: n, More: false} +} diff --git a/session/handler_test.go b/session/handler_test.go new file mode 100644 index 0000000..7288656 --- /dev/null +++ b/session/handler_test.go @@ -0,0 +1,1439 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/peterhellberg/link" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + . "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" +) + +func init() { + corpx.RegisterFakes() +} + +func send(code int) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(code) + } +} + +func TestSessionWhoAmI(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t) + ts, _, r, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + ctx := context.Background() + + email := "foo" + uuid.Must(uuid.NewV4()).String() + "@bar.sh" + externalID := x.NewUUID().String() + i := &identity.Identity{ + ID: x.NewUUID(), + ExternalID: sqlxx.NullString(externalID), + State: identity.StateActive, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{x.NewUUID().String()}, + Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + }, + }, + Traits: identity.Traits(`{"email": "` + email + `","baz":"bar","foo":true,"bar":2.5}`), + MetadataAdmin: []byte(`{"admin":"ma"}`), + MetadataPublic: []byte(`{"public":"mp"}`), + RecoveryAddresses: []identity.RecoveryAddress{ + { + Value: email, + Via: identity.AddressTypeEmail, + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: email, + Via: identity.AddressTypeEmail, + }, + }, + } + h, _ := testhelpers.MockSessionCreateHandlerWithIdentity(t, reg, i) + + r.GET("/set", h) + + t.Run("case=aal requirements", func(t *testing.T) { + h1, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, + newAAL2Identity(), + []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeWebAuthn}) + r.GET("/set/aal2-aal2", h1) + + h2, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, + newAAL2Identity(), + []identity.CredentialsType{identity.CredentialsTypePassword}) + r.GET("/set/aal2-aal1", h2) + + h3, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, + newAAL1Identity(), + []identity.CredentialsType{identity.CredentialsTypePassword}) + r.GET("/set/aal1-aal1", h3) + + run := func(t *testing.T, endpoint string, kind string, code int) string { + client := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set/"+kind) + + res, err := client.Get(ts.URL + endpoint) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + assert.EqualValues(t, code, res.StatusCode) + return string(body) + } + + for k, e := range map[string]string{ + "whoami": RouteWhoami, + "collection": RouteCollection, + } { + t.Run(fmt.Sprintf("endpoint=%s", k), func(t *testing.T) { + t.Run("case=aal2-aal2", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, e, "aal2-aal2", http.StatusOK) + }) + + t.Run("case=aal2-aal2", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + run(t, e, "aal2-aal2", http.StatusOK) + }) + + t.Run("case=aal2-aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + body := run(t, e, "aal2-aal1", http.StatusForbidden) + assert.EqualValues(t, NewErrAALNotSatisfied("").Reason(), gjson.Get(body, "error.reason").String(), body) + }) + + t.Run("case=aal2-aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + run(t, e, "aal2-aal1", http.StatusOK) + }) + + t.Run("case=aal1-aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, e, "aal1-aal1", http.StatusOK) + }) + }) + } + }) + + t.Run("case=http methods", func(t *testing.T) { + 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 + res, err := client.Get(ts.URL + RouteWhoami) + require.NoError(t, err) + testhelpers.AssertNoCSRFCookieInResponse(t, res) // Test that no CSRF cookie is ever set here. + + 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")) + } + + // Set cookie + reg.CSRFHandler().IgnorePath("/set") + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") + + // Cookie set -> 200 (GET) + for _, method := range []string{ + "GET", + "POST", + "PUT", + "DELETE", + } { + t.Run("http_method="+method, func(t *testing.T) { + req, err := http.NewRequest(method, ts.URL+RouteWhoami, nil) + require.NoError(t, err) + + res, err = client.Do(req) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + testhelpers.AssertNoCSRFCookieInResponse(t, res) // Test that no CSRF cookie is ever set here. + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) + + if cacheEnabled { + 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")) + } + + assert.Empty(t, gjson.GetBytes(body, "identity.credentials")) + assert.Equal(t, "mp", gjson.GetBytes(body, "identity.metadata_public.public").String(), "%s", body) + assert.False(t, gjson.GetBytes(body, "identity.metadata_admin").Exists()) + + assert.NotEmpty(t, gjson.GetBytes(body, "identity.recovery_addresses").String(), "%s", body) + assert.NotEmpty(t, gjson.GetBytes(body, "identity.verifiable_addresses").String(), "%s", body) + + assert.Equal(t, externalID, gjson.GetBytes(body, "identity.external_id").String(), "%s", body) + }) + } + } + + t.Run("cache disabled", func(t *testing.T) { + run(t, false, 0) + }) + + t.Run("cache enabled", func(t *testing.T) { + run(t, true, 0) + }) + + t.Run("cache enabled with max age", func(t *testing.T) { + run(t, true, time.Minute) + }) + }) + + t.Run("tokenize", func(t *testing.T) { + conf.MustSet(t.Context(), config.ViperKeySessionTokenizerTemplates+".es256", &config.SessionTokenizeFormat{ + TTL: time.Minute, + JWKSURL: "file://stub/jwk.es256.json", + }) + conf.MustSet(ctx, config.ViperKeySessionWhoAmICaching, true) + + h3, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, newAAL1Identity(), []identity.CredentialsType{identity.CredentialsTypePassword}) + r.GET("/set/tokenize", h3) + + client := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set/"+"tokenize") + + res, err := client.Get(ts.URL + RouteWhoami + "?tokenize_as=es256") + require.NoError(t, err) + body := x.MustReadAll(res.Body) + assert.EqualValues(t, http.StatusOK, res.StatusCode, string(body)) + + token := gjson.GetBytes(body, "tokenized").String() + require.NotEmpty(t, token) + segments := strings.Split(token, ".") + require.Len(t, segments, 3, token) + decoded, err := base64.RawURLEncoding.DecodeString(segments[1]) + require.NoError(t, err) + + assert.NotEmpty(t, gjson.GetBytes(decoded, "sub").Str, decoded) + assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) + }) + + /* + t.Run("case=respects AAL config", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + + t.Run("required_aal=aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + + i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + 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) + + req, err := http.NewPostRequest("GET", pts.URL+"/session/get", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+s.Token) + + c := testhelpers.NewTestClient(t) + res, err := c.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("required_aal=aal2", func(t *testing.T) { + idAAL2 := identity.Identity{Traits: []byte("{}"), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte("{}")}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte("{}")}, + }} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &idAAL2)) + + idAAL1 := identity.Identity{Traits: []byte("{}"), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte("{}")}, + }} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &idAAL1)) + + run := func(t *testing.T, complete []identity.CredentialsType, expectedCode int, i *identity.Identity) { + + s := session.NewInactiveSession() + for _, m := range complete { + s.CompletedLoginFor(m) + } + require.NoError(t, s.Activate(i, conf, time.Now().UTC())) + + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) + require.NotEmpty(t, s.Token) + + req, err := http.NewPostRequest("GET", pts.URL+"/session/get", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+s.Token) + + c := testhelpers.NewTestClient(t) + res, err := c.Do(req) + require.NoError(t, err) + assert.EqualValues(t, expectedCode, res.StatusCode) + } + + t.Run("fulfilled for aal2 if identity has aal2", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeWebAuthn}, 200, &idAAL2) + }) + + t.Run("rejected for aal1 if identity has aal2", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, 403, &idAAL2) + }) + + t.Run("fulfilled for aal1 if identity has aal2 but config is aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, 200, &idAAL2) + }) + + t.Run("fulfilled for aal2 if identity has aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeWebAuthn}, 200, &idAAL1) + }) + + t.Run("fulfilled for aal1 if identity has aal1", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, 200, &idAAL1) + }) + }) + }) + */ +} + +func TestIsNotAuthenticatedSecurecookie(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + r := httprouterx.NewRouterPublic() + r.GET("/public/with-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) + + ts := httptest.NewServer(r) + defer ts.Close() + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + + c := testhelpers.NewClientWithCookies(t) + c.Jar.SetCookies(urlx.ParseOrPanic(ts.URL), []*http.Cookie{ + { + Name: config.DefaultSessionCookieName, + // This is an invalid cookie because it is generated by a very random secret + Value: "MTU3Mjg4Njg0MXxEdi1CQkFFQ180SUFBUkFCRUFBQU52LUNBQUVHYzNSeWFXNW5EQVVBQTNOcFpBWnpkSEpwYm1jTUd3QVpUWFZXVUhSQlZVeExXRWRUUmxkVVoyUkpUVXhzY201SFNBPT187kdI3dMP-ep389egDR2TajYXGG-6xqC2mAlgnBi0vsg=", + HttpOnly: true, + Path: "/", + Expires: time.Now().Add(time.Hour), + }, + }) + + res, err := c.Get(ts.URL + "/public/with-callback") + require.NoError(t, err) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) +} + +func TestIsNotAuthenticated(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + r := httprouterx.NewRouterPublic() + + reg.WithCSRFHandler(new(nosurfx.FakeCSRFHandler)) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) + r.GET("/set", h) + r.GET("/public/with-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) + r.GET("/public/without-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), nil)) + ts := httptest.NewServer(r) + defer ts.Close() + + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + + sessionClient := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") + + for k, tc := range []struct { + c *http.Client + call string + code int + }{ + { + c: sessionClient, + call: "/public/with-callback", + code: http.StatusBadRequest, + }, + { + c: testhelpers.NewTestClient(t), + call: "/public/with-callback", + code: http.StatusOK, + }, + + { + c: sessionClient, + call: "/public/without-callback", + code: http.StatusForbidden, + }, + { + c: testhelpers.NewTestClient(t), + call: "/public/without-callback", + code: http.StatusOK, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + res, err := tc.c.Get(ts.URL + tc.call) + require.NoError(t, err) + + assert.EqualValues(t, tc.code, res.StatusCode) + }) + } +} + +func TestIsAuthenticated(t *testing.T) { + t.Parallel() + + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + reg.WithCSRFHandler(new(nosurfx.FakeCSRFHandler)) + r := httprouterx.NewRouterPublic() + + h, _ := testhelpers.MockSessionCreateHandler(t, reg) + r.GET("/set", h) + r.GET("/privileged/with-callback", reg.SessionHandler().IsAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) + r.GET("/privileged/without-callback", reg.SessionHandler().IsAuthenticated(send(http.StatusOK), nil)) + ts := httptest.NewServer(r) + defer ts.Close() + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + + sessionClient := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") + + for k, tc := range []struct { + c *http.Client + call string + code int + }{ + { + c: sessionClient, + call: "/privileged/with-callback", + code: http.StatusOK, + }, + { + c: testhelpers.NewTestClient(t), + call: "/privileged/with-callback", + code: http.StatusBadRequest, + }, + + { + c: sessionClient, + call: "/privileged/without-callback", + code: http.StatusOK, + }, + { + c: testhelpers.NewTestClient(t), + call: "/privileged/without-callback", + code: http.StatusUnauthorized, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + res, err := tc.c.Get(ts.URL + tc.call) + require.NoError(t, err) + + assert.EqualValues(t, tc.code, res.StatusCode) + }) + } +} + +func TestHandlerAdminSessionManagement(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json"))) + public, ts := testhelpers.NewKratosServer(t, reg) + + t.Run("case=should return 202 after invalidating all sessions", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + var s *Session + require.NoError(t, faker.FakeData(&s)) + s.Active = true + s.AMR = AuthenticationMethods{ + {Method: identity.CredentialsTypePassword, CompletedAt: time.Now().UTC().Round(time.Second)}, + {Method: identity.CredentialsTypeOIDC, CompletedAt: time.Now().UTC().Round(time.Second)}, + } + s.Identity.MetadataAdmin = []byte(`{"admin":"data"}`) + require.NoError(t, reg.Persister().CreateIdentity(t.Context(), s.Identity)) + + var expectedSessionDevice Device + require.NoError(t, faker.FakeData(&expectedSessionDevice)) + s.Devices = []Device{ + expectedSessionDevice, + } + + assert.Zero(t, s.ID) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), s)) + assert.NotZero(t, s.ID) + assert.NotZero(t, s.Identity.ID) + + t.Run("get session", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + var session Session + require.NoError(t, json.NewDecoder(res.Body).Decode(&session)) + assert.Equal(t, s.ID, session.ID) + assert.Nil(t, session.Identity) + assert.Empty(t, session.Devices) + }) + + t.Run("get session expand", func(t *testing.T) { + for _, tc := range []struct { + description string + expand string + expectedIdentityId string + expectedDevices int + }{ + { + description: "expand Identity", + expand: "?expand=Identity", + expectedIdentityId: s.Identity.ID.String(), + expectedDevices: 0, + }, + { + description: "expand Devices", + expand: "/?expand=Devices", + expectedIdentityId: "", + expectedDevices: 1, + }, + { + description: "expand Identity and Devices", + expand: "/?expand=Identity&expand=Devices", + expectedIdentityId: s.Identity.ID.String(), + expectedDevices: 1, + }, + } { + t.Run(fmt.Sprintf("description=%s", tc.description), func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String()+tc.expand, nil) + res, err := client.Do(req) + require.NoError(t, err) + + body := ioutilx.MustReadAll(res.Body) + require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body) + + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "id").String()) + assert.Equal(t, tc.expectedIdentityId, gjson.GetBytes(body, "identity.id").String()) + assert.EqualValuesf(t, tc.expectedDevices, gjson.GetBytes(body, "devices.#").Int(), "%s", gjson.GetBytes(body, "devices").Raw) + if tc.expectedIdentityId != "" { + assert.True(t, gjson.GetBytes(body, "identity.metadata_admin").Exists(), "expected metadata_admin to be present in admin session response, body: %s", body) + } + }) + } + }) + + t.Run("get session expand invalid", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String()+"/?expand=invalid", nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("should redirect to public for whoami", func(t *testing.T) { + client := testhelpers.NewHTTPClientWithSessionToken(t.Context(), t, reg, s) + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+"/admin/sessions/whoami", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode) + require.Equal(t, public.URL+"/sessions/whoami", res.Header.Get("Location")) + }) + + assertPageToken := func(t *testing.T, id, linkHeader string) { + t.Helper() + + g := link.Parse(linkHeader) + require.Len(t, g, 1) + u, err := url.Parse(g["first"].URI) + require.NoError(t, err) + pt, err := keysetpagination.NewMapPageToken(u.Query().Get("page_token")) + require.NoError(t, err) + mpt := pt.(keysetpagination.MapPageToken) + assert.Equal(t, id, mpt["id"]) + } + + t.Run("list sessions", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/", nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + assertPageToken(t, uuid.Nil.String(), res.Header.Get("Link")) + + var sessions []Session + require.NoError(t, json.NewDecoder(res.Body).Decode(&sessions)) + require.Len(t, sessions, 1) + assert.Equal(t, s.ID, sessions[0].ID) + assert.Empty(t, sessions[0].Identity) + assert.Empty(t, sessions[0].Devices) + }) + + t.Run("list sessions expand", func(t *testing.T) { + for _, tc := range []struct { + description string + expand string + expectedIdentityId string + expectedDevicesCount string + }{ + { + description: "expand nothing", + expand: "", + expectedIdentityId: "", + expectedDevicesCount: "", + }, + { + description: "expand Identity", + expand: "expand=identity&", + expectedIdentityId: s.Identity.ID.String(), + expectedDevicesCount: "", + }, + { + description: "expand Devices", + expand: "expand=devices&", + expectedIdentityId: "", + expectedDevicesCount: "1", + }, + { + description: "expand Identity and Devices", + expand: "expand=identity&expand=devices&", + expectedIdentityId: s.Identity.ID.String(), + expectedDevicesCount: "1", + }, + } { + t.Run(fmt.Sprintf("description=%s", tc.description), func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions?"+tc.expand, nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assertPageToken(t, uuid.Nil.String(), res.Header.Get("Link")) + + body := ioutilx.MustReadAll(res.Body) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "0.id").String()) + assert.Equal(t, tc.expectedIdentityId, gjson.GetBytes(body, "0.identity.id").String()) + assert.Equal(t, tc.expectedDevicesCount, gjson.GetBytes(body, "0.devices.#").String()) + if tc.expectedIdentityId != "" { + assert.True(t, gjson.GetBytes(body, "0.identity.metadata_admin").Exists(), "expected metadata_admin to be present in admin session response, body: %s", body) + } + }) + } + }) + + t.Run("should list sessions for an identity", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/identities/"+s.Identity.ID.String()+"/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + require.Equal(t, 1, int(gjson.GetBytes(body, "#").Int())) + assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "0.id").String()) + assert.True(t, gjson.GetBytes(body, "0.identity.metadata_admin").Exists(), "expected metadata_admin to be present in admin identity sessions response, body: %s", body) + }) + + t.Run("should revoke session by id", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + var session Session + require.NoError(t, json.NewDecoder(res.Body).Decode(&session)) + assert.Equal(t, s.ID, session.ID) + assert.True(t, session.Active) + + req, _ = http.NewRequest("DELETE", ts.URL+"/admin/sessions/"+s.ID.String(), nil) + res, err = client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + + req, _ = http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String(), nil) + res, err = client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + require.NoError(t, json.NewDecoder(res.Body).Decode(&session)) + assert.Equal(t, s.ID, session.ID) + assert.False(t, session.Active) + }) + + t.Run("case=session status should be false when session expiry is past", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + + s.ExpiresAt = time.Now().Add(-time.Hour * 1) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), s)) + + assert.NotEqual(t, uuid.Nil, s.ID) + assert.NotEqual(t, uuid.Nil, s.Identity.ID) + + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s.ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, "false", gjson.GetBytes(body, "active").String(), "%s", body) + }) + + t.Run("case=session status should be false for inactive identity", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + var s1 *Session + require.NoError(t, faker.FakeData(&s1)) + s1.Active = true + s1.Identity.State = identity.StateInactive + require.NoError(t, reg.Persister().CreateIdentity(t.Context(), s1.Identity)) + + assert.Equal(t, uuid.Nil, s1.ID) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), s1)) + assert.NotEqual(t, uuid.Nil, s1.ID) + assert.NotEqual(t, uuid.Nil, s1.Identity.ID) + + req, _ := http.NewRequest("GET", ts.URL+"/admin/sessions/"+s1.ID.String()+"?expand=Identity", nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, "false", gjson.GetBytes(body, "active").String(), "%s", body) + }) + + req, _ := http.NewRequest("DELETE", ts.URL+"/admin/identities/"+s.Identity.ID.String()+"/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + _, err = reg.SessionPersister().GetSession(t.Context(), s.ID, ExpandNothing) + require.True(t, errors.Is(err, sqlcon.ErrNoRows())) + + t.Run("should not list session", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/admin/identities/"+s.Identity.ID.String()+"/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.JSONEq(t, "[]", string(ioutilx.MustReadAll(res.Body))) + }) + }) + + t.Run("case=should return 400 when bad UUID is sent", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + + for _, method := range []string{http.MethodGet, http.MethodDelete} { + t.Run("http method="+method, func(t *testing.T) { + req, _ := http.NewRequest(method, ts.URL+"/admin/identities/BADUUID/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + } + }) + + t.Run("case=should return 404 when deleting with unknown UUID", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + someID, _ := uuid.NewV4() + req, _ := http.NewRequest("DELETE", ts.URL+"/admin/identities/"+someID.String()+"/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("case=should return pagination headers on list response", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + var i *identity.Identity + require.NoError(t, faker.FakeData(&i)) + require.NoError(t, reg.Persister().CreateIdentity(t.Context(), i)) + + numSessions := 5 + numSessionsActive := 2 + + sess := make([]Session, numSessions) + for j := range sess { + require.NoError(t, faker.FakeData(&sess[j])) + sess[j].Identity = i + if j < numSessionsActive { + sess[j].Active = true + sess[j].ExpiresAt = time.Now().UTC().Add(time.Hour) + } else { + sess[j].Active = false + sess[j].ExpiresAt = time.Now().UTC().Add(-time.Hour) + } + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &sess[j])) + } + + for _, tc := range []struct { + activeOnly string + expectedSessionIds []uuid.UUID + }{ + { + activeOnly: "true", + expectedSessionIds: []uuid.UUID{sess[0].ID, sess[1].ID}, + }, + { + activeOnly: "false", + expectedSessionIds: []uuid.UUID{sess[2].ID, sess[3].ID, sess[4].ID}, + }, + { + activeOnly: "", + expectedSessionIds: []uuid.UUID{sess[0].ID, sess[1].ID, sess[2].ID, sess[3].ID, sess[4].ID}, + }, + } { + t.Run(fmt.Sprintf("active=%#v", tc.activeOnly), func(t *testing.T) { + sessions, _, err := reg.SessionPersister().ListSessionsByIdentity(t.Context(), i.ID, nil, 1, 10, uuid.Nil, ExpandEverything) + require.NoError(t, err) + require.Len(t, sessions, 5) + assert.True(t, sort.IsSorted(sort.Reverse(byCreatedAt(sessions)))) + + reqURL := ts.URL + "/admin/identities/" + i.ID.String() + "/sessions" + if tc.activeOnly != "" { + reqURL += "?active=" + tc.activeOnly + } + req, _ := http.NewRequest("GET", reqURL, nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + var actualSessions []Session + require.NoError(t, json.NewDecoder(res.Body).Decode(&actualSessions)) + actualSessionIds := make([]uuid.UUID, 0) + for _, s := range actualSessions { + actualSessionIds = append(actualSessionIds, s.ID) + } + + assert.NotEqual(t, "", res.Header.Get("Link")) + assert.ElementsMatch(t, tc.expectedSessionIds, actualSessionIds) + }) + } + }) +} + +func TestHandlerManageSessions(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json"))) + _, ts := testhelpers.NewKratosServer(t, reg) + client := testhelpers.NewClientWithCookies(t) + + makeIdentityWithSession := func(t *testing.T) (*identity.Identity, *Session) { + var s *Session + require.NoError(t, faker.FakeData(&s)) + s.Active = true + require.NoError(t, reg.Persister().CreateIdentity(t.Context(), s.Identity)) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), s)) + return s.Identity, s + } + + post := func(t *testing.T, body string) *http.Response { + req, err := http.NewRequest("POST", ts.URL+"/admin/sessions", strings.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + return res + } + + decode := func(t *testing.T, res *http.Response) ManageSessionsResponse { + t.Helper() + var resp ManageSessionsResponse + require.NoError(t, json.NewDecoder(res.Body).Decode(&resp)) + return resp + } + + sessionActive := func(t *testing.T, sID uuid.UUID) bool { + s, err := reg.SessionPersister().GetSession(t.Context(), sID, ExpandNothing) + require.NoError(t, err) + return s.Active + } + + sessionExists := func(t *testing.T, sID uuid.UUID) bool { + _, err := reg.SessionPersister().GetSession(t.Context(), sID, ExpandNothing) + if errors.Is(err, sqlcon.ErrNoRows()) { + return false + } + require.NoError(t, err) + return true + } + + t.Run("case=disable by identities", func(t *testing.T) { + i, s := makeIdentityWithSession(t) + body := fmt.Sprintf(`{"action":"disable","identities":["%s"]}`, i.ID) + res := post(t, body) + require.Equal(t, http.StatusOK, res.StatusCode) + resp := decode(t, res) + assert.Equal(t, 1, resp.Processed) + assert.False(t, resp.More) + assert.False(t, sessionActive(t, s.ID)) + }) + + t.Run("case=disable by sessions", func(t *testing.T) { + _, s := makeIdentityWithSession(t) + body := fmt.Sprintf(`{"action":"disable","sessions":["%s"]}`, s.ID) + res := post(t, body) + require.Equal(t, http.StatusOK, res.StatusCode) + resp := decode(t, res) + assert.Equal(t, 1, resp.Processed) + assert.False(t, resp.More) + assert.False(t, sessionActive(t, s.ID)) + }) + + t.Run("case=delete by identities", func(t *testing.T) { + i, s := makeIdentityWithSession(t) + body := fmt.Sprintf(`{"action":"delete","identities":["%s"]}`, i.ID) + res := post(t, body) + require.Equal(t, http.StatusOK, res.StatusCode) + resp := decode(t, res) + assert.Equal(t, 1, resp.Processed) + assert.False(t, resp.More) + assert.False(t, sessionExists(t, s.ID)) + }) + + t.Run("case=delete by sessions", func(t *testing.T) { + _, s := makeIdentityWithSession(t) + body := fmt.Sprintf(`{"action":"delete","sessions":["%s"]}`, s.ID) + res := post(t, body) + require.Equal(t, http.StatusOK, res.StatusCode) + resp := decode(t, res) + assert.Equal(t, 1, resp.Processed) + assert.False(t, resp.More) + assert.False(t, sessionExists(t, s.ID)) + }) + + t.Run("case=invalid action", func(t *testing.T) { + res := post(t, `{"action":"flarble","identities":["00000000-0000-0000-0000-000000000001"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=both filters set", func(t *testing.T) { + res := post(t, `{"action":"disable","identities":["00000000-0000-0000-0000-000000000001"],"sessions":["00000000-0000-0000-0000-000000000002"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=neither filter set", func(t *testing.T) { + res := post(t, `{"action":"disable"}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=empty array", func(t *testing.T) { + res := post(t, `{"action":"disable","identities":[]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=malformed UUID", func(t *testing.T) { + res := post(t, `{"action":"disable","identities":["not-a-uuid"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=wildcard mixed with explicit id rejected", func(t *testing.T) { + res := post(t, `{"action":"disable","identities":["*","00000000-0000-0000-0000-000000000001"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=wildcard via sessions field rejected (disable)", func(t *testing.T) { + res := post(t, `{"action":"disable","sessions":["*"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=wildcard via sessions field rejected (delete)", func(t *testing.T) { + res := post(t, `{"action":"delete","sessions":["*"]}`) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=oversize batch rejected", func(t *testing.T) { + ids := make([]string, ManageSessionsMaxIDs+1) + for i := range ids { + ids[i] = uuid.Must(uuid.NewV4()).String() + } + body := fmt.Sprintf(`{"action":"disable","identities":[%q`, ids[0]) + for _, id := range ids[1:] { + body += "," + fmt.Sprintf("%q", id) + } + body += `]}` + res := post(t, body) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=non-existent identity is silent no-op", func(t *testing.T) { + ghost := uuid.Must(uuid.NewV4()).String() + res := post(t, fmt.Sprintf(`{"action":"disable","identities":[%q]}`, ghost)) + require.Equal(t, http.StatusOK, res.StatusCode) + resp := decode(t, res) + assert.Equal(t, 0, resp.Processed) + assert.False(t, resp.More) + }) + +} + +// TestHandlerManageSessionsWildcard exercises the `["*"]` wildcard variants +// of the manage-sessions endpoint. Each subtest disables or deletes every +// session in the network, so they are isolated to their own registry to avoid +// contaminating other tests that share state. +func TestHandlerManageSessionsWildcard(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + assertAfter func(t *testing.T, persister Persister, ids []uuid.UUID) + }{ + { + name: "case=wildcard disable disables all sessions in network", + body: `{"action":"disable","identities":["*"]}`, + assertAfter: func(t *testing.T, persister Persister, ids []uuid.UUID) { + for _, id := range ids { + s, err := persister.GetSession(t.Context(), id, ExpandNothing) + require.NoError(t, err) + assert.False(t, s.Active, "session %s must be inactive", id) + } + }, + }, + { + name: "case=wildcard delete deletes all sessions in network", + body: `{"action":"delete","identities":["*"]}`, + assertAfter: func(t *testing.T, persister Persister, ids []uuid.UUID) { + for _, id := range ids { + _, err := persister.GetSession(t.Context(), id, ExpandNothing) + assert.ErrorIs(t, err, sqlcon.ErrNoRows(), "session %s must be deleted", id) + } + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json"))) + _, ts := testhelpers.NewKratosServer(t, reg) + client := testhelpers.NewClientWithCookies(t) + + ids := make([]uuid.UUID, 0, 2) + for range 2 { + var s *Session + require.NoError(t, faker.FakeData(&s)) + s.Active = true + require.NoError(t, reg.Persister().CreateIdentity(t.Context(), s.Identity)) + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), s)) + ids = append(ids, s.ID) + } + + req, err := http.NewRequest("POST", ts.URL+"/admin/sessions", strings.NewReader(tc.body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + require.NoError(t, err) + t.Cleanup(func() { _ = res.Body.Close() }) + + require.Equal(t, http.StatusOK, res.StatusCode, "wildcard requests return 200 with a body") + var resp ManageSessionsResponse + require.NoError(t, json.NewDecoder(res.Body).Decode(&resp)) + assert.Equal(t, 2, resp.Processed, "the two seeded sessions are processed in the single batch") + assert.False(t, resp.More, "two sessions fit into one batch; nothing more to drain") + tc.assertAfter(t, reg.SessionPersister(), ids) + }) + } +} + +func TestHandlerSelfServiceSessionManagement(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + ts, _, r, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + var setup func(t *testing.T) (*http.Client, *identity.Identity, *Session) + { + // we limit the scope of the channels, so you cannot accidentally mess up a test case + ident := make(chan *identity.Identity, 1) + sess := make(chan *Session, 1) + r.GET("/set", func(w http.ResponseWriter, r *http.Request) { + h, s := testhelpers.MockSessionCreateHandlerWithIdentity(t, reg, <-ident) + h(w, r) + sess <- s + }) + + setup = func(t *testing.T) (*http.Client, *identity.Identity, *Session) { + client := testhelpers.NewClientWithCookies(t) + i := identity.NewIdentity("") // the identity is created by the handler + + ident <- i + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") + return client, i, <-sess + } + } + + t.Run("case=list should return pagination headers", func(t *testing.T) { + client, i, _ := setup(t) + + numSessions := 5 + numSessionsActive := 2 + + sess := make([]Session, numSessions) + for j := range sess { + require.NoError(t, faker.FakeData(&sess[j])) + sess[j].Identity = i + if j < numSessionsActive { + sess[j].Active = true + } else { + sess[j].Active = false + } + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &sess[j])) + } + + reqURL := ts.URL + "/sessions" + req, _ := http.NewRequest("GET", reqURL, nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + require.NotEqual(t, "", res.Header.Get("Link")) + }) + + t.Run("case=should return 200 and number after invalidating all other sessions", func(t *testing.T) { + client, i, currSess := setup(t) + + otherSess := Session{} + require.NoError(t, faker.FakeData(&otherSess)) + otherSess.Identity = i + otherSess.Active = true + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &otherSess)) + + req, _ := http.NewRequest("DELETE", ts.URL+"/sessions", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, int64(1), gjson.GetBytes(body, "count").Int(), "%s", body) + + actualOther, err := reg.SessionPersister().GetSession(t.Context(), otherSess.ID, ExpandNothing) + require.NoError(t, err) + assert.False(t, actualOther.Active) + + actualCurr, err := reg.SessionPersister().GetSession(t.Context(), currSess.ID, ExpandNothing) + require.NoError(t, err) + assert.True(t, actualCurr.Active) + }) + + t.Run("case=should revoke specific other session", func(t *testing.T) { + client, i, _ := setup(t) + + others := make([]Session, 2) + for j := range others { + require.NoError(t, faker.FakeData(&others[j])) + others[j].Identity = i + others[j].Active = true + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &others[j])) + } + + req, _ := http.NewRequest("DELETE", ts.URL+"/sessions/"+others[0].ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + actualOthers, total, err := reg.SessionPersister().ListSessionsByIdentity(t.Context(), i.ID, nil, 1, 10, uuid.Nil, ExpandNothing) + require.NoError(t, err) + require.Len(t, actualOthers, 3) + require.EqualValues(t, 3, total) + + for _, s := range actualOthers { + if s.ID == others[0].ID { + assert.False(t, s.Active) + } else { + assert.True(t, s.Active) + } + } + }) + + t.Run("case=should not revoke current session", func(t *testing.T) { + client, _, currSess := setup(t) + + req, _ := http.NewRequest("DELETE", ts.URL+"/sessions/"+currSess.ID.String(), nil) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("case=should not error on unknown or revoked session", func(t *testing.T) { + client, i, _ := setup(t) + + otherSess := Session{} + require.NoError(t, faker.FakeData(&otherSess)) + otherSess.Identity = i + otherSess.Active = false + require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &otherSess)) + + for j, id := range []uuid.UUID{otherSess.ID, uuid.Must(uuid.NewV4())} { + req, _ := http.NewRequest("DELETE", ts.URL+"/sessions/"+id.String(), nil) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode, "case=%d", j) + } + }) + + t.Run("case=whoami should not issue cookie for up to date session", func(t *testing.T) { + client, _, _ := setup(t) + + req, _ := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + assert.Empty(t, resp.Cookies()) + }) + + t.Run("case=whoami should reissue cookie for outdated session", func(t *testing.T) { + client, _, session := setup(t) + oldExpires := session.ExpiresAt + + session.ExpiresAt = time.Now().Add(time.Hour * 24 * 30).UTC().Round(time.Hour) + err := reg.SessionPersister().UpsertSession(context.Background(), session) + require.NoError(t, err) + + resp, err := client.Get(ts.URL + "/sessions/whoami") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + require.Len(t, resp.Cookies(), 1) + for _, c := range resp.Cookies() { + assert.WithinDuration(t, session.ExpiresAt, c.Expires, 5*time.Second, "Ensure the expiry does not deviate +- 5 seconds from the expiry of the session for cookie: %s", c.Name) + assert.NotEqual(t, oldExpires, c.Expires, "%s", c.Name) + } + }) + + t.Run("case=whoami should not issue cookie if request is token based", func(t *testing.T) { + _, _, session := setup(t) + + session.ExpiresAt = time.Now().Add(time.Hour * 24 * 30).UTC().Round(time.Hour) + err := reg.SessionPersister().UpsertSession(context.Background(), session) + require.NoError(t, err) + + req, err := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+session.Token) + + resp, err := testhelpers.NewTestClient(t).Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + require.Len(t, resp.Cookies(), 0) + }) +} + +func TestHandlerRefreshSessionBySessionID(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + publicServer, adminServer, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + i := identity.NewIdentity("") + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + s := &Session{Identity: i, ExpiresAt: time.Now().Add(5 * time.Minute)} + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) + + t.Run("case=should return 200 after refreshing one session", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + + req, _ := http.NewRequest("PATCH", adminServer.URL+"/admin/sessions/"+s.ID.String()+"/extend", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + updatedSession, err := reg.SessionPersister().GetSession(context.Background(), s.ID, ExpandNothing) + require.Nil(t, err) + require.True(t, s.ExpiresAt.Before(updatedSession.ExpiresAt)) + }) + + t.Run("case=should return 400 when bad UUID is sent", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + req, _ := http.NewRequest("PATCH", adminServer.URL+"/admin/sessions/BADUUID/extend", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=should return 404 when calling with missing UUID", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + someID, _ := uuid.NewV4() + req, _ := http.NewRequest("PATCH", adminServer.URL+"/admin/sessions/"+someID.String()+"/extend", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("case=should return 404 when calling puplic server", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "PATCH", publicServer.URL+"/sessions/"+s.ID.String()+"/extend", nil) + + res, err := publicServer.Client().Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + body := ioutilx.MustReadAll(res.Body) + assert.NotEqual(t, gjson.GetBytes(body, "error.id").String(), "security_csrf_violation") + }) +} + +func TestExchangeCode(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + ts, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + ctx := context.Background() + + newRegistrationFlow := func(t *testing.T) *registration.Flow { + t.Helper() + req := &http.Request{URL: urlx.ParseOrPanic("/")} + f, err := registration.NewFlow(reg, req, flow.TypeAPI) + require.NoError(t, err) + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, f)) + return f + } + + exchangeURL := func(initCode, returnToCode string) string { + return fmt.Sprintf("%s/sessions/token-exchange?init_code=%s&return_to_code=%s", ts.URL, initCode, returnToCode) + } + + t.Run("case=returns 400 when codes are missing", func(t *testing.T) { + t.Parallel() + res, err := ts.Client().Get(ts.URL + "/sessions/token-exchange") + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=returns 404 for invalid codes", func(t *testing.T) { + t.Parallel() + res, err := ts.Client().Get(exchangeURL("invalid_init", "invalid_return")) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("case=returns 422 with flow when exchanger exists but has no session", func(t *testing.T) { + t.Parallel() + f := newRegistrationFlow(t) + + e, err := reg.SessionTokenExchangePersister().CreateSessionTokenExchanger(ctx, f.ID) + require.NoError(t, err) + + res, err := ts.Client().Get(exchangeURL(e.InitCode, e.ReturnToCode)) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + assert.Equal(t, http.StatusUnprocessableEntity, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, f.ID.String(), gjson.GetBytes(body, "id").String()) + }) + + t.Run("case=returns 404 when exchanger exists but flow was deleted", func(t *testing.T) { + t.Parallel() + // Create an exchanger with a flow ID that has no corresponding persisted flow. + orphanFlowID := uuid.Must(uuid.NewV4()) + e, err := reg.SessionTokenExchangePersister().CreateSessionTokenExchanger(ctx, orphanFlowID) + require.NoError(t, err) + + res, err := ts.Client().Get(exchangeURL(e.InitCode, e.ReturnToCode)) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("case=returns 200 with session when exchanger has a session", func(t *testing.T) { + t.Parallel() + f := newRegistrationFlow(t) + + e, err := reg.SessionTokenExchangePersister().CreateSessionTokenExchanger(ctx, f.ID) + require.NoError(t, err) + + i := identity.NewIdentity("") + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + req := &http.Request{URL: urlx.ParseOrPanic("/")} + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + require.NoError(t, reg.SessionTokenExchangePersister().UpdateSessionOnExchanger(ctx, f.ID, sess.ID)) + + res, err := ts.Client().Get(exchangeURL(e.InitCode, e.ReturnToCode)) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + + assert.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotEmpty(t, gjson.GetBytes(body, "session_token").String()) + assert.Equal(t, sess.ID.String(), gjson.GetBytes(body, "session.id").String()) + }) +} + +type byCreatedAt []Session + +func (s byCreatedAt) Len() int { return len(s) } +func (s byCreatedAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byCreatedAt) Less(i, j int) bool { + return s[i].CreatedAt.Before(s[j].CreatedAt) +} diff --git a/session/helper.go b/session/helper.go new file mode 100644 index 0000000..33edf4d --- /dev/null +++ b/session/helper.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "net/http" + "strings" +) + +func bearerTokenFromRequest(r *http.Request) (string, bool) { + parts := strings.Split(r.Header.Get("Authorization"), " ") + + if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" { + return parts[1], true + } + + return "", false +} diff --git a/session/helper_test.go b/session/helper_test.go new file mode 100644 index 0000000..afef0af --- /dev/null +++ b/session/helper_test.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBearerTokenFromRequest(t *testing.T) { + for k, tc := range []struct { + h http.Header + t string + f bool + }{ + { + h: http.Header{"Authorization": {"Bearer token"}}, + t: "token", f: true, + }, + { + h: http.Header{"Authorization": {"bearer token"}}, + t: "token", f: true, + }, + { + h: http.Header{"Authorization": {"beaRer token"}}, + t: "token", f: true, + }, + { + h: http.Header{"Authorization": {"BEARER token"}}, + t: "token", f: true, + }, + { + h: http.Header{"Authorization": {"notbearer token"}}, + }, + { + h: http.Header{"Authorization": {"token"}}, + }, + { + h: http.Header{"Authorization": {}}, + }, + { + h: http.Header{}, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, found := bearerTokenFromRequest(&http.Request{Header: tc.h}) + assert.Equal(t, tc.f, found) + assert.Equal(t, tc.t, token) + }) + } +} diff --git a/session/manager.go b/session/manager.go new file mode 100644 index 0000000..323d648 --- /dev/null +++ b/session/manager.go @@ -0,0 +1,191 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/ory/kratos/identity" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/swagger" + + "github.com/gofrs/uuid" + + "github.com/ory/herodot" +) + +// ErrNoActiveSessionFound is returned when no active cookie session could be found in the request. +type ErrNoActiveSessionFound struct { + *herodot.DefaultError `json:"error"` + + // True when the request had no credentials in it. + CredentialsMissing bool +} + +// NewErrNoActiveSessionFound creates a new ErrNoActiveSessionFound +func NewErrNoActiveSessionFound() *ErrNoActiveSessionFound { + return &ErrNoActiveSessionFound{ + DefaultError: herodot.ErrUnauthorized().WithID(text.ErrIDNoActiveSession).WithError("request does not have a valid authentication session").WithReason("No active session was found in this request."), + } +} + +// NewErrNoCredentialsForSession creates a new NewErrNoCredentialsForSession +func NewErrNoCredentialsForSession() *ErrNoActiveSessionFound { + e := NewErrNoActiveSessionFound() + e.CredentialsMissing = true + return e +} + +func (e *ErrNoActiveSessionFound) EnhanceJSONError() interface{} { + return e +} + +// Is returned when an active session was found but the requested AAL is not satisfied. +// +// swagger:model errorAuthenticatorAssuranceLevelNotSatisfied +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type errorAuthenticatorAssuranceLevelNotSatisfied struct { + Error swagger.GenericError `json:"error"` + + // Points to where to redirect the user to next. + RedirectTo string `json:"redirect_browser_to"` +} + +// ErrAALNotSatisfied is returned when an active session was found but the requested AAL is not satisfied. +type ErrAALNotSatisfied struct { + *herodot.DefaultError `json:"error"` + RedirectTo string `json:"redirect_browser_to"` +} + +func (e *ErrAALNotSatisfied) EnhanceJSONError() interface{} { + return e +} + +func (e *ErrAALNotSatisfied) PassReturnToAndLoginChallengeParameters(requestURL string) error { + req, err := url.Parse(requestURL) + if err != nil { + return err + } + + u, err := url.Parse(e.RedirectTo) + if err != nil { + return err + } + q := u.Query() + + hlc := req.Query().Get("login_challenge") + if len(hlc) != 0 { + q.Set("login_challenge", hlc) + } + + returnTo := req.Query().Get("return_to") + if len(returnTo) != 0 { + q.Set("return_to", returnTo) + } + + u.RawQuery = q.Encode() + e.RedirectTo = u.String() + + return nil +} + +// NewErrAALNotSatisfied creates a new ErrAALNotSatisfied. +func NewErrAALNotSatisfied(redirectTo string) *ErrAALNotSatisfied { + return &ErrAALNotSatisfied{ + RedirectTo: redirectTo, + DefaultError: &herodot.DefaultError{ + IDField: text.ErrIDHigherAALRequired, + StatusField: http.StatusText(http.StatusForbidden), + ErrorField: "Session does not fulfill the requested Authenticator Assurance Level", + ReasonField: "An active session was found but it does not fulfill the requested Authenticator Assurance Level. Please verify yourself with a second factor to resolve this issue.", + CodeField: http.StatusForbidden, + DetailsField: map[string]interface{}{ + "redirect_browser_to": redirectTo, + }, + }, + } +} + +// Manager handles identity sessions. +type Manager interface { + // UpsertAndIssueCookie stores a session in the database and issues a cookie by calling IssueCookie. + // + // Also regenerates CSRF tokens due to assumed principal change. + UpsertAndIssueCookie(context.Context, http.ResponseWriter, *http.Request, *Session) error + + // IssueCookie issues a cookie for the given session. + // + // Also regenerates CSRF tokens due to assumed principal change. + IssueCookie(context.Context, http.ResponseWriter, *http.Request, *Session) error + + // RefreshCookie checks if the request uses an outdated cookie and refreshes the cookie if needed. + RefreshCookie(context.Context, http.ResponseWriter, *http.Request, *Session) error + + // FetchFromRequest creates an HTTP session using cookies. + // + // sessionExpand controls which session associations (devices, identity) are loaded, and + // identityExpand controls which identity relations are loaded when sessionExpand includes + // ExpandSessionIdentity. Pass ExpandEverything/identity.ExpandEverything unless you are + // certain the caller never reads an association: expanding the identity's credentials in + // particular is by far the most expensive part of fetching a session, so hot paths that + // never read Identity.Credentials (such as /sessions/whoami) can pass + // identity.ExpandEverythingButCredentials, and callers that only need session columns + // (ID, IdentityID, AAL, ...) can pass ExpandNothing/identity.ExpandNothing. AAL + // enforcement stays intact: DoesSessionSatisfy loads the identity and hydrates the + // credentials lazily when it has to. + FetchFromRequest(ctx context.Context, r *http.Request, sessionExpand Expandables, identityExpand identity.Expandables) (*Session, error) + + // FetchFromRequestContext returns the session from the context or if that is unset, falls back to FetchFromRequest. + FetchFromRequestContext(context.Context, *http.Request) (*Session, error) + + // SessionActiveForRequest reports whether the request carries an active session, without loading the identity. + SessionActiveForRequest(context.Context, *http.Request) error + + // PurgeFromRequest removes an HTTP session. + PurgeFromRequest(context.Context, http.ResponseWriter, *http.Request) 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(ctx context.Context, sess *Session, matcher string, opts ...ManagerOptions) error + + // SessionAddAuthenticationMethods adds one or more authentication method to the session + // and refreshes the session's authenticated_at timestamp, because completing an + // authentication method counts as an authentication event. + SessionAddAuthenticationMethods(ctx context.Context, sid uuid.UUID, methods ...AuthenticationMethod) error + + // 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 + + // IsPrivileged checks if a session can be considered privileged. + // https://ory.com/docs/kratos/session-management/session-lifespan#privileged-sessions + IsPrivileged(ctx context.Context, session *Session) bool +} + +type ManagementProvider interface { + SessionManager() Manager +} diff --git a/session/manager_http.go b/session/manager_http.go new file mode 100644 index 0000000..32e3f13 --- /dev/null +++ b/session/manager_http.go @@ -0,0 +1,548 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + stderrors "errors" + "net/http" + "net/url" + "time" + + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/logrusx" + + "go.opentelemetry.io/otel/attribute" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/otelx" + + "github.com/ory/x/randx" + + "github.com/gorilla/sessions" + + "github.com/ory/x/urlx" + + "github.com/gofrs/uuid" + + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + + "github.com/ory/x/sqlcon" + + "github.com/ory/herodot" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/transaction" +) + +func ErrNoAALAvailable() *herodot.DefaultError { + return herodot.ErrForbidden().WithReasonf("Unable to detect available authentication methods. Perform account recovery or contact support.") +} + +type ( + managerHTTPDependencies interface { + config.Provider + identity.PoolProvider + identity.PrivilegedPoolProvider + identity.ManagementProvider + x.CookieProvider + logrusx.Provider + nosurfx.CSRFProvider + otelx.Provider + transaction.PersistenceProvider + PersistenceProvider + sessiontokenexchange.PersistenceProvider + } + ManagerHTTP struct { + cookieName func(ctx context.Context) string + r managerHTTPDependencies + } +) + +func NewManagerHTTP(r managerHTTPDependencies) *ManagerHTTP { + return &ManagerHTTP{ + r: r, + cookieName: func(ctx context.Context) string { + return r.Config().SessionName(ctx) + }, + } +} + +type options struct { + requestURL string + upsertAAL bool +} + +type ManagerOptions func(*options) + +// WithRequestURL passes along query parameters from the requestURL to the new URL (if any exist) +func WithRequestURL(requestURL string) ManagerOptions { + return func(opts *options) { + opts.requestURL = requestURL + } +} + +// UpsertAAL will update the available AAL of the identity if it was previoulsy unset. This is used to migrate +// identities from older versions of Ory Kratos. +func UpsertAAL(opts *options) { + opts.upsertAAL = true +} + +func (s *ManagerHTTP) UpsertAndIssueCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, ss *Session) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.UpsertAndIssueCookie") + defer otelx.End(span, &err) + + if err := s.r.SessionPersister().UpsertSession(ctx, ss); err != nil { + return err + } + + if err := s.IssueCookie(ctx, w, r, ss); err != nil { + return err + } + + return nil +} + +func (s *ManagerHTTP) RefreshCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.RefreshCookie") + defer otelx.End(span, &err) + + // If it is a session token there is nothing to do. + _, cookieErr := r.Cookie(s.cookieName(r.Context())) + if errors.Is(cookieErr, http.ErrNoCookie) { + return nil + } + + cookie, err := s.getCookie(r) + if err != nil { + return err + } + + expiresAt := getCookieExpiry(cookie) + if expiresAt == nil || expiresAt.Before(session.ExpiresAt) { + if err := s.IssueCookie(ctx, w, r, session); err != nil { + return err + } + } + + return nil +} + +func (s *ManagerHTTP) IssueCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.IssueCookie") + defer otelx.End(span, &err) + + cookie, err := s.r.CookieManager(ctx).Get(r, s.cookieName(ctx)) + // Fix for https://github.com/ory/kratos/issues/1695 + if err != nil && cookie == nil { + return errors.WithStack(err) + } + + if s.r.Config().SessionPath(ctx) != "" { + cookie.Options.Path = s.r.Config().SessionPath(ctx) + } + + if domain := s.r.Config().SessionDomain(ctx); domain != "" { + cookie.Options.Domain = domain + } + + if alias := s.r.Config().SelfPublicURL(ctx); s.r.Config().SelfPublicURL(ctx).String() != alias.String() { + // If a domain alias is detected use that instead. + cookie.Options.Domain = alias.Hostname() + cookie.Options.Path = alias.Path + } + + // Rotate the anti-CSRF token unless the request was already authenticated + // as the same identity: regenerate when there is no prior active session or + // when it belongs to a different identity. This decision only needs the + // prior session's identity ID, so we deliberately do not expand the + // identity. Expanding it here loads the full identity graph, which on a + // multi-region deployment can trigger a slow cross-region read (observed at + // ~2.4s on the login path) purely to read a single UUID. + var priorIdentityID uuid.UUID + hasPriorSession := false + if token := s.extractToken(r.WithContext(ctx)); token != "" { + if prior, ferr := s.r.SessionPersister().GetSessionByToken(ctx, token, ExpandNothing, identity.ExpandNothing); ferr == nil && prior.IsActive() { + priorIdentityID = prior.IdentityID + hasPriorSession = true + } + } + if !hasPriorSession || priorIdentityID != session.IdentityID { + _ = s.r.CSRFHandler().RegenerateToken(w, r) + } + + if s.r.Config().SessionSameSiteMode(ctx) != 0 { + cookie.Options.SameSite = s.r.Config().SessionSameSiteMode(ctx) + } + + cookie.Options.MaxAge = 0 + if s.r.Config().SessionPersistentCookie(ctx) { + if session.ExpiresAt.IsZero() { + cookie.Options.MaxAge = int(s.r.Config().OrganizationSessionLifespan(ctx, session.OrganizationID()).Seconds()) + } else { + cookie.Options.MaxAge = int(time.Until(session.ExpiresAt).Seconds()) + } + } + + cookie.Values["session_token"] = session.Token + cookie.Values["expires_at"] = session.ExpiresAt.UTC().Format(time.RFC3339Nano) + cookie.Values["nonce"] = randx.MustString(8, randx.Alpha) // Guarantee new kratos session identifier + + if err := cookie.Save(r, w); err != nil { + return errors.WithStack(err) + } + return nil +} + +func getCookieExpiry(s *sessions.Session) *time.Time { + expiresAt, ok := s.Values["expires_at"].(string) + if !ok { + return nil + } + + n, err := time.Parse(time.RFC3339Nano, expiresAt) + if err != nil { + return nil + } + return &n +} + +func (s *ManagerHTTP) getCookie(r *http.Request) (*sessions.Session, error) { + return s.r.CookieManager(r.Context()).Get(r, s.cookieName(r.Context())) +} + +func (s *ManagerHTTP) extractToken(r *http.Request) string { + ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.extractToken") + defer span.End() + + if token := r.Header.Get("X-Session-Token"); len(token) > 0 { + return token + } + + cookie, err := s.getCookie(r.WithContext(ctx)) + if err != nil { + token, _ := bearerTokenFromRequest(r.WithContext(ctx)) + return token + } + + token, ok := cookie.Values["session_token"].(string) + if ok { + return token + } + + token, _ = bearerTokenFromRequest(r.WithContext(ctx)) + return token +} + +func (s *ManagerHTTP) FetchFromRequestContext(ctx context.Context, r *http.Request) (_ *Session, err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.FetchFromRequestContext") + defer otelx.End(span, &err) + + if sess, ok := ctx.Value(sessionInContextKey).(*Session); ok { + return sess, nil + } + + // Don't reduce the expansion here unless you want bad performance down the line (because we + // constantly are unsure if we have the full data fetched or not). + return s.FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverything) +} + +func (s *ManagerHTTP) FetchFromRequest(ctx context.Context, r *http.Request, sessionExpand Expandables, identityExpand identity.Expandables) (_ *Session, err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.FetchFromRequest", + trace.WithAttributes( + attribute.StringSlice("session.expand", sessionExpand.ToEager()), + attribute.StringSlice("identity.expand", identityExpand.ToEager()), + )) + defer endSpanIgnoreNoActiveSession(span, &err) + + token := s.extractToken(r.WithContext(ctx)) + if token == "" { + return nil, errors.WithStack(NewErrNoCredentialsForSession()) + } + + se, err := s.r.SessionPersister().GetSessionByToken(ctx, token, sessionExpand, identityExpand) + if err != nil { + if errors.Is(err, herodot.ErrNotFound()) || errors.Is(err, sqlcon.ErrNoRows()) { + return nil, errors.WithStack(NewErrNoActiveSessionFound()) + } + return nil, err + } + + trace.SpanFromContext(ctx).AddEvent(events.NewSessionChecked(ctx, se.ID, se.IdentityID)) + + if !se.IsActive() { + return nil, errors.WithStack(NewErrNoActiveSessionFound()) + } + + return se, nil +} + +// endSpanIgnoreNoActiveSession ends the span without recording ErrNoActiveSessionFound as a span +// error, because a request without an active session is an expected condition on these paths. +func endSpanIgnoreNoActiveSession(span trace.Span, err *error) { + if _, ok := stderrors.AsType[*ErrNoActiveSessionFound](*err); ok { + span.End() + } else { + otelx.End(span, err) + } +} + +// SessionActiveForRequest reports whether the request carries a token for an +// active session, without loading the identity. It returns nil when an active +// session exists and the same errors as FetchFromRequest otherwise, so callers +// that only branch on the error keep identical behavior while skipping the +// cross-region identity load. +func (s *ManagerHTTP) SessionActiveForRequest(ctx context.Context, r *http.Request) error { + _, err := s.FetchFromRequest(ctx, r, ExpandNothing, identity.ExpandNothing) + return err +} + +func (s *ManagerHTTP) PurgeFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.PurgeFromRequest") + defer otelx.End(span, &err) + + if token, ok := bearerTokenFromRequest(r); ok { + _, err := s.r.SessionPersister().RevokeSessionByToken(ctx, token) + return errors.WithStack(err) + } + + cookie, _ := s.r.CookieManager(r.Context()).Get(r, s.cookieName(ctx)) + token, ok := cookie.Values["session_token"].(string) + if !ok { + return nil + } + + if _, err := s.r.SessionPersister().RevokeSessionByToken(ctx, token); err != nil { + return errors.WithStack(err) + } + + cookie.Options.MaxAge = -1 + if err := cookie.Save(r, w); err != nil { + return errors.WithStack(err) + } + return nil +} + +func (s *ManagerHTTP) DoesSessionSatisfy(ctx context.Context, sess *Session, requestedAAL string, opts ...ManagerOptions) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.DoesSessionSatisfy") + defer otelx.End(span, &err) + + sess.SetAuthenticatorAssuranceLevel() + + // If we already have AAL2 there is no need to check further because it is the highest AAL. + if sess.AuthenticatorAssuranceLevel == identity.AuthenticatorAssuranceLevel2 { + return nil + } + + managerOpts := &options{} + for _, o := range opts { + o(managerOpts) + } + + loginURL := urlx.AppendPaths(s.r.Config().SelfPublicURL(ctx), "/self-service/login/browser") + query := url.Values{ + "aal": {"aal2"}, + } + + // return to the requestURL if it was set + if managerOpts.requestURL != "" { + query.Set("return_to", managerOpts.requestURL) + } + + loginURL.RawQuery = query.Encode() + + switch requestedAAL { + case string(identity.AuthenticatorAssuranceLevel1): + if sess.AuthenticatorAssuranceLevel >= identity.AuthenticatorAssuranceLevel1 { + return nil + } + return NewErrAALNotSatisfied(loginURL.String()) + 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 + } + } + + 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: + + // 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 + } + } + + // Great, now we determine the identity's available AAL + if err := sess.Identity.SetAvailableAAL(ctx, s.r.IdentityManager()); err != nil { + return err + } + + // 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)) + } + + 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. + + // 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()) + } + + return errors.Errorf("requested unknown aal: %s", requestedAAL) +} + +func (s *ManagerHTTP) SessionAddAuthenticationMethods(ctx context.Context, sid uuid.UUID, ams ...AuthenticationMethod) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.SessionAddAuthenticationMethods") + defer otelx.End(span, &err) + + // Since we added the method, it also means that we have authenticated it + sess, err := s.r.SessionPersister().GetSession(ctx, sid, ExpandNothing) + if err != nil { + return err + } + for _, m := range ams { + sess.CompletedLoginForMethod(m) + } + // Completing an authentication method is an authentication event, so the + // session's authenticated_at is refreshed as well. This matches login + // flows, which set authenticated_at when a second factor completes. + sess.AuthenticatedAt = time.Now().UTC() + sess.SetAuthenticatorAssuranceLevel() + return s.r.SessionPersister().UpsertSession(ctx, sess) +} + +func (s *ManagerHTTP) MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID, uiNode node.UiNodeGroup) (handled bool, err error) { + ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.MaybeRedirectAPICodeFlow") + defer otelx.End(span, &err) + + if uiNode != node.OpenIDConnectGroup { + return false, nil + } + + code, ok, _ := s.r.SessionTokenExchangePersister().CodeForFlow(ctx, f.GetID()) + if !ok { + return false, nil + } + + returnTo := s.r.Config().SelfServiceBrowserDefaultReturnTo(ctx) + if redirecter, ok := f.(flow.FlowWithRedirect); ok { + r, err := redir.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.r)...) + if err == nil { + returnTo = r + } + } + + if err = s.r.SessionTokenExchangePersister().UpdateSessionOnExchanger(ctx, f.GetID(), sessionID); err != nil { + return false, errors.WithStack(err) + } + + q := returnTo.Query() + q.Set("code", code.ReturnToCode) + returnTo.RawQuery = q.Encode() + http.Redirect(w, r, returnTo.String(), http.StatusSeeOther) + + 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().OrganizationSessionLifespan(ctx, session.OrganizationID())) + session.AuthenticatedAt = authenticatedAt + + session.SetSessionDeviceInformation(r.WithContext(ctx)) + session.SetAuthenticatorAssuranceLevel() + + span.SetAttributes( + attribute.String("identity.available_aal", session.Identity.InternalAvailableAAL.String), + ) + + return nil +} + +func (s *ManagerHTTP) IsPrivileged(ctx context.Context, session *Session) bool { + if session == nil { + return false + } + + if session.Identity == nil { + return false + } + + if session.AuthenticatedAt.IsZero() { + return false + } + + privilegedSessionLifespan := s.r.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx) + if privilegedSessionLifespan <= 0 { + return true + } + + return session.AuthenticatedAt.Add(privilegedSessionLifespan).After(time.Now()) +} diff --git a/session/manager_http_test.go b/session/manager_http_test.go new file mode 100644 index 0000000..34c994d --- /dev/null +++ b/session/manager_http_test.go @@ -0,0 +1,1299 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/nosurf" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/urlx" +) + +var _ nosurf.Handler = new(mockCSRFHandler) + +type mockCSRFHandler struct { + c int +} + +func (f *mockCSRFHandler) DisablePath(string) {} +func (f *mockCSRFHandler) DisableGlob(string) {} +func (f *mockCSRFHandler) DisableGlobs(...string) {} +func (f *mockCSRFHandler) IgnoreGlob(string) {} +func (f *mockCSRFHandler) IgnoreGlobs(...string) {} +func (f *mockCSRFHandler) ExemptPath(string) {} +func (f *mockCSRFHandler) IgnorePath(string) {} +func (f *mockCSRFHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} + +func (f *mockCSRFHandler) RegenerateToken(_ http.ResponseWriter, _ *http.Request) string { + f.c++ + return nosurfx.FakeCSRFToken +} + +func newAAL2Identity() *identity.Identity { + return &identity.Identity{ + SchemaID: "default", + Traits: []byte("{}"), + State: identity.StateActive, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + Identifiers: []string{testhelpers.RandomEmail()}, + }, + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), + Identifiers: []string{testhelpers.RandomEmail()}, + }, + }, + } +} + +func newAAL1Identity() *identity.Identity { + return &identity.Identity{ + SchemaID: "default", + Traits: []byte("{}"), + State: identity.StateActive, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + Identifiers: []string{testhelpers.RandomEmail()}, + }, + }, + } +} + +func TestManagerHTTP(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("case=regenerate csrf on principal change", func(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t) + mock := new(mockCSRFHandler) + reg.WithCSRFHandler(mock) + + require.NoError(t, reg.SessionManager().IssueCookie(t.Context(), httptest.NewRecorder(), new(http.Request), new(session.Session))) + assert.Equal(t, 1, mock.c) + }) + + t.Run("case=csrf rotation depends on the prior session identity", func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/fake-session.schema.json")), + ) + mock := new(mockCSRFHandler) + reg.WithCSRFHandler(mock) + + // Two persisted, active sessions for two different identities. + idA := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &idA)) + sA, err := testhelpers.NewActiveSession(req, reg, &idA, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sA)) + + idB := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &idB)) + sB, err := testhelpers.NewActiveSession(req, reg, &idB, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sB)) + + // issue calls IssueCookie with priorToken as the request's existing + // session token and returns how many times the anti-CSRF token was + // rotated by that call. + issue := func(t *testing.T, priorToken string, issued *session.Session) int { + before := mock.c + r := httptest.NewRequest("GET", "https://example.com/", nil) + if priorToken != "" { + r.Header.Set("X-Session-Token", priorToken) + } + require.NoError(t, reg.SessionManager().IssueCookie(ctx, httptest.NewRecorder(), r, issued)) + return mock.c - before + } + + t.Run("no prior session rotates", func(t *testing.T) { + assert.Equal(t, 1, issue(t, "", sA)) + }) + t.Run("same identity keeps the token", func(t *testing.T) { + assert.Equal(t, 0, issue(t, sA.Token, sA)) + }) + t.Run("different identity rotates", func(t *testing.T) { + assert.Equal(t, 1, issue(t, sA.Token, sB)) + }) + t.Run("unknown prior token rotates", func(t *testing.T) { + assert.Equal(t, 1, issue(t, "unknown-token", sA)) + }) + }) + + t.Run("case=cookie settings", func(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(map[string]any{ + "dev": false, + config.ViperKeyPublicBaseURL: "https://baseurl.com/base_url", + }), + ) + mock := new(mockCSRFHandler) + reg.WithCSRFHandler(mock) + s := &session.Session{Identity: new(identity.Identity)} + + getCookie := func(ctx context.Context, t *testing.T, req *http.Request) *http.Cookie { + rec := httptest.NewRecorder() + require.NoError(t, reg.SessionManager().IssueCookie(ctx, rec, req, s)) + require.Len(t, rec.Result().Cookies(), 1) + return rec.Result().Cookies()[0] + } + + t.Run("case=immutability", func(t *testing.T) { + cookie1 := getCookie(t.Context(), t, testhelpers.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) + cookie2 := getCookie(t.Context(), t, testhelpers.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) + + assert.NotEqual(t, cookie1.Value, cookie2.Value) + }) + + t.Run("case=with default options", func(t *testing.T) { + actual := getCookie(t.Context(), t, httptest.NewRequest("GET", "https://baseurl.com/bar", nil)) + assert.EqualValues(t, "", actual.Domain, "Domain is empty because unset as a config option") + assert.EqualValues(t, "/", actual.Path, "Path is the default /") + assert.EqualValues(t, http.SameSiteLaxMode, actual.SameSite) + assert.True(t, actual.HttpOnly) + assert.True(t, actual.Secure) + }) + + t.Run("case=with base cookie customization", func(t *testing.T) { + ctx := contextx.WithConfigValues(t.Context(), map[string]any{ + config.ViperKeyCookiePath: "/cookie", + config.ViperKeyCookieDomain: "cookie.com", + config.ViperKeyCookieSameSite: "Strict", + }) + + actual := getCookie(ctx, t, httptest.NewRequest("GET", "https://baseurl.com/bar", nil)) + assert.EqualValues(t, "cookie.com", actual.Domain, "Domain is empty because unset as a config option") + assert.EqualValues(t, "/cookie", actual.Path, "Path is the default /") + assert.EqualValues(t, http.SameSiteStrictMode, actual.SameSite) + assert.True(t, actual.HttpOnly) + assert.True(t, actual.Secure) + }) + + t.Run("case=with base session customization", func(t *testing.T) { + ctx := contextx.WithConfigValues(t.Context(), map[string]any{ + config.ViperKeySessionPath: "/session", + config.ViperKeySessionDomain: "session.com", + config.ViperKeySessionSameSite: "None", + }) + + actual := getCookie(ctx, t, httptest.NewRequest("GET", "https://baseurl.com/bar", nil)) + assert.EqualValues(t, "session.com", actual.Domain, "Domain is empty because unset as a config option") + assert.EqualValues(t, "/session", actual.Path, "Path is the default /") + assert.EqualValues(t, http.SameSiteNoneMode, actual.SameSite) + assert.True(t, actual.HttpOnly) + assert.True(t, actual.Secure) + }) + }) + + t.Run("suite=SessionActivate", func(t *testing.T) { + t.Parallel() + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("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=SessionActivate applies the organization session lifespan", func(t *testing.T) { + t.Parallel() + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + orgWithOverride := x.NewUUID() + orgWithoutOverride := x.NewUUID() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySessionLifespan: "24h", + "selfservice.methods.b2b.config.organizations": []map[string]any{ + {"id": orgWithOverride.String(), "domains": []string{"override.example.com"}, "session_lifespan": "1h"}, + {"id": orgWithoutOverride.String(), "domains": []string{"default.example.com"}}, + }, + }), + ) + + i := &identity.Identity{Traits: []byte("{}"), State: identity.StateActive} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + + activate := func(t *testing.T, org string) *session.Session { + authAt := time.Now().UTC() + sess := session.NewInactiveSession() + if org != "" { + sess.AMR = session.AuthenticationMethods{{Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1, Organization: org}} + } + require.NoError(t, reg.SessionManager().ActivateSession(req, sess, i, authAt)) + return sess + } + + t.Run("case=org with override uses the override", func(t *testing.T) { + sess := activate(t, orgWithOverride.String()) + assert.WithinDuration(t, sess.IssuedAt.Add(1*time.Hour), sess.ExpiresAt, 5*time.Second) + }) + + t.Run("case=org without override uses the project default", func(t *testing.T) { + sess := activate(t, orgWithoutOverride.String()) + assert.WithinDuration(t, sess.IssuedAt.Add(24*time.Hour), sess.ExpiresAt, 5*time.Second) + }) + + t.Run("case=no org uses the project default", func(t *testing.T) { + sess := activate(t, "") + assert.WithinDuration(t, sess.IssuedAt.Add(24*time.Hour), sess.ExpiresAt, 5*time.Second) + }) + }) + + t.Run("suite=SessionAddAuthenticationMethod", func(t *testing.T) { + t.Parallel() + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + + i := &identity.Identity{Traits: []byte("{}"), State: identity.StateActive} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + sess := session.NewInactiveSession() + // Activate with a stale timestamp so the refresh below is observable. + staleAuthenticatedAt := time.Now().Add(-time.Hour).UTC() + require.NoError(t, reg.SessionManager().ActivateSession(req, sess, i, staleAuthenticatedAt)) + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) + require.NoError(t, reg.SessionManager().SessionAddAuthenticationMethods(context.Background(), sess.ID, + session.AuthenticationMethod{ + Method: identity.CredentialsTypeOIDC, + AAL: identity.AuthenticatorAssuranceLevel1, + }, + session.AuthenticationMethod{ + Method: identity.CredentialsTypeWebAuthn, + AAL: identity.AuthenticatorAssuranceLevel2, + })) + assert.Len(t, sess.AMR, 0) + + actual, err := reg.SessionPersister().GetSession(context.Background(), sess.ID, session.ExpandNothing) + require.NoError(t, err) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, actual.AuthenticatorAssuranceLevel) + for _, amr := range actual.AMR { + assert.True(t, amr.Method == identity.CredentialsTypeWebAuthn || amr.Method == identity.CredentialsTypeOIDC) + } + assert.Len(t, actual.AMR, 2) + + // Completing an authentication method counts as an authentication + // event, so the timestamp must be refreshed from its stale value. + assert.WithinDuration(t, time.Now().UTC(), actual.AuthenticatedAt, 5*time.Second) + }) + + t.Run("suite=lifecycle", func(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/fake-session.schema.json")), + ) + + var s *session.Session + rp := httprouterx.NewRouterPublic() + rp.GET("/session/revoke", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().PurgeFromRequest(r.Context(), w, r)) + w.WriteHeader(http.StatusOK) + }) + + rp.GET("/session/set", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, reg.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s)) + w.WriteHeader(http.StatusOK) + }) + + rp.GET("/session/get", func(w http.ResponseWriter, r *http.Request) { + sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything) + if err != nil { + t.Logf("Got error on lookup: %s %T", err, errors.Unwrap(err)) + reg.Writer().WriteError(w, r, err) + return + } + reg.Writer().Write(w, r, sess) + }) + + rp.GET("/session/get-middleware", reg.SessionHandler().IsAuthenticated(func(w http.ResponseWriter, r *http.Request) { + sess, err := reg.SessionManager().FetchFromRequestContext(r.Context(), r) + if err != nil { + t.Logf("Got error on lookup: %s %T", err, errors.Unwrap(err)) + reg.Writer().WriteError(w, r, err) + return + } + reg.Writer().Write(w, r, sess) + }, session.RedirectOnUnauthenticated("https://failed.com"))) + + pts := httptest.NewServer(nosurfx.NewTestCSRFHandler(rp, reg)) + t.Cleanup(pts.Close) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, pts.URL) + reg.RegisterPublicRoutes(context.Background(), rp) + + t.Run("case=valid", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + conf.MustSet(req.Context(), config.ViperKeySessionLifespan, "1m") + + i := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + + c := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") + + res, err := c.Get(pts.URL + "/session/get") + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + res, err = c.Get(pts.URL + "/session/get-middleware") + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=key rotation", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + original := conf.GetProvider(ctx).Strings(config.ViperKeySecretsCookie) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySecretsCookie, original) + }) + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + conf.MustSet(ctx, config.ViperKeySecretsCookie, []string{"foo"}) + + i := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + + c := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") + + res, err := c.Get(pts.URL + "/session/get") + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + conf.MustSet(ctx, config.ViperKeySecretsCookie, []string{"bar", "foo"}) + res, err = c.Get(pts.URL + "/session/get") + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=no panic on invalid cookie name", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + conf.MustSet(ctx, config.ViperKeySessionName, "$%˜\"") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionName, "") + }) + + rp.GET("/session/set/invalid", func(w http.ResponseWriter, r *http.Request) { + require.Error(t, reg.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s)) + w.WriteHeader(http.StatusInternalServerError) + }) + + i := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + 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") + require.NoError(t, err) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=valid bearer auth as fallback", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + + i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + 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) + + req, err = http.NewRequest("GET", pts.URL+"/session/get", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+s.Token) + + c := testhelpers.NewTestClient(t) + res, err := c.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=valid x-session-token auth even if bearer is set", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + + i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + 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)) + + req, err = http.NewRequest("GET", pts.URL+"/session/get", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer invalid") + req.Header.Set("X-Session-Token", s.Token) + + c := testhelpers.NewTestClient(t) + res, err := c.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=expired", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1ns") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + }) + + i := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + + c := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") + + time.Sleep(time.Nanosecond * 2) + + res, err := c.Get(pts.URL + "/session/get") + require.NoError(t, err) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + }) + + t.Run("case=revoked", func(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, _ = testhelpers.NewActiveSession(req, reg, &i, 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") + + res, err := c.Get(pts.URL + "/session/revoke") + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + + res, err = c.Get(pts.URL + "/session/get") + require.NoError(t, err) + assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) + }) + + t.Run("case=respects AAL config", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") + + t.Run("required_aal=aal2", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + run := func(t *testing.T, complete []identity.CredentialsType, requested string, i *identity.Identity, expectedError error) { + s := session.NewInactiveSession() + for _, m := range complete { + aal := identity.AuthenticatorAssuranceLevel1 + switch m { + case identity.CredentialsTypeTOTP, + identity.CredentialsTypeWebAuthn, + identity.CredentialsTypeLookup: + aal = identity.AuthenticatorAssuranceLevel2 + } + s.CompletedLoginFor(m, aal) + } + require.NoError(t, reg.SessionManager().ActivateSession(req, s, i, time.Now().UTC())) + err := reg.SessionManager().DoesSessionSatisfy(ctx, s, requested) + if expectedError != nil { + assert.EqualExportedValues(t, expectedError, err) + } else { + require.NoError(t, err) + } + } + + test := func(t *testing.T, idAAL1, idAAL2 *identity.Identity) { + t.Run("fulfilled for aal2 if identity has aal2", func(t *testing.T) { + run(t, []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeWebAuthn}, config.HighestAvailableAAL, idAAL2, nil) + }) + + t.Run("rejected for aal1 if identity has aal2", func(t *testing.T) { + returnURL := urlx.AppendPaths(reg.Config().SelfPublicURL(ctx), "/self-service/login/browser") + returnURL.RawQuery = "aal=aal2" + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, config.HighestAvailableAAL, idAAL2, + session.NewErrAALNotSatisfied(returnURL.String())) + }) + + t.Run("fulfilled for aal1 if identity has aal2 but config is aal1", func(t *testing.T) { + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, "aal1", idAAL2, nil) + }) + + t.Run("fulfilled for aal2 if identity has aal1", func(t *testing.T) { + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, "aal1", idAAL2, nil) + }) + + t.Run("fulfilled for aal1 if identity has aal1", func(t *testing.T) { + run(t, []identity.CredentialsType{identity.CredentialsTypePassword}, "aal1", idAAL1, nil) + }) + } + + t.Run("identity available AAL is not hydrated", func(t *testing.T) { + idAAL2 := newAAL2Identity() + idAAL1 := newAAL1Identity() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), idAAL1)) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), idAAL2)) + test(t, idAAL1, idAAL2) + }) + + t.Run("identity available AAL is hydrated and updated in the 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. + idAAL1 := newAAL2Identity() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), idAAL1)) + + s := session.NewInactiveSession() + s.CompletedLoginFor(identity.CredentialsTypePassword, "") + require.NoError(t, reg.SessionManager().ActivateSession(req, s, idAAL1, time.Now().UTC())) + require.Error(t, reg.SessionManager().DoesSessionSatisfy(ctx, 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.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 := newAAL2Identity() + idAAL2.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel2) + + idAAL1 := newAAL1Identity() + idAAL1.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel1) + + test(t, idAAL1, idAAL2) + }) + }) + }) + }) +} + +func TestDoesSessionSatisfy(t *testing.T) { + t.Parallel() + + conf, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + + passwordEmpty := func() identity.Credentials { + return identity.Credentials{Type: identity.CredentialsTypePassword, Config: []byte(`{}`), Identifiers: []string{testhelpers.RandomEmail()}} + } + password := func() identity.Credentials { + return 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 := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{testhelpers.RandomEmail()}, + Config: []byte(`{"use_password_migration_hook":true}`), + } + } + + code := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{testhelpers.RandomEmail()}, + Config: []byte(`{"address_type":"email","used_at":{"Time":"0001-01-01T00:00:00Z","Valid":false}}`), + } + } + + codeV2 := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{testhelpers.RandomEmail()}, + Config: []byte(`{"addresses":[{"channel":"email","address":"test@ory.sh"}]}`), + } + } + + codeEmpty := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{}, + Config: []byte(`{}`), + } + } + + oidc := func() identity.Credentials { + sub := testhelpers.RandomEmail() + return identity.Credentials{ + Type: identity.CredentialsTypeOIDC, + Config: []byte(fmt.Sprintf(`{"providers":[{"subject":"%s","provider":"hydra","initial_id_token":"65794a6862","initial_access_token":"5234475274","initial_refresh_token":"32787233"}]}`, sub)), + Identifiers: []string{"hydra:" + sub}, + } + } + // oidcEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeOIDC, + // Config: []byte(`{}`), + // Identifiers: []string{"hydra:0.fywegkf7hd@ory.sh"}, + // } + + lookupSecrets := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeLookup, + Config: []byte(`{"recovery_codes": [{"code": "abcde", "used_at": null}]}`), + } + } + // lookupSecretsEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeLookup, + // Config: []byte(`{}`), + // } + + totp := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeTOTP, + Config: []byte(`{"totp_url": "otpauth://totp/..."}`), + } + } + // totpEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeTOTP, + // Config: []byte(`{}`), + // } + + // passkey + passkey := func() identity.Credentials { + return 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 := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), + Identifiers: []string{testhelpers.RandomEmail()}, + } + } + passwordlessWebAuth := func() identity.Credentials { + return identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Config: []byte(`{"credentials":[{"is_passwordless":true}]}`), + Identifiers: []string{testhelpers.RandomEmail()}, + } + } + webAuthEmpty := func() identity.Credentials { + return identity.Credentials{Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{}`), Identifiers: []string{testhelpers.RandomEmail()}} + } + + amrs := map[identity.CredentialsType]session.AuthenticationMethod{} + for _, strat := range reg.AllLoginStrategies() { + amrs[strat.ID()] = strat.CompletedAuthenticationMethod(t.Context()) + } + + for _, tc := range []struct { + desc string + withContext func(*testing.T, context.Context) context.Context + errAs error + errIs error + matcher identity.AuthenticatorAssuranceLevel + creds []identity.Credentials + withAMR session.AuthenticationMethods + sessionManagerOptions []session.ManagerOptions + expectedFunc func(t *testing.T, err error, tcError error) + }{ + { + desc: "with highest_available a password user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + // No error + }, + { + 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 contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.password_migration.enabled": true, + }) + }, + // No error + }, + { + // 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 contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.password_migration.enabled": false, + }) + }, + // No error + }, + { + 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 + }, + { + desc: "with highest_available a otp codeV2 user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{codeV2()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeCodeAuth]}, + // No error + }, + { + desc: "with highest_available a empty mfa code user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{codeEmpty()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeCodeAuth]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.mfa_enabled": true, + }) + }, + // No error + }, + { + desc: "with highest_available a password user with empty mfa code is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password(), codeEmpty()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.passwordless_enabled": false, + "selfservice.methods.code.mfa_enabled": true, + }) + }, + // No error + }, + { + desc: "with highest_available a oidc user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{oidc()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeOIDC]}, + // No error + }, + { + desc: "with highest_available a passkey user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{passkey()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePasskey]}, + // No error + }, + { + 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 + }, + { + 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. + { + 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), + }, + { + 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), + }, + { + 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), + }, + { + 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), + }, + { + desc: "with highest_available a recovery link user requires aal2 if they have 2fa code configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{code()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryLink]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.passwordless_enabled": false, + "selfservice.methods.code.mfa_enabled": true, + }) + }, + errIs: new(session.ErrAALNotSatisfied), + }, + { + desc: "with highest_available a recovery link user requires aal2 if they have 2fa code v2 configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{codeV2()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryLink]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return contextx.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.passwordless_enabled": false, + "selfservice.methods.code.mfa_enabled": true, + }) + }, + errIs: new(session.ErrAALNotSatisfied), + }, + + // Legacy tests + { + 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+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_passwordless", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password(), passwordlessWebAuth()}, + withAMR: session.AuthenticationMethods{amrs[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 contextx.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+codeV2-mfa", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password(), codeV2()}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + errAs: new(session.ErrAALNotSatisfied), + withContext: func(t *testing.T, ctx context.Context) context.Context { + return contextx.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", + 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}}, + }, + + // 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(t.Context()), "/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()}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypeRecoveryCode}}, + errAs: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(t.Context()), "/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") + require.Equal(t, tcError.(*session.ErrAALNotSatisfied).RedirectTo, err.(*session.ErrAALNotSatisfied).RedirectTo) + }, + }, + { + 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(t.Context()), "/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(tc.desc, func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + 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(ctx, id, identity.ManagerAllowWriteProtectedTraits)) + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + s := session.NewInactiveSession() + for _, m := range tc.withAMR { + s.CompletedLoginFor(m.Method, m.AAL) + } + require.NoError(t, reg.SessionManager().ActivateSession(req, s, id, time.Now().UTC())) + + err := reg.SessionManager().DoesSessionSatisfy(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.errIs) + } + 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 ... + s.Identity = nil + err = reg.SessionManager().DoesSessionSatisfy(ctx, s, string(tc.matcher), tc.sessionManagerOptions...) + if tc.errAs != nil { + if tc.expectedFunc != nil { + // If there is no identity, we can't expect the error to contain the identity + // schema in the RedirectTo URL. + var errAALNotSatisfied *session.ErrAALNotSatisfied + errors.As(tc.errAs, &errAALNotSatisfied) + u := x.Must(url.Parse(errAALNotSatisfied.RedirectTo)) + q := u.Query() + q.Del("identity_schema") + u.RawQuery = q.Encode() + + tc.expectedFunc(t, err, session.NewErrAALNotSatisfied(u.String())) + } else { + assert.ErrorAs(t, err, &tc.errAs) + } + } else { + assert.NoError(t, err) + } + + // ... or no credentials attached. + s.Identity = id + s.Identity.Credentials = nil + err = reg.SessionManager().DoesSessionSatisfy(ctx, s, string(tc.matcher), tc.sessionManagerOptions...) + if tc.errAs != nil { + if tc.expectedFunc != nil { + tc.expectedFunc(t, err, tc.errAs) + } else { + assert.ErrorAs(t, err, &tc.errAs) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestFetchFromRequestMinimalExpansion(t *testing.T) { + t.Parallel() + + ctx := t.Context() + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/fake-session.schema.json")), + ) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + i := identity.Identity{Traits: []byte("{}")} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &i)) + s, err := testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s)) + + withToken := func(token string) *http.Request { + r := httptest.NewRequest("GET", "https://example.com/", nil) + if token != "" { + r.Header.Set("X-Session-Token", token) + } + return r + } + + t.Run("ExpandNothing returns the session without the identity and devices", func(t *testing.T) { + got, err := reg.SessionManager().FetchFromRequest(ctx, withToken(s.Token), session.ExpandNothing, identity.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID, got.ID) + assert.Equal(t, i.ID, got.IdentityID) + assert.Nil(t, got.Identity, "identity must not be loaded") + assert.Empty(t, got.Devices, "devices must not be loaded") + }) + + t.Run("expands only the requested session associations", func(t *testing.T) { + got, err := reg.SessionManager().FetchFromRequest(ctx, withToken(s.Token), session.Expandables{session.ExpandSessionDevices}, identity.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, s.ID, got.ID) + assert.Nil(t, got.Identity, "identity must not be loaded") + assert.NotEmpty(t, got.Devices, "devices must be loaded") + }) + + t.Run("errors without a token", func(t *testing.T) { + _, err := reg.SessionManager().FetchFromRequest(ctx, withToken(""), session.ExpandNothing, identity.ExpandNothing) + require.Error(t, err) + }) + + t.Run("errors for an unknown token", func(t *testing.T) { + _, err := reg.SessionManager().FetchFromRequest(ctx, withToken("unknown-token"), session.ExpandNothing, identity.ExpandNothing) + e := new(session.ErrNoActiveSessionFound) + assert.ErrorAs(t, err, &e) + }) + + t.Run("SessionActiveForRequest is nil for an active session", func(t *testing.T) { + assert.NoError(t, reg.SessionManager().SessionActiveForRequest(ctx, withToken(s.Token))) + }) + + t.Run("SessionActiveForRequest errors for missing or unknown tokens", func(t *testing.T) { + require.Error(t, reg.SessionManager().SessionActiveForRequest(ctx, withToken(""))) + require.Error(t, reg.SessionManager().SessionActiveForRequest(ctx, withToken("unknown-token"))) + }) +} + +func TestFetchFromRequestExpandEverythingButCredentials(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/fake-session.schema.json")), + ) + + newFetchRequest := func(t *testing.T, token string) *http.Request { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req.Header.Set("X-Session-Token", token) + return req + } + + t.Run("case=does not expand identity credentials but expands everything else", func(t *testing.T) { + t.Parallel() + ctx := t.Context() + + i := newAAL2Identity() + email := testhelpers.RandomEmail() + i.VerifiableAddresses = []identity.VerifiableAddress{{Value: email, Via: identity.AddressTypeEmail}} + i.RecoveryAddresses = []identity.RecoveryAddress{{Value: email, Via: identity.AddressTypeEmail}} + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + fetched, err := reg.SessionManager().FetchFromRequest(ctx, newFetchRequest(t, sess.Token), session.ExpandEverything, identity.ExpandEverythingButCredentials) + require.NoError(t, err) + require.NotNil(t, fetched.Identity) + assert.Empty(t, fetched.Identity.Credentials, "credentials must not be expanded") + assert.NotEmpty(t, fetched.Identity.VerifiableAddresses, "verifiable addresses must still be expanded") + assert.NotEmpty(t, fetched.Identity.RecoveryAddresses, "recovery addresses must still be expanded") + + // Control: the regular fetch expands the credentials. + full, err := reg.SessionManager().FetchFromRequest(ctx, newFetchRequest(t, sess.Token), session.ExpandEverything, identity.ExpandEverything) + require.NoError(t, err) + require.NotNil(t, full.Identity) + assert.NotEmpty(t, full.Identity.Credentials) + }) + + t.Run("case=aal highest_available is still enforced without expanded credentials", func(t *testing.T) { + t.Parallel() + ctx := t.Context() + + // The identity has a second factor configured, but its available AAL is not yet + // persisted in the database. + i := newAAL2Identity() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + // The available AAL is unknown, so DoesSessionSatisfy must lazily hydrate the + // credentials to determine it. + fetched, err := reg.SessionManager().FetchFromRequest(ctx, newFetchRequest(t, sess.Token), session.ExpandEverything, identity.ExpandEverythingButCredentials) + require.NoError(t, err) + require.NotNil(t, fetched.Identity) + require.Empty(t, fetched.Identity.Credentials) + + err = reg.SessionManager().DoesSessionSatisfy(ctx, fetched, config.HighestAvailableAAL, session.UpsertAAL) + aalErr, ok := errors.AsType[*session.ErrAALNotSatisfied](err) + require.Truef(t, ok, "expected *session.ErrAALNotSatisfied but got %v", err) + assert.NotEmpty(t, aalErr.RedirectTo) + + // The lazily computed available AAL was persisted through the UpsertAAL option. + result, err := reg.IdentityPool().GetIdentity(ctx, i.ID, identity.ExpandNothing) + require.NoError(t, err) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, result.InternalAvailableAAL.String) + + // A fresh fetch now uses the precomputed available AAL and never hydrates the + // credentials. + fetched, err = reg.SessionManager().FetchFromRequest(ctx, newFetchRequest(t, sess.Token), session.ExpandEverything, identity.ExpandEverythingButCredentials) + require.NoError(t, err) + require.Empty(t, fetched.Identity.Credentials) + + err = reg.SessionManager().DoesSessionSatisfy(ctx, fetched, config.HighestAvailableAAL, session.UpsertAAL) + _, ok = errors.AsType[*session.ErrAALNotSatisfied](err) + require.Truef(t, ok, "expected *session.ErrAALNotSatisfied but got %v", err) + assert.Empty(t, fetched.Identity.Credentials, "the precomputed available AAL path must not hydrate the credentials") + }) + + t.Run("case=missing token returns ErrNoActiveSessionFound with missing credentials", func(t *testing.T) { + t.Parallel() + + _, err := reg.SessionManager().FetchFromRequest(t.Context(), testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil), session.ExpandEverything, identity.ExpandEverythingButCredentials) + noSess, ok := errors.AsType[*session.ErrNoActiveSessionFound](err) + require.Truef(t, ok, "expected *session.ErrNoActiveSessionFound but got %v", err) + assert.True(t, noSess.CredentialsMissing) + }) + + t.Run("case=inactive session returns ErrNoActiveSessionFound", func(t *testing.T) { + t.Parallel() + ctx := t.Context() + + i := newAAL1Identity() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + sess.Active = false + require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess)) + + _, err = reg.SessionManager().FetchFromRequest(ctx, newFetchRequest(t, sess.Token), session.ExpandEverything, identity.ExpandEverythingButCredentials) + _, ok := errors.AsType[*session.ErrNoActiveSessionFound](err) + require.Truef(t, ok, "expected *session.ErrNoActiveSessionFound but got %v", err) + }) +} diff --git a/session/manager_test.go b/session/manager_test.go new file mode 100644 index 0000000..60ee713 --- /dev/null +++ b/session/manager_test.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/herodot" + "github.com/ory/kratos/session" +) + +func TestErrAALNotSatisfied_PassReturnToAndLoginChallengeParameters(t *testing.T) { + cases := []struct { + name string + instance *session.ErrAALNotSatisfied + requestURL string + wantErr assert.ErrorAssertionFunc + expected string + }{ + { + name: "no parameter", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://localhost/?foo=bar", + }, + requestURL: "https://localhost:1234/", + wantErr: assert.NoError, + expected: "https://localhost/?foo=bar", + }, + { + name: "pass return_to parameter", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://localhost/?foo=bar", + }, + requestURL: "https://localhost:1234/?return_to=https%3A%2F%2Fory.sh", + wantErr: assert.NoError, + expected: "https://localhost/?foo=bar&return_to=https%3A%2F%2Fory.sh", + }, + { + name: "pass login_challenge parameter", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://localhost/?foo=bar", + }, + requestURL: "https://localhost:1234/?login_challenge=badee1", + wantErr: assert.NoError, + expected: "https://localhost/?foo=bar&login_challenge=badee1", + }, + { + name: "pass login_challenge and return_to parameters", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://localhost/?foo=bar", + }, + requestURL: "https://localhost:1234/?return_to=https%3A%2F%2Fory.sh&login_challenge=badee1", + wantErr: assert.NoError, + expected: "https://localhost/?foo=bar&login_challenge=badee1&return_to=https%3A%2F%2Fory.sh", + }, + { + name: "invalid RedirectTo URL", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://user:{{{@localhost/?foo=bar", + }, + requestURL: "https://localhost:1234/?return_to=https%3A%2F%2Fory.sh", + wantErr: assert.Error, + }, + { + name: "invalid request URL URL", + instance: &session.ErrAALNotSatisfied{ + DefaultError: &herodot.DefaultError{}, + RedirectTo: "https://localhost/?foo=bar", + }, + requestURL: "https://user:{{{@localhost:1234/?return_to=https%3A%2F%2Fory.sh", + wantErr: assert.Error, + }, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("case=%s", tc.name), func(t *testing.T) { + err := tc.instance.PassReturnToAndLoginChallengeParameters(tc.requestURL) + + tc.wantErr(t, err) + if err == nil { + assert.Equal(t, tc.expected, tc.instance.RedirectTo) + } + }) + } +} diff --git a/session/persistence.go b/session/persistence.go new file mode 100644 index 0000000..7949256 --- /dev/null +++ b/session/persistence.go @@ -0,0 +1,116 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "time" + + "github.com/ory/pop/v6" + + "github.com/ory/kratos/identity" + + "github.com/ory/x/pagination/keysetpagination" + + "github.com/gofrs/uuid" +) + +type PersistenceProvider interface { + SessionPersister() Persister +} + +// RevokedSession identifies the session revoked by RevokeSessionByToken so +// callers can attach the IDs to observability events. Grouping the two IDs in a +// struct avoids confusing the session ID with the identity ID at the call site. +type RevokedSession struct { + // ID is the ID of the revoked session. + ID uuid.UUID + // IdentityID is the ID of the identity that owned the revoked session. + IdentityID uuid.UUID +} + +type Persister interface { + GetConnection(ctx context.Context) *pop.Connection + + // GetSession retrieves a session from the store. + GetSession(ctx context.Context, sid uuid.UUID, expandables Expandables) (*Session, error) + + // ListSessions retrieves all sessions. + ListSessions(ctx context.Context, active *bool, paginatorOpts []keysetpagination.Option, expandables Expandables) ([]Session, *keysetpagination.Paginator, error) + + // ListSessionsByIdentity retrieves sessions for an identity from the store. + ListSessionsByIdentity(ctx context.Context, iID uuid.UUID, active *bool, page, perPage int, except uuid.UUID, expandables Expandables) ([]Session, int64, error) + + // 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 + + // DeleteSessionsByIdentity removes all active session from the store for the given identity. + DeleteSessionsByIdentity(ctx context.Context, identity uuid.UUID) error + + // GetSessionByToken gets the session associated with the given token. + // + // Functionality is similar to GetSession but accepts a session token + // instead of a session ID. + GetSessionByToken(ctx context.Context, token string, expandables Expandables, identityExpandables identity.Expandables) (*Session, error) + + // DeleteExpiredSessions deletes sessions that expired before the given time. + DeleteExpiredSessions(context.Context, time.Time, int) error + + // DeleteSessionByToken deletes a session associated with the given token. + // + // Functionality is similar to DeleteSession but accepts a session token + // instead of a session ID. + DeleteSessionByToken(context.Context, string) error + + // RevokeSessionByToken marks a session inactive with the given token and + // returns the IDs of the revoked session so callers can emit observability + // events without a separate GetSessionByToken round trip. + // Returns sqlcon.ErrNoRows() if no matching session exists in the caller's + // network. The returned IDs are uuid.Nil in the error case. + RevokeSessionByToken(ctx context.Context, token string) (RevokedSession, error) + + // RevokeSessionById marks a session inactive with the specified uuid + RevokeSessionById(ctx context.Context, sID uuid.UUID) error + + // RevokeSession marks a given session inactive. + RevokeSession(ctx context.Context, iID, sID uuid.UUID) error + + // RevokeSessionsIdentityExcept marks all except the given session of an identity inactive. It returns the number of sessions that were revoked. + RevokeSessionsIdentityExcept(ctx context.Context, iID, sID uuid.UUID) (int, error) + + // RevokeSessionsByIdentities marks all active sessions inactive for the given identity IDs. Returns the number of rows updated. + RevokeSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (int, error) + + // RevokeSessionsByIDs marks the listed sessions inactive (only ones currently active). Returns the number of rows updated. + RevokeSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (int, error) + + // RevokeAllSessions deactivates up to `limit` currently-active sessions + // in the caller's network in a single SQL statement and returns the + // number of rows actually updated (in the range [0, limit]). + // Already-inactive sessions are skipped. A returned count below `limit` + // signals that no more matching rows remain. + RevokeAllSessions(ctx context.Context, limit int) (int, error) + + // DeleteSessionsByIdentities permanently deletes all sessions belonging to the given identity IDs. Returns the number of rows deleted. + DeleteSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (int, error) + + // DeleteSessionsByIDs permanently deletes the listed sessions. Returns the number of rows deleted. + DeleteSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (int, error) + + // DeleteAllSessions permanently deletes up to `limit` sessions in the + // caller's network in a single SQL statement and returns the number of + // rows actually deleted (in the range [0, limit]). A returned count + // below `limit` signals that no more matching rows remain. + DeleteAllSessions(ctx context.Context, limit int) (int, error) +} + +type DevicePersister interface { + CreateDevice(ctx context.Context, d *Device) error +} diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..212ac01 --- /dev/null +++ b/session/session.go @@ -0,0 +1,436 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "database/sql/driver" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/x/httpx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/randx" +) + +func ErrIdentityDisabled() *herodot.DefaultError { + return herodot.ErrBadRequest().WithID(text.ErrIDIdentityDisabled).WithError("identity is disabled").WithReason("This account was disabled.") +} + +// lifespanProvider supplies the session lifespan, with optional per-organization overrides. +type lifespanProvider interface { + SessionLifespan(ctx context.Context) time.Duration + OrganizationSessionLifespan(ctx context.Context, orgID uuid.UUID) time.Duration +} + +type refreshWindowProvider interface { + SessionRefreshMinTimeLeft(ctx context.Context) time.Duration +} + +// Device corresponding to a Session +// +// swagger:model sessionDevice +type Device struct { + // Device record ID + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id"` + + // SessionID is a helper struct field for gobuffalo.pop. + SessionID uuid.UUID `json:"-" faker:"-" db:"session_id"` + + // IPAddress of the client + IPAddress *string `json:"ip_address" faker:"ptr_ipv4" db:"ip_address"` + + // UserAgent of the client + UserAgent *string `json:"user_agent" faker:"-" db:"user_agent"` + + // Geo Location corresponding to the IP Address + Location *string `json:"location" faker:"ptr_geo_location" db:"location"` + + // Time of capture + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // Last updated at + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID *uuid.UUID `json:"-" faker:"-" db:"identity_id"` +} + +func (Device) TableName() string { return "session_devices" } + +// A Session +// +// swagger:model session +type Session struct { + // Session ID + // + // required: true + ID uuid.UUID `json:"id" faker:"-" db:"id"` + + // Active state. If false the session is no longer active. + Active bool `json:"active" db:"active"` + + // The Session Expiry + // + // When this session expires at. + ExpiresAt time.Time `json:"expires_at" db:"expires_at" faker:"time_type"` + + // The Session Authentication Timestamp + // + // When this session was authenticated at. If multi-factor authentication was used this + // is the time when the last factor was authenticated (e.g. the TOTP code challenge was completed). + AuthenticatedAt time.Time `json:"authenticated_at" db:"authenticated_at" faker:"time_type"` + + // AuthenticationMethod Assurance Level (AAL) + // + // The authenticator assurance level can be one of "aal1", "aal2", or "aal3". A higher number means that it is harder + // for an attacker to compromise the account. + // + // Generally, "aal1" implies that one authentication factor was used while AAL2 implies that two factors (e.g. + // password + TOTP) have been used. + // + // To learn more about these levels please head over to: https://www.ory.com/kratos/docs/concepts/credentials + AuthenticatorAssuranceLevel identity.AuthenticatorAssuranceLevel `faker:"aal_type" db:"aal" json:"authenticator_assurance_level"` + + // Authentication Method References (AMR) + // + // A list of authentication methods (e.g. password, oidc, ...) used to issue this session. + AMR AuthenticationMethods `db:"authentication_methods" json:"authentication_methods"` + + // The Session Issuance Timestamp + // + // When this session was issued at. Usually equal or close to `authenticated_at`. + IssuedAt time.Time `json:"issued_at" db:"issued_at" faker:"time_type"` + + // The Logout Token + // + // Use this token to log out a user. + LogoutToken string `json:"-" db:"logout_token"` + + // The Session Identity + // + // The identity that authenticated this session. + // + // If 2FA is required for the user, and the authentication process only solved the first factor, this field will be + // null until the session has been fully authenticated with the second factor. + Identity *identity.Identity `json:"identity" faker:"identity" db:"-" belongs_to:"identities" fk_id:"IdentityID"` + + // Devices has history of all endpoints where the session was used + Devices []Device `json:"devices" faker:"-" has_many:"session_devices" fk_id:"session_id"` + + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // Tokenized is the tokenized (e.g. JWT) version of the session. + // + // It is only set when the `tokenize_as` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized string `json:"tokenized,omitempty" faker:"-" db:"-"` + + // The Session Token + // + // The token of this session. + Token string `json:"-" db:"token"` + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (s Session) PageToken() keysetpagination.PageToken { + return keysetpagination.MapPageToken{ + "id": s.ID.String(), + "created_at": s.CreatedAt.Format(x.MapPaginationDateFormat), + } +} + +func (Session) DefaultPageToken() keysetpagination.PageToken { + return keysetpagination.MapPageToken{ + "id": uuid.Nil.String(), + "created_at": time.Date(2200, 12, 31, 23, 59, 59, 0, time.UTC).Format(x.MapPaginationDateFormat), + } +} + +func (Session) TableName() string { return "sessions" } + +// AdminSession is a Session that preserves metadata_admin on the embedded +// identity during JSON marshaling. Use this type (or AdminSessions for slices) +// when writing responses from admin endpoints. +type AdminSession Session + +func (s AdminSession) MarshalJSON() ([]byte, error) { + type sessionAlias Session + type adminSession struct { + sessionAlias + Identity *identity.WithAdminMetadataInJSON `json:"identity"` + } + session := Session(s) + session.Active = session.IsActive() + + return json.Marshal(adminSession{ + sessionAlias: sessionAlias(session), + Identity: (*identity.WithAdminMetadataInJSON)(session.Identity), + }) +} + +// AdminSessions converts a slice of Session to a slice of AdminSession for +// admin endpoint responses that need to include metadata_admin. +func AdminSessions(sessions []Session) []AdminSession { + result := make([]AdminSession, len(sessions)) + for i, s := range sessions { + result[i] = AdminSession(s) + } + return result +} + +func (s *Session) CompletedLoginForMethod(method AuthenticationMethod) { + method.CompletedAt = time.Now().UTC() + s.AMR = append(s.AMR, method) +} + +func (s *Session) CompletedLoginFor(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel) { + s.CompletedLoginForMethod(AuthenticationMethod{Method: method, AAL: aal}) +} + +func (s *Session) CompletedLoginForWithProvider(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel, providerID string, organizationID string) { + s.CompletedLoginForMethod(AuthenticationMethod{ + Method: method, + AAL: aal, + Provider: providerID, + Organization: organizationID, + }) +} + +// CompletedLoginForOIDC appends an OIDC authentication method to the +// session and records the upstream `acr` / `amr` claim values for +// auditing. Use this when the caller has access to the upstream claims; +// otherwise use CompletedLoginForWithProvider. +func (s *Session) CompletedLoginForOIDC(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel, providerID, organizationID, upstreamACR string, upstreamAMR []string) { + s.CompletedLoginForMethod(AuthenticationMethod{ + Method: method, + AAL: aal, + Provider: providerID, + Organization: organizationID, + UpstreamACR: upstreamACR, + UpstreamAMR: upstreamAMR, + }) +} + +func (s *Session) AuthenticatedVia(method identity.CredentialsType) bool { + for _, authMethod := range s.AMR { + if authMethod.Method == method { + return true + } + } + return false +} + +func (s *Session) SetAuthenticatorAssuranceLevel() { + if len(s.AMR) == 0 { + s.AuthenticatorAssuranceLevel = identity.NoAuthenticatorAssuranceLevel + return + } + + var isAAL1, isAAL2 bool + for _, amr := range s.AMR { + switch amr.AAL { + case identity.AuthenticatorAssuranceLevel1: + isAAL1 = true + case identity.AuthenticatorAssuranceLevel2: + isAAL2 = true + if m := amr.Method; m == identity.CredentialsTypeOIDC || m == identity.CredentialsTypeSAML { + // OIDC or SAML can elevate a session to AAL2 on their own + // because the upstream provider performed the MFA itself + // (carried over via the `acr` / `amr` claims and the provider's + // AAL2ACRValues / AAL2AMRValues config). + isAAL1 = true + } + } + } + + if isAAL1 && isAAL2 { + s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel2 + } else { + // AAL1 covers every other case: single first-factor methods (password, + // OIDC without upstream MFA), an unpaired second-factor entry, or only + // recovery credentials. + s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 + } +} + +func NewInactiveSession() *Session { + return &Session{ + ID: uuid.Nil, + Token: x.OrySessionToken + randx.MustString(32, randx.AlphaNum), + LogoutToken: x.OryLogoutToken + randx.MustString(32, randx.AlphaNum), + Active: false, + AuthenticatorAssuranceLevel: identity.NoAuthenticatorAssuranceLevel, + } +} + +func (s *Session) SetSessionDeviceInformation(r *http.Request) { + device := Device{ + SessionID: s.ID, + IdentityID: new(s.IdentityID), + IPAddress: new(httpx.ClientIP(r)), + } + + agent := r.Header["User-Agent"] + if len(agent) > 0 { + device.UserAgent = new(strings.Join(agent, " ")) + } + + var clientGeoLocation []string + if r.Header.Get("Cf-Ipcity") != "" { + clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcity")) + } + if r.Header.Get("Cf-Ipcountry") != "" { + clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry")) + } + loc := strings.Join(clientGeoLocation, ", ") + device.Location = &loc + + s.Devices = append(s.Devices, device) +} + +func (s Session) Declassified() *Session { + s.Identity = s.Identity.CopyWithoutCredentials() + return &s +} + +func (s *Session) IsActive() bool { + return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive()) +} + +func (s *Session) Refresh(ctx context.Context, c lifespanProvider) *Session { + s.ExpiresAt = time.Now().Add(c.OrganizationSessionLifespan(ctx, s.OrganizationID())).UTC() + return s +} + +// OrganizationID returns the organization ID that issued this session, taken +// from the first AMR entry whose Organization field parses as a UUID. Returns +// uuid.Nil if no AMR entry carries a parseable organization ID, or if s is +// nil. +func (s *Session) OrganizationID() uuid.UUID { + if s == nil { + return uuid.Nil + } + for _, m := range s.AMR { + if m.Organization == "" { + continue + } + id, err := uuid.FromString(m.Organization) + if err != nil { + continue + } + return id + } + return uuid.Nil +} + +func (s *Session) MarshalJSON() ([]byte, error) { + type ss Session + out := ss(*s) + out.Active = s.IsActive() + return json.Marshal(out) +} + +func (s *Session) CanBeRefreshed(ctx context.Context, c refreshWindowProvider) bool { + return s.ExpiresAt.Add(-c.SessionRefreshMinTimeLeft(ctx)).Before(time.Now()) +} + +// List of (Used) AuthenticationMethods +// +// A list of authenticators which were used to authenticate the session. +// +// swagger:model sessionAuthenticationMethods +type AuthenticationMethods []AuthenticationMethod + +// AuthenticationMethod identifies an authentication method +// +// A singular authenticator used during authentication / login. +// +// swagger:model sessionAuthenticationMethod +type AuthenticationMethod struct { + // The method used in this authenticator. + Method identity.CredentialsType `json:"method"` + + // The AAL this method introduced. + AAL identity.AuthenticatorAssuranceLevel `json:"aal" faker:"aal_type"` + + // When the authentication challenge was completed. + CompletedAt time.Time `json:"completed_at"` + + // OIDC or SAML provider id used for authentication + Provider string `json:"provider,omitempty"` + + // The Organization id used for authentication + Organization string `json:"organization,omitempty"` + + // UpstreamACR is the `acr` claim reported by the upstream OIDC + // provider, if any. Populated only for OIDC login methods when the + // upstream ID token contained an `acr` claim. + UpstreamACR string `json:"upstream_acr,omitempty"` + + // UpstreamAMR is the `amr` claim reported by the upstream OIDC + // provider, if any. Populated only for OIDC login methods when the + // upstream ID token contained an `amr` claim. + UpstreamAMR []string `json:"upstream_amr,omitempty"` +} + +// Scan implements the Scanner interface. +func (n *AuthenticationMethod) Scan(value interface{}) error { + if value == nil { + return nil + } + v := fmt.Sprintf("%s", value) + if len(v) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal([]byte(v), n)) +} + +// Value implements the driver Valuer interface. +func (n AuthenticationMethod) Value() (driver.Value, error) { + value, err := json.Marshal(n) + if err != nil { + return nil, errors.WithStack(err) + } + return string(value), nil +} + +// Scan implements the Scanner interface. +func (n *AuthenticationMethods) Scan(value interface{}) error { + if value == nil { + return nil + } + v := fmt.Sprintf("%s", value) + if len(v) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal([]byte(v), n)) +} + +// Value implements the driver Valuer interface. +func (n AuthenticationMethods) Value() (driver.Value, error) { + value, err := json.Marshal(n) + if err != nil { + return nil, errors.WithStack(err) + } + return string(value), nil +} diff --git a/session/session_test.go b/session/session_test.go new file mode 100644 index 0000000..d04a46c --- /dev/null +++ b/session/session_test.go @@ -0,0 +1,624 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/gofrs/uuid" + "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/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +func TestAdminSessionMarshal(t *testing.T) { + t.Parallel() + + metadataAdmin := []byte(`{"role":"admin"}`) + ident := &identity.Identity{ + MetadataAdmin: metadataAdmin, + State: identity.StateActive, + } + sess := session.Session{ + ID: x.NewUUID(), + Active: true, + Identity: ident, + ExpiresAt: time.Now().Add(time.Hour), + } + + t.Run("case=regular Session omits metadata_admin", func(t *testing.T) { + t.Parallel() + b, err := json.Marshal(sess) + require.NoError(t, err) + assert.False(t, gjson.GetBytes(b, "identity.metadata_admin").Exists()) + assert.Equal(t, sess.ID.String(), gjson.GetBytes(b, "id").String()) + assert.True(t, gjson.GetBytes(b, "active").Bool()) + }) + + t.Run("case=AdminSession includes metadata_admin", func(t *testing.T) { + t.Parallel() + b, err := json.Marshal(session.AdminSession(sess)) + require.NoError(t, err) + assert.Equal(t, `{"role":"admin"}`, gjson.GetBytes(b, "identity.metadata_admin").Raw) + assert.Equal(t, sess.ID.String(), gjson.GetBytes(b, "id").String()) + assert.True(t, gjson.GetBytes(b, "active").Bool()) + }) + + t.Run("case=AdminSession uses runtime active semantics for expired sessions", func(t *testing.T) { + t.Parallel() + + expiredSess := session.Session{ + ID: x.NewUUID(), + Active: true, + Identity: ident, + ExpiresAt: time.Now().Add(-time.Minute), + } + + b, err := json.Marshal(session.AdminSession(expiredSess)) + require.NoError(t, err) + assert.Equal(t, `{"role":"admin"}`, gjson.GetBytes(b, "identity.metadata_admin").Raw) + assert.Equal(t, expiredSess.ID.String(), gjson.GetBytes(b, "id").String()) + assert.False(t, gjson.GetBytes(b, "active").Bool()) + }) + + t.Run("case=AdminSessions converts slice", func(t *testing.T) { + t.Parallel() + sessions := []session.Session{sess, sess} + adminSessions := session.AdminSessions(sessions) + require.Len(t, adminSessions, 2) + for _, as := range adminSessions { + b, err := json.Marshal(as) + require.NoError(t, err) + assert.Equal(t, `{"role":"admin"}`, gjson.GetBytes(b, "identity.metadata_admin").Raw) + assert.Equal(t, sess.ID.String(), gjson.GetBytes(b, "id").String()) + assert.True(t, gjson.GetBytes(b, "active").Bool()) + } + }) +} + +func TestSession(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + ) + authAt := time.Now() + + t.Run("case=active session", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + i := new(identity.Identity) + i.State = identity.StateActive + 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) + 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()) + }) + + t.Run("case=expired", func(t *testing.T) { + assert.False(t, (&session.Session{ExpiresAt: time.Now().Add(time.Hour)}).IsActive()) + assert.False(t, (&session.Session{Active: true}).IsActive()) + }) + + t.Run("case=amr add", func(t *testing.T) { + s := session.NewInactiveSession() + s.CompletedLoginFor(identity.CredentialsTypeOIDC, identity.AuthenticatorAssuranceLevel1) + assert.EqualValues(t, identity.CredentialsTypeOIDC, s.AMR[0].Method) + s.CompletedLoginFor(identity.CredentialsTypeRecoveryLink, identity.AuthenticatorAssuranceLevel1) + assert.EqualValues(t, identity.CredentialsTypeOIDC, s.AMR[0].Method) + assert.EqualValues(t, identity.CredentialsTypeRecoveryLink, s.AMR[1].Method) + s.CompletedLoginFor(identity.CredentialsTypeRecoveryCode, identity.AuthenticatorAssuranceLevel1) + assert.EqualValues(t, identity.CredentialsTypeRecoveryCode, s.AMR[2].Method) + }) + + t.Run("case=activate", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + s := session.NewInactiveSession() + 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, 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) + }) + + t.Run("case=client information reverse proxy forward", func(t *testing.T) { + for _, tc := range []struct { + input string + expected string + }{ + { + input: "10.10.8.1, 172.19.2.7", + expected: "", + }, + { + input: "217.73.188.139,162.158.203.149, 172.19.2.7", + expected: "162.158.203.149", + }, + { + input: "122.122.122.122 , 123.123.123.123", + expected: "123.123.123.123", + }, + } { + t.Run("case=parse "+tc.input, func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} + req.Header.Set("X-Forwarded-For", tc.input) + + s := session.NewInactiveSession() + 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) + assert.Equal(t, 1, len(s.Devices)) + assert.Equal(t, s.ID.String(), s.Devices[0].SessionID.String()) + assert.Equal(t, tc.expected, *s.Devices[0].IPAddress) + assert.Equal(t, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", *s.Devices[0].UserAgent) + assert.Equal(t, "", *s.Devices[0].Location) + }) + } + }) + + t.Run("case=client information reverse proxy real IP set", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} + req.Header.Set("X-Real-IP", "54.155.246.155") + req.Header["X-Forwarded-For"] = []string{"54.155.246.232", "10.145.1.10"} + + s := session.NewInactiveSession() + 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) + assert.Equal(t, 1, len(s.Devices)) + assert.Equal(t, s.ID.String(), s.Devices[0].SessionID.String()) + assert.NotNil(t, s.Devices[0].UpdatedAt) + assert.NotNil(t, s.Devices[0].CreatedAt) + assert.Equal(t, "54.155.246.155", *s.Devices[0].IPAddress) + assert.Equal(t, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", *s.Devices[0].UserAgent) + assert.Equal(t, "", *s.Devices[0].Location) + }) + + t.Run("case=client information CF true client IP set", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} + req.Header.Set("True-Client-IP", "54.155.246.155") + req.Header.Set("X-Forwarded-For", "217.73.188.139,162.158.203.149, 172.19.2.7") + + s := session.NewInactiveSession() + 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) + assert.Equal(t, 1, len(s.Devices)) + assert.Equal(t, s.ID.String(), s.Devices[0].SessionID.String()) + assert.NotNil(t, s.Devices[0].UpdatedAt) + assert.NotNil(t, s.Devices[0].CreatedAt) + assert.Equal(t, "54.155.246.155", *s.Devices[0].IPAddress) + assert.Equal(t, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", *s.Devices[0].UserAgent) + assert.Equal(t, "", *s.Devices[0].Location) + }) + + t.Run("case=client information CF", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} + req.Header.Set("True-Client-IP", "54.155.246.232") + req.Header.Set("Cf-Ipcity", "Munich") + req.Header.Set("Cf-Ipcountry", "Germany") + + s := session.NewInactiveSession() + 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) + assert.Equal(t, 1, len(s.Devices)) + assert.Equal(t, s.ID.String(), s.Devices[0].SessionID.String()) + assert.Equal(t, "54.155.246.232", *s.Devices[0].IPAddress) + assert.Equal(t, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", *s.Devices[0].UserAgent) + assert.Equal(t, "Munich, Germany", *s.Devices[0].Location) + }) + + for k, tc := range []struct { + d string + methods []session.AuthenticationMethod + expected identity.AuthenticatorAssuranceLevel + }{ + { + d: "no amr means no assurance", + expected: identity.NoAuthenticatorAssuranceLevel, + }, + { + d: "password is aal1", + methods: []session.AuthenticationMethod{{Method: identity.CredentialsTypePassword}}, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "oidc is aal1", + methods: []session.AuthenticationMethod{{Method: identity.CredentialsTypeOIDC}}, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "recovery link is aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryLink}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "recovery code is aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryCode}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "mix of password, oidc, recovery link is still aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryLink}, + {Method: identity.CredentialsTypeOIDC}, + {Method: identity.CredentialsTypePassword}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "mix of password, oidc, recovery code is still aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryCode}, + {Method: identity.CredentialsTypeOIDC}, + {Method: identity.CredentialsTypePassword}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "just totp is gracefully aal1", + methods: []session.AuthenticationMethod{{Method: identity.CredentialsTypeTOTP}}, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "password + totp is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword}, + {Method: identity.CredentialsTypeTOTP}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "password + lookup is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword}, + {Method: identity.CredentialsTypeLookup}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "oidc + totp is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeOIDC}, + {Method: identity.CredentialsTypeTOTP}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "oidc + lookup is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeOIDC}, + {Method: identity.CredentialsTypeLookup}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "recovery link + totp is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryLink}, + {Method: identity.CredentialsTypeTOTP}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "recovery code + totp is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryCode}, + {Method: identity.CredentialsTypeTOTP}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "recovery link + lookup is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryLink}, + {Method: identity.CredentialsTypeLookup}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "recovery code + lookup is aal2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryCode}, + {Method: identity.CredentialsTypeLookup}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "recovery link + passwordless webauth is aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryLink}, + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "recovery code + passwordless webauth is aal1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeRecoveryCode}, + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "respects AAL on AAL1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1}, + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "respects AAL on AAL2 without AAL1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel2}, + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "respects AAL on AAL2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1}, + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + s := session.NewInactiveSession() + for _, m := range tc.methods { + s.CompletedLoginFor(m.Method, m.AAL) + } + }) + } + + t.Run("case=federated AAL2 carry-over", func(t *testing.T) { + // A single OIDC or SAML method tagged AAL2 elevates the + // session to AAL2 on its own. This is the upstream-MFA + // carry-over path: the provider config matched the upstream + // `acr` or `amr` claims, so Ory trusts that the upstream IdP + // already performed a second factor. Local methods (password, + // TOTP, WebAuthn, lookup, recovery) still need to be paired + // with an AAL1 entry to reach AAL2. + for _, tc := range []struct { + d string + methods []session.AuthenticationMethod + expected identity.AuthenticatorAssuranceLevel + }{ + { + d: "single OIDC method with explicit AAL2 is AAL2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "single SAML method with explicit AAL2 is AAL2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeSAML, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + { + d: "single password method tagged AAL2 stays AAL1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "single WebAuthn method tagged AAL2 stays AAL1", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel1, + }, + { + d: "OIDC AAL2 carry-over plus extra password AAL1 stays AAL2", + methods: []session.AuthenticationMethod{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1}, + {Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel2}, + }, + expected: identity.AuthenticatorAssuranceLevel2, + }, + } { + t.Run("case="+tc.d, func(t *testing.T) { + s := session.NewInactiveSession() + for _, m := range tc.methods { + s.CompletedLoginFor(m.Method, m.AAL) + } + s.SetAuthenticatorAssuranceLevel() + assert.Equal(t, tc.expected, s.AuthenticatorAssuranceLevel) + }) + } + }) + + t.Run("case=authentication method upstream acr/amr round-trip", func(t *testing.T) { + t.Parallel() + in := session.AuthenticationMethod{ + Method: identity.CredentialsTypeOIDC, + AAL: identity.AuthenticatorAssuranceLevel2, + CompletedAt: time.Now().UTC().Round(time.Second), + Provider: "hydra", + Organization: "acme", + UpstreamACR: "urn:mfa", + UpstreamAMR: []string{"pwd", "mfa"}, + } + + value, err := in.Value() + require.NoError(t, err) + + var out session.AuthenticationMethod + require.NoError(t, out.Scan(value)) + assert.Equal(t, in, out) + + // Also round-trip via a list, to exercise the AuthenticationMethods + // Scan/Value path used by the sessions table column. + methods := session.AuthenticationMethods{in} + listValue, err := methods.Value() + require.NoError(t, err) + + var outList session.AuthenticationMethods + require.NoError(t, outList.Scan(listValue)) + require.Len(t, outList, 1) + assert.Equal(t, in, outList[0]) + }) + + t.Run("case=CompletedLoginForOIDC populates upstream claims", func(t *testing.T) { + t.Parallel() + s := session.NewInactiveSession() + s.CompletedLoginForOIDC( + identity.CredentialsTypeOIDC, + identity.AuthenticatorAssuranceLevel2, + "hydra", + "acme", + "urn:mfa", + []string{"pwd", "mfa"}, + ) + require.Len(t, s.AMR, 1) + assert.Equal(t, identity.CredentialsTypeOIDC, s.AMR[0].Method) + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, s.AMR[0].AAL) + assert.Equal(t, "hydra", s.AMR[0].Provider) + assert.Equal(t, "acme", s.AMR[0].Organization) + assert.Equal(t, "urn:mfa", s.AMR[0].UpstreamACR) + assert.Equal(t, []string{"pwd", "mfa"}, s.AMR[0].UpstreamAMR) + assert.False(t, s.AMR[0].CompletedAt.IsZero()) + }) + + t.Run("case=session refresh", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + ctx := contextx.WithConfigValues(t.Context(), map[string]any{ + config.ViperKeySessionLifespan: "24h", + config.ViperKeySessionRefreshMinTimeLeft: "12h", + }) + i := new(identity.Identity) + i.State = identity.StateActive + 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, reg.Config()), "fresh session is not refreshable") + + s.ExpiresAt = s.ExpiresAt.Add(-12 * time.Hour) + assert.True(t, s.CanBeRefreshed(ctx, reg.Config()), "session is refreshable after 12hrs") + }) + + t.Run("case=organization id", func(t *testing.T) { + t.Run("case=returns uuid.Nil for nil session", func(t *testing.T) { + var s *session.Session + assert.Equal(t, uuid.Nil, s.OrganizationID()) + }) + + t.Run("case=returns uuid.Nil for empty AMR", func(t *testing.T) { + s := &session.Session{} + assert.Equal(t, uuid.Nil, s.OrganizationID()) + }) + + t.Run("case=returns uuid.Nil when AMR has no org-tagged entry", func(t *testing.T) { + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1}, + }} + assert.Equal(t, uuid.Nil, s.OrganizationID()) + }) + + t.Run("case=returns uuid.Nil when the org is not a parseable UUID", func(t *testing.T) { + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1, Organization: "not-a-uuid"}, + }} + assert.Equal(t, uuid.Nil, s.OrganizationID()) + }) + + t.Run("case=returns the org from the first AMR entry that carries a UUID", func(t *testing.T) { + orgA := uuid.Must(uuid.NewV4()) + orgB := uuid.Must(uuid.NewV4()) + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1}, + {Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1, Organization: orgA.String()}, + {Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1, Organization: orgB.String()}, + }} + assert.Equal(t, orgA, s.OrganizationID()) + }) + }) + + t.Run("case=refresh uses organization lifespan", func(t *testing.T) { + // Use a real config (not a mock provider) so Refresh exercises the + // actual Config.OrganizationSessionLifespan resolution. + orgWithOverride := uuid.Must(uuid.NewV4()) + orgWithoutOverride := uuid.Must(uuid.NewV4()) + _, oreg := pkg.NewFastRegistryWithMocks(t, + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/identity.schema.json")), + configx.WithValues(map[string]any{ + config.ViperKeySessionLifespan: "24h", + "selfservice.methods.b2b.config.organizations": []map[string]any{ + {"id": orgWithOverride.String(), "domains": []string{"override.example.com"}, "session_lifespan": "1h"}, + {"id": orgWithoutOverride.String(), "domains": []string{"default.example.com"}}, + }, + }), + ) + ctx := t.Context() + + t.Run("case=no org in AMR uses project default", func(t *testing.T) { + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypePassword}, + }} + s.Refresh(ctx, oreg.Config()) + assert.WithinDuration(t, time.Now().Add(24*time.Hour), s.ExpiresAt, 2*time.Second) + }) + + t.Run("case=org in AMR with override uses override", func(t *testing.T) { + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypeOIDC, Organization: orgWithOverride.String()}, + }} + s.Refresh(ctx, oreg.Config()) + assert.WithinDuration(t, time.Now().Add(1*time.Hour), s.ExpiresAt, 2*time.Second) + }) + + t.Run("case=org in AMR without override uses project default", func(t *testing.T) { + s := &session.Session{AMR: session.AuthenticationMethods{ + {Method: identity.CredentialsTypeOIDC, Organization: orgWithoutOverride.String()}, + }} + s.Refresh(ctx, oreg.Config()) + assert.WithinDuration(t, time.Now().Add(24*time.Hour), s.ExpiresAt, 2*time.Second) + }) + }) +} diff --git a/session/stub/fake-session.schema.json b/session/stub/fake-session.schema.json new file mode 100644 index 0000000..558106d --- /dev/null +++ b/session/stub/fake-session.schema.json @@ -0,0 +1,7 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": {} +} diff --git a/session/stub/identity.schema.json b/session/stub/identity.schema.json new file mode 100644 index 0000000..da89843 --- /dev/null +++ b/session/stub/identity.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.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/session/stub/jwk.es256.json b/session/stub/jwk.es256.json new file mode 100644 index 0000000..860ff10 --- /dev/null +++ b/session/stub/jwk.es256.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "247f1420-e581-4023-88e0-07ee662f80da", + "crv": "P-256", + "alg": "ES256", + "x": "1odGSu9bvVq_9QqqNny8TvvUElscLYoTExxhnomYOgQ", + "y": "pa4d4Ql1lO86PBnQ8efYzSzW9nUrsfLlomn3RIpH2Ic", + "d": "kPoEy2OcUeHobxp9jK00YKTs0CBoRTMWZJoPOe9K5hQ" + } + ] +} diff --git a/session/stub/jwk.es512.broken.json b/session/stub/jwk.es512.broken.json new file mode 100644 index 0000000..e645648 --- /dev/null +++ b/session/stub/jwk.es512.broken.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "bc7f7afc-6742-427c-bb9e-164fe0f8b6a7", + "crv": "P-521", + "alg": "ES512", + "x": "ASj36HQOpsWiaGyzK1F0GkxXRt37R01M-OCWFk8rFqH8UnFBk0qnCmVYWv3pwVPPsN0CfFiaXTrV1gUSapkkDgWY", + "y": "ALf5bqXExUq6FzQNQg01hDhR2lOKzkrC02Bc6Alld8Zji3-echbimNZltoOi4MhXbSJeWHpU8wzb3v9XAAW4eovn", + "d": "ALP0Sf7cmcELc9CQ2bWd6Qs-YxMu0N9EYZhDmR6qbYdGnvv-lcGy_ySoEJD0vPMKagA8PHDvFhC7ORwP-sBIJ4O_" + } + ] + diff --git a/session/stub/jwk.es512.json b/session/stub/jwk.es512.json new file mode 100644 index 0000000..d8ab0ab --- /dev/null +++ b/session/stub/jwk.es512.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "bc7f7afc-6742-427c-bb9e-164fe0f8b6a7", + "crv": "P-521", + "alg": "ES512", + "x": "ASj36HQOpsWiaGyzK1F0GkxXRt37R01M-OCWFk8rFqH8UnFBk0qnCmVYWv3pwVPPsN0CfFiaXTrV1gUSapkkDgWY", + "y": "ALf5bqXExUq6FzQNQg01hDhR2lOKzkrC02Bc6Alld8Zji3-echbimNZltoOi4MhXbSJeWHpU8wzb3v9XAAW4eovn", + "d": "ALP0Sf7cmcELc9CQ2bWd6Qs-YxMu0N9EYZhDmR6qbYdGnvv-lcGy_ySoEJD0vPMKagA8PHDvFhC7ORwP-sBIJ4O_" + } + ] +} diff --git a/session/stub/jwk.rs512.json b/session/stub/jwk.rs512.json new file mode 100644 index 0000000..1d70bb1 --- /dev/null +++ b/session/stub/jwk.rs512.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "95311ff8-ff91-486b-9ad9-21df8bdc95d7", + "alg": "RS512", + "n": "6_ygtx-8qvTeN7ts_qFJCuOIEyxOnUpggbx04cG3vqjtyfzZMfi0wlidgMH3zhglij2MwC5lPLbze5n4lGQk26s8bl0uhdWlFHO_44hN3l2NVbPcocVZDWwqOdct2qRx1sEdRAt-P1a-2gxYN4HaemER9lgZSgbikJhmL3EEKhcr0QklUZyMcUnbaHAopzdiMKpnykR28-SXEizBi7JTI0hRDgCVmjuCRsciI5GAFy-nQ5n3Ofm0x8wGflKN1RAeWvolpakb7YJgQAXKQhOY3huoHlr-sh3ZO9vQjBgVQ1AM3k-z4OiQjJwvgogfLa1lSLmh9_Ax3LJQ5iax-aN9yQ", + "e": "AQAB", + "d": "DjZhy4WazEUBGSQtlUxLZN99M4Jonap8E3QxKeOL2Gy-HXsf7ZWH9Wh22-lSrlPf9upsDqr8p-Jw2ZHVWcKKQbyXYCI2ihLq5UdvWBm-btT9jRrO_-Mt0NQh2uftuAxNWty4kX-Ls-7agbFaosUsTlCIT2jQ5RdzD7hN9y98S7iJ6oUiBKdc7cRkNj2lGqowwM_IR4IoU2rY2pC7OtHZg_BKU8UnSYcPopn9tnobpr4E_AA1x49iUOkEny3wSXEfP2gEqb2Frqsf342b0pOtiq4WuiAopG2vBfMCKGddDBG_fFsxp858i03G9S4IuMDYk1uSeXd0zc_nb0GxFjXpXQ", + "p": "-_rHgqAtRJpeUbdTWhWQxlUFp-g0Yt1WCnKhCYXzgwM1kG-m9mzHW9mEJxTc0olygeo2A0vJ7htZJpd9NtJkD6g5yDZ7GIm5Emr9fNk4glz8nkVV9udU2gv6hVe83XU86cI4B4XWXhjdNvjQd7ohO7kBwB3uBmekVRm6ZaUCD-c", + "q": "78CGezUjroDlUA5CiF75_apyBgrEI41eNiOPwN9X7Jy-3zpvecY9ZfIuyf5O1yNkC_zjanGEKom6L3iqcAV_1-wzGMOMvtIwhmkTPT9g0f1Z1utRQVljyXFBml7__xb9cCfq62YnH7irsM5nbe9Txziq2zvBjWCkkjJwTBPWDs8", + "dp": "lXjhuJ8Du1pG8Ppqu1lnk_8DZ-LakHrzeyccV-XZ2bGhqJhS1oMYj2esePJrO4jFIEOq3rGqi1A1xiq-4DJVoOQNwrJuutOXsVE-JT1FxC8cu1Yt9FSthNruNQMiycut4oyPaAcAbrkZIG7gWuVSqXbJjwkyFSKN3N1yMLF9U6k", + "dq": "MLapnHcbnOVLsoxzMEo7-TKcoGWnnKGots9a8hFvSABBOBIjfFavOvuOTjSByGzEczsa6hHOjOYXEnYuCzzS0QiJCUsSWeNTQLww0I0EGyajDmwZwnFrOQ7uCXOsCCSfsh4qOVI0ONnI6M_HbCrolt4IuSrXFObCCYJ-FrchEzk", + "qi": "fBkpKoiyeXnwBYezKcxkeu6E2F2tWiRgRikM-s50kRu2fGgmAZ-nIIHU5glPXCPbn4IolURxeLm8ZZyCCo2tNPWiAQV2C7UF04Kh8QANULoUdpObjplLEWQuCDgz5m9EkR4Sx6FGGA1TvUpYCjks8YxvdSIa56o3l72I49aL1t8" + } + ] +} diff --git a/session/stub/rs512-template.jsonnet b/session/stub/rs512-template.jsonnet new file mode 100644 index 0000000..50735e8 --- /dev/null +++ b/session/stub/rs512-template.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); +local session = std.extVar('session'); + +{ + claims: { + foo: "bar", + sub: "can not be overwritten", + schema_id: session.identity.schema_id, + aal: session.authenticator_assurance_level, + second_claim: claims.exp, + [if std.objectHas(session.identity, 'external_id') then 'external_id']: session.identity.external_id, + } +} diff --git a/session/test/persistence.go b/session/test/persistence.go new file mode 100644 index 0000000..0395a24 --- /dev/null +++ b/session/test/persistence.go @@ -0,0 +1,973 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/go-faker/faker/v4" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/contextx" + "github.com/ory/x/dbal" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/randx" + "github.com/ory/x/sqlcon" +) + +func TestPersister(ctx context.Context, conf *config.Config, p interface { + persistence.Persister +}, +) func(t *testing.T) { + return func(t *testing.T) { + _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + t.Run("case=not found", func(t *testing.T) { + _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) + require.Error(t, err) + }) + + t.Run("case=create session", func(t *testing.T) { + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.Active = true + expected.AMR = session.AuthenticationMethods{ + {Method: identity.CredentialsTypePassword, CompletedAt: time.Now().UTC().Round(time.Second)}, + {Method: identity.CredentialsTypeOIDC, CompletedAt: time.Now().UTC().Round(time.Second)}, + } + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + + var expectedSessionDevice session.Device + require.NoError(t, faker.FakeData(&expectedSessionDevice)) + expected.Devices = []session.Device{ + expectedSessionDevice, + } + + assert.Equal(t, uuid.Nil, expected.ID) + require.NoError(t, p.UpsertSession(ctx, &expected)) + assert.NotEqual(t, uuid.Nil, expected.ID) + + check := func(actual *session.Session, err error) { + require.NoError(t, err) + assert.Equal(t, expected.Identity.ID, actual.Identity.ID) + assert.NotEmpty(t, actual.Identity.SchemaURL) + assert.NotEmpty(t, actual.Identity.SchemaID) + assert.Equal(t, expected.ID, actual.ID) + assert.Equal(t, expected.Active, actual.Active) + assert.Equal(t, expected.Token, actual.Token) + assert.EqualValues(t, expected.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) + assert.Equal(t, expected.AuthenticatedAt.Unix(), actual.AuthenticatedAt.Unix()) + assert.Equal(t, expected.IssuedAt.Unix(), actual.IssuedAt.Unix()) + assert.Equal(t, expected.AuthenticatorAssuranceLevel, actual.AuthenticatorAssuranceLevel) + assert.Equal(t, expected.AMR, actual.AMR) + } + + checkDevices := func(actual []session.Device, err error) { + require.NoError(t, err) + assert.Equal(t, len(expected.Devices), len(actual)) + + for i, d := range actual { + assert.Equal(t, expected.Devices[i].SessionID, d.SessionID) + assert.Equal(t, expected.Devices[i].NID, d.NID) + assert.Equal(t, *expected.Devices[i].IPAddress, *d.IPAddress) + assert.Equal(t, expected.Devices[i].UserAgent, d.UserAgent) + assert.Equal(t, *expected.Devices[i].Location, *d.Location) + assert.Equal(t, *expected.Devices[i].IdentityID, *d.IdentityID) + } + } + + t.Run("method=get by id", func(t *testing.T) { + sess, err := p.GetSession(ctx, expected.ID, session.ExpandEverything) + check(sess, err) + checkDevices(sess.Devices, err) + + t.Run("on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetSession(ctx, expected.ID, session.ExpandEverything) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("method=get by token", func(t *testing.T) { + sess, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandEverything, identity.ExpandDefault) + check(sess, err) + checkDevices(sess.Devices, err) + + t.Run("on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandNothing, identity.ExpandDefault) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("case=update session", func(t *testing.T) { + expected.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, p.UpsertSession(ctx, &expected)) + + actual, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandDefault, identity.ExpandDefault) + check(actual, err) + assert.Equal(t, identity.AuthenticatorAssuranceLevel1, actual.AuthenticatorAssuranceLevel) + }) + + t.Run("case=remove amr and update", func(t *testing.T) { + expected.AMR = nil + require.NoError(t, p.UpsertSession(ctx, &expected)) + + actual, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandDefault, identity.ExpandDefault) + check(actual, err) + assert.Empty(t, actual.AMR) + }) + }) + + t.Run("case=list sessions", func(t *testing.T) { + var identity1 identity.Identity + require.NoError(t, faker.FakeData(&identity1)) + + // Second identity to test listing by identity isolation + var identity2 identity.Identity + var identity2Session session.Session + require.NoError(t, faker.FakeData(&identity2)) + require.NoError(t, faker.FakeData(&identity2Session)) + + // Create seed identities + _, l := testhelpers.NewNetwork(t, ctx, p) + require.NoError(t, l.CreateIdentity(ctx, &identity1)) + require.NoError(t, l.CreateIdentity(ctx, &identity2)) + + seedSessionIDs := make([]uuid.UUID, 5) + seedSessionsList := make([]session.Session, 5) + now := time.Now().UTC() + for j := range seedSessionsList { + require.NoError(t, faker.FakeData(&seedSessionsList[j])) + seedSessionsList[j].Identity = &identity1 + seedSessionsList[j].Active = j%2 == 0 + + if seedSessionsList[j].Active { + seedSessionsList[j].ExpiresAt = time.Now().UTC().Add(time.Hour) + } else { + seedSessionsList[j].ExpiresAt = time.Now().UTC().Add(-time.Hour) + } + + var device session.Device + require.NoError(t, faker.FakeData(&device)) + seedSessionsList[j].Devices = []session.Device{ + device, + } + require.NoError(t, l.UpsertSession(ctx, &seedSessionsList[j])) + require.NoError(t, p.GetConnection(ctx). + RawQuery( + "UPDATE sessions SET created_at = ?, updated_at = ? WHERE id = ?", + now.Add(time.Duration(j)*time.Minute).Round(time.Second), + now.Add(time.Duration(j)*time.Minute).Round(time.Second), + seedSessionsList[j].ID). + Exec()) + seedSessionIDs[j] = seedSessionsList[j].ID + } + + identity2Session.Identity = &identity2 + identity2Session.Active = true + identity2Session.ExpiresAt = time.Now().UTC().Add(time.Hour) + require.NoError(t, l.UpsertSession(ctx, &identity2Session)) + + for _, tc := range []struct { + desc string + except uuid.UUID + expectedSessionIds []uuid.UUID + active *bool + }{ + { + desc: "all", + expectedSessionIds: seedSessionIDs, + }, + { + desc: "except one", + except: seedSessionsList[0].ID, + expectedSessionIds: []uuid.UUID{ + seedSessionIDs[1], + seedSessionIDs[2], + seedSessionIDs[3], + seedSessionIDs[4], + }, + }, + { + desc: "active only", + active: new(true), + expectedSessionIds: []uuid.UUID{ + seedSessionIDs[0], + seedSessionIDs[2], + seedSessionIDs[4], + }, + }, + { + desc: "active only and except", + active: new(true), + except: seedSessionsList[0].ID, + expectedSessionIds: []uuid.UUID{ + seedSessionIDs[2], + seedSessionIDs[4], + }, + }, + { + desc: "inactive only", + active: new(false), + expectedSessionIds: []uuid.UUID{ + seedSessionIDs[1], + seedSessionIDs[3], + }, + }, + { + desc: "inactive only and except", + active: new(false), + except: seedSessionsList[3].ID, + expectedSessionIds: []uuid.UUID{ + seedSessionIDs[1], + }, + }, + } { + t.Run("case=by Identity "+tc.desc, func(t *testing.T) { + actual, total, err := l.ListSessionsByIdentity(ctx, identity1.ID, tc.active, 1, 10, tc.except, session.ExpandEverything) + require.NoError(t, err) + + actualSessionIds := make([]uuid.UUID, 0) + for _, s := range actual { + actualSessionIds = append(actualSessionIds, s.ID) + } + + assert.Equal(t, int64(len(tc.expectedSessionIds)), total) + assert.ElementsMatch(t, tc.expectedSessionIds, actualSessionIds) + }) + } + + t.Run("case=by Identity on other network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + actual, total, err := other.ListSessionsByIdentity(ctx, identity1.ID, nil, 1, 10, uuid.Nil, session.ExpandNothing) + require.NoError(t, err) + require.Equal(t, int64(0), total) + assert.Len(t, actual, 0) + }) + + for _, tc := range []struct { + desc string + except uuid.UUID + expected []session.Session + active *bool + }{ + { + desc: "all", + expected: append(seedSessionsList, identity2Session), + }, + { + desc: "active only", + active: new(true), + expected: []session.Session{ + seedSessionsList[0], + seedSessionsList[2], + seedSessionsList[4], + identity2Session, + }, + }, + { + desc: "inactive only", + active: new(false), + expected: []session.Session{ + seedSessionsList[1], + seedSessionsList[3], + }, + }, + } { + t.Run("case=all "+tc.desc, func(t *testing.T) { + paginatorOpts := make([]keysetpagination.Option, 0) + actual, nextPage, err := l.ListSessions(ctx, tc.active, paginatorOpts, session.ExpandEverything) + require.NoError(t, err, "%+v", err) + + require.Equal(t, len(tc.expected), len(actual)) + assert.Equal(t, true, nextPage.IsLast()) + + mapPageToken := nextPage.Token().Parse("") + assert.Equal(t, uuid.Nil.String(), mapPageToken["id"]) + + assert.Equal(t, 250, nextPage.Size()) + for _, es := range tc.expected { + found := false + for _, as := range actual { + if as.ID == es.ID { + found = true + assert.Equal(t, len(es.Devices), len(as.Devices)) + assert.Equal(t, es.Identity.ID.String(), as.Identity.ID.String()) + } + } + assert.True(t, found) + } + }) + } + + t.Run("case=all sessions pagination only one page", func(t *testing.T) { + paginatorOpts := make([]keysetpagination.Option, 0) + actual, page, err := l.ListSessions(ctx, nil, paginatorOpts, session.ExpandEverything) + require.NoError(t, err) + + require.Equal(t, 6, len(actual)) + assert.Equal(t, true, page.IsLast()) + mapPageToken := page.Token().Parse("") + assert.Equal(t, uuid.Nil.String(), mapPageToken["id"]) + assert.Equal(t, 250, page.Size()) + }) + + t.Run("case=all sessions pagination multiple pages", func(t *testing.T) { + paginatorOpts := make([]keysetpagination.Option, 0) + paginatorOpts = append(paginatorOpts, keysetpagination.WithSize(3)) + firstPageItems, page1, err := l.ListSessions(ctx, nil, paginatorOpts, session.ExpandEverything) + require.NoError(t, err) + assert.Len(t, firstPageItems, 3) + + assert.Equal(t, false, page1.IsLast()) + mapPageToken := page1.Token().Parse("") + assert.Equal(t, firstPageItems[len(firstPageItems)-1].ID.String(), mapPageToken["id"]) + assert.Equal(t, 3, page1.Size()) + + // Validate secondPageItems page + secondPageItems, page2, err := l.ListSessions(ctx, nil, page1.ToOptions(), session.ExpandEverything) + require.NoError(t, err) + assert.Len(t, secondPageItems, 3) + + acutalIDs := make([]uuid.UUID, 0) + for _, s := range append(firstPageItems, secondPageItems...) { + acutalIDs = append(acutalIDs, s.ID) + } + expect := append(seedSessionIDs, identity2Session.ID) + require.Len(t, acutalIDs, len(expect)) + assert.ElementsMatch(t, expect, acutalIDs) + + assert.True(t, page2.IsLast()) + assert.Equal(t, 3, page2.Size()) + }) + }) + + t.Run("case=delete session", func(t *testing.T) { + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + err := other.DeleteSession(ctx, expected.ID) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + + _, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + assert.NoError(t, err) + }) + + require.NoError(t, p.DeleteSession(ctx, expected.ID)) + _, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + + t.Run("case=delete session by token", func(t *testing.T) { + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + err := other.DeleteSessionByToken(ctx, expected.Token) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + + _, err = p.GetSessionByToken(ctx, expected.Token, session.ExpandNothing, identity.ExpandDefault) + assert.NoError(t, err) + }) + + require.NoError(t, p.DeleteSessionByToken(ctx, expected.Token)) + _, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.Error(t, err) + }) + + t.Run("case=revoke session by token", func(t *testing.T) { + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.Active = true + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + revoked, err := other.RevokeSessionByToken(ctx, expected.Token) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + assert.Equal(t, uuid.Nil, revoked.ID, "session id must be nil when not found") + assert.Equal(t, uuid.Nil, revoked.IdentityID, "identity id must be nil when not found") + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + }) + + revoked, err := p.RevokeSessionByToken(ctx, expected.Token) + require.NoError(t, err) + assert.Equal(t, expected.ID, revoked.ID, "must return the session id") + assert.Equal(t, expected.IdentityID, revoked.IdentityID, "must return the identity id") + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.False(t, actual.Active) + + // Re-revoking an already-inactive session is silently + // idempotent — the CTE-based implementation returns the matched + // row, not the flipped one, so the IDs come back regardless. + revoked, err = p.RevokeSessionByToken(ctx, expected.Token) + require.NoError(t, err, + "re-revoking an already-inactive session must succeed silently on %s", + p.GetConnection(ctx).Dialect.Name()) + assert.Equal(t, expected.ID, revoked.ID, "matched id must be returned on re-revoke") + assert.Equal(t, expected.IdentityID, revoked.IdentityID, "matched identity id must be returned on re-revoke") + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err, "session must still exist after re-revoke") + assert.False(t, actual.Active, "session must remain inactive after re-revoke") + + t.Run("returns ErrNoRows for an unknown token", func(t *testing.T) { + revoked, err := p.RevokeSessionByToken(ctx, "this-token-does-not-exist-"+x.NewUUID().String()) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + assert.Equal(t, uuid.Nil, revoked.ID) + assert.Equal(t, uuid.Nil, revoked.IdentityID) + }) + }) + + t.Run("case=revoke session by id", func(t *testing.T) { + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.Active = true + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + err := other.RevokeSessionById(ctx, expected.ID) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + }) + + require.NoError(t, p.RevokeSessionById(ctx, expected.ID)) + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.False(t, actual.Active) + + // Re-revoking an already-inactive session is silently + // idempotent — the admin disableSession handler relies on it. + require.NoError(t, p.RevokeSessionById(ctx, expected.ID), + "re-revoking an already-inactive session must succeed silently on %s", + p.GetConnection(ctx).Dialect.Name()) + + actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err, "session must still exist after re-revoke") + assert.False(t, actual.Active, "session must remain inactive after re-revoke") + + t.Run("returns ErrNoRows for an unknown id", func(t *testing.T) { + err := p.RevokeSessionById(ctx, x.NewUUID()) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + }) + }) + + t.Run("method=revoke other sessions for identity", func(t *testing.T) { + // here we set up 2 identities with each having 2 sessions + sessions := make([]session.Session, 4) + for i := range sessions { + require.NoError(t, faker.FakeData(&sessions[i])) + } + require.NoError(t, p.CreateIdentity(ctx, sessions[0].Identity)) + require.NoError(t, p.CreateIdentity(ctx, sessions[2].Identity)) + sessions[1].IdentityID, sessions[1].Identity = sessions[0].IdentityID, sessions[0].Identity + sessions[3].IdentityID, sessions[3].Identity = sessions[2].IdentityID, sessions[2].Identity + for i := range sessions { + sessions[i].Active = true + require.NoError(t, p.UpsertSession(ctx, &sessions[i])) + } + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + n, err := other.RevokeSessionsIdentityExcept(ctx, sessions[0].IdentityID, sessions[0].ID) + require.NoError(t, err) + assert.Equal(t, 0, n) + + for _, s := range sessions { + actual, err := p.GetSession(ctx, s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + } + }) + + n, err := p.RevokeSessionsIdentityExcept(ctx, sessions[0].IdentityID, sessions[0].ID) + require.NoError(t, err) + assert.Equal(t, 1, n) + + actual, total, err := p.ListSessionsByIdentity(ctx, sessions[0].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing) + require.NoError(t, err) + require.Len(t, actual, 2) + require.Equal(t, int64(2), total) + + if actual[0].ID == sessions[0].ID { + assert.True(t, actual[0].Active) + assert.False(t, actual[1].Active) + } else { + assert.Equal(t, actual[0].ID, sessions[1].ID) + assert.True(t, actual[1].Active) + assert.False(t, actual[0].Active) + } + + otherIdentitiesSessions, total, err := p.ListSessionsByIdentity(ctx, sessions[2].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing) + require.NoError(t, err) + require.Len(t, actual, 2) + require.Equal(t, int64(2), total) + + for _, s := range otherIdentitiesSessions { + assert.True(t, s.Active) + } + + t.Run("returns matched count, including already-inactive sessions", func(t *testing.T) { + // One identity, three sessions: the keeper, one already- + // inactive other, one active other. Returned count must be 2 + // (both "other" sessions matched the predicate) — not 1 + // (only the active "other" was actually flipped). The CTE on + // CRDB/Postgres must preserve matched-count, not flipped-count. + var keeper session.Session + require.NoError(t, faker.FakeData(&keeper)) + keeper.Active = true + require.NoError(t, p.CreateIdentity(ctx, keeper.Identity)) + require.NoError(t, p.UpsertSession(ctx, &keeper)) + + makeSibling := func() session.Session { + var s session.Session + require.NoError(t, faker.FakeData(&s)) + s.Identity = keeper.Identity + s.IdentityID = keeper.IdentityID + s.Active = true + require.NoError(t, p.UpsertSession(ctx, &s)) + return s + } + alreadyInactive := makeSibling() + require.NoError(t, p.RevokeSessionById(ctx, alreadyInactive.ID)) + _ = makeSibling() // stillActive — will be flipped by the call below + + n, err := p.RevokeSessionsIdentityExcept(ctx, keeper.IdentityID, keeper.ID) + require.NoError(t, err) + assert.Equal(t, 2, n, + "matched count must include already-inactive sessions on %s", + p.GetConnection(ctx).Dialect.Name()) + }) + }) + + t.Run("method=revoke specific session for identity", func(t *testing.T) { + sessions := make([]session.Session, 2) + for i := range sessions { + require.NoError(t, faker.FakeData(&sessions[i])) + } + require.NoError(t, p.CreateIdentity(ctx, sessions[0].Identity)) + sessions[1].IdentityID, sessions[1].Identity = sessions[0].IdentityID, sessions[0].Identity + for i := range sessions { + sessions[i].Active = true + require.NoError(t, p.UpsertSession(ctx, &sessions[i])) + } + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + require.NoError(t, other.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID)) + + for _, s := range sessions { + actual, err := p.GetSession(ctx, s.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual.Active) + } + }) + + require.NoError(t, p.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID)) + + actual, total, err := p.ListSessionsByIdentity(ctx, sessions[0].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing) + require.NoError(t, err) + require.Len(t, actual, 2) + require.Equal(t, int64(2), total) + + if actual[0].ID == sessions[0].ID { + assert.False(t, actual[0].Active) + assert.True(t, actual[1].Active) + } else { + assert.Equal(t, actual[0].ID, sessions[1].ID) + assert.False(t, actual[1].Active) + assert.True(t, actual[0].Active) + } + + // Re-revoking an already-inactive session must succeed silently — + // the `AND active = true` predicate added on the CRDB/Postgres path + // must not change the function's nil-on-no-rows contract. + require.NoError(t, p.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID), + "re-revoking an already-inactive session must succeed silently") + + // Wrong identity_id for an existing session id is a silent no-op, + // not an error. + require.NoError(t, p.RevokeSession(ctx, x.NewUUID(), sessions[1].ID)) + actual1, err := p.GetSession(ctx, sessions[1].ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, actual1.Active, "session must stay active when identity_id mismatches") + }) + + t.Run("case=delete session for", func(t *testing.T) { + var expected1 session.Session + var expected2 session.Session + require.NoError(t, faker.FakeData(&expected1)) + require.NoError(t, p.CreateIdentity(ctx, expected1.Identity)) + + require.NoError(t, p.UpsertSession(ctx, &expected1)) + + require.NoError(t, faker.FakeData(&expected2)) + expected2.Identity = expected1.Identity + expected2.IdentityID = expected1.IdentityID + require.NoError(t, p.UpsertSession(ctx, &expected2)) + + t.Run("on another network", func(t *testing.T) { + _, other := testhelpers.NewNetwork(t, ctx, p) + err := other.DeleteSessionsByIdentity(ctx, expected2.IdentityID) + assert.ErrorIs(t, err, sqlcon.ErrNoRows()) + + _, err = p.GetSession(ctx, expected1.ID, session.ExpandNothing) + require.NoError(t, err) + }) + + require.NoError(t, p.DeleteSessionsByIdentity(ctx, expected2.IdentityID)) + _, err := p.GetSession(ctx, expected1.ID, session.ExpandNothing) + require.Error(t, err) + _, err = p.GetSession(ctx, expected2.ID, session.ExpandNothing) + require.Error(t, err) + }) + + t.Run("network isolation", func(t *testing.T) { + nid1, p := testhelpers.NewNetwork(t, ctx, p) + nid2, _ := testhelpers.NewNetwork(t, ctx, p) + + iid1, iid2 := x.NewUUID(), x.NewUUID() + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid1, nid1, time.Now(), time.Now()).Exec()) + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid2, nid2, time.Now(), time.Now()).Exec()) + + t1, t2 := randx.MustString(32, randx.AlphaNum), randx.MustString(32, randx.AlphaNum) + sid1, sid2 := x.NewUUID(), x.NewUUID() + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO sessions (id, nid, identity_id, token, expires_at,authenticated_at, created_at, updated_at, logout_token, authentication_methods) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", sid1, nid1, iid1, t1, time.Now().Add(time.Hour), time.Now(), time.Now(), time.Now(), randx.MustString(32, randx.AlphaNum), "[]").Exec()) + require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO sessions (id, nid, identity_id, token, expires_at,authenticated_at, created_at, updated_at, logout_token, authentication_methods) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", sid2, nid2, iid2, t2, time.Now().Add(time.Hour), time.Now(), time.Now(), time.Now(), randx.MustString(32, randx.AlphaNum), "[]").Exec()) + + _, err := p.GetSession(ctx, sid1, session.ExpandEverything) + require.NoError(t, err) + _, err = p.GetSession(ctx, sid2, session.ExpandNothing) + require.ErrorIs(t, err, sqlcon.ErrNoRows()) + + _, err = p.GetSessionByToken(ctx, t1, session.ExpandNothing, identity.ExpandDefault) + require.NoError(t, err) + _, 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 := contextx.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 := contextx.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 := contextx.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 := atomic.Bool{} + g := errgroup.Group{} + for range 10 { + g.Go(func() error { + err := p.ExtendSession(ctx, expected.ID) + if errors.Is(err, sqlcon.ErrNoRows()) { + foundExpectedCockroachError.Store(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.Load(), "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED") + }) + + // Helpers for the manage-sessions persister batch tests below. Each + // helper creates an active session belonging to a fresh identity and + // returns both, so callers can mutate state independently across + // subtests without sharing fixtures. + newActiveSession := func(t *testing.T) *session.Session { + t.Helper() + var s session.Session + require.NoError(t, faker.FakeData(&s)) + s.Active = true + require.NoError(t, p.CreateIdentity(ctx, s.Identity)) + require.NoError(t, p.UpsertSession(ctx, &s)) + return &s + } + isActive := func(t *testing.T, sID uuid.UUID) bool { + t.Helper() + s, err := p.GetSession(ctx, sID, session.ExpandNothing) + require.NoError(t, err) + return s.Active + } + exists := func(t *testing.T, sID uuid.UUID) bool { + t.Helper() + _, err := p.GetSession(ctx, sID, session.ExpandNothing) + if errors.Is(err, sqlcon.ErrNoRows()) { + return false + } + require.NoError(t, err) + return true + } + + t.Run("case=RevokeSessionsByIdentities updates only matching active rows", func(t *testing.T) { + s1 := newActiveSession(t) + s2 := newActiveSession(t) + other := newActiveSession(t) + + n, err := p.RevokeSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID, s2.IdentityID}) + require.NoError(t, err) + assert.Equal(t, 2, n) + assert.False(t, isActive(t, s1.ID)) + assert.False(t, isActive(t, s2.ID)) + assert.True(t, isActive(t, other.ID), "untargeted session must stay active") + + n, err = p.RevokeSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID}) + require.NoError(t, err) + assert.Equal(t, 0, n, "already-inactive sessions must not be re-touched") + }) + + t.Run("case=RevokeSessionsByIdentities is network-isolated", func(t *testing.T) { + s := newActiveSession(t) + _, other := testhelpers.NewNetwork(t, ctx, p) + n, err := other.RevokeSessionsByIdentities(ctx, []uuid.UUID{s.IdentityID}) + require.NoError(t, err) + assert.Equal(t, 0, n) + assert.True(t, isActive(t, s.ID)) + }) + + t.Run("case=RevokeSessionsByIdentities with empty slice is no-op", func(t *testing.T) { + n, err := p.RevokeSessionsByIdentities(ctx, nil) + require.NoError(t, err) + assert.Equal(t, 0, n) + }) + + t.Run("case=RevokeSessionsByIDs updates only matching active rows", func(t *testing.T) { + s1 := newActiveSession(t) + s2 := newActiveSession(t) + untouched := newActiveSession(t) + + n, err := p.RevokeSessionsByIDs(ctx, []uuid.UUID{s1.ID, s2.ID}) + require.NoError(t, err) + assert.Equal(t, 2, n) + assert.False(t, isActive(t, s1.ID)) + assert.False(t, isActive(t, s2.ID)) + assert.True(t, isActive(t, untouched.ID)) + + n, err = p.RevokeSessionsByIDs(ctx, []uuid.UUID{s1.ID}) + require.NoError(t, err) + assert.Equal(t, 0, n, "already-inactive sessions must not be re-touched") + }) + + t.Run("case=RevokeSessionsByIDs is network-isolated", func(t *testing.T) { + s := newActiveSession(t) + _, other := testhelpers.NewNetwork(t, ctx, p) + n, err := other.RevokeSessionsByIDs(ctx, []uuid.UUID{s.ID}) + require.NoError(t, err) + assert.Equal(t, 0, n) + assert.True(t, isActive(t, s.ID)) + }) + + t.Run("case=RevokeSessionsByIDs with empty slice is no-op", func(t *testing.T) { + n, err := p.RevokeSessionsByIDs(ctx, nil) + require.NoError(t, err) + assert.Equal(t, 0, n) + }) + + t.Run("case=RevokeAllSessions deactivates only currently-active rows in this network", func(t *testing.T) { + _, scoped := testhelpers.NewNetwork(t, ctx, p) + active1 := newActiveSession(t) + active2 := newActiveSession(t) + + // Create one session that's already inactive, in the original network. + alreadyInactive := newActiveSession(t) + require.NoError(t, p.RevokeSessionById(ctx, alreadyInactive.ID)) + + // Create a session in the other network — must remain untouched. + var foreign session.Session + require.NoError(t, faker.FakeData(&foreign)) + foreign.Active = true + require.NoError(t, scoped.CreateIdentity(ctx, foreign.Identity)) + require.NoError(t, scoped.UpsertSession(ctx, &foreign)) + + n, err := p.RevokeAllSessions(ctx, 100) + require.NoError(t, err) + assert.GreaterOrEqual(t, n, 2, "must have flipped active1 and active2 at least") + + assert.False(t, isActive(t, active1.ID)) + assert.False(t, isActive(t, active2.ID)) + // The foreign session must still be active when read via its own network. + fs, err := scoped.GetSession(ctx, foreign.ID, session.ExpandNothing) + require.NoError(t, err) + assert.True(t, fs.Active, "foreign-network session must not be touched") + }) + + t.Run("case=RevokeAllSessions caps the work at limit", func(t *testing.T) { + // Isolated network so previous tests can't influence the count. + _, scoped := testhelpers.NewNetwork(t, ctx, p) + ids := make([]uuid.UUID, 3) + for i := range ids { + var s session.Session + require.NoError(t, faker.FakeData(&s)) + s.Active = true + require.NoError(t, scoped.CreateIdentity(ctx, s.Identity)) + require.NoError(t, scoped.UpsertSession(ctx, &s)) + ids[i] = s.ID + } + + n, err := scoped.RevokeAllSessions(ctx, 2) + require.NoError(t, err) + assert.Equal(t, 2, n, "must flip exactly limit rows") + + activeCount := 0 + for _, id := range ids { + s, err := scoped.GetSession(ctx, id, session.ExpandNothing) + require.NoError(t, err) + if s.Active { + activeCount++ + } + } + assert.Equal(t, 1, activeCount, "limit must leave one session active") + }) + + t.Run("case=DeleteSessionsByIdentities removes matching rows", func(t *testing.T) { + s1 := newActiveSession(t) + s2 := newActiveSession(t) + survivor := newActiveSession(t) + + n, err := p.DeleteSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID, s2.IdentityID}) + require.NoError(t, err) + assert.Equal(t, 2, n) + assert.False(t, exists(t, s1.ID)) + assert.False(t, exists(t, s2.ID)) + assert.True(t, exists(t, survivor.ID)) + }) + + t.Run("case=DeleteSessionsByIdentities is network-isolated", func(t *testing.T) { + s := newActiveSession(t) + _, other := testhelpers.NewNetwork(t, ctx, p) + n, err := other.DeleteSessionsByIdentities(ctx, []uuid.UUID{s.IdentityID}) + require.NoError(t, err) + assert.Equal(t, 0, n) + assert.True(t, exists(t, s.ID)) + }) + + t.Run("case=DeleteSessionsByIDs removes matching rows", func(t *testing.T) { + s1 := newActiveSession(t) + s2 := newActiveSession(t) + survivor := newActiveSession(t) + + n, err := p.DeleteSessionsByIDs(ctx, []uuid.UUID{s1.ID, s2.ID}) + require.NoError(t, err) + assert.Equal(t, 2, n) + assert.False(t, exists(t, s1.ID)) + assert.False(t, exists(t, s2.ID)) + assert.True(t, exists(t, survivor.ID)) + }) + + t.Run("case=DeleteSessionsByIDs is network-isolated", func(t *testing.T) { + s := newActiveSession(t) + _, other := testhelpers.NewNetwork(t, ctx, p) + n, err := other.DeleteSessionsByIDs(ctx, []uuid.UUID{s.ID}) + require.NoError(t, err) + assert.Equal(t, 0, n) + assert.True(t, exists(t, s.ID)) + }) + + t.Run("case=DeleteAllSessions removes only this network's rows", func(t *testing.T) { + _, scoped := testhelpers.NewNetwork(t, ctx, p) + s1 := newActiveSession(t) + s2 := newActiveSession(t) + + var foreign session.Session + require.NoError(t, faker.FakeData(&foreign)) + foreign.Active = true + require.NoError(t, scoped.CreateIdentity(ctx, foreign.Identity)) + require.NoError(t, scoped.UpsertSession(ctx, &foreign)) + + n, err := p.DeleteAllSessions(ctx, 100) + require.NoError(t, err) + assert.GreaterOrEqual(t, n, 2) + + assert.False(t, exists(t, s1.ID)) + assert.False(t, exists(t, s2.ID)) + + fs, err := scoped.GetSession(ctx, foreign.ID, session.ExpandNothing) + require.NoError(t, err) + assert.NotNil(t, fs, "foreign-network session must survive") + }) + } +} diff --git a/session/tokenizer.go b/session/tokenizer.go new file mode 100644 index 0000000..e8b4c6e --- /dev/null +++ b/session/tokenizer.go @@ -0,0 +1,181 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "encoding/json" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/fetcher" + "github.com/ory/x/httpx" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/jwksx" + "github.com/ory/x/otelx" +) + +type ( + tokenizerDependencies interface { + jsonnetsecure.VMProvider + otelx.Provider + httpx.ClientProvider + config.Provider + x.JWKSFetchProvider + } + Tokenizer struct { + r tokenizerDependencies + nowFunc func() time.Time + cache *ristretto.Cache[[]byte, []byte] + } + TokenizerProvider interface { + SessionTokenizer() *Tokenizer + } +) + +func NewTokenizer(r tokenizerDependencies) *Tokenizer { + cache, _ := ristretto.NewCache(&ristretto.Config[[]byte, []byte]{ + MaxCost: 50 << 20, // 50MB, + NumCounters: 500_000, // 1kB per snippet -> 50k snippets -> 500k counters + BufferItems: 64, + }) + return &Tokenizer{r: r, nowFunc: time.Now, cache: cache} +} + +func (s *Tokenizer) SetNowFunc(t func() time.Time) { + s.nowFunc = t +} + +func SetSubjectClaim(claims jwt.MapClaims, session *Session, subjectSource string) error { + switch subjectSource { + case "", "id": + claims["sub"] = session.IdentityID.String() + case "external_id": + if session.Identity.ExternalID == "" { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("The session's identity does not have an external ID set, but it is required for the subject claim.")) + } + claims["sub"] = session.Identity.ExternalID.String() + default: + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unknown subject source %q", subjectSource)) + } + return nil +} + +func (s *Tokenizer) TokenizeSession(ctx context.Context, template string, session *Session) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.TokenizeSession") + defer otelx.End(span, &err) + + tpl, err := s.r.Config().TokenizeTemplate(ctx, template) + if err != nil { + return err + } + + key, err := s.r.JWKSFetcher().ResolveKey( + ctx, + tpl.JWKSURL, + jwksx.WithCacheEnabled(), + jwksx.WithCacheTTL(time.Hour), + jwksx.WithAllowedSchemes("file", "base64")) // HTTP(S) makes no sense here. + if err != nil { + if errors.Is(err, jwksx.ErrUnableToFindKeyID) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Could not find key a suitable key for tokenization in the JWKS url.")) + } + return err + } + + alg := jwt.GetSigningMethod(key.Algorithm()) + if alg == nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("The JSON Web Key must include a valid \"alg\" parameter but \"%s\" was given.", key.Algorithm())) + } + + vm, err := s.r.JsonnetVM(ctx) + if err != nil { + return err + } + + now := s.nowFunc() + token := jwt.New(alg) + token.Header["kid"] = key.KeyID() + claims := jwt.MapClaims{ + "jti": uuid.Must(uuid.NewV4()).String(), + "iss": s.r.Config().SelfPublicURL(ctx).String(), + "exp": now.Add(tpl.TTL).Unix(), + "sid": session.ID.String(), + "nbf": now.Unix(), + "iat": now.Unix(), + } + + if err = SetSubjectClaim(claims, session, tpl.SubjectSource); err != nil { + return err + } + + if mapper := tpl.ClaimsMapperURL; len(mapper) > 0 { + sessionRaw, err := json.Marshal(session) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to encode session to JSON.")) + } + + claimsRaw, err := json.Marshal(&claims) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to encode claims to JSON.")) + } + + vm.ExtCode("session", string(sessionRaw)) + vm.ExtCode("claims", string(claimsRaw)) + + httpClient := s.r.HTTPClient(ctx) + fetcher := fetcher.NewFetcher(fetcher.WithClient(httpClient), fetcher.WithCache(s.cache, 60*time.Minute)) + jsonnet, err := fetcher.FetchContext(ctx, mapper) + if err != nil { + return err + } + evaluated, err := vm.EvaluateAnonymousSnippet(tpl.ClaimsMapperURL, jsonnet.String()) + if err != nil { + trace.SpanFromContext(ctx).AddEvent(events.NewJsonnetMappingFailed( + ctx, err, jsonnet.Bytes(), evaluated, "", "", + )) + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithDebug(err.Error()).WithReasonf("Unable to execute tokenizer JsonNet.")) + } + + evaluatedClaims := gjson.Get(evaluated, "claims") + if !evaluatedClaims.IsObject() { + trace.SpanFromContext(ctx).AddEvent(events.NewJsonnetMappingFailed( + ctx, err, jsonnet.Bytes(), evaluated, "", "", + )) + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Expected tokenizer JsonNet to return a claims object but it did not.")) + } + + if err := json.Unmarshal([]byte(evaluatedClaims.Raw), &claims); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to encode tokenized claims.")) + } + } + if err = SetSubjectClaim(claims, session, tpl.SubjectSource); err != nil { + return err + } + + var privateKey interface{} + if err := key.Raw(&privateKey); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to decode the given private key.")) + } + + token.Claims = claims + result, err := token.SignedString(privateKey) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to sign JSON Web Token.")) + } + + trace.SpanFromContext(ctx).AddEvent(events.NewSessionJWTIssued(ctx, session.ID, session.IdentityID, tpl.TTL)) + session.Tokenized = result + return nil +} diff --git a/session/tokenizer_test.go b/session/tokenizer_test.go new file mode 100644 index 0000000..27aceed --- /dev/null +++ b/session/tokenizer_test.go @@ -0,0 +1,166 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "context" + _ "embed" + "net/http/httptest" + "testing" + "time" + + "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" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/pkg/testhelpers" + "github.com/ory/kratos/session" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" + "github.com/ory/x/snapshotx" +) + +//go:embed stub/jwk.es256.json +var es256Key []byte + +//go:embed stub/jwk.es512.json +var es512Key []byte + +func validateTokenized(t *testing.T, raw string, key []byte) *jwt.Token { + token, err := jwt.Parse( + raw, + func(token *jwt.Token) (target interface{}, _ error) { + set, err := jwk.Parse(key) + if err != nil { + return nil, err + } + key, _ := set.Key(0) + if pk, err := key.PublicKey(); err != nil { + return nil, err + } else if err := pk.Raw(&target); err != nil { + return nil, err + } + return target, nil + }, + // We use a fixed time function for snapshot testing, and thus can not validate claims. + jwt.WithoutClaimsValidation(), + ) + require.NoError(t, err) + return token +} + +func setTokenizeConfig(ctx context.Context, templateID, keyFile, mapper string) context.Context { + return contextx.WithConfigValue(ctx, config.ViperKeySessionTokenizerTemplates+"."+templateID, &config.SessionTokenizeFormat{ + TTL: time.Minute, + JWKSURL: "file://stub/" + keyFile, + ClaimsMapperURL: mapper, + }) +} +func setTokenizeConfigWitSubjectSource(ctx context.Context, templateID, keyFile, mapper, subjectSource string) context.Context { + return contextx.WithConfigValue(ctx, config.ViperKeySessionTokenizerTemplates+"."+templateID, &config.SessionTokenizeFormat{ + TTL: time.Minute, + JWKSURL: "file://stub/" + keyFile, + ClaimsMapperURL: mapper, + SubjectSource: subjectSource, + }) +} + +func TestTokenizer(t *testing.T) { + t.Parallel() + + _, reg := pkg.NewFastRegistryWithMocks(t, + configx.WithValue(config.ViperKeyPublicBaseURL, "http://localhost/"), + configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("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 { + return nowDate.Round(time.Second) + }) + + r := httptest.NewRequest("GET", "/sessions/whoami", nil) + i := identity.NewIdentity("default") + i.ID = uuid.FromStringOrNil("7458af86-c1d8-401c-978a-8da89133f78b") + i.ExternalID = "external-id" + i.NID = uuid.Must(uuid.NewV4()) + + s, err := testhelpers.NewActiveSession(r, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + s.ID = uuid.FromStringOrNil("432caf86-c1d8-401c-978a-8da89133f78b") + + iWithoutExtID := identity.NewIdentity("default") + iWithoutExtID.ID = uuid.FromStringOrNil("710678c5-7761-455a-9e3b-be66e3019da2") + iWithoutExtID.NID = i.NID + + s2, err := testhelpers.NewActiveSession(r, reg, iWithoutExtID, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + s2.ID = uuid.FromStringOrNil("44de370d-c8ae-4e2c-b943-5e9d9cc385da") + + t.Run("case=es256-without-jsonnet", func(t *testing.T) { + tid := "es256-no-template" + ctx := setTokenizeConfig(t.Context(), tid, "jwk.es256.json", "") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es256Key) + + resultClaims := token.Claims.(jwt.MapClaims) + assert.Equal(t, i.ID.String(), resultClaims["sub"]) + assert.Equal(t, s.ID.String(), resultClaims["sid"]) + assert.NotEmpty(t, resultClaims["jti"]) + assert.EqualValues(t, resultClaims["exp"], nowDate.Add(time.Minute).Unix()) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=es512-without-jsonnet", func(t *testing.T) { + tid := "es512-no-template" + ctx := setTokenizeConfig(t.Context(), tid, "jwk.es512.json", "") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es512Key) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=rs512-with-jsonnet", func(t *testing.T) { + tid := "rs512-template" + ctx := setTokenizeConfigWitSubjectSource(t.Context(), tid, "jwk.es512.json", "file://stub/rs512-template.jsonnet", "id") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es512Key) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=rs512-with-external_id-in-sub", func(t *testing.T) { + tid := "rs512-template" + ctx := setTokenizeConfigWitSubjectSource(t.Context(), tid, "jwk.es512.json", "file://stub/rs512-template.jsonnet", "external_id") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es512Key) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=rs512-with-empty-external_id-in-sub", func(t *testing.T) { + tid := "rs512-template" + ctx := setTokenizeConfigWitSubjectSource(t.Context(), tid, "jwk.es512.json", "file://stub/rs512-template.jsonnet", "external_id") + + // This should fail because the identity does not have an external ID set. + require.Error(t, tkn.TokenizeSession(ctx, tid, s2)) + }) + + t.Run("case=rs512-with-broken-keyfile", func(t *testing.T) { + tid := "rs512-template" + ctx := setTokenizeConfig(t.Context(), tid, "jwk.es512.broken.json", "file://stub/rs512-template.jsonnet") + err := tkn.TokenizeSession(ctx, tid, s) + require.ErrorIs(t, err, herodot.ErrBadRequest()) + }) +} diff --git a/spec/api.go b/spec/api.go new file mode 100644 index 0000000..3e8dbef --- /dev/null +++ b/spec/api.go @@ -0,0 +1,9 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import _ "embed" + +//go:embed api.json +var API []byte diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore new file mode 100644 index 0000000..7741d04 --- /dev/null +++ b/test/e2e/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +/playwright/playwright.env +/playwright/kratos.config.json diff --git a/test/e2e/.go-version b/test/e2e/.go-version new file mode 100644 index 0000000..2844977 --- /dev/null +++ b/test/e2e/.go-version @@ -0,0 +1 @@ +1.21.1 diff --git a/test/e2e/.npmignore b/test/e2e/.npmignore new file mode 100644 index 0000000..e601d72 --- /dev/null +++ b/test/e2e/.npmignore @@ -0,0 +1,7 @@ +proxy +profiles +hydra-login-consent +cypress/videos +cypress/screenshots +*.log +*.yml diff --git a/test/e2e/cypress.config.ts b/test/e2e/cypress.config.ts new file mode 100644 index 0000000..828b102 --- /dev/null +++ b/test/e2e/cypress.config.ts @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig } from "cypress" +import got from "got" +const CRI = require("chrome-remote-interface") + +let criPort = 0, + criClient = null + +export default defineConfig({ + chromeWebSecurity: false, + defaultCommandTimeout: 10000, + requestTimeout: 10000, + projectId: "bc48bg", + video: true, + videoCompression: false, + screenshotOnRunFailure: true, + + e2e: { + retries: { + runMode: 6, + openMode: 1, + }, + experimentalRunAllSpecs: true, + videosFolder: "cypress/videos", + screenshotsFolder: "cypress/screenshots", + excludeSpecPattern: ["**/*snapshots.js", "playwright/**"], + supportFile: "cypress/support/index.js", + specPattern: "**/*.spec.{js,ts}", + baseUrl: "http://localhost:4455/", + setupNodeEvents(on, config) { + on("before:browser:launch", (browser, launchOptions) => { + criPort = ensureRdpPort(launchOptions.args) + console.log("criPort is", criPort) + + if (browser.name.includes("chrom") && browser.isHeadless) { + launchOptions.args.push("--headless=new") + } + return launchOptions + }) + + on("task", { + httpRequest(params) { + return got(params).then(({ body }) => body) + }, + // Reset chrome remote interface for clean state + async resetCRI() { + if (criClient) { + const c = criClient + criClient = null + await c.close() + } + + return Promise.resolve(true) + }, + // Execute CRI command + async sendCRI(args) { + if (!criClient) { + criClient = await CRI({ port: criPort }) + } + + return criClient.send(args.query, args.opts) + }, + }) + }, + }, +}) + +function ensureRdpPort(args) { + const existing = args.find( + (arg) => arg.slice(0, 23) === "--remote-debugging-port", + ) + + if (existing) { + return Number(existing.split("=")[1]) + } + + const port = 40000 + Math.round(Math.random() * 25000) + args.push(`--remote-debugging-port=${port}`) + return port +} diff --git a/test/e2e/cypress/fixtures/example.json b/test/e2e/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/test/e2e/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/test/e2e/cypress/helpers/express.ts b/test/e2e/cypress/helpers/express.ts new file mode 100644 index 0000000..4631edc --- /dev/null +++ b/test/e2e/cypress/helpers/express.ts @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APP_URL } from "./index" + +export const routes = { + base: APP_URL, + login: APP_URL + "/login", + registration: APP_URL + "/registration", + settings: APP_URL + "/settings", + recovery: APP_URL + "/recovery", + verification: APP_URL + "/verification", + welcome: APP_URL + "/welcome", +} diff --git a/test/e2e/cypress/helpers/httpbin.ts b/test/e2e/cypress/helpers/httpbin.ts new file mode 100644 index 0000000..ebd89ec --- /dev/null +++ b/test/e2e/cypress/helpers/httpbin.ts @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import * as oauth2 from "./oauth2" + +export function checkToken( + client: oauth2.oAuth2Client, + scope: string[], + check: (token: any) => void, +) { + cy.location("href") + .should( + "match", + new RegExp( + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything[?]code=.*", + ), + ) + .then(() => { + cy.get("body") + .invoke("text") + .then((text) => { + const result = JSON.parse(text) + const tokenParams = { + code: result.args.code, + redirect_uri: + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + scope: scope.join(" "), + } + oauth2 + .getToken( + client.token_endpoint, + client.id, + client.secret, + "authorization_code", + tokenParams.code, + tokenParams.redirect_uri, + tokenParams.scope, + ) + .then((res) => { + const token = res.body + check(token) + }) + }) + }) +} diff --git a/test/e2e/cypress/helpers/index.ts b/test/e2e/cypress/helpers/index.ts new file mode 100644 index 0000000..ef61a55 --- /dev/null +++ b/test/e2e/cypress/helpers/index.ts @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export const email = () => Math.random().toString(36) + "@ory.sh" +export const blockedEmail = () => + Math.random().toString(36) + "_blocked" + "@ory.sh" + +export const password = () => Math.random().toString(36) + +export const assertVerifiableAddress = + ({ isVerified, email }) => + (session) => { + const { identity } = session + expect(identity).to.have.property("verifiable_addresses") + expect(identity.verifiable_addresses).to.have.length(1) + + const address = identity.verifiable_addresses[0] + expect(address.id).to.not.be.empty + expect(address.verified).to.equal(isVerified) + expect(address.value).to.equal(email) + + if (isVerified) { + expect(address.verified_at).to.not.be.null + } else { + expect(address).to.not.have.property("verified_at") + } + } + +export const assertRecoveryAddress = + ({ email }) => + ({ identity }) => { + expect(identity).to.have.property("recovery_addresses") + expect(identity.recovery_addresses).to.have.length(1) + + const address = identity.recovery_addresses[0] + expect(address.id).to.not.be.empty + expect(address.value).to.equal(email) + } + +export const parseHtml = (html: string) => + new DOMParser().parseFromString(html, "text/html") + +export const APP_URL = ( + Cypress.env("app_url") || "http://localhost:4455" +).replace(/\/$/, "") + +export const MOBILE_URL = ( + Cypress.env("mobile_url") || "http://localhost:19006" +).replace(/\/$/, "") +export const SPA_URL = ( + Cypress.env("react_url") || "http://localhost:4455" +).replace(/\/$/, "") +export const KRATOS_ADMIN = ( + Cypress.env("kratos_admin") || "http://localhost:4434" +) + .replace() + .replace(/\/$/, "") + +export const KRATOS_PUBLIC = ( + Cypress.env("kratos_public") || "http://localhost:4433" +) + .replace() + .replace(/\/$/, "") + +export const MAIL_API = ( + Cypress.env("mail_url") || "http://localhost:4437" +).replace(/\/$/, "") + +export const website = "https://www.ory.sh/" + +export const gen = { + email, + blockedEmail, + password, + identity: () => ({ email: email(), password: password() }), + identityWithWebsite: () => ({ + email: email(), + password: password(), + fields: { "traits.website": "https://www.ory.sh" }, + }), +} + +// Format is +export const verifyHrefPattern = + /^http:.*\/self-service\/verification\?(((&|)code|(&|)token|(&|)flow)=([\-a-zA-Z0-9]+)){2}$/ + +// intervals define how long to wait for something, +export const pollInterval = 250 // how long to wait before retry + +// Adding 1+ second on top because MySQL doesn't do millisecs. +export const verifyLifespan = 5000 + 1000 +export const privilegedLifespan = 5000 + 1000 + +export const appPrefix = (app) => `[data-testid="app-${app}"] ` + +export const codeRegex = /(\d{6})/ + +export function extractOTPCode(body: string): string | null { + const result = codeRegex.exec(body) + if (result != null && result.length > 0) { + return result[0] + } + return null +} diff --git a/test/e2e/cypress/helpers/oauth2.ts b/test/e2e/cypress/helpers/oauth2.ts new file mode 100644 index 0000000..8fde8c7 --- /dev/null +++ b/test/e2e/cypress/helpers/oauth2.ts @@ -0,0 +1,106 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import * as uuid from "uuid" + +export type oAuth2Client = { + auth_endpoint: string + token_endpoint: string + id: string + secret: string + token_endpoint_auth_method: string + grant_types: string[] + response_types: string[] + scopes: string[] + callbacks: string[] +} + +export function getDefaultAuthorizeURL(client: oAuth2Client) { + const state = uuid.v4() + const nonce = uuid.v4() + return getAuthorizeURL( + client.auth_endpoint, + "", + client.id, + undefined, + nonce, + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + "code", + ["offline", "openid"], + state, + undefined, + ) +} + +export function getAuthorizeURL( + auth_endpoint: string, + audience: string, + client_id: string, + max_age: string | undefined, + nonce: string, + redirect_uri: string, + response_type: + | "code" + | "id_token" + | "id_token token" + | "code id_token" + | "code token" + | "code id_token token", + scopes: string[], + state: string, + code_challenge?: string, +): string { + const r = new URL(auth_endpoint) + r.searchParams.append("audience", audience) + r.searchParams.append("client_id", client_id) + if (max_age !== undefined) { + r.searchParams.append("max_age", max_age) + } + r.searchParams.append("nonce", nonce) + r.searchParams.append("prompt", "") + r.searchParams.append("redirect_uri", redirect_uri) + r.searchParams.append("response_type", response_type) + r.searchParams.append("scope", scopes.join(" ")) + r.searchParams.append("state", state) + + code_challenge && r.searchParams.append("code_challenge", code_challenge) + return r.toString() +} + +export function getToken( + token_endpoint: string, + client_id: string, + client_secret: string, + grant_type: "authorization_code", + code: string, + redirect_uri: string, + scope: string, +) { + let urlEncodedData = "" + const urlEncodedDataPairs = [] + urlEncodedDataPairs.push( + encodeURIComponent("grant_type") + "=" + encodeURIComponent(grant_type), + ) + urlEncodedDataPairs.push( + encodeURIComponent("code") + "=" + encodeURIComponent(code), + ) + urlEncodedDataPairs.push( + encodeURIComponent("redirect_uri") + "=" + encodeURIComponent(redirect_uri), + ) + urlEncodedDataPairs.push( + encodeURIComponent("scope") + "=" + encodeURIComponent(scope), + ) + + urlEncodedData = urlEncodedDataPairs.join("&").replace(/%20/g, "+") + + return cy.request({ + method: "POST", + url: token_endpoint, + form: true, + body: urlEncodedData, + headers: { + Accept: "application/json", + Authorization: "Basic " + btoa(client_id + ":" + client_secret), + }, + }) +} diff --git a/test/e2e/cypress/helpers/react.ts b/test/e2e/cypress/helpers/react.ts new file mode 100644 index 0000000..4325e41 --- /dev/null +++ b/test/e2e/cypress/helpers/react.ts @@ -0,0 +1,13 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { SPA_URL } from "./index" + +export const routes = { + base: SPA_URL, + login: SPA_URL + "/login", + registration: SPA_URL + "/registration", + settings: SPA_URL + "/settings", + recovery: SPA_URL + "/recovery", + verification: SPA_URL + "/verification", +} diff --git a/test/e2e/cypress/helpers/webhook.ts b/test/e2e/cypress/helpers/webhook.ts new file mode 100644 index 0000000..a5da27a --- /dev/null +++ b/test/e2e/cypress/helpers/webhook.ts @@ -0,0 +1,70 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { fail } from "assert" +import { gen } from "." + +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 testFlowWebhook = ( + configSetup: ( + hooks: Array<{ hook: string; config?: any }>, + ) => Cypress.Chainable, + act: () => Cypress.Chainable | void, +) => { + const documentID = gen.password() + configSetup([ + { + hook: "web_hook", + config: { + body: "base64://" + jsonnet, + url: documentUrl(documentID), + method: "PUT", + }, + }, + ]) + + const transient_payload = { + stuff: { + blub: [42, 3.14152], + fu: "bar", + }, + consent: true, + } + 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() + }, + ) + + act() + + cy.request(documentUrl(documentID)).then(({ body, status }) => { + const b = JSON.parse(body) + expect(status).to.equal(200) + expect(b.identity).is.not.undefined + expect(b.flow.transient_payload).to.deep.equal(transient_payload) + }) +} diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts new file mode 100644 index 0000000..08bc581 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -0,0 +1,203 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { MOBILE_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Login error messages with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, + { + route: MOBILE_URL + "/Login", + app: "mobile" as "mobile", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + const Selectors = { + mobile: { + identity: '[data-testid="identifier"]', + code: '[data-testid="code"]', + }, + express: { + identity: '[data-testid="login-flow-code"] input[name="identifier"]', + code: 'input[name="code"]', + }, + react: { + identity: 'input[name="identifier"]', + code: 'input[name="code"]', + }, + } + + before(() => { + if (app !== "mobile") { + cy.proxy(app) + } + }) + + beforeEach(() => { + cy.useConfigProfile(profile) + cy.deleteMail() + cy.clearAllCookies() + + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email, traits: { "traits.tos": 1 } }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + it("should show error message when account identifier does not exist", () => { + const email = gen.email() + + cy.get(Selectors[app]["identity"]).type(email) + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/4000035"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + // playwright:migrated + it("should show error message when code is invalid", () => { + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).clear().type(email.toString()) + }) + + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/1010014"]').should("exist") + + cy.get(Selectors[app]["code"]).type("123456") + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "The login code is invalid or has already been used. Please try again.", + ) + }) + + // playwright:migrated + it("should show error message when identifier has changed", () => { + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).type(email.toString()) + }) + + cy.submitCodeForm(app) + + if (app !== "express") { + cy.intercept("POST", "/self-service/login*", (req) => { + req.body = { + ...req.body, + identifier: gen.email(), + } + req.continue() + }).as("login") + } else { + cy.get(Selectors[app]["identity"]) + .type("{selectall}{backspace}", { force: true }) + .type(gen.email(), { force: true }) + } + + cy.get(Selectors[app]["code"]).type("123456") + + cy.submitCodeForm(app) + if (app !== "express") { + cy.wait("@login") + } + cy.get('[data-testid="ui/message/4000035"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + // playwright:migrated + it("should show error message when required fields are missing", () => { + cy.removeAttribute([Selectors[app]["identity"]], "required") + + cy.submitCodeForm(app) + if (app === "mobile") { + cy.get('[data-testid="field/identifier"]').should( + "contain", + "Property identifier is missing", + ) + } else { + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property identifier is missing", + ) + } + + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).type(email.toString()) + }) + + cy.submitCodeForm(app) + + cy.removeAttribute([Selectors[app]["code"]], "required") + cy.submitCodeForm(app) + + if (app === "mobile") { + cy.get('[data-testid="field/code"]').should( + "contain", + "Property code is missing", + ) + } else { + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + } + }) + + // playwright:migrated + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code = { + passwordless_enabled: true, + config: { + lifespan: "1ns", + }, + } + return config + }).then(() => { + cy.visit(route) + }) + + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).type(email.toString()) + }) + cy.submitCodeForm(app) + + cy.get("@email").then((email) => { + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + }) + }) + + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "The login code is invalid or has already been used", + ) + + cy.noSession() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts new file mode 100644 index 0000000..a027e07 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -0,0 +1,332 @@ +// 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" + +context("Login success with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, + { + route: MOBILE_URL + "/Login", + app: "mobile" as "mobile", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + const Selectors = { + mobile: { + identity: '[data-testid="identifier"]', + code: '[data-testid="code"]', + submit: '[data-testid="field/method/code"]', + resend: '[data-testid="field/resend/code"]', + }, + express: { + identity: '[data-testid="login-flow-code"] input[name="identifier"]', + code: 'input[name="code"]', + submit: 'button[name="method"][value="code"]', + resend: 'button[name="resend"]', + }, + react: { + identity: 'input[name="identifier"]', + code: 'input[name="code"]', + submit: 'button[name="method"][value="code"]', + resend: 'button[name="resend"]', + }, + } + + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + if (app !== "mobile") { + cy.proxy(app) + } + cy.setPostCodeRegistrationHooks([]) + cy.setupHooks("login", "after", "code", []) + }) + + beforeEach(() => { + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email, traits: { "traits.tos": 1 } }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + it("should be able to sign in with code", () => { + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).clear().type(email.toString()) + cy.submitCodeForm(app) + + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + + cy.get(Selectors[app]["submit"]).click() + }) + + cy.location("pathname").should("not.contain", "login") + + 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(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + // playwright:migrated + it("should be able to sign in with code on account registered with password", () => { + const email = gen.email() + // register account with password + cy.register({ + email, + password: gen.password(), + fields: { "traits.tos": 1 }, + }) + + cy.getVerificationCodeFromEmail(email).then((code) => { + expect(code).to.not.be.empty + cy.deleteMail() + }) + + cy.clearAllCookies() + + cy.visit(route) + + cy.get(Selectors[app]["identity"]).clear().type(email) + cy.submitCodeForm(app) + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + + 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({ + 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(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + // playwright:migrated + it("should be able to resend login code", () => { + cy.get("@email").then((email) => { + cy.get(Selectors[app]["identity"]).clear().type(email.toString()) + cy.submitCodeForm(app) + + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.wrap(code).as("code1") + }) + + cy.get(Selectors[app]["resend"]).click() + + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + cy.get("@code2").then((code2) => { + expect(code1).to.not.equal(code2) + }) + }) + + // attempt to submit code 1 + cy.get("@code1").then((code1) => { + cy.get(Selectors[app]["code"]).clear().type(code1.toString()) + }) + + cy.get(Selectors[app]["submit"]).click() + + cy.get("[data-testid='ui/message/4010008']").contains( + "The login code is invalid or has already been used", + ) + + // attempt to submit code 2 + cy.get("@code2").then((code2) => { + cy.get(Selectors[app]["code"]).clear().type(code2.toString()) + }) + + 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() + } + + 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((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + it("should be able to login to un-verfied email", () => { + const email = gen.email() + const email2 = gen.email() + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.registerWithCode({ + email: email, + traits: { + "traits.username": Math.random().toString(36), + "traits.email2": email2, + }, + }) + + // 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 }) + + cy.visit(route) + + cy.get(Selectors[app]["identity"]).clear().type(email2) + cy.submitCodeForm(app) + + cy.getLoginCodeFromEmail(email2).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + 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({ + 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") + cy.get('[data-testid="session-content"]').should("contain", email2) + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then((session) => { + expect(session?.identity?.verifiable_addresses).to.have.length(2) + expect(session?.identity?.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(session.identity.verifiable_addresses[1].status).to.equal( + "completed", + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts new file mode 100644 index 0000000..fd7fc2c --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -0,0 +1,197 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { UiNode } from "@ory/kratos-client" +import { gen, MOBILE_URL } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration error messages with code method", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "code", + }, + { + route: react.registration, + app: "react" as "react", + profile: "code", + }, + { + route: MOBILE_URL + "/Registration", + app: "mobile" as "mobile", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + email: "[data-testid='field/traits.email']", + tos: "[data-testid='traits.tos']", + code: "[data-testid='field/code']", + }, + express: { + identifier: + "[data-testid='registration-flow-code'] input[name='identifier']", + email: + "[data-testid='registration-flow-code'] input[name='traits.email']", + tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label", + code: "input[name='code']", + }, + react: { + identifier: "input[name='identifier']", + email: "input[name='traits.email']", + tos: "[name='traits.tos'] + label", + code: "input[name='code']", + }, + } + + before(() => { + if (app !== "mobile") { + cy.proxy(app) + } + cy.useConfigProfile(profile) + cy.deleteMail() + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + it("should show error message when code is invalid", () => { + const email = gen.email() + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["tos"]).click() + + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.get(Selectors[app]["code"]).type("123456") + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/4040003"]').should( + "contain", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + // playwright:migrated + it("should show error message when traits have changed", () => { + const email = gen.email() + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["tos"]).click() + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + if (app !== "express") { + // the mobile app doesn't render hidden fields in the DOM + // we need to replace the request body + cy.intercept("POST", "/self-service/registration*", (req) => { + req.body = { + ...req.body, + "traits.email": "changed-email@email.com", + } + req.continue() + }).as("registration") + } else { + cy.get(Selectors[app]["email"]) + .type("{selectall}{backspace}", { force: true }) + .type("changed-email@email.com", { force: true }) + } + + cy.get(Selectors[app]["code"]).type("123456") + cy.submitCodeForm(app) + + if (app !== "express") { + cy.wait("@registration") + } + + cy.get('[data-testid="ui/message/4000036"]').should( + "contain", + "The provided traits do not match the traits previously associated with this flow.", + ) + }) + + // playwright:migrated + it("should show error message when required fields are missing", () => { + cy.removeAttribute([Selectors[app]["email"]], "required") + const email = gen.email() + + cy.get(Selectors[app]["tos"]).click() + cy.submitCodeForm(app) + + if (app === "mobile") { + cy.get('[data-testid="field/traits.email"]').should( + "contain", + "Property email is missing", + ) + } else { + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property email is missing", + ) + } + cy.get(Selectors[app]["email"]).type(email) + cy.submitCodeForm(app) + + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.removeAttribute([Selectors[app]["code"]], "required") + cy.submitCodeForm(app) + + if (app === "mobile") { + cy.get('[data-testid="field/code"]').should( + "contain", + "Property code is missing", + ) + } else { + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + } + }) + + // playwright:migrated + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ns" + return config + }) + cy.visit(route) + + const email = gen.email() + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["tos"]).click() + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.submitCodeForm(app) + }) + + cy.get('[data-testid="ui/message/4040003"]').should( + "contain", + "The registration code is invalid or has already been used", + ) + + cy.noSession() + + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1h" + return config + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts new file mode 100644 index 0000000..e535a0f --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -0,0 +1,418 @@ +// 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']", + 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", () => { + ;[ + { + route: express.registration, + login: express.login, + recovery: express.recovery, + app: "express" as "express", + profile: "code", + }, + { + route: react.registration, + login: react.login, + recovery: react.recovery, + app: "react" as "react", + profile: "code", + }, + { + route: MOBILE_URL + "/Registration", + login: MOBILE_URL + "/Login", + recovery: MOBILE_URL + "/Recovery", + app: "mobile" as "mobile", + profile: "code", + }, + ].forEach(({ route, login, recovery, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + if (app !== "mobile") { + cy.proxy(app) + } + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + 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"]).click() + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.getRegistrationCodeFromEmail(email).then((code) => + cy.wrap(code).as("code1"), + ) + + 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().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.", + ) + }) + + cy.get("@code2").then((code2) => { + cy.get(Selectors[app]["code"]).clear().type(code2.toString()) + cy.submitCodeForm(app) + }) + + 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 { + // Wait for the post-registration UI to settle before querying + // /sessions/whoami. cy.request does not retry the HTTP call on a + // non-2xx status, so without this sync getSession races the + // browser's session-cookie persistence and intermittently 401s. + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + 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(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + // playwright:migrated + it("should sign up and be logged in with session hook", () => { + const email = gen.email() + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["tos"]).click() + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.getRegistrationCodeFromEmail(email).then((code) => { + 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", + 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.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + // playwright:migrated + it("should be able to sign up without session hook", () => { + cy.setPostCodeRegistrationHooks([]) + const email = gen.email() + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["tos"]).click() + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + cy.getRegistrationCodeFromEmail(email).then((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 === "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.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to recover account when registered with code", () => { + if (app === "mobile") { + cy.log("WARNING: skipping test for mobile app") + return + } + if (app === "react") { + // This test is flaky on React, so we skip it for now. + return + } + const email = gen.email() + cy.registerWithCode({ email, traits: { "traits.tos": 1 } }) + + cy.clearAllCookies() + cy.visit(recovery) + + cy.get(Selectors[app]["recoveryEmail"]).type(email) + cy.get(Selectors[app]["submitRecovery"]).click() + + cy.recoveryEmailWithCode({ expect: { email, enterCode: false } }).then( + () => { + cy.get("@recoveryCode").then((code) => { + cy.get(Selectors[app]["recoveryCode"]).type(code) + }) + }, + ) + + cy.get(Selectors[app]["submitRecovery"]).click() + + // Wait for the post-recovery settings page to render so the session + // cookie is in Cypress's jar before we hit /sessions/whoami. + cy.location("pathname").should("contain", "settings") + + cy.getSession({ expectAal: "aal1" }).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", + }, + { + hook: "show_verification_ui", + }, + ]) + + // 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.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should("be.visible") + + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).then( + (code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }, + ) + cy.get('[data-testid="ui/message/1080003"]').should("be.visible") + + 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, + }).then((code) => { + 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", + 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.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/email/error/ui.spec.ts b/test/e2e/cypress/integration/profiles/email/error/ui.spec.ts new file mode 100644 index 0000000..dcedbf3 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/error/ui.spec.ts @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { appPrefix } from "../../../../helpers" + +// playwright:migrated +describe("Handling self-service error flows", () => { + ;[ + { + route: express.base, + app: "express" as "express", + profile: "email", + }, + { + route: react.base, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, app, profile }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + // playwright:migrated + it("should show the error", () => { + cy.visit(`${route}/error?id=stub:500`, { + failOnStatusCode: false, + }) + + if (app === "express") { + cy.get(`${appPrefix(app)} [data-testid="ui/error/message"]`).should( + "contain.text", + "This is a stub error.", + ) + } else { + cy.get(`${appPrefix(app)}code`).should( + "contain.text", + "This is a stub error.", + ) + } + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/login/error.spec.ts b/test/e2e/cypress/integration/profiles/email/login/error.spec.ts new file mode 100644 index 0000000..2bba75b --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/login/error.spec.ts @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +// playwright:migrated +describe("Basic email profile with failing login flows", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "email", + }, + { + route: react.login, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + it("fails when CSRF cookies are missing", () => { + cy.get(`${appPrefix(app)}input[name="identifier"]`).type( + "i-do-not-exist", + ) + cy.get('input[name="password"]').type("invalid-password") + + cy.shouldHaveCsrfError({ app }) + }) + + // playwright:migrated + it("fails when a disallowed return_to url is requested", () => { + cy.shouldErrorOnDisallowedReturnTo( + route + "?return_to=https://not-allowed", + { app }, + ) + }) + + // playwright:migrated - partially + describe("shows validation errors when invalid signup data is used", () => { + // playwright:migrated + it("should show an error when the identifier is missing", () => { + // the browser will prevent the form from submitting if the fields are empty since they are required + // here we just remove the required attribute to make the form submit + cy.removeAttribute( + ['input[name="identifier"]', 'input[name="password"]'], + "required", + ) + cy.submitPasswordForm() + cy.get('*[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property identifier is missing", + ) + cy.get('*[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property password is missing", + ) + }) + + // playwright:migrated + it("should show an error when the password is missing", () => { + const identity = gen.email() + cy.get('input[name="identifier"]') + .type(identity) + .should("have.value", identity) + + // the browser will prevent the form from submitting if the fields are empty since they are required + // here we just remove the required attribute to make the form submit + cy.removeAttribute(['input[name="password"]'], "required") + + cy.submitPasswordForm() + cy.get('*[data-testid^="ui/message/"]') + .invoke("text") + .then((text) => { + expect(text.trim()).to.be.oneOf([ + "length must be >= 1, but got 0", + "Property password is missing.", + ]) + }) + }) + + // playwright:migrated + it("should show fail to sign in", () => { + cy.get('input[name="identifier"]').type("i-do-not-exist") + cy.get('input[name="password"]').type("invalid-password") + + cy.submitPasswordForm() + cy.get('*[data-testid="ui/message/4000006"]').should( + "contain.text", + "credentials are invalid", + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/login/success.spec.ts b/test/e2e/cypress/integration/profiles/email/login/success.spec.ts new file mode 100644 index 0000000..54d1dac --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/login/success.spec.ts @@ -0,0 +1,131 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APP_URL, appPrefix, gen, website } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +// playwright:migrated +describe("Basic email profile with succeeding login flows", () => { + const email = gen.email() + const password = gen.password() + + before(() => { + cy.registerApi({ email, password, fields: { "traits.website": website } }) + }) + ;[ + { + route: express.login, + app: "express" as "express", + profile: "email", + }, + { + route: react.login, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.proxy(app) + }) + + beforeEach(() => { + cy.useConfigProfile(profile) + cy.clearAllCookies() + cy.visit(route) + }) + + // playwright:migrated + it("should sign in and be logged in", () => { + 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") + + 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) + }) + }) + + // playwright:migrated + it("should sign in with case insensitive identifier surrounded by whitespace", () => { + cy.get('input[name="identifier"]').type( + " " + email.toUpperCase() + " ", + ) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + cy.location("pathname").should("not.contain", "/login") + + 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) + }) + }) + + // playwright:migrated + it("should sign in and be redirected", () => { + cy.browserReturnUrlOry() + cy.visit(route + "?return_to=https://www.example.org/") + + cy.get('input[name="identifier"]').type(email.toUpperCase()) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + cy.url().should("eq", "https://www.example.org/") + }) + }) + }) + + // playwright:migrated + describe("for app express handle return_to correctly for expired flows", () => { + before(() => { + cy.proxy("express") + cy.useConfigProfile("email") + + cy.browserReturnUrlOry() + }) + + beforeEach(() => { + cy.clearAllCookies() + }) + + // playwright:migrated + it("should redirect to return_to when retrying expired flow", () => { + cy.shortLoginLifespan() + cy.wait(500) + + cy.visit(express.login + "?return_to=https://www.example.org/") + + cy.longLoginLifespan() + + cy.get(appPrefix("express") + 'input[name="identifier"]').type( + email.toUpperCase(), + ) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4010001"]').should( + "contain.text", + "The login flow expired", + ) + + // try again with long lifespan set + cy.get('input[name="identifier"]').type(email.toUpperCase()) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + // check that redirection has happened + cy.url().should("eq", "https://www.example.org/") + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts b/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts new file mode 100644 index 0000000..a093d1d --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { appPrefix } from "../../../../helpers" + +// playwright:migrated +context("UI tests using the email profile", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "email", + }, + { + route: react.login, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.visit(route) + }) + + // playwright:migrated + it("should use the json schema titles", () => { + cy.get(`${appPrefix(app)}input[name="identifier"]`) + .parent() + .should("contain.text", "Your E-Mail") + + cy.get('input[name="password"]') + .parentsUntil("label") + .should("contain.text", "Password") + cy.get('button[value="password"]').should("contain.text", "Sign in") + }) + + // playwright:migrated + it("clicks the log in link", () => { + cy.get('a[href*="registration"]').click() + cy.location("pathname").should("include", "registration") + + if (app === "express") { + cy.location("search").should("not.be.empty") + } + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts b/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts new file mode 100644 index 0000000..9545c9a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts @@ -0,0 +1,146 @@ +// 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" + +context("Testing logout flows", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "email", + settings: express.settings, + welcome: express.welcome, + }, + { + route: react.login, + app: "react" as "react", + profile: "spa", + settings: react.settings, + welcome: "", + }, + ].forEach(({ route, profile, app, settings, welcome }) => { + describe(`for app ${app}`, () => { + let email: string + let password: string + + before(() => { + cy.proxy(app) + + email = gen.email() + password = gen.password() + + cy.useConfigProfile(profile) + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.login({ email, password, cookieUrl: route }) + cy.visit(route) + }) + + // playwright:migrated + it("should sign out and be able to sign in again", () => { + cy.getSession() + cy.getCookie("ory_kratos_session").should("not.be.null") + if (app === "express") { + cy.get( + `${appPrefix(app)} [data-testid="logout"] a:not(.disabled)`, + ).click() + } else { + cy.get( + `${appPrefix(app)} [data-testid="logout"]:not(.disabled)`, + ).click() + } + cy.getCookie("ory_kratos_session").should("be.null") + cy.noSession() + cy.url().should("include", "/login") + }) + + // playwright:migrated + it("should be able to sign out on settings page", () => { + if (app === "react") { + return + } + cy.sessionRequiresNo2fa() + cy.useLaxAal() + + cy.getSession({ expectAal: "aal1" }) + cy.getCookie("ory_kratos_session").should("not.be.null") + + cy.visit(settings, { + qs: { + return_to: "https://www.example.org", + }, + }) + + cy.get("a[href*='logout']").click() + cy.location("host").should("eq", "www.example.org") + }) + + // playwright:migrated + it("should be able to sign out on welcome page", () => { + if (app === "react") { + return + } + cy.sessionRequiresNo2fa() + cy.useLaxAal() + + cy.getSession({ expectAal: "aal1" }) + + cy.visit(welcome, { + qs: { + return_to: "https://www.example.org", + }, + }) + + cy.get("a[href*='logout']").click() + cy.location("host").should("eq", "www.example.org") + }) + + it("should be able to sign out at 2fa page", () => { + if (app === "react") { + return + } + cy.useLookupSecrets(true) + cy.sessionRequires2fa() + cy.getSession({ expectAal: "aal1" }) + cy.getCookie("ory_kratos_session").should("not.be.null") + + // add 2fa to account + cy.visit(settings) + cy.get( + appPrefix(app) + 'button[name="lookup_secret_regenerate"]', + ).click() + cy.get('button[name="lookup_secret_confirm"]').click() + cy.expectSettingsSaved() + + cy.logout() + cy.visit(route, { + qs: { + return_to: "https://www.example.org", + }, + }) + + cy.get('[name="identifier"]').clear().type(email) + + cy.reauth({ + expect: { email, success: false }, + type: { password: password }, + }) + + cy.get("a[href*='logout']").click() + + cy.location("host").should("eq", "www.example.org") + cy.useLookupSecrets(false) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/registration/errors.spec.ts b/test/e2e/cypress/integration/profiles/email/registration/errors.spec.ts new file mode 100644 index 0000000..9c31258 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/registration/errors.spec.ts @@ -0,0 +1,268 @@ +// 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" + +describe("Registration failures with email profile", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "email", + }, + { + route: react.registration, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + cy.updateConfigFile((config) => { + config.selfservice.flows.registration.login_hints = true + return config + }) + }) + + beforeEach(() => { + cy.visit(route) + }) + + const identity = gen.email() + const password = gen.password() + + it("fails when CSRF cookies are missing", () => { + cy.get(`${appPrefix(app)} input[name="traits.website"]`).type( + "https://www.ory.sh", + ) + cy.get('input[name="traits.email"]') + .type(identity) + .should("have.value", identity) + cy.get('input[name="password"]') + .type("12345678") + .should("have.value", "12345678") + + cy.shouldHaveCsrfError({ app }) + }) + + it("fails when a disallowed return_to url is requested", () => { + cy.shouldErrorOnDisallowedReturnTo( + route + "?return_to=https://not-allowed", + { app }, + ) + }) + + describe("show errors when invalid signup data is used", () => { + // playwright:migrated + it("should show an error when the password has leaked before", () => { + cy.get('input[name="traits.website"]').type("https://www.ory.sh") + cy.get('input[name="traits.email"]') + .type(identity) + .should("have.value", identity) + cy.get('input[name="password"]') + .type("12345678") + .should("have.value", "12345678") + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000034"]').should( + "contain.text", + "data breaches", + ) + }) + + // playwright:migrated + it("should show an error when the password is too similar", () => { + cy.get('input[name="traits.website"]').type("https://www.ory.sh") + cy.get('input[name="traits.email"]').type(identity) + cy.get('input[name="password"]').type(identity) + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000031"]').should( + "contain.text", + "too similar", + ) + }) + + // playwright:migrated + it("should show an error when the password is empty", () => { + cy.get('input[name="traits.website"]').type("https://www.ory.sh") + cy.get('input[name="traits.email"]').type(identity) + + // 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="password"]'], "required") + + cy.submitPasswordForm() + cy.get('*[data-testid^="ui/message/"]') + .invoke("text") + .then((text) => { + expect(text.trim()).to.be.oneOf([ + "length must be >= 1, but got 0", + "Property password is missing.", + ]) + }) + }) + + // playwright:migrated + it("should show an error when the email is empty", () => { + cy.get('input[name="traits.website"]').type("https://www.ory.sh") + cy.get('input[name="password"]').type(password) + + // 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"]'], "required") + + cy.submitPasswordForm() + cy.get('*[data-testid^="ui/message/"]') + .invoke("text") + .then((text) => { + expect(text.trim()).to.be.oneOf([ + '"" is not valid "email"', + "length must be >= 3, but got 0", + "Property email is missing.", + ]) + }) + }) + + // playwright:migrated + it("should show an error when the email is not an email", () => { + cy.get('input[name="traits.website"]').type("https://www.ory.sh") + cy.get('input[name="password"]').type(password) + + // the browser will prevent the form from being submitted if the input data doesn't conform to the input field type + // in this case an invalid email will prevent the form from being submitted by the browser + // we should remove it to ensure kratos is validating the payload + cy.get('input[name="traits.email"]').then(($el) => + $el.removeAttr("type"), + ) + cy.get('input[name="traits.email"]').type("not-an-email") + + cy.submitPasswordForm() + cy.get( + '*[data-testid="ui/message/4000040"], *[data-testid="ui/message/4000002"]', + ).should("exist") + }) + + // playwright:migrated + it("should show a missing indicator if no fields are set", () => { + // 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"]', + 'input[name="password"]', + ], + "required", + ) + + cy.submitPasswordForm() + cy.get( + '*[data-testid="ui/message/4000001"], *[data-testid="ui/message/4000002"]', + ).should("exist") + }) + + it("should show an error when the website is not a valid URI", () => { + cy.get('input[name="traits.website"]') + .type("1234") + .then(($input) => { + expect( + ($input[0] as HTMLInputElement).validationMessage, + ).to.contain("URL") + }) + }) + + // playwright:migrated + it("should show an error when the website is too short", () => { + // 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="password"]'], + "required", + ) + + cy.get('input[name="traits.website"]').type("http://s") + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + }) + + // playwright:migrated + it("should show an error when required params are missing", () => { + // the browser will prevent the form from being submitted if the input field is required + // we should remove it from the DOM entirely to simulate the data not being sent + cy.get('input[name="traits.website"]').then(($el) => $el.remove()) + cy.get('input[name="traits.email"]').then(($el) => $el.remove()) + cy.get('input[name="password"]').then(($el) => $el.remove()) + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing.", + ) + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property email is missing.", + ) + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property password is missing.", + ) + }) + + // playwright:migrated + it("should show an error when the age is too high", () => { + // 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"]', + 'input[name="password"]', + ], + "required", + ) + + cy.get('input[name="traits.age"]').type("600") + + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000020"]').should( + "contain.text", + "must be <= 300 but found 600", + ) + }) + + // playwright:migrated + it("should show a hint for existing account", () => { + const email = gen.email() + const password = gen.password() + + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="traits.age"]').type(`30`) + cy.submitPasswordForm() + cy.get('[data-testid="ui/message/4000028"]').should( + "contain.text", + "You tried signing in with " + + email + + " which is already in use by another account. You can sign in using your password.", + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts new file mode 100644 index 0000000..123d7b5 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts @@ -0,0 +1,210 @@ +// 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 email profile", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "email", + }, + { + route: react.registration, + app: "react" as "react", + profile: "spa", + }, + ].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") + } + }) + + // playwright:migrated + it("should sign up and be logged in", () => { + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + const age = 30 + + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="traits.age"]').type(`${age}`) + cy.get('[type="checkbox"][name="traits.tos"]').click({ force: true }) + + cy.submitPasswordForm() + + cy.url().should("contain", "verification") + cy.getVerificationCodeFromEmail(email).then((code) => { + cy.get("input[name=code]").type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.get('[data-testid="ui/message/1080002"]').should( + "have.text", + "You successfully verified your email address.", + ) + + cy.get("[data-testid='node/anchor/continue']").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.verifiable_addresses).to.have.length(1) + 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) + expect(identity.traits.age).to.equal(age) + expect(identity.traits.tos).to.equal(true) + }) + }) + + // playwright:migrated + it("should sign up with advanced form field values be logged in", () => { + const email = gen.email() + const password = gen.password() + + cy.get('input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + const website = "https://www.example.org/" + cy.get('input[name="traits.website"]').type(website) + + cy.submitPasswordForm() + + cy.url().should("contain", "verification") + cy.getVerificationCodeFromEmail(email).then((code) => { + cy.get("input[name=code]").type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.get('[data-testid="ui/message/1080002"]').should( + "have.text", + "You successfully verified your email address.", + ) + + cy.get("[data-testid='node/anchor/continue']").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.verifiable_addresses).to.have.length(1) + 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) + expect(identity.traits.age).to.be.undefined + expect(identity.traits.tos).to.be.oneOf([false, undefined]) + }) + }) + + // playwright:migrated + it("should sign up and be redirected", () => { + cy.disableVerification() + cy.browserReturnUrlOry() + cy.visit(route + "?return_to=https://www.example.org/") + + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + + cy.get('input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + cy.url().should("eq", "https://www.example.org/") + }) + }) + }) + + describe("redirect for express app", () => { + it("should redirect to return_to after flow expires", () => { + // Wait for flow to expire + cy.useConfigProfile("email") + cy.shortRegisterLifespan() + cy.browserReturnUrlOry() + cy.proxy("express") + cy.visit(express.registration + "?return_to=https://www.example.org/") + cy.wait(105) + + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + + cy.get(`${appPrefix("express")} input[name="traits"]`).should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="password"]').type(password) + + cy.longRegisterLifespan() + cy.submitPasswordForm() + + cy.get('[data-testid="ui/message/4040001"]').should( + "contain.text", + "The registration flow expired", + ) + + // Try again with long lifespan set + cy.get('input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + cy.url().should("eq", "https://www.example.org/") + }) + + it("should not redirect to verification_flow if not configured", () => { + cy.deleteMail() + cy.useConfigProfile("email") + cy.enableVerification() + cy.proxy("express") + cy.visit(express.registration + "?return_to=https://www.example.org/") + + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + + cy.get(`${appPrefix("express")} input[name="traits"]`).should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website').type(website) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + + // Verify that the verification code is still sent + cy.getVerificationCodeFromEmail(email).should("not.be.undefined") + + cy.url().should("eq", "https://www.example.org/") + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/registration/ui.spec.ts b/test/e2e/cypress/integration/profiles/email/registration/ui.spec.ts new file mode 100644 index 0000000..9410d06 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/registration/ui.spec.ts @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { appPrefix } from "../../../../helpers" + +describe("Registration UI for email flows", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "email", + }, + { + route: react.registration, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.visit(route) + }) + + describe("use ui elements", () => { + // playwright:migrated + it("should use the json schema titles", () => { + cy.get(appPrefix(app) + 'input[name="traits.email"]') + .parent() + .should("contain.text", "Your E-Mail") + cy.get('input[name="traits.website"]') + .parent() + .should("contain.text", "Your website") + cy.get('button[value="password"]').should("contain.text", "Sign up") + }) + + // playwright:migrated + it("clicks the log in link", () => { + cy.get('*[data-testid="cta-link"]').click() + cy.location("pathname").should("include", "/login") + if (app === "express") { + cy.location("search").should("not.be.empty") + } + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/settings/errors.spec.ts b/test/e2e/cypress/integration/profiles/email/settings/errors.spec.ts new file mode 100644 index 0000000..38d6035 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/settings/errors.spec.ts @@ -0,0 +1,314 @@ +// 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" + +context("Settings failures with email profile", () => { + ;[ + { + route: express.settings, + base: express.base, + app: "express" as "express", + profile: "email", + }, + { + route: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ route, profile, app, base }) => { + describe(`for app ${app}`, () => { + let email = gen.email() + let password = gen.password() + + const emailSecond = `second-${gen.email()}` + const passwordSecond = gen.password() + + const up = (value) => `not-${value}` + + before(() => { + cy.proxy(app) + cy.useConfigProfile(profile) + cy.registerApi({ + email: emailSecond, + password: passwordSecond, + fields: { "traits.website": "https://github.com/ory/kratos" }, + }) + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + beforeEach(() => { + cy.longPrivilegedSessionTime() + + cy.visit(base) + cy.clearAllCookies() + + cy.login({ email, password, cookieUrl: base }) + cy.visit(route) + }) + + describe("profile", () => { + beforeEach(() => { + cy.visit(route) + }) + + // playwright:migrated + it("fails with validation errors", () => { + cy.get('input[name="traits.website"]').clear().type("http://s") + cy.get('[name="method"][value="profile"]').click() + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + }) + + it("fails because reauth is another person", () => { + // Force this because it is hidden + cy.get('input[name="traits.email"]').clear().type(up(email)) + cy.shortPrivilegedSessionTime() + + cy.location().then((loc) => { + cy.get('button[value="profile"]').click() + + cy.reauthWithOtherAccount({ + previousUrl: loc.toString(), + expect: { email }, + type: { email: emailSecond, password: passwordSecond }, + }) + + cy.location("pathname").should("contain", "/settings") + }) + + // We end up in a new settings flow for the second user + cy.get('input[name="traits.email"]').should("have.value", emailSecond) + + // Try to log in with updated credentials -> should fail + cy.clearAllCookies() + cy.login({ + email: up(email), + password, + expectSession: false, + cookieUrl: base, + }) + }) + + it("does not update data because resumable session was removed", () => { + cy.get('input[name="traits.email"]').clear().type(up(email)) + cy.shortPrivilegedSessionTime() + cy.get('button[value="profile"]').click() + + cy.login({ email, password, cookieUrl: base }) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.traits.email).to.equal(email) + }) + }) + + it("does not update without re-auth", () => { + cy.get('input[name="traits.email"]').clear().type(up(email)) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="profile"]').click() + + cy.visit(base) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.traits.email).to.equal(email) + }) + }) + + it("does not resume another failed request", () => { + // checks here that we're checking settingsRequest.id == cookie.stored.id + cy.get('input[name="traits.email"]').clear().type(up(email)) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="profile"]').click() + cy.location("pathname").should("not.contain", "/settings") + + cy.visit(route) + cy.get('input[name="traits.website"]') + .clear() + .type("http://github.com/aeneasr") + cy.get('button[value="profile"]').click() + cy.expectSettingsSaved() + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.traits.email).to.equal(email) // this is NOT up(email) + expect(identity.traits.website).to.equal( + "http://github.com/aeneasr", + ) // this is NOT up(email) + }) + }) + }) + + describe("password", () => { + beforeEach(() => { + cy.longPrivilegedSessionTime() + }) + + afterEach(() => { + cy.longPrivilegedSessionTime() + }) + + // playwright:migrated + it("fails if password policy is violated", () => { + cy.get('input[name="password"]').clear().type("12345678") + cy.get('button[value="password"]').click() + cy.get('[data-testid="ui/message/4000034"]').should( + "contain.text", + "data breaches", + ) + }) + + it("fails because reauth is another person", () => { + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('input[name="password"]').clear().type(up(password)) + + let firstSession + cy.getSession().then((session) => { + firstSession = session + }) + + cy.location().then((loc) => { + cy.get('button[value="password"]').click() + + cy.reauthWithOtherAccount({ + previousUrl: loc.toString(), + expect: { email }, + type: { email: emailSecond, password: passwordSecond }, + }) + + cy.location("pathname").should("contain", "/settings") + }) + + // We want to ensure that the reauth session is completely different from the one we had in the first place. + cy.getSession().then((session) => { + expect(session.authentication_methods).to.have.length(1) + expect(session.identity.traits.email).to.eq(emailSecond) + expect(session.id).to.not.eq(firstSession.id) + expect(session.identity.id).to.not.eq(firstSession.identity.id) + expect(session.authenticated_at).to.not.eq( + firstSession.authenticated_at, + ) + }) + + // We end up in a new settings flow for the second user + cy.get('input[name="traits.email"]').should("have.value", emailSecond) + + // Try to log in with updated credentials -> should fail + cy.clearAllCookies() + cy.login({ + email, + password: up(password), + expectSession: false, + cookieUrl: base, + }) + }) + + it("does not update without re-auth", () => { + cy.get('input[name="password"]').clear().type(up(password)) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="password"]').click() + + cy.visit(base) + cy.clearAllCookies() + cy.login({ + email, + password: up(password), + expectSession: false, + cookieUrl: base, + }) + }) + + it("does not update data because resumable session was removed", () => { + cy.get('input[name="password"]').clear().type(up(password)) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="password"]').click() + + cy.clearAllCookies() + cy.login({ email, password, cookieUrl: base }) + cy.clearAllCookies() + cy.login({ + email, + password: up(password), + expectSession: false, + cookieUrl: base, + }) + }) + + it("does not resume another queued request", () => { + const email = gen.email() + const password = gen.password() + cy.clearAllCookies() + cy.register({ + email, + password, + fields: { "traits.website": website }, + }) + cy.visit(route) + + // checks here that we're checking settingsRequest.id == cookie.stored.id + const invalidPassword = "invalid-" + gen.password() + cy.get('input[name="password"]').clear().type(invalidPassword) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="password"]').click() + cy.location("pathname").should("include", "/login") + + const validPassword = "valid-" + gen.password() + cy.visit(route) + cy.get('input[name="password"]').clear().type(validPassword) + cy.get('button[value="password"]').click() + + cy.location("pathname").should("include", "/login") + cy.reauth({ expect: { email }, type: { password: password } }) + + cy.location("pathname").should("include", "/settings") + cy.get('input[name="password"]').should("exist") + + // This should pass because it is the correct password + cy.clearAllCookies() + cy.login({ email, password: validPassword, cookieUrl: base }) + + // This should fail because it is the wrong password + cy.clearAllCookies() + cy.login({ + email, + password: invalidPassword, + expectSession: false, + cookieUrl: base, + }) + + cy.clearAllCookies() + cy.login({ + email, + password: password, + expectSession: false, + cookieUrl: base, + }) + }) + }) + + describe("global errors", () => { + it("fails when CSRF is incorrect", () => { + cy.get(appPrefix(app) + 'input[name="password"]').type("12345678") + cy.shouldHaveCsrfError({ app }) + }) + + it("fails when a disallowed return_to url is requested", () => { + cy.shouldErrorOnDisallowedReturnTo( + route + "?return_to=https://not-allowed", + { app }, + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/email/settings/success.spec.ts new file mode 100644 index 0000000..30dd254 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/settings/success.spec.ts @@ -0,0 +1,194 @@ +// 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" + +context("Settings success with email profile", () => { + ;[ + { + route: express.settings, + base: express.base, + app: "express" as "express", + profile: "email", + login: express.login, + }, + { + route: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + login: react.login, + }, + ].forEach(({ route, profile, app, base, login }) => { + describe(`for app ${app}`, () => { + let email = gen.email() + const firstPassword = gen.password() + const secondPassword = gen.password() + const thirdPassword = gen.password() + let password = firstPassword + + const up = (value: string) => `not-${value}` + const down = (value: string) => value.replace(/not-/, "") + + before(() => { + cy.useConfigProfile(profile) + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.login({ email, password, cookieUrl: base }) + cy.visit(route) + }) + + // playwright:migrated + it("shows all settings forms", () => { + cy.get(appPrefix(app) + "h3").should("contain.text", "Profile") + cy.get('input[name="traits.email"]').should("contain.value", email) + cy.get('input[name="traits.website"]').should("contain.value", website) + + cy.get("h3").should("contain.text", "Password") + cy.get('input[name="password"]').should("be.empty") + }) + + describe("password", () => { + // playwright:migrated + it("modifies the password with privileged session", () => { + // Once input weak password to test which error message is cleared after updating successfully + cy.get('input[name="password"]').clear().type("123") + cy.get('button[value="password"]').click() + cy.get('[data-testid="ui/message/1050001"]').should("not.exist") + cy.get('[data-testid="ui/message/4000032"]').should("exist") + cy.get('input[name="password"]').should("be.empty") + + password = secondPassword + cy.get('input[name="password"]').clear().type(secondPassword) + cy.get('button[value="password"]').click() + cy.expectSettingsSaved() + cy.get('[data-testid="ui/message/4000032"]').should("not.exist") + cy.get('[data-testid="ui/message/1050001"]').should("exist") + cy.get('input[name="password"]').should("be.empty") + }) + + // playwright:migrated + it("is unable to log in with the old password", () => { + cy.login({ + email: email, + password: firstPassword, + expectSession: false, + cookieUrl: base, + }) + }) + + it("modifies the password with an unprivileged session", () => { + password = thirdPassword + cy.get('input[name="password"]').clear().type(password) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="password"]').click() + + cy.reauth({ expect: { email }, type: { password: secondPassword } }) + + cy.url().should("include", "/settings") + cy.expectSettingsSaved() + cy.get('input[name="password"]').should("be.empty") + }) + }) + + describe("profile", () => { + // playwright:migrated + it("modifies an unprotected traits", () => { + cy.get('input[name="traits.website"]') + .clear() + .type("https://github.com/ory") + cy.get('input[name="traits.age"]').clear().type("30") + cy.get('input[type="checkbox"][name="traits.tos"]').click({ + force: true, + }) + cy.submitProfileForm() + cy.expectSettingsSaved() + + cy.get('input[name="traits.website"]').should( + "contain.value", + "https://github.com/ory", + ) + cy.get('input[type="checkbox"][name="traits.tos"]') + .should("be.checked") + .click({ force: true }) + cy.get('input[name="traits.age"]') + .should("have.value", "30") + .clear() + .type("90") + + cy.submitProfileForm() + cy.expectSettingsSaved() + + cy.get('input[type="checkbox"][name="traits.tos"]').should( + "not.be.checked", + ) + cy.get('input[name="traits.age"]').should("have.value", "90") + }) + + // playwright:migrated + it("modifies a protected trait with privileged session", () => { + email = up(email) + cy.disableVerification() + cy.get('input[name="traits.email"]').clear().type(email) + cy.get('button[value="profile"]').click() + cy.expectSettingsSaved() + cy.get('input[name="traits.email"]').should("contain.value", email) + }) + + // playwright:migrated + it("is unable to log in with the old email", () => { + cy.login({ + email: down(email), + password, + expectSession: false, + cookieUrl: base, + }) + }) + + it("modifies a protected trait with unprivileged session", () => { + email = up(email) + cy.get('input[name="traits.email"]').clear().type(email) + cy.shortPrivilegedSessionTime() // wait for the privileged session to time out + cy.get('button[value="profile"]').click() + + cy.reauth({ expect: { email: down(email) }, type: { password } }) + + cy.url().should("include", "/settings") + cy.expectSettingsSaved() + cy.get('input[name="traits.email"]').should("contain.value", email) + }) + + if (app === "react") { + it("shows verification screen after email update", () => { + cy.deleteMail() + cy.enableVerification() + email = up(email) + cy.get('input[name="traits.email"]').clear().type(email) + cy.get('button[value="profile"]').click() + + cy.url().should("contain", "verification") + cy.getVerificationCodeFromEmail(email).then((code) => { + cy.get("input[name=code]").type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.get('[data-testid="ui/message/1080002"]').should( + "have.text", + "You successfully verified your email address.", + ) + }) + } + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/email/settings/ui.spec.ts b/test/e2e/cypress/integration/profiles/email/settings/ui.spec.ts new file mode 100644 index 0000000..9ce5b45 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/email/settings/ui.spec.ts @@ -0,0 +1,75 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Settings errors with email profile", () => { + ;[ + { + base: express.base, + app: "express" as "express", + profile: "email", + }, + { + base: react.base, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ profile, app, base }) => { + describe(`for app ${app}`, () => { + const identity = gen.identity() + + before(() => { + cy.useConfigProfile(profile) + cy.registerApi({ + ...identity, + fields: { "traits.website": "https://www.ory.sh/" }, + }) + cy.proxy(app) + }) + + beforeEach(() => { + cy.login({ ...identity, cookieUrl: base }) + cy.visit(base) + }) + + describe("use ui elements", () => { + // playwright:migrated + it("should use the json schema titles", () => { + const settingsLink = appPrefix(app) + 'a[href*="settings"]' + if (app === "express") { + cy.get(settingsLink).should("have.attr", "target", "_blank") + cy.removeAttribute([settingsLink], "target") + } + cy.get(settingsLink).click() + cy.get('input[name="traits.email"]') + .parent() + .should("contain.text", "Your E-Mail") + cy.get('input[name="traits.website"]') + .parent() + .should("contain.text", "Your website") + + cy.get('input[name="password"]') + .parentsUntil("label") + .should("contain.text", "Password") + + cy.get('button[value="profile"]').should("contain.text", "Save") + cy.get('button[value="password"]').should("contain.text", "Save") + }) + + // playwright:migrated + it("clicks the settings link", () => { + const settingsLink = 'a[href*="settings"]' + if (app === "express") { + cy.get(settingsLink).should("have.attr", "target", "_blank") + cy.removeAttribute([settingsLink], "target") + } + cy.get(settingsLink).click() + cy.location("pathname").should("include", "settings") + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/import/import.spec.ts b/test/e2e/cypress/integration/profiles/import/import.spec.ts new file mode 100644 index 0000000..4119161 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/import/import.spec.ts @@ -0,0 +1,133 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { routes as express } from "../../../helpers/express" +import { gen, KRATOS_ADMIN, website } from "../../../helpers" + +context("Import Identities", () => { + before(() => { + cy.useConfigProfile("oidc") + cy.proxy("express") + }) + + beforeEach(() => { + cy.clearAllCookies() + }) + + const password = gen.password() + for (const tc of [ + { + name: "cleartext", + config: { + password, + }, + checkPassword: password, + }, + { + name: "pbkdf2", + config: { + hashed_password: + "$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A", + }, + checkPassword: "123456", + }, + { + name: "bcrypt", + config: { + hashed_password: + "$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq", + }, + checkPassword: "123456", + }, + { + name: "argon2id", + config: { + hashed_password: + "$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg", + }, + checkPassword: "123456", + }, + ]) { + // playwright:migrated + it(`should be able to sign in using an imported password (${tc.name})`, () => { + const email = gen.email() + cy.request("POST", `${KRATOS_ADMIN}/identities`, { + schema_id: "default", + traits: { + email, + website, + }, + credentials: { + password: { + config: tc.config, + }, + }, + }) + + cy.visit(express.login) + + // Try to sign in with an incorrect password + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type("invalid-password") + cy.submitPasswordForm() + cy.get('*[data-testid="ui/message/4000006"]').should( + "contain.text", + "credentials are invalid", + ) + + // But with correct password it succeeds + cy.get('input[name="password"]').type(tc.checkPassword) + cy.submitPasswordForm() + + cy.location("pathname").should("not.contain", "/login") + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + }) + }) + } + + it(`should be able to sign in using imported oidc credentials`, () => { + const email = gen.email() + const website = "https://" + gen.password() + ".com" + cy.request("POST", `${KRATOS_ADMIN}/identities`, { + schema_id: "default", + traits: { + email, + website, + }, + credentials: { + oidc: { + config: { + providers: [ + { + provider: "hydra", + subject: email, + }, + ], + }, + }, + }, + }) + + cy.visit(express.login) + cy.triggerOidc("express") + + 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() + + // Wait for the post-OIDC-consent redirect chain to settle so the session + // cookie is in Cypress's jar before we hit /sessions/whoami. + cy.location("pathname").should("not.contain", "/login") + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/import/import_mfa.spec.ts b/test/e2e/cypress/integration/profiles/import/import_mfa.spec.ts new file mode 100644 index 0000000..835ac0a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/import/import_mfa.spec.ts @@ -0,0 +1,131 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { TOTP } from "otpauth" + +import { gen, KRATOS_ADMIN, website } from "../../../helpers" +import { routes as express } from "../../../helpers/express" + +// Regression coverage for https://github.com/ory/kratos/issues/4561. +// +// When TOTP or lookup-secret credentials are imported through the admin +// identity API, the user must be able to use them to complete an AAL2 +// login. Previously the import wrote the credential row but not the +// matching identity_credential_identifiers row, so AAL2 login would fail +// with "You have no TOTP device set up." or "You have not configured +// backup codes yet.". +context("Import Identities with MFA credentials", () => { + before(() => { + cy.useConfigProfile("mfa") + cy.proxy("express") + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.useConfig((builder) => + builder.longPrivilegedSessionTime().disableCodeMfa(), + ) + }) + + // playwright:migrated + it("should be able to complete AAL2 login using imported TOTP credentials", () => { + const email = gen.email() + const password = gen.password() + + // Build the TOTP secret on the test side so we can both ship its + // otpauth URL into the import payload and generate matching codes + // later when we authenticate. + const totp = new TOTP({ + issuer: "Ory", + label: email, + algorithm: "SHA1", + digits: 6, + period: 30, + }) + + cy.request("POST", `${KRATOS_ADMIN}/identities`, { + schema_id: "default", + traits: { email, website }, + credentials: { + password: { config: { password } }, + totp: { config: { totp_url: totp.toString() } }, + }, + }) + + // Force AAL2 on sign-in so we exercise the TOTP step-up path. + cy.requireStrictAal() + cy.visit(express.login) + + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + cy.shouldShow2FAScreen() + + cy.get('input[name="totp_code"]').type(totp.generate()) + cy.get('*[name="method"][value="totp"]').click() + + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + }) + + // playwright:migrated + it("should be able to complete AAL2 login using imported lookup-secret credentials", () => { + const email = gen.email() + const password = gen.password() + + // Generate a few human-readable codes so failures in the test log + // are easy to read; in production these would be opaque strings. + const codes = [ + "imported-recovery-1", + "imported-recovery-2", + "imported-recovery-3", + "imported-recovery-4", + ] + + cy.request("POST", `${KRATOS_ADMIN}/identities`, { + schema_id: "default", + traits: { email, website }, + credentials: { + password: { config: { password } }, + lookup_secret: { + config: { + codes: codes.map((code) => ({ code })), + }, + }, + }, + }) + + cy.requireStrictAal() + cy.visit(express.login) + + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + cy.shouldShow2FAScreen() + + // Reject an invalid code first to make sure we are exercising the + // lookup-secret branch and not silently bypassing it. + cy.get('input[name="lookup_secret"]').type("not-a-real-code") + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.get('[data-testid="ui/message/4000016"]').should( + "contain.text", + "The backup recovery code is not valid.", + ) + + // The form clears between attempts; type one of the imported codes. + cy.get('input[name="lookup_secret"]').should("have.value", "") + cy.get('input[name="lookup_secret"]').type(codes[0]) + cy.get('*[name="method"][value="lookup_secret"]').click() + + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "lookup_secret"], + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts new file mode 100644 index 0000000..06a1481 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts @@ -0,0 +1,157 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../helpers" +import { routes as express } from "../../../helpers/express" + +context("2FA code", () => { + ;[ + // { + // login: react.login, + // settings: react.settings, + // base: react.base, + // app: "react" as "react", + // profile: "spa", + // }, + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let email: string + let password: string + + describe("when using highest_available aal", () => { + beforeEach(() => { + cy.useConfig((builder) => + builder + .longPrivilegedSessionTime() + .useHighestAvailable() + .enableCodeMFA(), + ) + }) + + // playwright:migrated + 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.visit(settings) + cy.location("pathname").should("contain", "/login") // we get redirected to login + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) + }) + }) + + 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("input[name='code']").should("be.visible") + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) + }) + + it("can't use different email in 2fa request", () => { + // Setting up another 2fa method to prevent fast-login to happen + cy.visit(base) + cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) + cy.login({ email, password, cookieUrl: base }) + cy.longPrivilegedSessionTime() + cy.visit(settings) + cy.get( + appPrefix(app) + 'button[name="lookup_secret_regenerate"]', + ).click() + cy.get('button[name="lookup_secret_confirm"]').click() + cy.expectSettingsSaved() + cy.visit(settings) + cy.clearAllCookies() + cy.useConfig((builder) => builder.enableCodeMFA()) + cy.login({ email: email, password: password, cookieUrl: base }) + + cy.visit(login + "?aal=aal2") + cy.get('[name="method"][value="lookup_secret"]').should("exist") + cy.get('[name="address"]').should("exist") + + 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") + + // The current session should be unchanged + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + }) + + it("entering wrong code should not invalidate correct codes", () => { + 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/code_optional.spec.ts b/test/e2e/cypress/integration/profiles/mfa/code_optional.spec.ts new file mode 100644 index 0000000..55c0bc6 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/code_optional.spec.ts @@ -0,0 +1,213 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../helpers" +import { routes as express } from "../../../helpers/express" + +context("2FA code with optional field", () => { + ;[ + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa-optional", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + describe("when using highest_available aal with empty optionalMfaEmail field", () => { + let email: string + let password: string + + beforeEach(() => { + email = gen.email() + password = gen.password() + + // Configure system to use highest available AAL + cy.useConfig((builder) => + builder.longPrivilegedSessionTime().useHighestAvailable(), + ) + + // Register a user without setting up code MFA (empty optionalMfaEmail field) + cy.register({ + email, + password, + fields: { "traits.optionalMfaEmail": "" }, + }) + cy.deleteMail() + }) + + // playwright:migrated + it("should not show 2FA page during login when optionalMfaEmail field is empty", () => { + // Log out first + cy.clearAllCookies() + + // Login with the user + cy.visit(login) + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('button[name="method"][value="password"]').click() + + // Should be logged in directly without 2FA page + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + + // Verify we're not asked for 2FA when visiting settings + cy.visit(settings) + cy.location("pathname").should("contain", "/settings") + cy.get('input[name="traits.email"]').should("contain.value", email) + }) + }) + + describe("when using highest_available aal with configured optionalMfaEmail field", () => { + let email: string + let mfaEmail: string + let password: string + + beforeEach(() => { + email = gen.email() + mfaEmail = gen.email() + password = gen.password() + + // Configure system to use highest available AAL + cy.useConfig((builder) => + builder.longPrivilegedSessionTime().useHighestAvailable(), + ) + + // Register a user with optionalMfaEmail field set + cy.register({ + email, + password, + fields: { "traits.optionalMfaEmail": mfaEmail }, + }) + cy.deleteMail() + }) + + // playwright:migrated + it("should show 2FA page during login when optionalMfaEmail field is configured", () => { + // Log out first + cy.clearAllCookies() + + // Login with the user + cy.visit(login) + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('button[name="method"][value="password"]').click() + + // Should see the code input field + cy.location("pathname").should("contain", "/login") + cy.get("input[name='code']").should("be.visible") + + // Get the code from email and enter it + cy.getLoginCodeFromEmail(mfaEmail).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) + + // Should be logged in with AAL2 + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) + }) + + it("should require 2FA for settings flow when optionalMfaEmail field is configured", () => { + // Log out first + cy.clearAllCookies() + + // Try to access settings directly + cy.visit(settings) + + // Should be redirected to login + cy.location("pathname").should("contain", "/login") + + // Login with password + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('button[name="method"][value="password"]').click() + + // Should see the code input field + cy.location("pathname").should("contain", "/login") + cy.get("input[name='code']").should("be.visible") + + // Get the code from email and enter it + cy.getLoginCodeFromEmail(mfaEmail).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) + + // Go the the settings page again + cy.visit(settings) + + // Should now be at settings with AAL2 + cy.location("pathname").should("contain", "/settings") + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) + }) + }) + + describe("when user with configured optionalMfaEmail logs in with aal1 session", () => { + let email: string + let mfaEmail: string + let password: string + + beforeEach(() => { + email = gen.email() + mfaEmail = gen.email() + password = gen.password() + + // Configure system to allow aal1 login but still require highest_available for settings + cy.useConfig((builder) => + builder + .longPrivilegedSessionTime() + .useLaxSessionAal() + .useHighestSettingsFlowAal(), + ) + + // Register a user with optionalMfaEmail field set + cy.register({ + email, + password, + fields: { "traits.optionalMfaEmail": mfaEmail }, + }) + cy.deleteMail() + }) + + it("should not allow access to settings page with aal1 session", () => { + // Log out first + cy.clearAllCookies() + + // Login with just password (aal1) + cy.visit(login) + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.get('button[name="method"][value="password"]').click() + + // Verify we have an aal1 session + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + + // Try to access settings page + cy.visit(settings) + + // Should be redirected to login for 2FA + cy.location("pathname").should("contain", "/login") + + // Verify we're being asked for 2FA code input + cy.get("input[name='code']").should("be.visible") + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts b/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts new file mode 100644 index 0000000..2e3c210 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts @@ -0,0 +1,305 @@ +// 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" + +context("2FA lookup secrets", () => { + ;[ + { + login: react.login, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let email = gen.email() + let password = gen.password() + + beforeEach(() => { + cy.visit(base) + cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) + email = gen.email() + password = gen.password() + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.login({ email, password, cookieUrl: base }) + + cy.longPrivilegedSessionTime() + cy.sessionRequiresNo2fa() + }) + + // playwright:migrated + it("should be able to remove lookup codes", () => { + cy.sessionRequires2fa() + cy.longPrivilegedSessionTime() + cy.visit(settings) + cy.get( + appPrefix(app) + 'button[name="lookup_secret_regenerate"]', + ).click() + cy.get('button[name="lookup_secret_confirm"]').click() + cy.expectSettingsSaved() + cy.visit(settings) + + cy.shortPrivilegedSessionTime() + cy.get('button[name="lookup_secret_disable"]').click() + cy.reauth({ + expect: { email }, + type: { email: email, password: password }, + }) + cy.expectSettingsSaved() + + cy.clearAllCookies() + cy.login({ email: email, password: password, cookieUrl: base }) + + cy.visit(login + "?aal=aal2") + cy.get("h2") + .invoke("text") + .should( + "match", + /Second factor authentication|Two-Factor Authentication/, + ) + cy.get('*[name="method"][value="totp"]').should("not.exist") + cy.get('*[name="method"][value="lookup_secret"]').should("not.exist") + cy.get('*[name="method"][value="password"]').should("not.exist") + }) + + // playwright:migrated + it("should go through several lookup secret lifecycles", () => { + cy.visit(settings) + + cy.get('[data-testid="node/text/lookup_secret_codes/label"]').should( + "not.exist", + ) + cy.get('[data-testid="text-lookup_secret_codes-content"] code').should( + "not.exist", + ) + cy.get('button[name="lookup_secret_confirm"]').should("not.exist") + cy.get('button[name="lookup_secret_regenerate"]').click() + cy.get('[data-testid="node/text/lookup_secret_codes/label"]').should( + "contain.text", + "These are your back up recovery codes.", + ) + cy.get('[data-testid="text-lookup_secret_codes-content"] code').should( + "not.be.empty", + ) + + let codes: string[] + cy.getLookupSecrets().should((c) => { + codes = c + }) + + cy.get('button[name="lookup_secret_confirm"]').click() + cy.expectSettingsSaved() + + cy.get('button[name="lookup_secret_reveal"]').should("exist") + cy.get('[data-testid="text-lookup_secret_codes-content"] code').should( + "not.exist", + ) + cy.get('button[name="lookup_secret_confirm"]').should("not.exist") + cy.get('button[name="lookup_secret_regenerate"]').should("not.exist") + + cy.get('button[name="lookup_secret_reveal"]').click() + cy.getLookupSecrets().should((c) => { + codes = c + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "lookup_secret"], + }) + + // Try to log in with a recovery code now + cy.visit(login + "?aal=aal2&refresh=true") + cy.location("pathname").should("contain", "login") + + cy.get('*[name="method"][value="lookup_secret"]').should("exist") + cy.get('*[name="method"][value="password"]').should("not.exist") + + // Type an invalid code + cy.get('input[name="lookup_secret"]').should("exist") + cy.get('input[name="lookup_secret"]').type("invalid-code") + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.get('[data-testid="ui/message/4000016"]').should( + "contain.text", + "The backup recovery code is not valid.", + ) + + // Type a valid code + cy.get('input[name="lookup_secret"]').should("exist") + cy.get('input[name="lookup_secret"]').should("have.value", "") + cy.get('input[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(codes[0]) + }) + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.location("pathname").should("not.contain", "login") + + let authenticatedAt + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "lookup_secret", "lookup_secret"], + }).then((session) => { + authenticatedAt = session.authenticated_at + expect(session.authenticator_assurance_level).to.equal("aal2") + }) + + // Retry auth with the used code + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + cy.get('input[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(codes[0]) + }) + cy.get('*[name="method"][value="lookup_secret"]').click() + // Use a valid code + cy.get('[data-testid="ui/message/4000012"]').should( + "contain.text", + "This backup recovery code has already been used.", + ) + + cy.get('input[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(codes[1]) + }) + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.location("pathname").should("not.contain", "login") + + cy.getSession({ + expectAal: "aal2", + expectMethods: [ + "password", + "lookup_secret", + "lookup_secret", + "lookup_secret", + ], + }).then((session) => { + expect(session.authenticated_at).to.not.equal(authenticatedAt) + }) + + // Going back to the settings UI we should see that the codes have been "used" + cy.visit(settings) + cy.get('button[name="lookup_secret_reveal"]').click() + cy.getLookupSecrets().should((c) => { + 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 + cy.get("*[name=lookup_secret_regenerate]").click() + cy.get("*[name=lookup_secret_confirm]").should("exist") + let regenCodes + cy.getLookupSecrets().should((c) => { + regenCodes = c + }) + + // Confirm it + cy.get("*[name=lookup_secret_confirm]").click() + cy.get('*[name="lookup_secret_reveal"]').click() + cy.getLookupSecrets().should((c) => { + expect(c).to.eql(regenCodes) + }) + + // Log in and see if we can use the old / new keys + cy.visit(login + "?aal=aal2&refresh=true") + cy.location("pathname").should("contain", "login") + + // Using an old code fails + cy.get('input[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(codes[3]) + }) + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.get('[data-testid="ui/message/4000016"]').should("exist") + + // Using a new code succeeds + cy.get('input[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(regenCodes[0]) + }) + cy.get('*[name="method"][value="lookup_secret"]').click() + cy.location("pathname").should("not.contain", "login") + + // Going back to the settings UI we should see that the codes have been "used" + cy.visit(settings) + cy.get('button[name="lookup_secret_reveal"]').click() + cy.getLookupSecrets().should((c) => { + expect(c.slice(1)).to.eql(regenCodes.slice(1)) + expect(c[0]).to.match(/(Secret was used at )|(Used)/g) + }) + }) + + it("should end up at login screen if trying to reveal without privileged session", () => { + cy.shortPrivilegedSessionTime() + cy.visit(settings) + cy.get('button[name="lookup_secret_regenerate"]').click() + cy.reauth({ + expect: { email }, + type: { email: email, password: password }, + }) + + let codes + cy.getLookupSecrets().should((c) => { + codes = c + }) + + cy.shortPrivilegedSessionTime() + cy.get('button[name="lookup_secret_confirm"]').click() + cy.reauth({ + expect: { email }, + type: { email: email, password: password }, + }) + if (app === "react") { + cy.get('button[value="profile"]').click() + } + cy.expectSettingsSaved() + + if (app !== "react") { + cy.shortPrivilegedSessionTime() + cy.get('button[name="lookup_secret_reveal"]').click() + cy.reauth({ + expect: { email }, + type: { email: email, password: password }, + }) + cy.getLookupSecrets().should((c) => { + expect(c).to.not.be.empty + }) + cy.getSession({ + expectAal: "aal2", + }) + } + }) + + // playwright:migrated + it("should not show lookup as an option if not configured", () => { + cy.visit(login + "?aal=aal2") + cy.get('*[name="method"][value="totp"]').should("not.exist") + cy.get('*[name="method"][value="lookup_secret"]').should("not.exist") + cy.get('*[name="method"][value="password"]').should("not.exist") + cy.get("h2") + .invoke("text") + .should( + "match", + /Second factor authentication|Two-Factor Authentication/, + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/mix.spec.ts b/test/e2e/cypress/integration/profiles/mfa/mix.spec.ts new file mode 100644 index 0000000..ecdfa7e --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/mix.spec.ts @@ -0,0 +1,188 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../helpers" +import { routes as react } from "../../../helpers/react" +import { routes as express } from "../../../helpers/express" +import { TOTP } from "otpauth" + +context("2FA with various methods", () => { + beforeEach(() => { + cy.task("resetCRI", {}) + }) + ;[ + { + login: react.login, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + let email = gen.email() + let password = gen.password() + + beforeEach(() => { + cy.clearAllCookies() + email = gen.email() + password = gen.password() + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.clearAllCookies() + cy.login({ email, password, cookieUrl: base }) + cy.longPrivilegedSessionTime() + cy.task("sendCRI", { + query: "WebAuthn.disable", + opts: {}, + }) + }) + + it("should set up an use all mfa combinations", () => { + cy.visit(settings) + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }).then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then(() => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + + cy.visit(settings) + // Set up TOTP + let secret: string + cy.get( + appPrefix(app) + '[data-testid="node/text/totp_secret_key/text"]', + ).then(($e) => { + secret = $e.text().trim() + }) + cy.get('[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + + // Set up lookup secrets + cy.visit(settings) + cy.get('[name="lookup_secret_regenerate"]').click() + let codes: string[] + cy.getLookupSecrets().then((c) => { + codes = c + }) + cy.get('[name="lookup_secret_confirm"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp", "lookup_secret"], + }) + + // Set up WebAuthn + cy.visit(settings) + cy.get('[name="webauthn_register_displayname"]').type("my-key") + // We need a workaround here. So first we click, then we submit + cy.clickWebAuthButton("register") + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp", "webauthn", "lookup_secret"], + }) + + cy.visit(login + "?aal=aal2&refresh=true") + cy.get('[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + + cy.get('[name="method"][value="totp"]').click() + cy.location("pathname").should("not.include", "/login") + + cy.getSession({ + expectAal: "aal2", + expectMethods: [ + "password", + "totp", + "webauthn", + "lookup_secret", + "totp", + ], + }) + + // Use TOTP + cy.visit(login + "?aal=aal2&refresh=true") + cy.clickWebAuthButton("login") + cy.getSession({ + expectAal: "aal2", + expectMethods: [ + "password", + "totp", + "webauthn", + "lookup_secret", + "totp", + "webauthn", + ], + }) + + // Use lookup + cy.visit(login + "?aal=aal2&refresh=true") + cy.get('[name="lookup_secret"]').then(($e) => { + cy.wrap($e).type(codes[1]) + }) + cy.get('[name="method"][value="lookup_secret"]').click() + cy.location("pathname").should("not.include", "/login") + + cy.getSession({ + expectAal: "aal2", + expectMethods: [ + "password", + "totp", + "webauthn", + "lookup_secret", + "totp", + "webauthn", + "lookup_secret", + ], + }) + }) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts b/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts new file mode 100644 index 0000000..329cb0f --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts @@ -0,0 +1,68 @@ +// 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" + +context("2FA UI settings tests", () => { + ;[ + { + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + ].forEach(({ settings, profile, base, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + const email = gen.email() + const password = gen.password() + + before(() => { + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) + cy.login({ email, password, cookieUrl: base }) + cy.visit(settings) + }) + + it("shows all settings forms", () => { + cy.get(appPrefix(app) + "h3").should("contain.text", "Profile Settings") + cy.get("h3").should("contain.text", "Change Password") + cy.get("h3").should("contain.text", "Manage 2FA Backup Recovery Codes") + cy.get("h3").should("contain.text", "Manage 2FA TOTP Authenticator App") + cy.get("h3").should("contain.text", "Manage Hardware Tokens") + cy.get('input[name="traits.email"]').should("contain.value", email) + cy.get('input[name="traits.website"]').should("contain.value", website) + + cy.get('[data-testid="node/text/totp_secret_key/label"]').should( + "contain.text", + "This is your authenticator app secret", + ) + cy.get("button").should( + "contain.text", + "Generate new backup recovery codes", + ) + cy.get("button").should("contain.text", "Add security key") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts b/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts new file mode 100644 index 0000000..d9b762f --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts @@ -0,0 +1,364 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, website } from "../../../helpers" +import { routes as express } from "../../../helpers/express" +import { routes as react } from "../../../helpers/react" +import { TOTP } from "otpauth" + +context("2FA TOTP", () => { + ;[ + { + login: react.login, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let email = gen.email() + let password = gen.password() + + beforeEach(() => { + cy.useConfig((builder) => + builder.longPrivilegedSessionTime().useLaxAal().disableCodeMfa(), + ) + email = gen.email() + password = gen.password() + + cy.register({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + // playwright:migrated + it("should be be asked to sign in with 2fa if set up", () => { + cy.visit(settings) + cy.requireStrictAal() + + let secret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + + cy.clearAllCookies() + cy.visit(login) + + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + // MFA is now requested + cy.shouldShow2FAScreen() + + // If we visit settings page we still end up at 2fa screen + cy.visit(settings) + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + + cy.shouldShow2FAScreen() + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.location("pathname").should((loc) => { + expect(loc).to.oneOf(["/welcome", "/", "/sessions", "/settings"]) + }) + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + }) + + it("signin with 2fa and be redirected", () => { + if (app !== "express") { + return + } + + cy.visit(settings) + cy.requireStrictAal() + + let secret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + + cy.clearAllCookies() + cy.visit(`${login}?return_to=https://www.example.org/`) + + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + + // MFA is now requested + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + cy.shouldShow2FAScreen() + + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + + cy.shouldShow2FAScreen() + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.url().should("eq", "https://www.example.org/") + }) + + // playwright:migrated + it("should go through several totp lifecycles", () => { + cy.visit(settings) + + cy.get('[data-testid="node/text/totp_secret_key/text"]').should("exist") + cy.get('img[data-testid="node/image/totp_qr"]').should("exist") + + // Set up TOTP + let secret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.get('[data-testid="node/text/totp_secret_key/text"]').should( + "not.exist", + ) + cy.get('img[data-testid="node/image/totp_qr"]').should("not.exist") + cy.get('*[name="method"][value="totp"]').should("not.exist") + cy.get('*[name="totp_unlink"]').should("exist") + + // Let's try to do 2FA + cy.visit(login + "?aal=aal2&refresh=true") + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + cy.get('*[name="method"][value="password"]').should("not.exist") + + // Typing a wrong code leaves us with an error message + cy.get('*[name="totp_code"]').type("111111") + cy.get('*[name="method"][value="totp"]').click() + + cy.get('[data-testid="ui/message/4000008"]').should( + "contain.text", + "The provided authentication code is invalid, please try again.", + ) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp", "totp"], + }) + + // Going to settings and unlinking the device + cy.visit(settings) + cy.get('*[name="totp_unlink"]').click() + cy.expectSettingsSaved() + cy.get('[data-testid="node/text/totp_secret_key/text"]').should("exist") + cy.get('img[data-testid="node/image/totp_qr"]').should("exist") + cy.get('*[name="method"][value="totp"]').should("exist") + cy.get('*[name="totp_unlink"]').should("not.exist") + + // 2FA should be gone + cy.visit(login + "?aal=aal2&refresh=true") + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + cy.get('*[name="method"][value="totp"]').should("not.exist") + + // Linking a new device works + cy.visit(settings) + let newSecret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + newSecret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret: newSecret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + + // Old secret no longer works in login + cy.visit(login + "?aal=aal2&refresh=true") + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.get('[data-testid="ui/message/4000008"]').should( + "contain.text", + "The provided authentication code is invalid, please try again.", + ) + + // But new one does! + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret: newSecret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.location("pathname").should((loc) => { + expect(loc).to.not.include("/login") + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp", "totp", "totp", "totp"], + }) + + // The React app keeps using the same flow. The following scenario used to be broken, + // because the internal context wasn't populated properly in the flow after settings were saved. + cy.visit(settings) + cy.get('*[name="totp_unlink"]').click() + cy.expectSettingsSaved() + + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + }) + + // playwright:migrated + it("should not show totp as an option if not configured", () => { + cy.visit(login + "?aal=aal2") + cy.location("pathname").should((loc) => { + expect(loc).to.include("/login") + }) + + cy.get('*[name="method"][value="totp"]').should("not.exist") + cy.get('*[name="method"][value="password"]').should("not.exist") + cy.shouldShow2FAScreen() + + cy.get('[data-testid="logout-link"]').click() + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + expect(loc.search).to.not.include("aal") + expect(loc.search).to.not.include("refresh") + }) + cy.noSession() + }) + + // playwright:migrated + it("should fail to set up totp if verify code is wrong", () => { + cy.visit(settings) + cy.get('input[name="totp_code"]').type("12345678") + cy.get('*[name="method"][value="totp"]').click() + cy.get('[data-testid="ui/message/4000008"]').should( + "contain.text", + "The provided authentication code is invalid, please try again.", + ) + }) + + // The React app keeps using the same flow. The following scenario used to be broken, + // because the internal context wasn't populated properly in the flow after settings were saved. + // playwright:migrated + it("should allow changing other settings and then setting up totp", () => { + cy.visit(settings) + cy.get('input[name="traits.website"]') + .clear() + .type("https://some-website.com") + cy.get('*[name="method"][value="profile"]').click() + cy.expectSettingsSaved() + + let secret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts b/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts new file mode 100644 index 0000000..4077ad7 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts @@ -0,0 +1,331 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../helpers" +import { routes as react } from "../../../helpers/react" +import { routes as express } from "../../../helpers/express" + +context("2FA WebAuthn", () => { + beforeEach(() => { + cy.task("resetCRI", {}) + }) + ;[ + { + login: express.login, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "mfa", + }, + { + login: react.login, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let email = gen.email() + let password = gen.password() + + beforeEach(() => { + cy.clearAllCookies() + + cy.useConfig((builder) => builder.disableCodeMfa()) + email = gen.email() + password = gen.password() + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + + cy.login({ email, password }) + + cy.longPrivilegedSessionTime() + cy.task("sendCRI", { + query: "WebAuthn.disable", + opts: {}, + }) + }) + + it("should be able to identify if the authenticator is wrong", () => { + cy.visit(settings) + + // Set up virtual authenticator + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }).then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then((addResult) => { + cy.get( + appPrefix(app) + '[name="webauthn_register_displayname"]', + ).type("key1") + + cy.clickWebAuthButton("register") + + cy.get('*[name="webauthn_remove"]').should("have.length", 1) + + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: addResult, + }).then(() => { + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + cy.clickWebAuthButton("login") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "webauthn"], + }) + + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then((addResult) => { + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + cy.clickWebAuthButton("login") + + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "webauthn"], + }) + }) + }) + }) + }) + }) + + it("should be able to link multiple authenticators", () => { + cy.visit(settings) + + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }).then(() => { + // Register the first credential with the first virtual authenticator. + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then((firstAuthenticator) => { + cy.get('*[name="webauthn_register_displayname"]').type("key1") + cy.clickWebAuthButton("register") + cy.get('*[name="webauthn_remove"]').should("have.length", 1) + + // Swap in a second authenticator for the next credential. The + // settings flow lists the first credential in excludeCredentials; + // with both authenticators attached, create() would race the + // excluded authenticator's InvalidStateError against the new one, so + // remove the first before registering on the second. The identity + // keeps both credentials server-side either way. + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: firstAuthenticator, + }) + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then(() => { + cy.get('*[name="webauthn_register_displayname"]').type("key2") + cy.clickWebAuthButton("register") + + cy.get('*[name="webauthn_remove"]').should("have.length", 2) + + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + cy.get('*[name="webauthn_login_trigger"]').should( + "have.length", + 1, + ) + cy.clickWebAuthButton("login") + }) + }) + }) + }) + + it("should not register the same authenticator twice", () => { + cy.visit(settings) + + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }).then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then(() => { + cy.get('*[name="webauthn_register_displayname"]').type("key1") + cy.clickWebAuthButton("register") + cy.get('*[name="webauthn_remove"]').should("have.length", 1) + + // Registering again with the same authenticator must be rejected: + // the settings flow lists the existing credential in + // excludeCredentials, so the browser refuses to create a duplicate + // and the credential count stays at one. + cy.get('*[name="webauthn_register_displayname"]').type("key2") + cy.clickWebAuthButton("register") + + cy.get('*[name="webauthn_remove"]').should("have.length", 1) + }) + }) + }) + + it("should be not be able to link provider if webauth is not enabled", () => { + cy.visit(settings) + cy.get('*[name="webauthn_register_displayname"]').type("my-key") + cy.clickWebAuthButton("register") + cy.get('*[name="webauthn_remove"]').should("not.exist") + }) + + it("should be able to link a webauthn provider", () => { + cy.visit(settings) + + // Set up virtual authenticator + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }).then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }).then((addResult) => { + // Signing up without a display name causes an error + cy.get('*[name="webauthn_remove"]').should("not.exist") + + cy.clickWebAuthButton("register") + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property webauthn_register_displayname is missing.", + ) + + // Setting up with key works + cy.get('*[name="webauthn_register_displayname"]').type("my-key") + + // We need a workaround here. So first we click, then we submit + cy.clickWebAuthButton("register") + + cy.expectSettingsSaved() + cy.get('*[name="webauthn_remove"]').should("exist") + + // Login without refresh + cy.login({ email, password }) + cy.visit(login + "?aal=aal2") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + + cy.get('*[name="webauthn_login_trigger"]').should("have.length", 1) + cy.clickWebAuthButton("login") + cy.location().should((loc) => { + expect(loc.href).to.not.include("/login") + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "webauthn"], + }) + + // Login with refresh + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + + cy.get('*[name="webauthn_login_trigger"]').should("have.length", 1) + cy.clickWebAuthButton("login") + cy.location().should((loc) => { + expect(loc.href).to.not.include("/login") + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "webauthn", "webauthn"], + }) + cy.visit(settings) + cy.get('*[name="webauthn_remove"]').click() + cy.get('*[name="webauthn_remove"]').should("not.exist") + + cy.visit(login + "?aal=aal2&refresh=true") + cy.location().should((loc) => { + expect(loc.href).to.include("/login") + }) + + cy.get('button[name="webauthn_login_trigger"]').should("not.exist") + cy.get('[data-testid="ui/message/1010003"]').should("exist") + }) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/login/errors.spec.ts b/test/e2e/cypress/integration/profiles/mobile/login/errors.spec.ts new file mode 100644 index 0000000..57cc920 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/login/errors.spec.ts @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("Login Flow Errors", () => { + before(() => { + cy.clearAllCookies() + cy.useConfigProfile("mobile") + }) + + beforeEach(() => { + cy.visit(MOBILE_URL + "/Login") + }) + + describe("shows validation errors when invalid signup data is used", () => { + it("should show an error when the identifier is missing", () => { + cy.get('input[data-testid="password"]').type(gen.password()) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('*[data-testid="field/identifier"]').should( + "contain.text", + "Property identifier is missing.", + ) + + cy.get('*[data-testid="field/password"]').should( + "not.contain.text", + "Property password is missing.", + ) + }) + + it("should show an error when the password is missing", () => { + const email = gen.email() + cy.get('input[data-testid="identifier"]') + .type(email) + .should("have.value", email) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('*[data-testid="field/password"]').should( + "contain.text", + "Property password is missing.", + ) + }) + + it("should show fail to sign in", () => { + cy.get('input[data-testid="identifier"]').type(gen.email()) + cy.get('input[data-testid="password"]').type(gen.password()) + cy.get( + '*[data-testid="field/method/password"] *[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="form-messages"]').should( + "contain.text", + "credentials are invalid", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/login/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/login/success.spec.ts new file mode 100644 index 0000000..900dcc0 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/login/success.spec.ts @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("Login Flow Success", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + const email = gen.email() + const password = gen.password() + + before(() => { + cy.registerApi({ email, password, fields: { "traits.website": website } }) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(MOBILE_URL + "/Login") + }) + + it("should sign up and be logged in", () => { + cy.get('input[data-testid="identifier"]').type(email) + cy.get('input[data-testid="password"]').type(password) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/mfa/backup.spec.ts b/test/e2e/cypress/integration/profiles/mobile/mfa/backup.spec.ts new file mode 100644 index 0000000..2bbcd38 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/mfa/backup.spec.ts @@ -0,0 +1,123 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("TOTP 2FA Flow", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + describe("password", () => { + let email = gen.email() + let password = gen.password() + + before(() => { + cy.clearAllCookies() + }) + + beforeEach(() => { + email = gen.email() + password = gen.password() + + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("should be able to lifecycle through lookup_secret flows", () => { + cy.get('[data-testid="field/lookup_secret_codes"]').should("not.exist") + cy.get('[data-testid="field/lookup_secret_confirm/true"]').should( + "not.exist", + ) + cy.get('[data-testid="field/lookup_secret_reveal/true"]').should( + "not.exist", + ) + cy.get('[data-testid="field/lookup_secret_regenerate/true"]').click() + + cy.get('[data-testid="field/lookup_secret_reveal/true"]').should( + "not.exist", + ) + cy.get('[data-testid="field/lookup_secret_codes"]').should("exist") + let codes + cy.get('[data-testid="field/lookup_secret_codes/text"]').then(($e) => { + codes = $e.text().trim().split(", ") + }) + cy.get('[data-testid="field/lookup_secret_confirm/true"]').click() + cy.expectSettingsSaved() + + cy.get('[data-testid="field/lookup_secret_confirm/true"]').should( + "not.exist", + ) + cy.get('[data-testid="field/lookup_secret_regenerate/true"]').should( + "not.exist", + ) + cy.get('[data-testid="field/lookup_secret_codes/true"]').should( + "not.exist", + ) + + cy.get('[data-testid="field/lookup_secret_reveal/true"]').click() + cy.get('[data-testid="field/lookup_secret_regenerate/true"]').should( + "exist", + ) + cy.get('[data-testid="field/lookup_secret_codes/text"]').then(($e) => { + const actualCodes = $e.text().trim().split(", ") + expect(actualCodes.join(", ")).to.eq(codes.join(", ")) + }) + + let newCodes + cy.get('[data-testid="field/lookup_secret_regenerate/true"]').click() + cy.get( + '[data-testid="field/lookup_secret_regenerate/true"]:disabled', + ).should("not.exist") + + cy.get('[data-testid="field/lookup_secret_codes/text"]') + .and(($e) => { + expect($e.text().trim().split(", ").join(", ")).to.not.eq( + codes.join(", "), + ) + }) + .then(($e) => { + newCodes = $e.text().trim().split(", ") + }) + + cy.get('[data-testid="field/lookup_secret_confirm/true"]').click() + cy.expectSettingsSaved() + + cy.get('[data-testid="field/lookup_secret_reveal/true"]').click() + cy.get('[data-testid="field/lookup_secret_codes/text"]').and(($e) => { + const actualCodes = $e.text().trim().split(", ") + expect(actualCodes.join(", ")).to.eq(newCodes.join(", ")) + }) + + cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true") + + // First use a wrong code + cy.get("[data-testid=lookup_secret]").then(($e) => { + console.log(codes) + cy.wrap($e).type("1234") + }) + cy.get('[data-testid="field/method/lookup_secret"]').click() + cy.get('[data-testid="form-messages"]').should( + "contain.text", + "The backup recovery code is not valid.", + ) + cy.get("[data-testid=lookup_secret]").then(($e) => { + cy.wrap($e).type(newCodes[0]) + }) + cy.get('[data-testid="field/method/lookup_secret"]').click() + cy.get('[data-testid="session-content"]').should("contain", "aal2") + cy.get('[data-testid="session-content"]').should( + "contain", + "lookup_secret", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/mfa/mix.spec.ts b/test/e2e/cypress/integration/profiles/mobile/mfa/mix.spec.ts new file mode 100644 index 0000000..3bdceb0 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/mfa/mix.spec.ts @@ -0,0 +1,91 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" +import { TOTP } from "otpauth" + +context("Mobile Profile", () => { + describe("TOTP 2FA Flow", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + describe("password", () => { + let email = gen.email() + let password = gen.password() + + before(() => { + cy.clearAllCookies() + }) + + beforeEach(() => { + email = gen.email() + password = gen.password() + + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("should be able to use both TOTP and lookup", () => { + // set up totp + let totpSecret + cy.get('*[data-testid="field/totp_secret_key/text"]').then(($e) => { + totpSecret = $e.text().trim() + }) + cy.get('*[data-testid="field/totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret: totpSecret, + }).generate(), + ) + }) + cy.get('*[data-testid="field/method/totp"]').click() + cy.expectSettingsSaved() + + // Set up backup code + cy.get('*[data-testid="field/lookup_secret_regenerate/true"]').click() + let recoveryCodes + cy.get('*[data-testid="field/lookup_secret_codes/text"]').then(($e) => { + recoveryCodes = $e.text().trim().split(", ") + }) + cy.get('*[data-testid="field/lookup_secret_confirm/true"]').click() + cy.expectSettingsSaved() + + // Lets sign in with TOTP + cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true") + cy.get('*[data-testid="field/totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret: totpSecret, + }).generate(), + ) + }) + cy.get('*[data-testid="field/method/totp"]').click() + + // We have AAL now + cy.get('[data-testid="session-content"]').should("contain", "aal2") + cy.get('[data-testid="session-content"]').should("contain", "totp") + + // Lets sign in with lookup secret + cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true") + cy.get('*[data-testid="field/lookup_secret"]').then(($e) => { + cy.wrap($e).type(recoveryCodes[0]) + }) + cy.get('*[data-testid="field/method/lookup_secret"]').click() + + // We have AAL now + cy.get('[data-testid="session-content"]').should("contain", "aal2") + cy.get('[data-testid="session-content"]').should("contain", "totp") + cy.get('[data-testid="session-content"]').should( + "contain", + "lookup_secret", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/mfa/totp.spec.ts b/test/e2e/cypress/integration/profiles/mobile/mfa/totp.spec.ts new file mode 100644 index 0000000..4505528 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/mfa/totp.spec.ts @@ -0,0 +1,103 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" +import { TOTP } from "otpauth" + +context("Mobile Profile", () => { + describe("TOTP 2FA Flow", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + describe("password", () => { + let email = gen.email() + let password = gen.password() + + before(() => { + cy.clearAllCookies() + }) + + beforeEach(() => { + email = gen.email() + password = gen.password() + + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("should be able to lifecycle through TOTP flows", () => { + cy.get('*[data-testid="field/totp_qr"]').should("exist") + cy.get('*[data-testid="field/totp_code"]').should("exist") + + // Set up TOTP with invalid key + cy.get('*[data-testid="field/totp_code"]').type("111111") + cy.get('*[data-testid="field/method/totp"]').click() + cy.get('*[data-testid="field/totp_code"]').should( + "contain.text", + "The provided authentication code is invalid, please try again.", + ) + + // Set up TOTP with valid key + let secret + cy.get('*[data-testid="field/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('*[data-testid="field/totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[data-testid="field/method/totp"]').click() + cy.expectSettingsSaved() + + // Form should look different now + cy.get('*[data-testid="field/totp_secret_key/text"]').should( + "not.exist", + ) + cy.get('*[data-testid="field/totp_code"]').should("not.exist") + cy.get('*[data-testid="field/totp_qr"]').should("not.exist") + cy.get('*[data-testid="field/totp_unlink/true"]').should("exist") + + // Lets sign in + cy.visit(MOBILE_URL + "/Login?aal=aal2&refresh=true") + + // First use a wrong code + cy.get('*[data-testid="field/totp_code"]').type("111111") + cy.get('*[data-testid="field/method/totp"]').click() + cy.get('*[data-testid="form-messages"]').should( + "contain.text", + "The provided authentication code is invalid, please try again.", + ) + + // Use the correct code + cy.get('*[data-testid="field/totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[data-testid="field/method/totp"]').click() + + // We have AAL now + cy.get('[data-testid="session-content"]').should("contain", "aal2") + cy.get('[data-testid="session-content"]').should("contain", "totp") + + // Go back to settings and unlink + cy.visit(MOBILE_URL + "/Settings") + cy.get('*[data-testid="field/totp_unlink/true"]').click() + cy.get('*[data-testid="field/totp_unlink/true"]').should("not.exist") + cy.get('*[data-testid="field/totp_qr"]').should("exist") + cy.get('*[data-testid="field/totp_code"]').should("exist") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/passkey/flows.spec.ts b/test/e2e/cypress/integration/profiles/mobile/passkey/flows.spec.ts new file mode 100644 index 0000000..0e12e57 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/passkey/flows.spec.ts @@ -0,0 +1,172 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +const signupMobilePasskey = (email = gen.email()) => { + cy.visit(MOBILE_URL + "/Registration") + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="traits.website"]').type(website) + cy.get('[data-testid="passkey-button"]').click() + + cy.wait(1000) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + return cy.wrap(email) +} + +// The RN app stores the session token in localStorage (AsyncStorage on web). +// cy.clearAllCookies() alone does not clear it, so login flows fail with a +// stale token. This helper clears both cookies and localStorage. +const clearMobileSession = () => { + cy.clearAllCookies() + cy.window().then((win) => win.localStorage.clear()) +} + +// Logout via the RN app's logout button which clears the stored session. +const logoutMobile = () => { + cy.get('*[data-testid="logout"]').click() + cy.get('input[data-testid="identifier"]').should("exist") +} + +context("Mobile Profile", () => { + describe("Passkey Flows", () => { + let authenticator: any + + before(() => { + cy.task("resetCRI", {}) + cy.useConfigProfile("passkey") + + 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(() => { + clearMobileSession() + cy.task("sendCRI", { + query: "WebAuthn.clearCredentials", + opts: authenticator, + }) + }) + + after(() => { + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: authenticator, + }).then(() => { + cy.task("resetCRI", {}) + }) + }) + + it("should register with passkey", () => { + const email = gen.email() + signupMobilePasskey(email) + }) + + it("should login with passkey after registration", () => { + const email = gen.email() + signupMobilePasskey(email).then((registeredEmail) => { + logoutMobile() + cy.visit(MOBILE_URL + "/Login") + + cy.get('[data-testid="passkey-button"]').click() + cy.wait(1000) + + cy.get('[data-testid="session-content"]').should( + "contain", + registeredEmail, + ) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + }) + + it("should add passkey in settings after password registration", () => { + const email = gen.email() + const password = gen.password() + + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + + cy.get( + '*[data-testid="settings-passkey"] [data-testid="passkey-button"]', + ).click() + cy.wait(1000) + cy.expectSettingsSaved() + }) + + it("should not be able to unlink last passkey", () => { + signupMobilePasskey().then(() => { + cy.visit(MOBILE_URL + "/Settings") + // React Native Web renders disabled as aria-disabled on TouchableOpacity + cy.get('[data-testid^="passkey-remove-"]').should( + "have.attr", + "aria-disabled", + "true", + ) + }) + }) + + it("should be able to link password then remove passkey", () => { + const email = gen.email() + const password = gen.password() + + signupMobilePasskey(email).then(() => { + cy.visit(MOBILE_URL + "/Settings") + + // Add a password + cy.get( + '*[data-testid="settings-password"] input[data-testid="password"]', + ) + .clear() + .type(password) + cy.get( + '*[data-testid="settings-password"] div[data-testid="submit-form"]', + ).click() + cy.expectSettingsSaved() + + // Now passkey remove should be enabled + cy.get('[data-testid^="passkey-remove-"]').should( + "not.have.attr", + "aria-disabled", + "true", + ) + cy.get('[data-testid^="passkey-remove-"]').click() + cy.expectSettingsSaved() + + // Logout and login with password + logoutMobile() + cy.loginMobile({ email, password }) + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/registration/errors.spec.ts b/test/e2e/cypress/integration/profiles/mobile/registration/errors.spec.ts new file mode 100644 index 0000000..1b0fc80 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/registration/errors.spec.ts @@ -0,0 +1,112 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("Registration Flow Errors", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + beforeEach(() => { + cy.visit(MOBILE_URL + "/Registration") + }) + + const email = gen.email() + const password = gen.password() + + describe("show errors when invalid signup data is used", () => { + it("should show an error when the password has leaked before", () => { + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="password"]').type("12345678") + cy.get('input[data-testid="traits.website"]').type(website) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('*[data-testid="field/password"]').should( + "contain.text", + "data breaches", + ) + }) + + it("should show an error when the password is too similar", () => { + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="password"]').type(email) + cy.get('input[data-testid="traits.website"]').type(website) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('*[data-testid="field/password"]').should( + "contain.text", + "too similar", + ) + }) + + it("should show an error when the password is empty", () => { + cy.get('input[data-testid="traits.website"]').type(website) + cy.get('input[data-testid="traits.email"]').type(email) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="field/password"]').should( + "contain.text", + "Property password is missing", + ) + }) + + it("should show an error when the email is empty", () => { + cy.get('input[data-testid="traits.website"]').type("https://www.ory.sh") + cy.get('input[data-testid="password"]').type(password) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="field/traits.email"]').should( + "contain.text", + "Property email is missing", + ) + }) + + it("should show an error when the email is not an email", () => { + cy.get('input[data-testid="traits.website"]').type("https://www.ory.sh") + cy.get('input[data-testid="traits.email"]').type("not-an-email") + cy.get('input[data-testid="password"]').type(password) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="field/traits.email"]').should( + "contain.text", + "Enter a valid email address", + ) + }) + + it("should show a missing indicator if no fields are set", () => { + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="field/password"]').should( + "contain.text", + "Property password is missing", + ) + }) + + it("should show an error when the website is too short", () => { + cy.get('input[data-testid="traits.website"]').type("http://s") + cy.get('input[data-testid="traits.email"]').type(email) + + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + cy.get('*[data-testid="field/traits.website"]').should( + "contain.text", + "length must be >= 10", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts new file mode 100644 index 0000000..8d1f726 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" +import { testFlowWebhook } from "../../../../helpers/webhook" + +context("Mobile Profile", () => { + describe("Login Flow Success", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + beforeEach(() => { + cy.deleteMail() + cy.visit(MOBILE_URL + "/Registration") + }) + + it("should sign up and be logged in", () => { + const email = gen.email() + const password = gen.password() + + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="password"]').type(password) + cy.get('input[data-testid="traits.website"]').type(website) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + + it("should pass transient_payload to webhook", () => { + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "password", [ + ...hooks, + { hook: "session" }, + ]), + () => { + const email = gen.email() + const password = gen.password() + + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="password"]').type(password) + cy.get('input[data-testid="traits.website"]').type(website) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }, + ) + }) + + it("should sign up and show verification form", () => { + cy.enableVerification() + const email = gen.email() + const password = gen.password() + + cy.get('input[data-testid="traits.email"]').type(email) + cy.get('input[data-testid="password"]').type(password) + cy.get('input[data-testid="traits.website"]').type(website) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() + + cy.get('div[data-testid="field/code"] input').should("be.visible") + + cy.getVerificationCodeFromEmail(email).then((code) => { + cy.get('div[data-testid="field/code"] input').type(code) + cy.get( + 'div[data-testid="field/method/code"] div[data-testid=submit-form]', + ).click() + }) + + cy.get('[data-testid="ui/message/1080002"]').should( + "have.text", + "You successfully verified your email address.", + ) + + cy.get("div[data-testid=continue-button]").click() + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/settings/errors.spec.ts b/test/e2e/cypress/integration/profiles/mobile/settings/errors.spec.ts new file mode 100644 index 0000000..dd8a228 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/settings/errors.spec.ts @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("Settings Flow Errors", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + let email, password + + before(() => { + email = gen.email() + password = gen.password() + cy.registerApi({ email, password, fields: { "traits.website": website } }) + }) + + beforeEach(() => { + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + describe("profile", () => { + it("fails with validation errors", () => { + cy.get( + '*[data-testid="settings-profile"] input[data-testid="traits.website"]', + ) + .clear() + .type("http://s") + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).click() + + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).should("not.have.attr", "data-focusable", "false") + + cy.get('*[data-testid="field/traits.website"]').should( + "contain.text", + "length must be >= 10", + ) + + cy.get('*[data-testid="settings-password"]').should("exist") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/settings/oidc.spec.ts b/test/e2e/cypress/integration/profiles/mobile/settings/oidc.spec.ts new file mode 100644 index 0000000..0b869bf --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/settings/oidc.spec.ts @@ -0,0 +1,99 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, KRATOS_ADMIN, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile OIDC Settings", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + describe("link buttons", () => { + const email = gen.email() + const password = gen.password() + + before(() => { + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("should show link buttons for unlinked providers", () => { + cy.get('[data-testid="settings-oidc"]').should("exist") + cy.get('[data-testid="settings-oidc"]').within(() => { + cy.get('[data-testid="field/link/hydra"]').should("exist") + cy.get('[data-testid="field/link/google"]').should("exist") + cy.get('[data-testid="field/link/github"]').should("exist") + }) + }) + }) + + describe("unlink", () => { + let email: string + let password: string + + beforeEach(() => { + email = gen.email() + password = gen.password() + + // Create an identity with both password and OIDC credentials via Admin API + cy.request({ + method: "POST", + url: KRATOS_ADMIN + "/admin/identities", + body: { + schema_id: "default", + traits: { email, website }, + credentials: { + password: { config: { password } }, + oidc: { + config: { + providers: [ + { + provider: "hydra", + subject: email, + }, + ], + }, + }, + }, + }, + }) + + cy.clearAllCookies() + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("should show unlink button for linked provider", () => { + cy.get('[data-testid="settings-oidc"]').should("exist") + cy.get('[data-testid="settings-oidc"]').within(() => { + cy.get('[data-testid="field/unlink/hydra"]').should("exist") + cy.get('[data-testid="field/link/google"]').should("exist") + cy.get('[data-testid="field/link/github"]').should("exist") + }) + }) + + it("should unlink a provider", () => { + cy.get('[data-testid="settings-oidc"]').within(() => { + cy.get('[data-testid="field/unlink/hydra"]').should("exist") + cy.get('[data-testid="field/unlink/hydra"]').click() + }) + + cy.expectSettingsSaved() + + // After unlinking, the button should change to link + cy.get('[data-testid="settings-oidc"]').within(() => { + cy.get('[data-testid="field/link/hydra"]').should("exist") + cy.get('[data-testid="field/unlink/hydra"]').should("not.exist") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts new file mode 100644 index 0000000..fbda236 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen, MOBILE_URL, website } from "../../../../helpers" + +context("Mobile Profile", () => { + describe("Login Flow Success", () => { + before(() => { + cy.useConfigProfile("mobile") + }) + + const up = (value) => `not-${value}` + + describe("password", () => { + const email = gen.email() + const password = gen.password() + + before(() => { + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.loginMobile({ email, password }) + cy.visit(MOBILE_URL + "/Settings") + }) + + it("modifies the password", () => { + const newPassword = up(password) + cy.get( + '*[data-testid="settings-password"] input[data-testid="password"]', + ) + .clear() + .type(newPassword) + cy.get( + '*[data-testid="settings-password"] div[data-testid="submit-form"]', + ).click() + cy.expectSettingsSaved() + + cy.get( + '*[data-testid="settings-password"] div[data-testid="submit-form"]', + ).should("not.have.attr", "data-focusable", "false") + cy.get('*[data-testid="logout"]').click() + cy.get('input[data-testid="identifier"]').should("exist") + + cy.loginMobile({ email, password }) + cy.get('[data-testid="session-token"]').should("not.exist") + cy.loginMobile({ email, password: newPassword }) + cy.get('[data-testid="session-token"]').should("not.be.empty") + }) + }) + + describe("profile", () => { + let email: string + let password: string + + beforeEach(() => { + cy.deleteMail() + email = gen.email() + password = gen.password() + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + cy.loginMobile({ email, password }) + cy.location("pathname").should("not.contain", "/Login") + cy.visit(MOBILE_URL + "/Settings") + }) + + it("modifies an unprotected trait", () => { + cy.get( + '*[data-testid="settings-profile"] input[data-testid="traits.website"]', + ) + .clear() + .type("https://github.com/ory") + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).click() + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).should("not.have.attr", "data-focusable", "false") + + cy.visit(MOBILE_URL + "/Home") + cy.get('[data-testid="session-content"]').should( + "contain", + "https://github.com/ory", + ) + }) + + it("modifies a protected trait", () => { + const newEmail = up(email) + cy.get( + '*[data-testid="settings-profile"] input[data-testid="traits.email"]', + ) + .clear() + .type(newEmail) + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).click() + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).should("not.have.attr", "data-focusable", "false") + + cy.visit(MOBILE_URL + "/Home") + cy.get('[data-testid="session-content"]').should("contain", newEmail) + }) + + it("shows verification screen after email update", () => { + cy.enableVerification() + const newEmail = up(email) + cy.get( + '*[data-testid="settings-profile"] input[data-testid="traits.email"]', + ) + .clear() + .type(newEmail) + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).click() + cy.get( + '*[data-testid="settings-profile"] div[data-testid="submit-form"]', + ).should("not.have.attr", "data-focusable", "false") + + cy.get('div[data-testid="field/code"] input').should("be.visible") + + cy.getVerificationCodeFromEmail(newEmail).then((code) => { + cy.get('div[data-testid="field/code"] input').type(code) + cy.get( + 'div[data-testid="field/method/code"] div[data-testid=submit-form]', + ).click() + }) + + cy.get('[data-testid="ui/message/1080002"]').should( + "have.text", + "You successfully verified your email address.", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/network/errors.spec.ts b/test/e2e/cypress/integration/profiles/network/errors.spec.ts new file mode 100644 index 0000000..0bf49ba --- /dev/null +++ b/test/e2e/cypress/integration/profiles/network/errors.spec.ts @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { routes as express } from "../../../helpers/express" +import { gen } from "../../../helpers" + +describe("Registration failures with email profile", () => { + before(() => { + cy.useConfigProfile("network") + cy.proxy("express") + }) + + it("should not be able to register if we need a localhost schema", () => { + cy.setDefaultIdentitySchema("localhost") + cy.visit(express.registration, { failOnStatusCode: false }) + cy.get('[data-testid="code-box"]').should( + "contain.text", + "no such host", // could be ::1 or 127.0.0.1 + ) + }) + + it("should not be able to register if we schema has a local ref", () => { + cy.setDefaultIdentitySchema("ref") + cy.visit(express.registration, { failOnStatusCode: false }) + cy.get('[data-testid="code-box"]').should( + "contain.text", + "no route to host", + ) + }) + + it("should not be able to login because pre webhook uses local url", () => { + cy.setDefaultIdentitySchema("working") + cy.visit(express.login, { failOnStatusCode: false }) + cy.get('[data-testid="code-box"]').should( + "contain.text", + "no route to host", + ) + }) + + it("should not be able to verify because post webhook uses local jsonnet", () => { + cy.setDefaultIdentitySchema("working") + cy.visit(express.registration, { failOnStatusCode: false }) + cy.get('input[name="traits.email"]').type(gen.email()) + cy.get('input[name="traits.website"]').type("https://google.com/") + cy.get('input[name="password"]').type(gen.password()) + cy.get('[type="submit"]').click() + cy.get('[data-testid="code-box"]').should( + "contain.text", + "no route to host", + ) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc-provider/login.spec.ts b/test/e2e/cypress/integration/profiles/oidc-provider/login.spec.ts new file mode 100644 index 0000000..695bbef --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc-provider/login.spec.ts @@ -0,0 +1,349 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../helpers" +import * as httpbin from "../../../helpers/httpbin" +import * as oauth2 from "../../../helpers/oauth2" + +context("OpenID Provider", () => { + const client = { + auth_endpoint: "http://localhost:4744/oauth2/auth", + token_endpoint: "http://localhost:4744/oauth2/token", + id: Cypress.env("OIDC_DUMMY_CLIENT_ID"), + secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"), + token_endpoint_auth_method: "client_secret_basic", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + scopes: ["openid", "offline", "email", "website"], + callbacks: [ + "http://localhost:5555/callback", + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + ], + } + + before(() => { + cy.deleteMail() + cy.useConfigProfile("oidc-provider") + cy.proxy("express") + }) + + it("login", () => { + const email = gen.email() + const password = gen.password() + cy.registerApi({ + email: email, + password: password, + fields: { "traits.website": "http://t1.local" }, + }) + + const url = oauth2.getDefaultAuthorizeURL(client) + + cy.visit(url) + + // kratos login ui + cy.get("[name=identifier]").type(email) + cy.get("[name=password]").type(password) + cy.get("[type='submit'][value='password']").click() + + // consent ui + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password"]) + }) + }) + + it("login-without-scopes", () => { + const email = gen.email() + const password = gen.password() + cy.registerApi({ + email: email, + password: password, + fields: { "traits.website": "http://t1.local" }, + }) + + const url = oauth2.getDefaultAuthorizeURL(client) + cy.visit(url) + + // kratos login ui + cy.get("[name=identifier]").type(email) + cy.get("[name=password]").type(password) + cy.get("[type='submit'][value='password']").click() + + // consent ui + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).not.to.have.property("id_token") + expect(token).not.to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("") + }) + }) + + it("respects-login-remember-config", () => { + let odicLogin = () => { + const email = gen.email() + const password = gen.password() + cy.registerApi({ + email: email, + password: password, + fields: { "traits.website": "http://t1.local" }, + }) + + let url = oauth2.getDefaultAuthorizeURL(client) + cy.visit(url) + + // kratos login ui + cy.get("[name=identifier]").type(email) + cy.get("[name=password]").type(password) + cy.get("[type='submit'][value='password']").click() + // consent ui + cy.get("#accept").click() + } + + cy.clearAllCookies() + cy.updateConfigFile((config) => { + config.session.cookie = config.session.cookie || {} + config.session.cookie.persistent = true + config.session.lifespan = "1234s" + return config + }) + + odicLogin() + cy.getCookies({ domain: "localhost" }) + cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).should( + "not.be.null", + ) + cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).then( + (cookie: Cypress.Cookie) => { + let expected = Date.now() / 1000 + 1234 + let precision = 10 + expect(cookie.expiry).to.be.lessThan(expected + precision) + expect(cookie.expiry).to.be.greaterThan(expected - precision) + }, + ) + + cy.clearAllCookies() + cy.updateConfigFile((config) => { + config.session.cookie = config.session.cookie || {} + config.session.cookie.persistent = false + return config + }) + + odicLogin() + cy.getCookie("ory_hydra_session_dev", { domain: "localhost" }).should( + "be.null", + ) + }) +}) + +context("OpenID Provider - change between flows", () => { + const client = { + auth_endpoint: "http://localhost:4744/oauth2/auth", + token_endpoint: "http://localhost:4744/oauth2/token", + id: Cypress.env("OIDC_DUMMY_CLIENT_ID"), + secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"), + token_endpoint_auth_method: "client_secret_basic", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + scopes: ["openid", "offline", "email", "website"], + callbacks: [ + "http://localhost:5555/callback", + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + ], + } + + function doConsent(amrs?: string[]) { + cy.url().should("contain", "/consent") + + // consent ui + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(amrs || ["password"]) + }) + } + + before(() => { + cy.useConfigProfile("oidc-provider") + cy.updateConfigFile((config) => { + config.selfservice.allowed_return_urls = [ + oauth2.getDefaultAuthorizeURL(client), + ] + config.oauth2_provider.override_return_to = true + return config + }) + cy.proxy("express") + }) + + it("switch to registration flow", () => { + const identity = gen.identityWithWebsite() + + const url = oauth2.getDefaultAuthorizeURL(client) + + cy.visit(url) + cy.get("[href*='/registration']").click() + cy.url().should("contain", "/registration") + + cy.get("[name='traits.email']").type(identity.email) + cy.get("[name='password']").type(identity.password) + cy.get("[name='traits.website']").type(identity.fields["traits.website"]) + cy.get("[type='submit'][value='password']").click() + + doConsent() + }) + + it("switch to recovery flow with password reset", () => { + cy.updateConfigFile((config) => { + config.selfservice.flows.recovery.lifespan = "1m" + config.selfservice.methods.link.config.lifespan = "1m" + config.selfservice.flows.verification.enabled = false + if (!config.selfservice.flows.recovery) { + config.selfservice.flows.recovery = {} + } + config.selfservice.flows.recovery.enabled = true + config.selfservice.flows.settings.privileged_session_max_age = "5m" + return config + }) + cy.deleteMail() + cy.useRecoveryStrategy("code") + cy.notifyUnknownRecipients("recovery", false) + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + + const url = oauth2.getDefaultAuthorizeURL(client) + cy.visit(url) + cy.get("[href*='/recovery']").click() + + cy.get("input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + // do a password change + const newPassword = gen.password() + cy.get('input[name="password"]').clear().type(newPassword) + cy.get('button[value="password"]').click() + + // since we aren't skipping the consent, Kratos will ask us to login again + cy.get('input[name="password"]').clear().type(newPassword) + cy.get('button[value="password"]').click() + + // we should now end up on the consent screen + doConsent(["code_recovery", "password"]) + }) + + it("switch to recovery flow with oidc link", () => { + cy.deleteMail() + cy.longRecoveryLifespan() + cy.longLinkLifespan() + cy.enableRecovery() + cy.useRecoveryStrategy("code") + cy.notifyUnknownRecipients("recovery", false) + cy.longPrivilegedSessionTime() + + const fakeOidcFlow = ( + identity: ReturnType, + ) => { + cy.get("input[name='username']").type(identity.email) + cy.get("button[name='action'][value='accept']").click() + // consent screen for the 'fake' oidc provider + cy.url().should("contain", "/consent") + // consent ui for 3rd party integration to Kratos (sign in with Google) + // it will look the same as Hydra consent ui since we are reusing it as + // a 'fake' oidc provider + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + } + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + + const url = oauth2.getDefaultAuthorizeURL(client) + cy.visit(url) + + cy.get("[href*='/recovery']").click() + cy.get("input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + // (OAauth2+Kratos provider '/settings' -> Fake OAuth2 '/login') + + // we are reusing Hydra as an OIDC provider for Kratos and doing an OIDC account + // linking process here + // this is to check if the user can recover their account through OIDC linking + // and still be redirected back to the original request + cy.get("button[value='google']").click() + cy.location("pathname").should("eq", "/login") + fakeOidcFlow(identity) + + // it should return us back to the initial OAauth2 login flow + // sign in through the 'fake' oidc provider + cy.get("button[type='submit'][value='google']").click() + + // login again at the 'fake' oidc provider + fakeOidcFlow(identity) + + // complete the consent screen + doConsent(["oidc"]) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc-provider/mfa.spec.ts b/test/e2e/cypress/integration/profiles/oidc-provider/mfa.spec.ts new file mode 100644 index 0000000..1916f14 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc-provider/mfa.spec.ts @@ -0,0 +1,166 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../helpers" +import { routes as express } from "../../../helpers/express" +import * as oauth2 from "../../../helpers/oauth2" +import * as httpbin from "../../../helpers/httpbin" +import { TOTP } from "otpauth" + +context("OIDC Provider 2FA", () => { + const client = { + auth_endpoint: "http://localhost:4744/oauth2/auth", + token_endpoint: "http://localhost:4744/oauth2/token", + id: Cypress.env("OIDC_DUMMY_CLIENT_ID"), + secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"), + token_endpoint_auth_method: "client_secret_basic", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + scopes: ["openid", "offline", "email", "website"], + callbacks: [ + "http://localhost:5555/callback", + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + ], + } + + ;[ + { + login: express.login, + settings: express.settings, + base: express.base, + profile: "oidc-provider-mfa", + app: "express" as "express", + }, + ].forEach(({ settings, login, profile, app, base }) => { + describe(`for app ${app}`, () => { + let email = gen.email() + let password = gen.password() + let secret + + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + + email = gen.email() + password = gen.password() + + cy.register({ + email, + password, + fields: { "traits.website": "http://t1.local" }, + }) + cy.visit(settings) + + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + + cy.clearAllCookies() + }) + + it("should be be asked to sign in with 2fa if set up", () => { + let url = oauth2.getDefaultAuthorizeURL(client) + + cy.get("body") + .then((body$) => { + // Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914 + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = url + }) + }) + .then(() => { + // kratos login ui + cy.get("[name=identifier]").type(email) + cy.get("[name=password]").type(password) + cy.get("[type=submit]").click() + + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + + // consent ui + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + let scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent( + escape(window.atob(token.id_token.split(".")[1])), + ), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "totp"]) + }) + + // We shouldn't need to authenticate again + url = oauth2.getDefaultAuthorizeURL(client) + + cy.get("body") + .then((body$) => { + // Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914 + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = + appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = url + }) + }) + .then(() => { + // We get the consent screen instead of login + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent( + escape(window.atob(token.id_token.split(".")[1])), + ), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "totp"]) + }) + }) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts b/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts new file mode 100644 index 0000000..f29e366 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts @@ -0,0 +1,183 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APP_URL, gen } from "../../../helpers" +import * as oauth2 from "../../../helpers/oauth2" +import * as httpbin from "../../../helpers/httpbin" + +context("OpenID Provider", () => { + before(() => { + cy.useConfigProfile("oidc-provider") + cy.proxy("express") + }) + const client = { + auth_endpoint: "http://localhost:4744/oauth2/auth", + token_endpoint: "http://localhost:4744/oauth2/token", + id: Cypress.env("OIDC_DUMMY_CLIENT_ID"), + secret: Cypress.env("OIDC_DUMMY_CLIENT_SECRET"), + token_endpoint_auth_method: "client_secret_basic", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + scopes: ["openid", "offline", "email", "website"], + callbacks: [ + "http://localhost:5555/callback", + "https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything", + ], + } + + it("registration", () => { + const url = oauth2.getDefaultAuthorizeURL(client) + + cy.visit(url) + cy.get("[data-testid=signup-link]").click() + + const email = gen.email() + const password = gen.password() + + cy.get('[name="traits.email"]').type(email) + cy.get("[name=password]").type(password) + cy.get('[name="traits.website"]').type("http://example.com") + cy.get('input[type=checkbox][name="traits.tos"]').click({ force: true }) + cy.get('[name="traits.age"]').type("199") + cy.get('input[type=checkbox][name="traits.consent"]').click({ force: true }) + cy.get('input[type=checkbox][name="traits.newsletter"]').click({ + force: true, + }) + + cy.get("[type='submit'][value='password']").click() + + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password"]) + }) + }) + + it("registration with session, skip=false and skip=true", () => { + const email = gen.email() + const password = gen.password() + + cy.register({ + email, + password, + fields: { + "traits.website": "https://www.ory.sh", + "traits.tos": "1", + "traits.age": 22, + }, + }) + + const url = oauth2.getDefaultAuthorizeURL(client) + + cy.request(url).then((res) => { + const lastResp = res.allRequestResponses[1]["Request URL"] + const login_challenge = new URL(lastResp).searchParams.get( + "login_challenge", + ) + expect(login_challenge).to.not.be.null + cy.visit( + APP_URL + + "/self-service/registration/browser?login_challenge=" + + login_challenge, + ) + }) + + cy.url().should("contain", "/login") + cy.get("[data-testid='login-flow']").should("exist") + cy.get("[data-testid='login-flow'] [name='password']").type(password) + cy.get( + "[data-testid='login-flow'] button[name='method'][value='password']", + ).click() + + // we want to skip the consent flow here + // so we ask to remember the user + cy.get("[name='remember']").click() + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "password"]) + }) + + // use the hydra origin to make a new OAuth request from it + cy.get("body") + .then((body$) => { + // Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914 + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = "http://localhost:4744/health/ready" + }) + }) + .then(() => { + // we don't want to redirect here since we only want the login challenge from hydra + // we reusing the challenge to navigate to the registration page + cy.request({ + url: oauth2.getDefaultAuthorizeURL(client), + followRedirect: false, + }) + .then((res) => { + expect(res.redirectedToUrl).to.include("login_challenge") + return new URL(res.redirectedToUrl).searchParams.get( + "login_challenge", + ) + }) + .then((login_challenge) => { + cy.get("body").then((body$) => { + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = + appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = + APP_URL + + "/self-service/registration/browser?login_challenge=" + + login_challenge + }) + }) + }) + }) + + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "password"]) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/login/error.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/error.spec.ts new file mode 100644 index 0000000..fd27b05 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/login/error.spec.ts @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Social Sign In Errors", () => { + ;[ + { + login: react.login, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ login, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(login) + }) + + // playwright:migrated + it("should fail when the login request is rejected", () => { + cy.triggerOidc(app) + cy.get("#reject").click() + cy.location("pathname").should("equal", "/login") + cy.get(appPrefix(app) + '[data-testid="ui/message/4000001"]').should( + "contain.text", + "login rejected request", + ) + cy.noSession() + }) + + // playwright:migrated + it("should fail when the consent request is rejected", () => { + const email = gen.email() + cy.triggerOidc(app) + cy.get("#username").type(email) + cy.get("#accept").click() + cy.get("#reject").click() + cy.location("pathname").should("equal", "/login") + cy.get('[data-testid="ui/message/4000001"]').should( + "contain.text", + "consent rejected request", + ) + cy.noSession() + }) + + it("should fail when the id_token is missing", () => { + const email = gen.email() + cy.triggerOidc(app) + cy.get("#username").type(email) + cy.get("#accept").click() + cy.get("#website").type(website) + cy.get("#accept").click() + cy.location("pathname").should("equal", "/login") + cy.get('[data-testid="ui/message/4000001"]').should( + "contain.text", + "no id_token", + ) + }) + + it("should fail to convert a sign in flow to a sign up flow when registration is disabled", () => { + cy.disableRegistration() + + 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/4000001"]').should( + "contain.text", + "Registration is not allowed because it was disabled", + ) + + cy.noSession() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts new file mode 100644 index 0000000..3a998de --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -0,0 +1,128 @@ +// 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" + +context("Social Sign In Successes", () => { + ;[ + { + login: react.login, + registration: react.registration, + settings: react.settings, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + registration: express.registration, + settings: express.settings, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ login, registration, profile, app, settings }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + }) + + // playwright:migrated + it("should be able to sign up, sign out, and then sign in", () => { + const email = gen.email() + cy.registerOidc({ app, email, website, route: registration }) + cy.logout() + cy.noSession() + cy.loginOidc({ app, url: login }) + }) + + it("should be able to sign up and link existing account", () => { + const email = gen.email() + const password = gen.password() + + // Create a new account + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + + // Try to log in with the same identifier through OIDC. This should fail and create a new login flow. + cy.registerOidc({ + app, + email, + website, + expectSession: false, + }) + cy.noSession() + + // Log in with the same identifier through the login flow. This should link the accounts. + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + cy.location("pathname").should("not.contain", "/login") + cy.getSession() + + // Hydra OIDC should now be linked + cy.visit(settings) + cy.get('[value="hydra"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink Ory") + }) + + 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() + cy.noSession() + cy.loginOidc({ + app, + url: login + "?return_to=https://www.example.org/", + }) + cy.location("href").should("eq", "https://www.example.org/") + }) + + it("should be able to log in with upstream parameters", () => { + 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() + cy.noSession() + cy.intercept("GET", "**/oauth2/auth*").as("getHydraLogin") + cy.loginOidc({ + app, + url: login + "?return_to=https://www.example.org/", + preTriggerHook: () => { + // Inject hd and login_hint into upstream_parameters. hd is + // forwarded; login_hint is never forwarded because a caller-supplied + // value cannot be trusted (HackerOne #3239672, ory-corp/cloud#8955). + cy.addInputElement("form", "upstream_parameters.hd", "ory.sh") + cy.addInputElement("form", "upstream_parameters.login_hint", email) + }, + }) + // once a request to getHydraLogin responds, 'cy.wait' will resolve + cy.wait("@getHydraLogin") + .its("request.url") + .should("include", "hd=ory.sh") + .and("not.include", "login_hint") + + cy.location("href").should("eq", "https://www.example.org/") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/logout/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/logout/success.spec.ts new file mode 100644 index 0000000..3a68f50 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/logout/success.spec.ts @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Social Sign Out Successes", () => { + ;[ + { + base: react.base, + registration: react.registration, + app: "react" as "react", + profile: "spa", + }, + { + base: express.base, + registration: express.registration, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ base, registration, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + before(() => { + cy.clearAllCookies() + }) + + beforeEach(() => { + cy.visit(base) + const email = gen.email() + cy.registerOidc({ app, email, website, route: registration }) + }) + + // playwright:migrated + it("should sign out and be able to sign in again", () => { + if (app === "express") { + cy.get( + `${appPrefix(app)} [data-testid="logout"] a:not(disabled)`, + ).click() + } else { + cy.get( + `${appPrefix(app)} [data-testid="logout"]:not(disabled)`, + ).click() + } + cy.noSession() + cy.url().should("include", "/login") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/oidc/registration/error.spec.ts new file mode 100644 index 0000000..074ad49 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/registration/error.spec.ts @@ -0,0 +1,74 @@ +// 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" + +context("Social Sign Up Errors", () => { + ;[ + { + registration: react.registration, + app: "react" as "react", + profile: "spa", + }, + { + registration: express.registration, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ registration, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(registration) + }) + + // playwright:migrated + it("should fail when the login request is rejected", () => { + cy.triggerOidc(app) + cy.get("#reject").click() + cy.location("pathname").should("equal", "/registration") + cy.get(appPrefix(app) + '[data-testid="ui/message/4000001"]').should( + "contain.text", + "login rejected request", + ) + cy.noSession() + }) + + // playwright:migrated + it("should fail when the consent request is rejected", () => { + const email = gen.email() + cy.triggerOidc(app) + cy.get("#username").type(email) + cy.get("#accept").click() + cy.get("#reject").click() + cy.location("pathname").should("equal", "/registration") + cy.get('[data-testid="ui/message/4000001"]').should( + "contain.text", + "consent rejected request", + ) + cy.noSession() + }) + + it("should fail when the id_token is missing", () => { + const email = gen.email() + cy.triggerOidc(app) + cy.get("#username").type(email) + cy.get("#accept").click() + cy.get("#website").type(website) + cy.get("#accept").click() + cy.location("pathname").should("equal", "/registration") + cy.get('[data-testid="ui/message/4000001"]').should( + "contain.text", + "no id_token", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts new file mode 100644 index 0000000..9339b9d --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts @@ -0,0 +1,274 @@ +// 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: "spa", + }, + { + login: express.login, + registration: express.registration, + app: "express" as "express", + profile: "oidc", + }, + ].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)) + }, + ) + }) + + // playwright:migrated + 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/") + + // hd is forwarded; login_hint is never forwarded because a + // caller-supplied value cannot be trusted (HackerOne #3239672, + // ory-corp/cloud#8955). + cy.addInputElement("form", "upstream_parameters.hd", "ory.sh") + 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", "hd=ory.sh") + .and("not.include", "login_hint") + }) + + it("oidc registration with duplicate identifier should return new login flow with duplicate error", () => { + cy.visit(registration) + + 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.website"]').type(website) + cy.get('[name="traits.consent"]').siblings("label").click() + cy.get('[name="traits.newsletter"]').siblings("label").click() + + cy.submitPasswordForm() + + cy.location("pathname").should("not.contain", "/registration") + + cy.getSession().should(shouldSession(email)) + + cy.logout() + cy.noSession() + + // register the account through the OIDC provider + cy.registerOidc({ + app, + acceptConsent: true, + acceptLogin: true, + expectSession: false, + email, + website, + route: registration, + }) + + cy.get('[data-testid="ui/message/1010016"]').should("be.visible") + + cy.location("href").should("contain", "/login") + + cy.get("[name='provider'][value='google']").should("be.visible") + cy.get("[name='provider'][value='github']").should("be.visible") + + if (app === "express") { + cy.get("[data-testid='forgot-password-link']").should("be.visible") + } + + cy.get("input[name='password']").type(password) + cy.submitPasswordForm() + cy.getSession() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/error.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/error.spec.ts new file mode 100644 index 0000000..da7cf52 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/settings/error.spec.ts @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APP_URL, appPrefix, gen, website } from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Social Sign In Settings Errors", () => { + ;[ + { + registration: react.registration, + settings: react.settings, + app: "react" as "react", + profile: "spa", + }, + { + registration: express.registration, + settings: express.settings, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ registration, profile, app, settings }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + let email + + beforeEach(() => { + cy.clearAllCookies() + email = gen.email() + + cy.registerOidc({ + app, + email, + expectSession: true, + website, + route: registration, + }) + cy.visit(settings) + }) + + describe("oidc", () => { + it("should fail to link google because id token is missing", () => { + cy.get(appPrefix(app) + 'button[value="google"]').click() + cy.get("#remember").click() + cy.get("#accept").click() + + cy.get('[data-testid="ui/message/4000001"]').should( + "contain.text", + 'Authentication failed because no id_token was returned. Please accept the "openid" permission and try again.', + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts new file mode 100644 index 0000000..89a5e2d --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts @@ -0,0 +1,252 @@ +// 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 { util } from "prettier" +import skip = util.skip + +context("Social Sign In Settings Success", () => { + ;[ + { + registration: react.registration, + settings: react.settings, + login: react.login, + app: "react" as "react", + profile: "spa", + }, + { + registration: express.registration, + settings: express.settings, + login: express.login, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ registration, login, profile, app, settings }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let email + + const hydraReauthFails = () => { + cy.clearAllCookies() + cy.visit(login) + cy.get(appPrefix(app) + '[value="hydra"]').click() + + cy.get("#username").type(email) + cy.get("#remember").click() + cy.get("#accept").click() + + cy.get('input[name="traits.website"]').clear().type(website) + cy.triggerOidc(app, "hydra") + + cy.get('[data-testid="ui/message/1010016"]').should( + "contain.text", + "as another way to sign in.", + ) + + cy.noSession() + } + + beforeEach(() => { + cy.clearAllCookies() + email = gen.email() + + cy.registerOidc({ + app, + email, + expectSession: true, + website, + route: registration, + }) + cy.visit(settings) + }) + + describe("oidc", () => { + beforeEach(() => { + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longVerificationLifespan() + .longPrivilegedSessionTime(), + ) + }) + + it("should show the correct options", () => { + cy.get('[value="hydra"]').should("not.exist") + + cy.get('[value="google"]') + .should("have.attr", "name", "link") + .should("contain.text", "Link google") + + cy.get('[value="github"]') + .should("have.attr", "name", "link") + .should("contain.text", "Link github") + }) + + it("should show the unlink once password is set", () => { + cy.get('[value="hydra"]').should("not.exist") + + cy.get('input[name="password"]').type(gen.password()) + cy.get('button[value="password"]').click() + + cy.get('[value="hydra"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink Ory") + }) + + it("should link google", () => { + cy.get('[value="google"]').click() + + cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click()) + cy.get("#remember").click() + cy.get("#accept").click() + + cy.visit(settings) + + cy.get('[value="google"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink google") + + cy.logout() + + cy.visit(login) + + // we create an intercept for login so we make sure the login endpoint is called + cy.intercept("GET", "**/oauth2/auth*").as("login") + + cy.get('[value="google"]').click() + + // wait for the login endpoint to be called + cy.wait("@login") + + // check the session + cy.getSession() + }) + + it("should link google after re-auth", () => { + cy.shortPrivilegedSessionTime() + cy.get('[value="google"]').click() + cy.location("pathname").should("equal", "/login") + + cy.longPrivilegedSessionTime() + cy.get('[value="hydra"]').click() + + // prompt=login means that we need to re-auth! + cy.get("#username").type(email) + cy.get("#accept").click() + + // we re-authed, now we do the google oauth2 dance + cy.get("#username").type(gen.email()) + cy.get("#accept").click() + cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click()) + cy.get("#accept").click() + + cy.expectSettingsSaved() + + cy.get('[value="google"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink google") + + cy.visit(settings) + + cy.get('[value="google"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink google") + }) + + it("should unlink hydra and no longer be able to sign in", () => { + if (app === "react") { + // This test is flaky on React, so we skip it for now. + return + } + cy.get('[value="hydra"]').should("not.exist") + cy.get('input[name="password"]').type(gen.password()) + cy.get('[value="password"]').click() + cy.expectSettingsSaved() + cy.visit(settings) + + cy.get('[value="hydra"]').click() + + // It will no longer be possible to sign up with this provider because of a UNIQUE key violation + // because of the email verification table. + // + // Basically what needs to happen is for the user to use the LINK feature to link this account. + hydraReauthFails() + }) + + it("should unlink hydra after reauth", () => { + cy.get('[value="hydra"]').should("not.exist") + + cy.get('input[name="password"]').type(gen.password()) + cy.get('[value="password"]').click() + cy.expectSettingsSaved() + cy.visit(settings) + + cy.shortPrivilegedSessionTime() + cy.get('[value="hydra"]').click() + + cy.longPrivilegedSessionTime() + cy.location("pathname").should("equal", "/login") + cy.get('[value="hydra"]').click() + + // prompt=login means that we need to re-auth! + cy.get("#username").type(email) + cy.get("#accept").click() + cy.expectSettingsSaved() + + hydraReauthFails() + }) + + it("should show only linked providers during reauth", () => { + cy.shortPrivilegedSessionTime() + + cy.get('input[name="password"]').type(gen.password()) + cy.get('[value="password"]').click() + + cy.location("pathname").should("equal", "/login") + + cy.get('[value="hydra"]').should("exist") + cy.get('[value="google"]').should("not.exist") + cy.get('[value="github"]').should("not.exist") + }) + + it("settings screen stays intact when the original sign up method gets removed", () => { + const expectSettingsOk = () => { + cy.get('[value="google"]', { timeout: 1000 }) + .should("have.attr", "name", "link") + .should("contain.text", "Link google") + + cy.get('[value="github"]', { timeout: 1000 }) + .should("have.attr", "name", "link") + .should("contain.text", "Link github") + } + + cy.visit(settings) + expectSettingsOk() + + // set password + cy.get('input[name="password"]').type(gen.password()) + cy.get('button[value="password"]').click() + + // remove the provider used to log in + cy.updateConfigFile((config) => { + config.selfservice.methods.oidc.config.providers = + config.selfservice.methods.oidc.config.providers.filter( + ({ id }) => id !== "hydra", + ) + return config + }) + + // visit settings and everything should still be fine + cy.visit(settings) + expectSettingsOk() + }) + }) + }) + }) +}) 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 0000000..78426b7 --- /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 new file mode 100644 index 0000000..313ccf2 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts @@ -0,0 +1,359 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, 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 emailTrait = `${ + app === "express" ? '[data-testid="passwordless-flow"]' : "" + } [name="traits.email"]` + const websiteTrait = `${ + app === "express" ? '[data-testid="passwordless-flow"]' : "" + } [name="traits.website"]` + + cy.get('[name="webauthn_register_displayname"]').type("key1") + cy.get(emailTrait).type(email) + cy.get(websiteTrait).type("https://www.ory.sh") + cy.clickWebAuthButton("register") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["webauthn"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) +} + +context("Passwordless 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: "passwordless", + }, + { + login: express.login, + registration: express.registration, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "passwordless", + }, + ].forEach(({ registration, login, profile, app, base, settings }) => { + describe(`for app ${app}`, () => { + let authenticator + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + cy.addVirtualAuthenticator().then((result) => { + authenticator = result + }) + cy.longPrivilegedSessionTime() + }) + + beforeEach(() => { + cy.clearAllCookies() + }) + + 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" ? `[data-testid="passwordless-flow"]` : "" + } [name="traits.website"]` + + const emailTrait = `${ + app === "express" ? `[data-testid="passwordless-flow"]` : "" + } [name="traits.email"]` + + cy.get(appPrefix(app) + '[name="webauthn_register_displayname"]').type( + "key1", + ) + cy.get(websiteTrait).type("b") + cy.clickWebAuthButton("register") + + 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.clickWebAuthButton("register") + + 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.clickWebAuthButton("register") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["webauthn"], + }).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", "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() + + signup(registration, app, email) + cy.logout() + cy.visit(login) + + const identifierTrait = `${ + app === "express" ? `[data-testid="passwordless-flow"]` : "" + } [name="identifier"]` + + cy.get(identifierTrait).type(email) + cy.get('[value="webauthn"]').click() + cy.get('[data-testid="ui/message/1010012"]').should("to.exist") + cy.get('[name="password"]').should("to.not.exist") + cy.clickWebAuthButton("login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["webauthn"], + }).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 security key", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(settings) + cy.get('[name="webauthn_remove"]').should("be.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="webauthn_remove"]').should("be.disabled") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.expectSettingsSaved() + cy.get('[name="webauthn_remove"]').click() + cy.expectSettingsSaved() + cy.logout() + cy.visit(login) + + const identifierTrait = `${ + app === "express" ? `[data-testid="passwordless-flow"]` : "" + } [name="identifier"]` + + cy.get(identifierTrait).type(email) + cy.get('[value="webauthn"]').click() + cy.get('[data-testid="ui/message/4000015"]').should("to.exist") + cy.get(identifierTrait).should("exist") + cy.get('[name="password"]').should("exist") + cy.get('[value="password"]').should("exist") + }) + + 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.clickWebAuthButton("login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["webauthn", "webauthn"], + }).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="webauthn"]').should("not.exist") + cy.get('[name="webauthn_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="webauthn_register_displayname"]').type("key2") + cy.clickWebAuthButton("register") + cy.expectSettingsSaved() + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').should("exist") + cy.clickWebAuthButton("login") + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "webauthn", "webauthn"], + }) + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "webauthn", "webauthn", "password"], + }) + + cy.logout() + cy.visit(login) + + const identifierTrait = `${ + app === "express" ? `[data-testid="passwordless-flow"]` : "" + } [name="identifier"]` + + cy.get(identifierTrait).type(email) + cy.get('[value="webauthn"]').click() + cy.clickWebAuthButton("login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["webauthn"], + }) + }) + + it("should not be able to use for MFA even when passwordless is false", () => { + const email = gen.email() + // Ensure passwordless is enabled for the initial sign-up, even if a + // previous (retried) attempt left it disabled below. + cy.updateConfigFile((config) => { + config.selfservice.methods.webauthn.config.passwordless = true + return config + }) + signup(registration, app, email) + cy.updateConfigFile((config) => { + config.selfservice.methods.webauthn.config.passwordless = false + return config + }) + cy.visit(login + "?aal=aal2") + cy.get('[value="webauthn"]').should("not.exist") + cy.get('[name="webauthn_login_trigger"]').should("not.exist") + + cy.visit(settings) + cy.get('[name="webauthn_remove"]').should("be.disabled") + + // The settings flow excludes every already-registered credential + // (including the passwordless one), so the new MFA credential must be + // created on a different authenticator. Swap the passwordless + // authenticator out entirely so only the new one is attached: with both + // present, create() races the excluded authenticator's + // InvalidStateError against the new authenticator, and the second-factor + // login (whose allowCredentials omits the passwordless credential) can + // resolve against the wrong device. + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: authenticator, + }) + cy.addVirtualAuthenticator().then((secondAuthenticator) => { + // Keep the handle valid for the after() cleanup hook. + cy.then(() => { + authenticator = secondAuthenticator + }) + + cy.get('[name="webauthn_register_displayname"]').type("key2") + cy.clickWebAuthButton("register") + cy.expectSettingsSaved() + + cy.visit(login + "?aal=aal2&refresh=true") + cy.clickWebAuthButton("login") + cy.getSession({ + expectAal: "aal2", + expectMethods: ["webauthn", "webauthn", "webauthn"], + }) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/recovery/code/errors.spec.ts b/test/e2e/cypress/integration/profiles/recovery/code/errors.spec.ts new file mode 100644 index 0000000..8c1a55e --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/code/errors.spec.ts @@ -0,0 +1,217 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, email, extractOTPCode, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Recovery Errors", () => { + ;[ + { + recovery: react.recovery, + app: "react" as "react", + profile: "spa", + }, + { + recovery: express.recovery, + app: "express" as "express", + profile: "recovery", + }, + ].forEach(({ recovery, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longCodeLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("code") + .notifyUnknownRecipients("recovery", false), + ) + }) + + it("should invalidate flow if wrong code is submitted too often", () => { + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.recoveryEmailWithCode({ + expect: { email: identity.email, enterCode: false }, + }) + for (let i = 0; i < 5; i++) { + cy.get("input[name='code']").type((i + "").repeat(8)) // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4060006"]').should( + "have.text", + "The recovery code is invalid or has already been used. Please try again.", + ) + cy.noSession() + } + + cy.get("input[name='code']").type("12312312") // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4000001"]').should( + "have.text", + "The request was submitted too often. Please request another code.", + ) + cy.noSession() + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.recoveryEmailWithCode({ + expect: { email: identity.email, enterCode: false }, + }) + }) + + it("shows code expired message if expired code is submitted", () => { + cy.visit(recovery) + + cy.shortCodeLifespan() + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/4060006"]').should( + "contain.text", + "The recovery code is invalid or has already been used. Please try again.", + ) + + cy.noSession() + }) + + it("should receive a stub email when recovering a non-existent account", () => { + cy.notifyUnknownRecipients("recovery") + cy.visit(recovery) + + const email = gen.email() + cy.get(appPrefix(app) + 'input[name="email"]').type(email) + cy.get('button[value="code"]').click() + + cy.location("pathname").should("eq", "/recovery") + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.get('input[name="code"]').should("be.visible") + + cy.getMail({ + subject: "Account access attempted", + email, + }).should((message) => { + expect(message.subject).to.equal("Account access attempted") + expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + + const code = extractOTPCode(message.body) + expect(code).to.be.null + }) + }) + + it("should cause form errors", () => { + cy.visit(recovery) + cy.removeAttribute(["input[name='email']"], "required") + cy.get('button[value="code"]').click() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property email is missing.", + ) + cy.get('[name="method"][value="code"]').should("exist") + }) + + it("is unable to recover the account if the code is incorrect", () => { + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.get("input[name='code']").type("01234567") // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4060006"]').should( + "have.text", + "The recovery code is invalid or has already been used. Please try again.", + ) + cy.noSession() + }) + + it("should cause non-repeating form errors after submitting empty form twice. see: #2512", () => { + cy.visit(recovery) + cy.location("pathname").should("eq", "/recovery") + cy.removeAttribute(["input[name='email']"], "required") + cy.get('button[value="code"]').click() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property email is missing.", + ) + cy.get("form") + .find('[data-testid="ui/message/4000002"]') + .should("have.length", 1) + cy.get('[name="method"][value="code"]').should("exist") + }) + + it("remote recovery email template (recovery_code_valid)", () => { + cy.remoteCourierRecoveryCodeTemplates() + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.getMail({ + subject: "recovery_code_valid REMOTE TEMPLATE SUBJECT", + email: identity.email, + }).then((mail) => { + expect(mail.body).to.include("recovery_code_valid REMOTE TEMPLATE") + }) + }) + + it("remote recovery email template (recovery_code_invalid)", () => { + cy.notifyUnknownRecipients("recovery") + cy.remoteCourierRecoveryCodeTemplates() + cy.visit(recovery) + const email = gen.email() + cy.get(appPrefix(app) + "input[name='email']").type(email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.getMail({ + subject: "recovery_code_invalid REMOTE TEMPLATE SUBJECT", + email: email, + }).then((mail) => { + expect(mail.body).to.include("recovery_code_invalid REMOTE TEMPLATE") + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts new file mode 100644 index 0000000..baafe5a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts @@ -0,0 +1,201 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Recovery With Code Success", () => { + ;[ + { + recovery: react.recovery, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + recovery: express.recovery, + base: express.base, + app: "express" as "express", + profile: "recovery", + }, + ].forEach(({ recovery, profile, base, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity + + beforeEach(() => { + cy.deleteMail() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("code") + .notifyUnknownRecipients("recovery", false), + ) + + identity = gen.identityWithWebsite() + cy.registerApi(identity) + }) + + it("should contain the recovery address in the session", () => { + cy.visit(recovery) + cy.login({ ...identity, cookieUrl: base }) + cy.getSession().should(assertRecoveryAddress(identity)) + }) + + it("should perform a recovery flow", () => { + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + const newPassword = gen.password() + cy.get(appPrefix(app) + 'input[name="password"]') + .clear() + .type(newPassword) + cy.get('button[value="password"]').click() + cy.expectSettingsSaved() + cy.get('input[name="password"]').should("be.empty") + + cy.logout() + cy.login({ + email: identity.email, + password: newPassword, + cookieUrl: base, + }) + }) + + it("should recover account with correct code after entering wrong code", () => { + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.get("input[name='code']").type("12312312") // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4060006"]').should( + "have.text", + "The recovery code is invalid or has already been used. Please try again.", + ) + cy.noSession() + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + cy.getSession() + cy.location("pathname").should("eq", "/settings") + cy.get('input[name="traits.email"]').should( + "have.value", + identity.email, + ) + }) + + it("should recover account after resending code", () => { + cy.visit(recovery) + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ + expect: { email: identity.email, enterCode: false }, + }) + + cy.get("button[name='email']").click() // resend code + cy.noSession() + + cy.recoveryEmailWithCode({ + expect: { email: identity.email }, + }) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + cy.getSession() + cy.location("pathname").should("eq", "/settings") + cy.get('input[name="traits.email"]').should( + "have.value", + identity.email, + ) + }) + it("should not notify an unknown recipient", () => { + const recipient = gen.email() + + cy.visit(recovery) + cy.get('input[name="email"]').type(recipient) + cy.get(`[name="method"][value="code"]`).click() + + cy.getCourierMessages().then((messages) => { + expect(messages.map((msg) => msg.recipient)).to.not.include(recipient) + }) + }) + }) + }) + + it("should recover, set password and be redirected", () => { + const app = "express" as "express" + cy.deleteMail() + cy.useConfigProfile("recovery") + cy.proxy(app) + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longCodeLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("code"), + ) + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + 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( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + cy.get('input[name="password"]').clear().type(gen.password()) + cy.get('button[value="password"]').click() + cy.url().should("eq", "https://www.example.org/") + }) +}) diff --git a/test/e2e/cypress/integration/profiles/recovery/link/errors.spec.ts b/test/e2e/cypress/integration/profiles/recovery/link/errors.spec.ts new file mode 100644 index 0000000..c91fbaa --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/link/errors.spec.ts @@ -0,0 +1,241 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, parseHtml } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Recovery Errors", () => { + ;[ + { + recovery: react.recovery, + app: "react" as "react", + profile: "spa", + }, + { + recovery: express.recovery, + app: "express" as "express", + profile: "recovery", + }, + ].forEach(({ recovery, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longLinkLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("link") + .disableRecoveryStrategy("code") + .notifyUnknownRecipients("verification", false), + ) + }) + + it("responds with a HTML response on link click of an API flow if the link is expired", () => { + cy.visit(recovery) + + cy.shortLinkLifespan() + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.recoverApi({ email: identity.email }) + cy.recoverEmailButExpired({ expect: { email: identity.email } }) + + cy.get('[data-testid="ui/message/4060005"]').should( + "contain.text", + "The recovery flow expired", + ) + + cy.noSession() + }) + + it("responds with a HTML response on link click of an API flow if the flow is expired", () => { + cy.visit(recovery) + + cy.updateConfigFile((config) => { + config.selfservice.flows.recovery.lifespan = "1s" + return config + }) + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.recoverApi({ email: identity.email }) + cy.wait(1000) + + cy.getMail({ + subject: "Recover access to your account", + email: identity.email, + }).then((message) => { + expect(message.subject).to.equal("Recover access to your account") + expect(message.toAddresses[0].trim()).to.equal(identity.email) + + const link = parseHtml(message.body).querySelector("a") + cy.longRecoveryLifespan() + cy.visit(link.href) + }) + + cy.get('[data-testid="ui/message/4060005"]').should( + "contain.text", + "The recovery flow expired", + ) + + cy.noSession() + }) + + it("should receive a stub email when recovering a non-existent account", () => { + cy.notifyUnknownRecipients("recovery") + cy.visit(recovery) + + const email = gen.email() + cy.get(appPrefix(app) + 'input[name="email"]').type(email) + cy.get('button[value="link"]').click() + + cy.location("pathname").should("eq", "/recovery") + cy.get('[data-testid="ui/message/1060002"]').should( + "have.text", + "An email containing a recovery link has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + cy.get('input[name="email"]').should("have.value", email) + + cy.getMail({ + subject: "Account access attempted", + email, + }).then((message) => { + expect(message.subject).to.equal("Account access attempted") + expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + + const link = parseHtml(message.body).querySelector("a") + expect(link).to.be.null + }) + }) + + it("should cause form errors", () => { + cy.visit(recovery) + + // we need to remove the required attribute of the element since the browser prevents us from submitting the form + // this is to simulate the case where the form is submitted with an empty input field. + cy.removeAttribute(["input[name='email']"], "required") + + cy.get('button[value="link"]').click() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property email is missing.", + ) + cy.get('[name="method"][value="link"]').should("exist") + }) + + it("should cause non-repeating form errors after submitting empty form twice. see: #2512", () => { + cy.visit(recovery) + // we need to remove the required attribute of the element since the browser prevents us from submitting the form + // this is to simulate the case where the form is submitted with an empty input field. + cy.removeAttribute(["input[name='email']"], "required") + cy.get('button[value="link"]').click() + cy.location("pathname").should("eq", "/recovery") + + // we need to remove the required attribute of the element since the browser prevents us from submitting the form + // this is to simulate the case where the form is submitted with an empty input field. + cy.removeAttribute(["input[name='email']"], "required") + cy.get('button[value="link"]').click() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property email is missing.", + ) + cy.get("form") + .find('[data-testid="ui/message/4000002"]') + .should("have.length", 1) + cy.get('[name="method"][value="link"]').should("exist") + }) + + it("is unable to recover the email address if the code is expired", () => { + cy.shortLinkLifespan() + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.recoverApi({ email: identity.email }) + cy.recoverEmailButExpired({ expect: { email: identity.email } }) + + cy.get('[data-testid="ui/message/4060005"]').should( + "contain.text", + "The recovery flow expired", + ) + + cy.noSession() + }) + + it("is unable to recover the account if the code is incorrect", () => { + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.recoverApi({ email: identity.email }) + + cy.getMail({ + subject: "Recover access to your account", + email: identity.email, + }).then((mail) => { + console.log(mail) + const link = parseHtml(mail.body).querySelector("a") + cy.visit(link.href + "-not") // add random stuff to the confirm challenge + cy.get('[data-testid="ui/message/4060004"]').should( + "have.text", + "The recovery token is invalid or has already been used. Please retry the flow.", + ) + cy.noSession() + }) + }) + + it("is unable to recover the account using the token twice", () => { + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.recoverApi({ email: identity.email }) + + cy.getMail({ + subject: "Recover access to your account", + email: identity.email, + }).then((mail) => { + const link = parseHtml(mail.body).querySelector("a") + + // Workaround for cypress cy.visit limitation. + cy.request(link.href).should((response) => { + // add random stuff to the confirm challenge + expect(response.status).to.eq(200) + }) + + cy.clearAllCookies() + + cy.visit(link.href) + cy.get('[data-testid="ui/message/4060004"]').should( + "have.text", + "The recovery token is invalid or has already been used. Please retry the flow.", + ) + cy.noSession() + }) + }) + + it("invalid remote recovery email template", () => { + cy.notifyUnknownRecipients("recovery") + cy.remoteCourierRecoveryTemplates() + const identity = gen.identityWithWebsite() + cy.recoverApi({ email: identity.email }) + + cy.getMail({ + subject: "Account Access Attempted", + email: identity.email, + }).then((mail) => { + expect(mail.body).to.include( + "this is a remote invalid recovery template", + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts new file mode 100644 index 0000000..abfa089 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts @@ -0,0 +1,165 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Recovery Success", () => { + ;[ + { + recovery: react.recovery, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + recovery: express.recovery, + base: express.base, + app: "express" as "express", + profile: "recovery", + }, + ].forEach(({ recovery, profile, base, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity + + beforeEach(() => { + cy.deleteMail() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longLinkLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("link"), + ) + + identity = gen.identityWithWebsite() + cy.registerApi(identity) + }) + + it("should contain the recovery address in the session", () => { + cy.visit(recovery) + cy.login({ ...identity, cookieUrl: base }) + cy.getSession().should(assertRecoveryAddress(identity)) + }) + + it("should perform a recovery flow", () => { + cy.recoverApi({ email: identity.email }) + + cy.recoverEmail({ expect: identity }) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + const newPassword = gen.password() + cy.get(appPrefix(app) + 'input[name="password"]') + .clear() + .type(newPassword) + cy.get('button[value="password"]').click() + cy.expectSettingsSaved() + cy.get('input[name="password"]').should("be.empty") + + cy.logout() + cy.login({ + email: identity.email, + password: newPassword, + cookieUrl: base, + }) + }) + + it("should not notify an unknown recipient", () => { + const recipient = gen.email() + + cy.visit(recovery) + cy.get('input[name="email"]').type(recipient) + cy.get(`[name="method"][value="link"]`).click() + + cy.getCourierMessages().then((messages) => { + expect(messages.map((msg) => msg.recipient)).to.not.include(recipient) + }) + }) + }) + }) + + it("should recover, set password and be redirected", () => { + const app = "express" as "express" + + cy.deleteMail() + cy.useConfigProfile("recovery") + cy.proxy(app) + + cy.deleteMail() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longLinkLifespan() + .disableVerification() + .enableRecovery(), + ) + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + + cy.recoverApi({ + email: identity.email, + returnTo: "https://www.example.org/", + }) + + cy.recoverEmail({ expect: identity }) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + cy.get(appPrefix(app) + 'input[name="password"]') + .clear() + .type(gen.password()) + cy.get('button[value="password"]').click() + cy.url().should("eq", "https://www.example.org/") + }) + + it("should recover even if already logged into another account", () => { + const app = "express" as "express" + + cy.deleteMail() + cy.useConfigProfile("recovery") + cy.proxy(app) + + cy.deleteMail() + + cy.useConfig((builder) => builder.disableVerification()) + + const identity1 = gen.identityWithWebsite() + cy.registerApi(identity1) + const identity2 = gen.identityWithWebsite() + cy.registerApi(identity2) + + cy.recoverApi({ email: identity2.email }) + + // first log in as identity1 + + cy.visit(express.login) + + cy.get(appPrefix(app) + 'input[name="identifier"]').type(identity1.email) + cy.get('input[name="password"]').type(identity1.password) + cy.get('button[value="password"]').click() + + cy.location("pathname").should("not.contain", "/login") + + // then recover identity2, while still logged in as identity1 + + cy.recoverEmail({ expect: identity2 }) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + cy.get('input[name="traits.email"]').should("have.value", identity2.email) + }) +}) 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 new file mode 100644 index 0000000..f85e5c2 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/return-to/success.spec.ts @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { TOTP } from "otpauth" + +context("Recovery with `return_to`", () => { + ;[ + { + recovery: react.recovery, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "recovery-mfa", + }, + { + recovery: express.recovery, + settings: express.settings, + app: "express" as "express", + profile: "recovery-mfa", + }, + ].forEach(({ app, recovery, settings, profile }) => { + describe("Recovery with `return_to` query paramter success", () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity: any + + beforeEach(() => { + cy.deleteMail() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .disableVerification() + .enableRecovery() + .useRecoveryStrategy("code") + .notifyUnknownRecipients("recovery", false) + .longPrivilegedSessionTime() + .requireStrictAal(), + ) + cy.clearAllCookies() + identity = gen.identityWithWebsite() + cy.registerApi(identity) + }) + + const doRecovery = () => { + cy.get(appPrefix(app) + "input[name='email']").type(identity.email) + cy.get("button[value='code']").click() + + cy.get('[data-testid="ui/message/1060003"]').should( + "have.text", + "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + ) + + cy.recoveryEmailWithCode({ expect: { email: identity.email } }) + cy.get("button[value='code']").click() + } + + it("should return to the `return_to` url after successful account recovery and settings update", () => { + cy.visit(recovery + "?return_to=https://www.example.org/") + doRecovery() + + cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( + "contain.text", + "You successfully recovered your account. ", + ) + + cy.getSession() + cy.location("pathname").should("eq", "/settings") + + const newPassword = gen.password() + cy.get(appPrefix(app) + 'input[name="password"]') + .clear() + .type(newPassword) + cy.get('button[value="password"]').click() + + 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", () => { + cy.visit(settings) + cy.get('input[name="identifier"]').type(identity.email) + cy.get('input[name="password"]').type(identity.password) + cy.get('button[value="password"]').click() + cy.visit(settings) + + // enable mfa for this account + let secret: string + cy.get('[data-testid="node/text/totp_secret_key/text"]').then(($e) => { + secret = $e.text().trim() + }) + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + cy.expectSettingsSaved() + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "totp"], + }) + + cy.logout() + cy.clearAllCookies() + + cy.visit(recovery + "?return_to=https://www.example.org/") + doRecovery() + + cy.shouldShow2FAScreen() + cy.get('input[name="totp_code"]').then(($e) => { + cy.wrap($e).type( + new TOTP({ + secret, + }).generate(), + ) + }) + cy.get('*[name="method"][value="totp"]').click() + + const newPassword = gen.password() + cy.get(appPrefix(app) + 'input[name="password"]') + .clear() + .type(newPassword) + cy.get('button[value="password"]').click() + cy.location("hostname").should("eq", "www.example.org") + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/recovery/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/settings/success.spec.ts new file mode 100644 index 0000000..f7e5188 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/recovery/settings/success.spec.ts @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, assertRecoveryAddress, gen } from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Account Recovery Success", () => { + ;[ + { + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "spa", + }, + { + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "recovery", + }, + ].forEach(({ settings, profile, base, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity + + beforeEach(() => { + cy.deleteMail() + + cy.useConfig((builder) => + builder + .longRecoveryLifespan() + .longLinkLifespan() + .disableVerification() + .enableRecovery(), + ) + + identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.login({ ...identity, cookieUrl: base }) + }) + + it("should update the recovery address when updating the email", () => { + cy.visit(settings) + const email = gen.email() + cy.get(appPrefix(app) + 'input[name="traits.email"]') + .clear() + .type(email) + cy.get('button[value="profile"]').click() + cy.expectSettingsSaved() + cy.get('input[name="traits.email"]').should("contain.value", email) + + cy.getSession().should(assertRecoveryAddress({ email })) + }) + + xit("should not show an immediate error when a recovery address already exists", () => { + // account enumeration prevention, needs to be implemented. + }) + }) + }) +}) 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 0000000..5766517 --- /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("be.visible") + + 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) + }) + }) + + // playwright:migrated + it("should sign up and be logged in with session hook", () => { + if (app === "react") { + // This test is flaky on React, so we skip it for now. + return + } + + 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("be.visible") + + cy.getRegistrationCodeFromEmail(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) + }) + }) + + // playwright:migrated + it("should be able to sign up without session hook", () => { + if (app === "react") { + // This test is flaky on React, so we skip it for now. + return + } + + 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("be.visible") + + cy.getRegistrationCodeFromEmail(email).then((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("be.visible") + + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).then( + (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, + }).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.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 0000000..466f3e3 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts @@ -0,0 +1,225 @@ +// 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/") + + // hd is forwarded; login_hint is never forwarded because a + // caller-supplied value cannot be trusted (HackerOne #3239672, + // ory-corp/cloud#8955). + cy.addInputElement("form", "upstream_parameters.hd", "ory.sh") + 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", "hd=ory.sh") + .and("not.include", "login_hint") + }) + }) + }) +}) 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 0000000..436d0b5 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts @@ -0,0 +1,117 @@ +// 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") + } + }) + + // playwright:migrated + 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) + }) + }) + + // playwright:migrated + 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/verification/login/errors.spec.ts b/test/e2e/cypress/integration/profiles/verification/login/errors.spec.ts new file mode 100644 index 0000000..55d2466 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/login/errors.spec.ts @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Verification Login Errors", () => { + ;[ + { + login: react.login, + app: "react" as "react", + profile: "verification", + }, + { + login: express.login, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, login, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.useConfig((builder) => builder.enableLoginForVerifiedAddressOnly()) + cy.proxy(app) + }) + + it("is unable to login as long as the email is not verified", () => { + cy.deleteMail() + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.visit(login) + + cy.get(appPrefix(app) + '[name="identifier"]').type(identity.email) + cy.get('[name="password"]').type(identity.password) + cy.get('[value="password"]').click() + + if (app === "express") { + cy.url().should("contain", "/verification") + } + + cy.get('[data-testid="ui/message/4000010"]').contains( + "Account not active yet", + ) + + cy.noSession() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/login/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/login/success.spec.ts new file mode 100644 index 0000000..a7a57c7 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/login/success.spec.ts @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen } from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Account Verification Login Success", () => { + ;[ + { + login: react.login, + app: "react" as "react", + profile: "verification", + }, + { + login: express.login, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, login, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.useConfig((builder) => builder.enableLoginForVerifiedAddressOnly()) + cy.proxy(app) + }) + + it("is able to login after successful email verification", () => { + cy.deleteMail() + + const identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.performEmailVerification({ expect: { email: identity.email } }) + + cy.visit(login) + + cy.get(appPrefix(app) + 'input[name="identifier"]').type(identity.email) + cy.get('input[name="password"]').type(identity.password) + cy.get('button[value="password"]').click() + + cy.location("pathname").should("not.contain", "/login") + + cy.getSession() + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/registration/errors.spec.ts b/test/e2e/cypress/integration/profiles/verification/registration/errors.spec.ts new file mode 100644 index 0000000..19b7d85 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/registration/errors.spec.ts @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { + appPrefix, + assertVerifiableAddress, + gen, + parseHtml, + verifyHrefPattern, +} from "../../../../helpers" + +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Verification Registration Errors", () => { + ;[ + { + login: react.login, + verification: react.verification, + app: "react" as "react", + profile: "verification", + }, + { + login: express.login, + verification: express.verification, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, login, verification, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity + beforeEach(() => { + cy.useConfig((builder) => + builder + .enableVerification() + .disableRecovery() + .shortCodeLifespan() + .longVerificationLifespan(), + ) + cy.deleteMail() + + identity = gen.identityWithWebsite() + cy.registerApi(identity) + cy.login(identity) + }) + + it("is unable to verify the email address if the code is no longer valid and resend the code", () => { + cy.verifyEmailButExpired({ + expect: { email: identity.email }, + }) + + cy.longCodeLifespan() + + cy.visit(verification) + cy.get(appPrefix(app) + 'input[name="email"]').should("be.empty") + cy.get('input[name="email"]').type(identity.email) + cy.get('button[value="code"]').click() + cy.contains("An email containing a verification") + cy.verifyEmail({ + expect: { email: identity.email, password: identity.password }, + }) + }) + + it("is unable to verify the email address if the code is incorrect", () => { + cy.getMail({ + body: "Verify your account", + email: identity.email, + }).then((mail) => { + const link = parseHtml(mail.body).querySelector("a") + + expect(verifyHrefPattern.test(link.href)).to.be.true + + cy.visit(link.href + "-not") // add random stuff to the confirm challenge + cy.getSession().should((session) => + assertVerifiableAddress({ + isVerified: false, + email: identity.email, + })(session), + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts new file mode 100644 index 0000000..5065b5f --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts @@ -0,0 +1,109 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { assertVerifiableAddress, gen } from "../../../../helpers" + +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Account Verification Registration Success", () => { + ;[ + { + registration: react.registration, + app: "react" as "react", + profile: "verification", + }, + { + registration: express.registration, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, registration, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.useConfig((builder) => builder.longVerificationLifespan()) + cy.deleteMail() + }) + + afterEach(() => { + cy.deleteMail() + }) + + const up = (value) => `up-${value}` + const { email, password } = gen.identity() + + it("is able to verify the email address after sign up", () => { + const identity = gen.identityWithWebsite() + const { email, password } = identity + cy.registerApi(identity) + cy.login(identity) + cy.getSession().should((session) => + assertVerifiableAddress({ + isVerified: false, + email, + })(session), + ) + + cy.verifyEmail({ expect: { email, password } }) + }) + + xit("sends the warning email on double sign up", () => { + // FIXME https://github.com/ory/kratos/issues/133 + cy.clearAllCookies() + cy.register({ email, password: up(password) }) + cy.clearAllCookies() + cy.login({ email, password }) + + cy.verifyEmail({ expect: { email, password } }) + }) + + it("is redirected to after_verification_return_to after verification", () => { + cy.clearAllCookies() + const { email, password } = gen.identity() + cy.register({ + email, + password, + query: { + return_to: "http://localhost:4455/verification_return_to_callback", + after_verification_return_to: + "http://localhost:4455/verification_callback", + }, + }) + cy.login({ email, password }) + cy.verifyEmail({ + expect: { + email, + password, + redirectTo: "http://localhost:4455/verification_callback", + }, + }) + }) + + it("is redirected to `return_to` after verification", () => { + cy.clearAllCookies() + const { email, password } = gen.identity() + cy.register({ + email, + password, + query: { + return_to: "http://localhost:4455/verification_return_to_callback", + }, + }) + cy.login({ email, password }) + cy.verifyEmail({ + expect: { + email, + password, + redirectTo: "http://localhost:4455/verification_return_to_callback", + }, + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/settings/error.spec.ts b/test/e2e/cypress/integration/profiles/verification/settings/error.spec.ts new file mode 100644 index 0000000..91ae2c5 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/settings/error.spec.ts @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { + appPrefix, + assertVerifiableAddress, + gen, + parseHtml, + verifyHrefPattern, +} from "../../../../helpers" + +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Account Verification Settings Error", () => { + ;[ + { + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "verification", + }, + { + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, settings, app, base }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + describe("error flow", () => { + let identity + before(() => { + cy.deleteMail() + }) + + beforeEach(() => { + cy.useConfig((builder) => builder.longCodeLifespan()) + identity = gen.identityWithWebsite() + cy.clearAllCookies() + cy.registerApi(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login({ ...identity, cookieUrl: base }) + cy.visit(settings) + }) + + it("is unable to verify the email address if the code is no longer valid", () => { + cy.shortCodeLifespan() + cy.visit(settings) + + const email = `not-${identity.email}` + cy.get(appPrefix(app) + 'input[name="traits.email"]') + .clear() + .type(email) + cy.get('button[value="profile"]').click() + + cy.verifyEmailButExpired({ + expect: { email }, + }) + }) + + it("is unable to verify the email address if the code is incorrect", () => { + const email = `not-${identity.email}` + cy.get('input[name="traits.email"]').clear().type(email) + cy.get('button[value="profile"]').click() + + cy.getMail({ + body: "Verify your account", + email, + }).then((mail) => { + const link = parseHtml(mail.body).querySelector("a") + + expect(verifyHrefPattern.test(link.href)).to.be.true + + cy.visit(link.href + "-not") // add random stuff to the confirm challenge + cy.log(link.href) + cy.getSession().then( + assertVerifiableAddress({ isVerified: false, email }), + ) + }) + }) + + xit("should not update the traits until the email has been verified and the old email has accepted the change", () => { + // FIXME https://github.com/ory/kratos/issues/292 + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts new file mode 100644 index 0000000..d17ee11 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { + APP_URL, + appPrefix, + assertVerifiableAddress, + gen, +} from "../../../../helpers" +import { routes as react } from "../../../../helpers/react" +import { routes as express } from "../../../../helpers/express" + +context("Account Verification Settings Success", () => { + ;[ + { + settings: react.settings, + app: "react" as "react", + profile: "verification", + }, + { + settings: express.settings, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, settings, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + let identity + + before(() => { + cy.deleteMail() + }) + + beforeEach(() => { + identity = gen.identity() + cy.register(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity) + cy.visit(settings) + }) + + it("should update the verify address and request a verification email", () => { + const email = `not-${identity.email}` + cy.get(appPrefix(app) + 'input[name="traits.email"]') + .clear() + .type(email) + cy.get('[value="profile"]').click() + + if (app == "express") { + cy.expectSettingsSaved() + cy.get('input[name="traits.email"]').should("contain.value", email) + cy.getSession().then( + assertVerifiableAddress({ isVerified: false, email }), + ) + } + cy.verifyEmail({ expect: { email } }) + }) + + xit("should should be able to allow or deny (and revert?) the address change", () => { + // FIXME https://github.com/ory/kratos/issues/292 + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts b/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts new file mode 100644 index 0000000..afb8d97 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts @@ -0,0 +1,204 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { + assertVerifiableAddress, + gen, + parseHtml, + verifyHrefPattern, +} from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { Strategy } from "../../../../support" + +context("Account Verification Error", () => { + ;[ + { + verification: react.verification, + base: react.base, + app: "react" as "react", + profile: "verification", + }, + { + verification: express.verification, + base: express.base, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, verification, app, base }) => { + for (let s of ["code", "link"] as Strategy[]) { + describe(`for strategy ${s}`, () => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + let identity + beforeEach(() => { + cy.clearAllCookies() + + cy.useConfig((builder) => + builder + .longVerificationLifespan() + .longLifespan(s) + .useVerificationStrategy(s) + .notifyUnknownRecipients("verification", false), + ) + + identity = gen.identity() + cy.registerApi(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + cy.login(identity) + cy.visit(verification) + }) + + it("responds with a HTML response on link click of an API flow if the flow is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.flows.verification.lifespan = "1s" + return config + }) + + cy.verificationApi({ + email: identity.email, + strategy: s, + }) + + cy.wait(1000) + cy.shortVerificationLifespan() + + cy.getMail({ + removeMail: true, + body: "Verify your account", + email: identity.email, + }).then((message) => { + expect(message.toAddresses[0].trim()).to.equal(identity.email) + + const link = parseHtml(message.body).querySelector("a") + + cy.longVerificationLifespan() + cy.visit(link.href) + cy.get('[data-testid="ui/message/4070005"]').should( + "contain.text", + "verification flow expired", + ) + + cy.getSession().should((session) => { + assertVerifiableAddress({ + isVerified: false, + email: identity.email, + })(session) + }) + }) + }) + + it("responds with a HTML response on link click of an API flow if the code is expired", () => { + cy.shortLifespan(s) + + // Init expired flow + cy.verificationApi({ + email: identity.email, + strategy: s, + }) + + cy.wait(1000) + + cy.verifyEmailButExpired({ + expect: { email: identity.email }, + strategy: s, + }) + }) + + it("is unable to verify the email address if the code is expired", () => { + cy.shortLifespan(s) + + cy.visit(verification) + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.contains("An email containing a verification") + cy.get(`[name="method"][value="${s}"]`).should("exist") + cy.verifyEmailButExpired({ + expect: { email: identity.email }, + strategy: s, + }) + }) + + it("is unable to verify the email address if the code is incorrect", () => { + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.contains("An email containing a verification") + + cy.getMail({ + email: identity.email, + body: "Verify your account", + }).then((mail) => { + const link = parseHtml(mail.body).querySelector("a") + + expect(verifyHrefPattern.test(link.href)).to.be.true + + cy.visit(link.href + "-not") // add random stuff to the confirm challenge + cy.getSession().then( + assertVerifiableAddress({ + isVerified: false, + email: identity.email, + }), + ) + }) + }) + + it("unable to verify non-existent account", () => { + cy.notifyUnknownRecipients("verification") + const email = gen.identity().email + cy.get('input[name="email"]').type(email) + cy.get(`button[value="${s}"]`).click() + cy.getMail({ + subject: "Someone tried to verify this email address", + email, + removeMail: true, + }).then((mail) => { + expect(mail.toAddresses).includes(email) + expect(mail.subject).eq( + "Someone tried to verify this email address", + ) + }) + }) + + if (s === "code") { + it("is unable to brute force the code", () => { + cy.visit(verification) + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.contains("An email containing a verification") + + for (let i = 0; i < 5; i++) { + cy.get("input[name='code']").type((i + "").repeat(8)) // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4070006"]').should( + "have.text", + "The verification code is invalid or has already been used. Please try again.", + ) + } + + cy.get("input[name='code']").type("12312312") // Invalid code + cy.get("button[value='code']").click() + cy.get('[data-testid="ui/message/4000001"]').should( + "have.text", + "The request was submitted too often. Please request another code.", + ) + cy.getSession().then( + assertVerifiableAddress({ + isVerified: false, + email: identity.email, + }), + ) + }) + } + }) + }) + } + }) +}) diff --git a/test/e2e/cypress/integration/profiles/verification/verify/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/verify/success.spec.ts new file mode 100644 index 0000000..7155b2a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/verification/verify/success.spec.ts @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APP_URL, assertVerifiableAddress, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { Strategy } from "../../../../support" + +context("Account Verification Settings Success", () => { + ;[ + { + verification: react.verification, + app: "react" as "react", + profile: "verification", + }, + { + verification: express.verification, + app: "express" as "express", + profile: "verification", + }, + ].forEach(({ profile, verification, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + for (let s of ["code", "link"] as Strategy[]) { + describe(`for strategy ${s}`, () => { + beforeEach(() => { + cy.useConfig((builder) => + builder + .useVerificationStrategy(s) + .notifyUnknownRecipients("verification", false), + ) + }) + + it("should request verification and receive an email and verify it", () => { + const identity = gen.identity() + cy.register(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity) + cy.visit(verification) + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.contains("An email containing a verification") + + cy.get(`[name="method"][value="${s}"]`).should("exist") + + cy.verifyEmail({ expect: { email: identity.email }, strategy: s }) + }) + + it("should request verification for an email that does not exist yet", () => { + cy.notifyUnknownRecipients("verification", true) + const identity = gen.identity() + cy.register(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity) + cy.visit(verification) + const email = `not-${identity.email}` + cy.get('input[name="email"]').type(email) + cy.get(`button[value="${s}"]`).click() + + cy.contains("An email containing a verification") + + cy.getMail({ + subject: "Someone tried to verify this email address", + email, + }).should((message) => { + expect(message.subject.trim()).to.equal( + "Someone tried to verify this email address", + ) + expect(message.fromAddress.trim()).to.equal( + "no-reply@ory.kratos.sh", + ) + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + }) + + cy.getSession().then( + assertVerifiableAddress({ + isVerified: false, + email: identity.email, + }), + ) + }) + + it("should not verify email when clicking on link received on different address", () => { + const identity = gen.identity() + cy.register(identity) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity) + cy.visit(verification) + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.verifyEmail({ expect: { email: identity.email }, strategy: s }) + + // identity is verified + cy.logout() + + // registered with other email address + const identity2 = gen.identity() + cy.register(identity2) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity2) + + cy.visit(APP_URL + "/verification") + + // request verification link for identity + cy.get('input[name="email"]').type(identity.email) + cy.get(`button[value="${s}"]`).click() + + cy.performEmailVerification({ + expect: { email: identity.email }, + strategy: s, + }) + + // expect current session to still not have a verified email address + cy.getSession().should( + assertVerifiableAddress({ + email: identity2.email, + isVerified: false, + }), + ) + }) + + it("should redirect to return_to after completing verification", () => { + cy.clearAllCookies() + // registered with other email address + const identity2 = gen.identity() + cy.register(identity2) + cy.deleteMail({ atLeast: 1 }) // clean up registration email + + cy.login(identity2) + + cy.visit(APP_URL + "/self-service/verification/browser", { + qs: { return_to: "http://localhost:4455/verification_callback" }, + }) + // request verification link for identity + cy.get('input[name="email"]').type(identity2.email) + cy.get(`[name="method"][value="${s}"]`).click() + cy.verifyEmail({ + expect: { + email: identity2.email, + redirectTo: "http://localhost:4455/verification_callback", + }, + strategy: s, + }) + }) + + it("should not notify an unknown recipient", () => { + const recipient = gen.email() + + cy.visit(APP_URL + "/self-service/verification/browser") + cy.get('input[name="email"]').type(recipient) + cy.get(`[name="method"][value="${s}"]`).click() + + cy.getCourierMessages().then((messages) => { + expect(messages.map((msg) => msg.recipient)).to.not.include( + recipient, + ) + }) + }) + }) + } + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/webhooks/login/error.spec.ts b/test/e2e/cypress/integration/profiles/webhooks/login/error.spec.ts new file mode 100644 index 0000000..99bc823 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/webhooks/login/error.spec.ts @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" + +describe("Basic email profile with failing login flows with webhooks", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "webhooks", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(route) + }) + + it("should show fail to sign in when webhooks rejects login", () => { + const email = gen.blockedEmail() + const password = gen.password() + + cy.registerApi({ email, password, fields: {} }) + cy.get('input[name="identifier"]').type(email) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + cy.get('*[data-testid="ui/message/1234"]').should( + "contain.text", + "email could not be validated", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/webhooks/login/success.spec.ts b/test/e2e/cypress/integration/profiles/webhooks/login/success.spec.ts new file mode 100644 index 0000000..8e9c00e --- /dev/null +++ b/test/e2e/cypress/integration/profiles/webhooks/login/success.spec.ts @@ -0,0 +1,60 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +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() + const password = gen.password() + + before(() => { + cy.registerApi({ email, password, fields: { "traits.website": website } }) + }) + ;[ + { + route: express.login, + app: "express" as "express", + profile: "webhooks", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.proxy(app) + }) + + beforeEach(() => { + cy.useConfigProfile(profile) + cy.clearAllCookies() + cy.visit(route) + }) + + it("should sign in and be logged in", () => { + 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") + + 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.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/webhooks/registration/errors.spec.ts b/test/e2e/cypress/integration/profiles/webhooks/registration/errors.spec.ts new file mode 100644 index 0000000..9e032e4 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/webhooks/registration/errors.spec.ts @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" + +describe("Registration failures with email profile with webhooks", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "webhooks", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.visit(route) + }) + + const blockedIdentity = gen.blockedEmail() + const password = gen.password() + + it("should show an error when the webhook is blocking registration", () => { + cy.get('input[name="traits.email"]').type(blockedIdentity) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + cy.get('input[name="traits.email"]').should( + "have.value", + blockedIdentity, + ) + cy.get('*[data-testid="ui/message/1234"]').should( + "contain.text", + "email could not be validated", + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/webhooks/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/webhooks/registration/success.spec.ts new file mode 100644 index 0000000..454a31e --- /dev/null +++ b/test/e2e/cypress/integration/profiles/webhooks/registration/success.spec.ts @@ -0,0 +1,172 @@ +// 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 { testFlowWebhook } from "../../../../helpers/webhook" + +context("Registration success with email profile with webhooks", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "webhooks", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(route) + }) + + it("should sign up and be logged in", () => { + const email = gen.email() + const password = gen.password() + + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + 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.email).to.equal(email) + }) + }) + + it("should work without transient_payload", () => { + const WEBHOOK_TARGET = "https://webhook-target-gsmwn5ab4a-uc.a.run.app" + const documentUrl = (key: string) => + `${WEBHOOK_TARGET}/documents/${key}` + const email = gen.email() + const password = gen.password() + const documentID = gen.password() + const jsonnet = "function(ctx) ctx" + cy.setupHooks("registration", "after", "password", [ + { + hook: "web_hook", + config: { + body: `base64://${Buffer.from(jsonnet).toString("base64")}`, + url: documentUrl(documentID), + method: "PUT", + }, + }, + ]) + + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + + cy.request({ + url: documentUrl(documentID), + method: "GET", + headers: { + Accept: "application/json", + }, + }).then(({ body, status }) => { + const b = JSON.parse(body) + expect(status).to.equal(200) + expect(b.identity).is.not.undefined + expect(b.flow.transient_payload).is.empty + }) + }) + + it("should pass transient_payload to webhook", () => { + 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.submitPasswordForm() + }) + }) + + it("should sign up and modify the identity", () => { + const email = gen.email() + const password = gen.password() + + const updatedEmail = { + identity: { + traits: { email: "updated-" + email }, + verifiable_addresses: [ + { via: "email", value: "updated-" + email, verified: true }, + { via: "email", value: "this-email-should-be-ignored" }, + { via: "email", value: "" }, + ], + recovery_addresses: [ + { via: "email", value: "updated-" + email }, + { via: "email", value: "this-email-should-be-ignored" }, + { via: "email", value: "" }, + ], + metadata_public: { some: "public fields" }, + }, + } + cy.setPostPasswordRegistrationHooks([ + { hook: "session" }, + { + hook: "web_hook", + config: { + url: + "http://127.0.0.1:4459/webhook/write?response=" + + encodeURIComponent(JSON.stringify(updatedEmail)), + method: "POST", + body: "file://test/e2e/profiles/webhooks/webhook_body.jsonnet", + response: { parse: true }, + }, + }, + ]) + + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) + + cy.submitPasswordForm() + + // Wait for the post-registration landing page (express welcome) so the + // session cookie is in Cypress's jar before we hit /sessions/whoami. + if (app === "express") { + cy.get("a[href*='sessions']").click() + } + cy.get("pre").should("contain.text", "updated-" + 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.email).to.equal("updated-" + email) + expect((identity as any).metadata_public.some).to.equal( + "public fields", + ) + expect(identity.verifiable_addresses[0].verified).to.equal(true) + expect(identity.verifiable_addresses[0].verified_at).not.to.be.empty + expect(identity.verifiable_addresses[0].via).to.eq("email") + expect(identity.verifiable_addresses[0].value).to.eq( + "updated-" + email, + ) + expect(identity.verifiable_addresses).to.have.length(1) + + expect(identity.recovery_addresses).to.have.length(1) + expect(identity.recovery_addresses[0].via).to.eq("email") + expect(identity.recovery_addresses[0].value).to.eq("updated-" + email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/plugins/index.js b/test/e2e/cypress/plugins/index.js new file mode 100644 index 0000000..a890191 --- /dev/null +++ b/test/e2e/cypress/plugins/index.js @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/// + +const got = require("got") +const CRI = require("chrome-remote-interface") +let criPort = 0, + criClient = null + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} + +function ensureRdpPort(args) { + const existing = args.find( + (arg) => arg.slice(0, 23) === "--remote-debugging-port", + ) + + if (existing) { + return Number(existing.split("=")[1]) + } + + const port = 40000 + Math.round(Math.random() * 25000) + args.push(`--remote-debugging-port=${port}`) + return port +} diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts new file mode 100644 index 0000000..bc4df57 --- /dev/null +++ b/test/e2e/cypress/support/commands.ts @@ -0,0 +1,1618 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { + APP_URL, + assertVerifiableAddress, + extractOTPCode, + gen, + KRATOS_ADMIN, + KRATOS_PUBLIC, + MAIL_API, + MOBILE_URL, + parseHtml, + pollInterval, +} from "../helpers" + +import dayjs from "dayjs" +import YAML from "yamljs" +import { MailMessage, Strategy } from "." +import { OryKratosConfiguration } from "../../shared/config" +import { UiNode } from "@ory/kratos-client" +import { ConfigBuilder } from "./configHelpers" + +const configFile = "kratos.generated.yml" + +const mergeFields = (form, fields) => { + const result = {} + form.nodes.forEach(({ attributes, type }) => { + if (type === "input") { + result[attributes.name] = attributes.value + } + }) + + return { ...result, ...fields } +} + +function checkConfigVersion(previous, tries = 0) { + cy.wait(50) + cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => { + if (previous !== body) { + return + } else if (tries > 8) { + console.warn( + "Config version did not change after 5 tries, maybe the changes did not have an effect?", + ) + return + } + cy.wait(50) + checkConfigVersion(previous, tries + 1) + }) +} + +const updateConfigFile = ( + cb: (arg: OryKratosConfiguration) => OryKratosConfiguration, +) => { + cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => { + cy.readFile(configFile).then((contents) => { + cy.writeFile(configFile, YAML.stringify(cb(YAML.parse(contents)))) + cy.wait(500) + }) + checkConfigVersion(body) + }) +} + +Cypress.Commands.add("useConfigProfile", (profile: string) => { + cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => { + console.log("Switching config profile to:", profile) + cy.readFile(`kratos.${profile}.yml`).then((contents) => + cy.writeFile(configFile, contents), + ) + checkConfigVersion(body) + }) +}) + +Cypress.Commands.add("proxy", (app: string) => { + console.log("Switching proxy profile to:", app) + cy.writeFile(`proxy.json`, `"${app}"`) + cy.request(APP_URL + "/") + .its("body", { log: false }) + .then((body) => { + expect(body.indexOf(`data-testid="app-${app}"`) > -1).to.be.true + }) +}) + +Cypress.Commands.add("shortPrivilegedSessionTime", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.settings.privileged_session_max_age = "1ms" + return config + }) +}) + +Cypress.Commands.add("setIdentitySchema", (schema: string) => { + updateConfigFile((config) => { + const id = gen.password() + config.identity.default_schema_id = id + config.identity.schemas = [ + ...(config.identity.schemas || []), + { + id, + url: schema, + }, + ] + return config + }) +}) + +Cypress.Commands.add("setDefaultIdentitySchema", (id: string) => { + updateConfigFile((config) => { + config.identity.default_schema_id = id + return config + }) +}) + +Cypress.Commands.add("longPrivilegedSessionTime", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.settings.privileged_session_max_age = "5m" + return config + }) +}) + +Cypress.Commands.add("longVerificationLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.verification.lifespan = "1m" + return config + }) +}) +Cypress.Commands.add("shortVerificationLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.verification.lifespan = "1ms" + return config + }) +}) +Cypress.Commands.add("sessionRequiresNo2fa", ({} = {}) => { + updateConfigFile((config) => { + config.session.whoami.required_aal = "aal1" + return config + }) +}) +Cypress.Commands.add("sessionRequires2fa", ({} = {}) => { + updateConfigFile((config) => { + config.session.whoami.required_aal = "highest_available" + return config + }) +}) +Cypress.Commands.add("shortLinkLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.link.config.lifespan = "1ms" + return config + }) +}) +Cypress.Commands.add("longLinkLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.link.config.lifespan = "1m" + return config + }) +}) + +Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ms" + return config + }) +}) + +Cypress.Commands.add("shortLifespan", (strategy: Strategy) => { + updateConfigFile((config) => { + config.selfservice.methods[strategy].config.lifespan = "1ms" + return config + }) +}) + +Cypress.Commands.add("longLifespan", (strategy: Strategy) => { + updateConfigFile((config) => { + config.selfservice.methods[strategy].config.lifespan = "1m" + return config + }) +}) + +Cypress.Commands.add("longCodeLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1m" + return config + }) +}) + +Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ms" + return config + }) +}) + +Cypress.Commands.add("longCodeLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1m" + return config + }) +}) + +Cypress.Commands.add("longRecoveryLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.recovery.lifespan = "1m" + return config + }) +}) + +Cypress.Commands.add("enableLoginForVerifiedAddressOnly", () => { + updateConfigFile((config) => { + config.selfservice.flows.login["after"] = { + password: { hooks: [{ hook: "require_verified_address" }] }, + } + return config + }) +}) + +Cypress.Commands.add("setupHooks", (flow, phase, kind, hooks) => { + updateConfigFile((config) => { + config.selfservice.flows[flow][phase][kind] = { hooks } + return config + }) +}) + +Cypress.Commands.add("setPostPasswordRegistrationHooks", (hooks) => { + cy.setupHooks("registration", "after", "password", hooks) +}) + +Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => { + cy.setupHooks("registration", "after", "code", hooks) +}) + +Cypress.Commands.add("shortLoginLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.login.lifespan = "100ms" + return config + }) +}) +Cypress.Commands.add("longLoginLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.login.lifespan = "1h" + return config + }) +}) + +Cypress.Commands.add("shortRecoveryLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.recovery.lifespan = "1ms" + return config + }) +}) + +Cypress.Commands.add("requireStrictAal", () => { + updateConfigFile((config) => { + config.selfservice.flows.settings.required_aal = "highest_available" + config.session.whoami.required_aal = "highest_available" + return config + }) +}) + +Cypress.Commands.add("useLaxAal", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.settings.required_aal = "aal1" + config.session.whoami.required_aal = "aal1" + return config + }) +}) + +Cypress.Commands.add("disableVerification", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.verification.enabled = false + return config + }) +}) + +Cypress.Commands.add("enableVerification", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.verification.enabled = true + return config + }) +}) + +Cypress.Commands.add("enableRecovery", ({} = {}) => { + updateConfigFile((config) => { + if (!config.selfservice.flows.recovery) { + config.selfservice.flows.recovery = {} + } + config.selfservice.flows.recovery.enabled = true + return config + }) +}) + +Cypress.Commands.add("useRecoveryStrategy", (strategy: Strategy) => { + updateConfigFile((config) => { + if (!config.selfservice.flows.recovery) { + config.selfservice.flows.recovery = {} + } + config.selfservice.flows.recovery.use = strategy + if (!config.selfservice.methods[strategy]) { + config.selfservice.methods[strategy] = {} + } + config.selfservice.methods[strategy].enabled = true + return config + }) +}) + +Cypress.Commands.add("disableRecoveryStrategy", (strategy: Strategy) => { + updateConfigFile((config) => { + config.selfservice.methods[strategy].enabled = false + return config + }) +}) + +Cypress.Commands.add("disableRecovery", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.recovery.enabled = false + return config + }) +}) + +Cypress.Commands.add("disableRegistration", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.registration.enabled = false + return config + }) +}) + +Cypress.Commands.add("enableRegistration", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.registration.enabled = true + return config + }) +}) + +Cypress.Commands.add("useLaxAal", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.settings.required_aal = "aal1" + config.session.whoami.required_aal = "aal1" + return config + }) +}) + +Cypress.Commands.add("updateConfigFile", (cb: (arg: any) => any) => { + updateConfigFile(cb) +}) + +Cypress.Commands.add( + "register", + ({ + email = gen.email(), + password = gen.password(), + query = {}, + fields = {}, + } = {}) => { + console.log("Creating user account: ", { email, password }) + + const cookieDomain = new URL(APP_URL).hostname + // Somehow, this line makes the clearCookies call work, without it, the cookies are not always cleared and the test fails randomly. Make it make sense. + cy.getCookies({ + domain: cookieDomain, + }) + cy.clearCookies({ + domain: cookieDomain, + }) + + cy.request({ + url: APP_URL + "/self-service/registration/browser", + followRedirect: false, + headers: { + Accept: "application/json", + }, + qs: query, + }) + .then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { + ...fields, + "traits.email": email, + password, + method: "password", + }), + url: form.action, + followRedirect: false, + }) + }) + .then(({ body }) => { + expect(body.identity.traits.email).to.contain(email) + }) + }, +) + +Cypress.Commands.add( + "registerWithCode", + ({ + email = gen.email(), + code = undefined, + traits = {}, + query = {}, + expectedMailCount = 1, + } = {}) => { + const cookieDomain = new URL(APP_URL).hostname + cy.clearCookies({ + domain: cookieDomain, + }) + cy.getCookies({ + domain: cookieDomain, + }).should("be.empty") // assert cookies are actually empty + + cy.request({ + url: APP_URL + "/self-service/registration/browser", + method: "GET", + followRedirect: false, + headers: { + Accept: "application/json", + }, + qs: query || {}, + }).then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + return cy + .request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + ...traits, + ...(code && { code }), + }), + url: form.action, + followRedirect: false, + failOnStatusCode: false, + }) + .then(({ body }) => { + if (!code) { + expect( + body.ui.nodes.find( + (f: UiNode) => + f.group === "default" && + "name" in f.attributes && + f.attributes.name === "traits.email", + )?.attributes.value, + ).to.eq(email) + + return cy + .getRegistrationCodeFromEmail(email, { + expectedCount: expectedMailCount, + }) + .then((code) => { + return cy.request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + code, + ...traits, + }), + url: form.action, + followRedirect: false, + }) + }) + } else { + expect(body.session).to.contain(email) + } + }) + }) + }, +) + +Cypress.Commands.add( + "registerApi", + ({ email = gen.email(), password = gen.password(), fields = {} } = {}) => + cy + .request({ + url: APP_URL + "/self-service/registration/api", + }) + .then(({ body }) => { + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { + ...fields, + "traits.email": email, + password, + method: "password", + }), + url: form.action, + }) + }) + .then(({ body }) => { + expect(body.identity.traits.email).to.contain(email) + return body + }), +) + +Cypress.Commands.add("settingsApi", ({ fields = {} } = {}) => + cy + .request({ + url: APP_URL + "/self-service/settings/api", + }) + .then(({ body }) => { + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { + ...fields, + }), + url: form.action, + }) + }) + .then(({ body }) => { + expect(body.statusCode).to.eq(200) + }), +) + +Cypress.Commands.add("loginApi", ({ email, password } = {}) => + cy + .request({ + url: APP_URL + "/self-service/login/api", + }) + .then(({ body }) => { + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { + identifier: email, + password, + method: "password", + }), + url: form.action, + }) + }) + .then(({ body }) => { + expect(body.session.identity.traits.email).to.contain(email) + return body + }), +) + +Cypress.Commands.add("loginApiWithoutCookies", ({ email, password } = {}) => { + cy.task("httpRequest", { + url: APP_URL + "/self-service/login/api", + headers: { + Accept: "application/json", + }, + responseType: "json", + }).then((body: any) => { + cy.task("httpRequest", { + method: body.ui.method, + json: mergeFields(body.ui, { + identifier: email, + password, + method: "password", + }), + headers: { + Accept: "application/json", + }, + responseType: "json", + url: body.ui.action, + }).then((body: any) => { + expect(body.session.identity.traits.email).to.contain(email) + return body + }) + }) +}) + +Cypress.Commands.add("recoverApi", ({ email, returnTo }) => { + let url = APP_URL + "/self-service/recovery/api" + if (returnTo) { + url += "?return_to=" + returnTo + } + cy.request({ url }) + .then(({ body }) => { + const form = body.ui + // label should still exist after request, for more detail: #2591 + expect(form.nodes[1].meta).to.not.be.null + expect(form.nodes[1].meta.label).to.not.be.null + expect(form.nodes[1].meta.label.text).to.equal("Email") + + return cy.request({ + method: form.method, + body: mergeFields(form, { email, method: "link" }), + url: form.action, + }) + }) + .then(({ body }) => { + expect(body.state).to.contain("sent_email") + }) +}) + +Cypress.Commands.add( + "verificationApi", + ({ email, returnTo, strategy = "code" }) => { + let url = APP_URL + "/self-service/verification/api" + if (returnTo) { + url += "?return_to=" + returnTo + } + cy.request({ url }) + .then(({ body }) => { + const form = body.ui + expect(form.nodes.some((node) => node.meta?.label?.text === "Email")).to + .be.true + + return cy.request({ + method: form.method, + body: mergeFields(form, { email, method: strategy }), + url: form.action, + headers: { + Accept: "application/json", // "Emulate" an API client, as kratos responds with a redirect otherwise + }, + }) + }) + .then(({ body }) => { + expect(body.state).to.contain("sent_email") + }) + }, +) + +Cypress.Commands.add( + "verificationApiExpired", + ({ email, returnTo, strategy = "code" }) => { + cy.shortVerificationLifespan() + let url = APP_URL + "/self-service/verification/api" + if (returnTo) { + url += "?return_to=" + returnTo + } + cy.request({ url }) + .then(({ body }) => { + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { email, method: strategy }), + url: form.action, + failOnStatusCode: false, + }) + }) + .then((response) => { + expect(response.status).to.eq(410) + expect(response.body.error.reason).to.eq( + "The verification flow has expired. Redirect the user to the verification flow init endpoint to initialize a new verification flow.", + ) + expect(response.body.error.details.redirect_to).to.eq( + "http://localhost:4455/self-service/verification/browser", + ) + }) + }, +) + +Cypress.Commands.add("verificationBrowser", ({ email, returnTo }) => { + let url = APP_URL + "/self-service/verification/browser" + if (returnTo) { + url += "?return_to=" + returnTo + } + cy.request({ url }) + .then(({ body }) => { + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { email, method: "link" }), + url: form.action, + }) + }) + .then(({ body }) => { + expect(body.state).to.contain("sent_email") + }) +}) +Cypress.Commands.add("addVirtualAuthenticator", () => + cy + .task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }) + .then(() => + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }), + ), +) + +Cypress.Commands.add( + "registerOidc", + ({ + app, + email, + website, + scopes, + rememberLogin = true, + rememberConsent = true, + acceptLogin = true, + acceptConsent = true, + expectSession = true, + route = APP_URL + "/registration", + }) => { + cy.visit(route) + + cy.triggerOidc(app) + + cy.get("#username").type(email) + if (rememberLogin) { + cy.get("#remember").click() + } + if (acceptLogin) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + + if (scopes) { + scopes.forEach((scope) => { + cy.get("#" + scope).click() + }) + } else { + cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click()) + } + + if (website) { + cy.get("#website").clear().type(website) + } + + if (rememberConsent) { + cy.get("#remember").click() + } + + if (acceptConsent) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + + cy.location("pathname").should("not.include", "consent") + + if (expectSession) { + cy.getSession() + } else { + cy.noSession() + } + }, +) + +Cypress.Commands.add("shortRegisterLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.registration.lifespan = "100ms" + return config + }) +}) + +Cypress.Commands.add("longRegisterLifespan", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.flows.registration.lifespan = "1h" + return config + }) +}) + +Cypress.Commands.add("browserReturnUrlOry", ({} = {}) => { + updateConfigFile((config) => { + config.selfservice.allowed_return_urls = [ + "https://www.ory.sh/", + "https://www.example.org/", + ] + return config + }) +}) + +Cypress.Commands.add("remoteCourierRecoveryTemplates", ({} = {}) => { + updateConfigFile((config) => { + config.courier.templates = { + recovery: { + invalid: { + email: { + body: { + html: "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=", + plaintext: + "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=", + }, + subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk", + }, + }, + valid: { + email: { + body: { + html: "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=", + plaintext: + "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgp7eyAuUmVjb3ZlcnlVUkwgfX0=", + }, + subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50", + }, + }, + }, + } + return config + }) +}) + +Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({} = {}) => { + updateConfigFile((config) => { + config.courier.templates = { + recovery_code: { + invalid: { + email: { + body: { + html: "base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBIVE1M", // only + plaintext: + "base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBUWFQ=", + }, + subject: + "base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBTVUJKRUNU", + }, + }, + valid: { + email: { + body: { + html: "base://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgSFRNTA==", + plaintext: + "base64://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgVFhU", + }, + subject: + "base64://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgU1VCSkVDVA==", + }, + }, + }, + } + return config + }) +}) + +Cypress.Commands.add("resetCourierTemplates", (type) => { + updateConfigFile((config) => { + if (config?.courier?.templates && type in config.courier.templates) { + delete config.courier.templates[type] + } + return config + }) +}) + +Cypress.Commands.add( + "loginOidc", + ({ app, expectSession = true, url = APP_URL + "/login", preTriggerHook }) => { + cy.visit(url) + if (preTriggerHook) { + preTriggerHook() + } + cy.triggerOidc(app, "hydra") + cy.location("href").should("not.eq", "/consent") + 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(500) // adding arbitrary wait here. not sure if there is a better way in this case + } + cy.getSession() + } else { + cy.noSession() + } + }, +) + +Cypress.Commands.add( + "login", + ({ email, password, expectSession = true, cookieUrl = APP_URL }) => { + if (expectSession) { + console.log("Singing in user: ", { email, password }) + } else { + console.log("Attempting user sign in: ", { email, password }) + } + + const cookieDomain = new URL(cookieUrl).hostname + // Somehow, this line makes the clearCookies call work, without it, the cookies are not always cleared and the test fails randomly. Make it make sense. + cy.getCookies({ + domain: cookieDomain, + }) + cy.clearCookies({ + domain: cookieDomain, + }) + + cy.request({ + url: APP_URL + "/self-service/login/browser", + followRedirect: false, + failOnStatusCode: false, + headers: { + Accept: "application/json", + }, + }) + .then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + return cy.request({ + method: form.method, + body: mergeFields(form, { + identifier: email, + password, + method: "password", + }), + headers: { + Accept: "application/json", + }, + url: form.action, + followRedirect: false, + failOnStatusCode: false, + }) + }) + .then(({ status, body }) => { + console.log("Login sequence completed: ", { + email, + password, + expectSession, + }) + if (expectSession) { + expect(status).to.eq(200, JSON.stringify(body)) + return cy.getSession() + } else { + expect(status).to.not.eq(200, body) + return cy.noSession() + } + }) + }, +) + +Cypress.Commands.add("loginMobile", ({ email, password }) => { + cy.visit(MOBILE_URL + "/Login") + cy.get('input[data-testid="identifier"]').type(email) + cy.get('input[data-testid="password"]').type(password) + cy.get( + 'div[data-testid="field/method/password"] div[data-testid="submit-form"]', + ).click() +}) + +Cypress.Commands.add("logout", () => { + cy.getCookies({ domain: "localhost" }).then((cookies) => { + const c = cookies.find( + ({ name }) => name.indexOf("ory_kratos_session") > -1, + ) + if (c) { + cy.clearCookie(c.name, { domain: "localhost" }) + } + }) + cy.noSession() +}) + +Cypress.Commands.add( + "reauthWithOtherAccount", + ({ + previousUrl, + expect: { email, success = true }, + type: { email: temail, password: tpassword } = { + email: undefined, + password: undefined, + }, + }) => { + cy.location("pathname").should("contain", "/login") + cy.location().then((loc) => { + const uri = new URLSearchParams(loc.search) + const flow = uri.get("flow") + expect(flow).to.not.be.empty + cy.request({ + url: APP_URL + `/self-service/login/flows?id=${flow}`, + followRedirect: false, + failOnStatusCode: false, + headers: { + Accept: "application/json", + }, + }).then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + console.log(form.action) + return cy + .request({ + method: form.method, + body: mergeFields(form, { + identifier: temail || email, + password: tpassword, + method: "password", + }), + headers: { + Accept: "application/json", + ContentType: "application/json", + }, + url: form.action, + followRedirect: false, + failOnStatusCode: false, + }) + .then((res) => { + expect(res.status).to.eq(200) + cy.visit(previousUrl) + }) + }) + }) + }, +) +Cypress.Commands.add( + "reauth", + ({ + expect: { email, success = true }, + type: { email: temail, password: tpassword } = { + email: undefined, + password: undefined, + }, + }) => { + cy.location("pathname").should("contain", "/login") + cy.get('input[name="identifier"]').should("have.value", email) + if (temail) { + cy.get('input[name="identifier"]').invoke("attr", "value", temail) + } + if (tpassword) { + cy.get('input[name="password"]').clear().type(tpassword) + } + cy.longPrivilegedSessionTime() + cy.get('button[value="password"]').click() + if (success) { + cy.location("pathname").should("not.contain", "/login") + } + }, +) + +Cypress.Commands.add("deleteMail", ({ atLeast = 0 } = {}) => { + let tries = 0 + let count = 0 + const req = () => + cy + .request("DELETE", `${MAIL_API}/mail`, { pruneCode: "all" }) + .then(({ body }) => { + count += parseInt(body) + if (count < atLeast && tries < 100) { + cy.log( + `Expected at least ${atLeast} messages but deleteted only ${count} so far (body: ${body})`, + ) + tries++ + cy.wait(pollInterval) + return req() + } + + return Promise.resolve() + }) + + try { + return req() + } catch (e) { + cy.log(e) + // just retry, since retry logic is already implemented in req() and will abort after enough tries + return req() + } +}) + +Cypress.Commands.add( + "getSession", + ({ expectAal = "aal1", expectMethods = [], token } = {}) => { + // Poll /sessions/whoami until it returns 200. cy.request does not retry on + // its own and chaining .its("status").should("eq", 200) does not re-invoke + // the request — it only re-evaluates the assertion against the cached + // response. Browser-driven flows can briefly land in a state where the + // session cookie has been issued but not yet propagated to Cypress's + // cookie jar; that's the race that makes the bare cy.request approach + // flaky on slow runners. + const buildRequest = () => ({ + method: "GET" as const, + url: `${KRATOS_PUBLIC}/sessions/whoami`, + failOnStatusCode: false, + ...(token && { + auth: { + bearer: token, + }, + }), + }) + + const waitForSession = ( + remaining: number, + ): Cypress.Chainable> => + cy.request(buildRequest()).then((resp) => { + if (resp.status === 200) { + return cy.wrap(resp, { log: false }) + } + if (remaining <= 0) { + throw new Error( + `cy.getSession: /sessions/whoami still ${resp.status} after retries`, + ) + } + cy.wait(250, { log: false }) + return waitForSession(remaining - 1) + }) + + // 20 retries × 250ms = 5s budget for the session cookie to land in + // Cypress's jar, then assertions on the response. + return waitForSession(20).then((response) => { + expect(response.body.id).to.not.be.empty + expect(dayjs().isBefore(dayjs(response.body.expires_at))).to.be.true + + // Add a grace second for MySQL which does not support millisecs. + expect(dayjs().isAfter(dayjs(response.body.issued_at).subtract(1, "s"))) + .to.be.true + expect( + dayjs().isAfter(dayjs(response.body.authenticated_at).subtract(1, "s")), + ).to.be.true + + expect(response.body.identity).to.exist + + expect(response.body.authenticator_assurance_level).to.equal(expectAal) + if (expectMethods.length > 0) { + expect(response.body.authentication_methods).to.have.lengthOf( + expectMethods.length, + ) + expectMethods.forEach((value) => { + expect( + response.body.authentication_methods.find( + ({ method }) => method === value, + ), + ).to.exist + }) + } + + return response.body + }) + }, +) + +Cypress.Commands.add("noSession", () => + cy + .request({ + method: "GET", + url: `${KRATOS_PUBLIC}/sessions/whoami`, + failOnStatusCode: false, + }) + .then((request) => { + expect(request.status).to.eq(401) + return request + }), +) + +Cypress.Commands.add( + "performEmailVerification", + ({ expect: { email, redirectTo }, strategy = "code" }) => { + cy.getMail({ + email, + body: "Verify your account", + }).then((message) => { + expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + + const link = parseHtml(message.body).querySelector("a") + expect(link).to.not.be.null + expect(link.href).to.contain(APP_URL) + const params = new URL(link.href).searchParams + + cy.visit(link.href) + if (strategy === "code") { + const code = params.get("code") + expect(code).to.not.be.null + cy.get(`button[name="method"][value="code"]`).click() + } + + if (redirectTo) { + cy.get(`[data-testid="node/anchor/continue"]`) + .contains("Continue") + .click() + cy.url().should("be.equal", redirectTo) + } + }) + }, +) + +Cypress.Commands.add( + "verifyEmail", + ({ expect: { email, password, redirectTo }, strategy }) => { + cy.performEmailVerification({ + expect: { email, redirectTo }, + strategy, + }).then(() => { + cy.getSession().should((session) => + assertVerifiableAddress({ email, isVerified: true })(session), + ) + }) + }, +) + +// Uses the verification email but waits so that it expires +Cypress.Commands.add("recoverEmailButExpired", ({ expect: { email } }) => { + cy.getMail({ + removeMail: true, + email, + subject: "Recover access to your account", + }).then((message) => { + const link = parseHtml(message.body).querySelector("a") + expect(link).to.not.be.null + expect(link.href).to.contain(APP_URL) + + cy.visit(link.href) + }) +}) + +Cypress.Commands.add( + "recoveryEmailWithCode", + ({ expect: { email, enterCode = true } }) => { + cy.getMail({ + removeMail: true, + email, + body: "Recover access to your account", + }) + .then((message) => extractOTPCode(message.body)) + .then((code) => { + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + cy.wrap(code).as("recoveryCode") + if (enterCode) { + cy.get("input[name='code']").type(code) + } + }) + }, +) + +Cypress.Commands.add( + "recoverEmail", + ({ expect: { email }, shouldVisit = true }) => + cy + .getMail({ + removeMail: true, + email, + subject: "Recover access to your account", + }) + .then((message) => { + expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + + const link = parseHtml(message.body).querySelector("a") + expect(link).to.not.be.null + expect(link.href).to.contain(APP_URL) + + if (shouldVisit) { + cy.visit(link.href) + } + }), +) + +// Uses the verification email but waits so that it expires +Cypress.Commands.add( + "verifyEmailButExpired", + ({ expect: { email }, strategy = "code" }) => { + cy.getMail({ + removeMail: true, + email, + body: "Verify your account", + }).then((message) => { + expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") + expect(message.toAddresses).to.have.length(1) + expect(message.toAddresses[0].trim()).to.equal(email) + + const link = parseHtml(message.body).querySelector("a") + cy.getSession().should((session) => { + assertVerifiableAddress({ + isVerified: false, + email: email, + })(session) + }) + + cy.visit(link.href) + if (strategy === "code") { + cy.get('button[name="method"][value="code"]').click() + cy.get('[data-testid="ui/message/4070006"]').should( + "contain.text", + "The verification code is invalid or has already been used", + ) + } else { + cy.get('[data-testid="ui/message/4070005"]').should( + "contain.text", + "verification flow expired", + ) + } + cy.location("pathname").should("include", "verification") + + cy.getSession().should((session) => { + assertVerifiableAddress({ + isVerified: false, + email: email, + })(session) + }) + }) + }, +) + +Cypress.Commands.add("useLookupSecrets", (value: boolean) => { + cy.updateConfigFile((config) => { + config.selfservice.methods = { + ...config.selfservice.methods, + lookup_secret: { + enabled: value, + }, + } + return config + }) +}) + +Cypress.Commands.add("getLookupSecrets", () => + cy + .get('[data-testid="node/text/lookup_secret_codes/text"] code') + .then(($e) => $e.map((_, e) => e.innerText.trim()).toArray()), +) +Cypress.Commands.add("expectSettingsSaved", () => { + cy.get('[data-testid="ui/message/1050001"]').should( + "contain.text", + "Your changes have been saved", + ) +}) + +Cypress.Commands.add( + "getMail", + ({ + removeMail = true, + expectedCount = 1, + email = undefined, + subject = undefined, + body = undefined, + }) => { + let tries = 0 + const req = () => + cy + .request(`${MAIL_API}/mail`) + .then((response: Cypress.Response<{ mailItems: MailMessage[] }>) => { + expect(response.body).to.have.property("mailItems") + let count = response.body.mailItems.length + if (count === 0 && tries < 100) { + tries++ + cy.wait(pollInterval) + return req() + } + + let mailItem: MailMessage + + if (!subject && !email) { + expect(count).to.equal(expectedCount) + mailItem = response.body.mailItems[0] + } else { + const filters: ((m: MailMessage) => boolean)[] = [] + if (email) { + filters.push((m: MailMessage) => m.toAddresses.includes(email)) + } + if (subject) { + filters.push((m: MailMessage) => m.subject.includes(subject)) + } + if (body) { + filters.push((m: MailMessage) => m.body.includes(body)) + } + const filtered = response.body.mailItems.filter((m) => { + return filters.every((f) => f(m)) + }) + + if (filtered.length === 0) { + tries++ + cy.wait(pollInterval) + return req() + } + + expect(filtered.length).to.equal(expectedCount) + mailItem = filtered[0] + + if (removeMail) { + return cy.deleteMail({ atLeast: count }).then(() => { + return Promise.resolve(mailItem) + }) + } + } + + return Promise.resolve(mailItem) + }) + + return req() + }, +) + +Cypress.Commands.add("submitPasswordForm", () => { + cy.get('[name="method"][value="password"]').click() + cy.get('[name="method"][value="password"]: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) => { + if (app === "mobile") { + cy.get('[data-testid="field/method/code"]').click() + cy.get('[data-testid="field/method/code"]:disabled').should("not.exist") + } else { + cy.get('button[name="method"][value="code"]').click() + cy.get('button[name="method"][value="code"]:disabled').should("not.exist") + } +}) + +Cypress.Commands.add("clickWebAuthButton", (type: string) => { + cy.get('*[data-testid="node/script/webauthn_script"]').should("exist") + cy.wait(500) // Wait for script to load + cy.get('*[name="webauthn_' + type + '_trigger"]').click() + cy.wait(500) // Wait webauth to pass +}) + +Cypress.Commands.add("shouldShow2FAScreen", () => { + cy.location().should((loc) => { + expect(loc.pathname).to.include("/login") + }) + cy.get("h2") + .invoke("text") + .should("match", /Second factor authentication|Two-Factor Authentication/) + cy.get('[data-testid="ui/message/1010004"]').should( + "contain.text", + "Please complete the second authentication challenge.", + ) +}) + +Cypress.Commands.add( + "shouldErrorOnDisallowedReturnTo", + (init: string, { app }: { app: "express" | "react" }) => { + cy.visit(init, { failOnStatusCode: false }) + if (app === "react") { + cy.location("href").should("include", init.split("?")[0]) + cy.get(".Toastify").should( + "contain.text", + "The return_to address is not allowed.", + ) + } else { + cy.location("pathname").should("contain", "error") + cy.get("div").should( + "contain.text", + 'Requested return_to URL "https://not-allowed" is not allowed.', + ) + } + }, +) + +Cypress.Commands.add( + "shouldHaveCsrfError", + ({ app }: { app: "express" | "react" }) => { + let initial: string + let pathname: string + cy.location().should((location) => { + initial = location.search + pathname = location.pathname + }) + + cy.getCookies().then((cookies) => { + const csrf = cookies.find(({ name }) => name.indexOf("csrf") > -1) + expect(csrf).to.not.be.undefined + cy.clearCookie(csrf.name) + }) + cy.submitPasswordForm() + + // We end up at a new flow + if (app === "express") { + cy.location().should((location) => { + expect(initial).to.not.be.empty + expect(location.search).to.not.eq(initial) + }) + + cy.location("pathname").should("include", "/error") + cy.get(`div`).should("contain.text", "CSRF") + } else { + cy.location("pathname").should((got) => { + expect(got).to.eql(pathname) + }) + cy.get(".Toastify").should( + "contain.text", + "A security violation was detected, please fill out the form again.", + ) + } + }, +) + +Cypress.Commands.add( + "triggerOidc", + (app: "react" | "express", provider: string = "hydra") => { + let initial, didHaveSearch + cy.location().then((loc) => { + didHaveSearch = loc.search.length > 0 + initial = loc.pathname + loc.search + }) + cy.get('[name="provider"][value="' + provider + '"]').click() + cy.location().should((loc) => { + if (app === "express" || didHaveSearch) { + return + } + expect(loc.pathname + loc.search).not.to.eql(initial) + }) + }, +) + +Cypress.Commands.add( + "removeAttribute", + (selectors: string[], attribute: string) => { + selectors.forEach((selector) => { + cy.get(selector).then(($el) => { + $el.removeAttr(attribute) + }) + }) + }, +) + +Cypress.Commands.add( + "addInputElement", + (parent: string, attribute: string, value: string) => { + cy.get(parent).then(($el) => { + $el.append(``) + }) + }, +) + +Cypress.Commands.add( + "notifyUnknownRecipients", + (flow: "recovery" | "verification", value: boolean = true) => { + cy.updateConfigFile((config) => { + config.selfservice.flows[flow].notify_unknown_recipients = value + return config + }) + }, +) + +Cypress.Commands.add("getCourierMessages", () => { + return cy.request(KRATOS_ADMIN + "/courier/messages").then((res) => { + return res.body + }) +}) + +Cypress.Commands.add( + "enableVerificationUIAfterRegistration", + (strategy: "password" | "oidc" | "webauthn") => { + cy.updateConfigFile((config) => { + if (!config.selfservice.flows.registration.after[strategy]) { + config.selfservice.flows.registration.after = { + [strategy]: { hooks: [] }, + } + } + + const hooks = + config.selfservice.flows.registration.after[strategy].hooks || [] + config.selfservice.flows.registration.after[strategy].hooks = [ + ...hooks.filter((h) => h.hook !== "show_verification_ui"), + { hook: "show_verification_ui" }, + ] + return config + }) + }, +) + +Cypress.Commands.add("getVerificationCodeFromEmail", (email) => { + return cy + .getMail({ + removeMail: true, + email, + body: "Verify your account", + }) + .then((message) => { + expect(message.toAddresses[0].trim()).to.equal(email) + const code = extractOTPCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) + +Cypress.Commands.add("getRegistrationCodeFromEmail", (email, opts) => { + return cy + .getMail({ + removeMail: true, + email, + body: "Complete your account registration with the following code", + ...opts, + }) + .then((message) => { + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractOTPCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) + +Cypress.Commands.add("getLoginCodeFromEmail", (email, opts) => { + return cy + .getMail({ + removeMail: true, + email, + body: "Login to your account with the following code", + ...opts, + }) + .then((message) => { + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractOTPCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) + +Cypress.Commands.add("useConfig", (cb) => { + cy.updateConfigFile((config) => { + const builder = cb(new ConfigBuilder(config)) + return builder.build() + }) +}) diff --git a/test/e2e/cypress/support/configHelpers.ts b/test/e2e/cypress/support/configHelpers.ts new file mode 100644 index 0000000..11edb03 --- /dev/null +++ b/test/e2e/cypress/support/configHelpers.ts @@ -0,0 +1,172 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { OryKratosConfiguration } from "../../shared/config" + +export class ConfigBuilder { + constructor(readonly config: OryKratosConfiguration) {} + + public build() { + return this.config + } + + public longRecoveryLifespan() { + this.config.selfservice.flows.recovery.lifespan = "1h" + return this + } + + public longLinkLifespan() { + this.config.selfservice.methods.link.config.lifespan = "1m" + return this + } + + public disableVerification() { + this.config.selfservice.flows.verification.enabled = false + 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 = {} + } + this.config.selfservice.flows.recovery.enabled = true + return this + } + + public disableRecovery() { + this.config.selfservice.flows.recovery.enabled = false + return this + } + + public enableVerification() { + this.config.selfservice.flows.verification.enabled = true + return this + } + + public useRecoveryStrategy(strategy: "link" | "code") { + if (!this.config.selfservice.flows.recovery) { + this.config.selfservice.flows.recovery = {} + } + this.config.selfservice.flows.recovery.use = strategy + if (!this.config.selfservice.methods[strategy]) { + this.config.selfservice.methods[strategy] = {} + } + this.config.selfservice.methods[strategy].enabled = true + return this + } + + public disableRecoveryStrategy(strategy: "link" | "code") { + this.config.selfservice.methods[strategy].enabled = false + return this + } + + public notifyUnknownRecipients( + flow: "recovery" | "verification", + value: boolean, + ) { + this.config.selfservice.flows[flow].notify_unknown_recipients = value + return this + } + + public longCodeLifespan() { + this.config.selfservice.methods.code.config.lifespan = "1m" + return this + } + + public shortCodeLifespan() { + this.config.selfservice.methods.code.config.lifespan = "1ms" + return this + } + + public longPrivilegedSessionTime() { + this.config.selfservice.flows.settings.privileged_session_max_age = "5m" + return this + } + + public requireStrictAal() { + this.config.selfservice.flows.settings.required_aal = "highest_available" + this.config.session.whoami.required_aal = "highest_available" + return this + } + + public enableLoginForVerifiedAddressOnly() { + this.config.selfservice.flows.login["after"] = { + password: { hooks: [{ hook: "require_verified_address" }] }, + } + return this + } + + public longVerificationLifespan() { + this.config.selfservice.flows.verification.lifespan = "1m" + return this + } + + public longLifespan(strategy: "link" | "code") { + this.config.selfservice.methods[strategy].config.lifespan = "1m" + return this + } + + public useVerificationStrategy(strategy: "link" | "code") { + this.config.selfservice.flows.verification.use = strategy + if (!this.config.selfservice.methods[strategy]) { + this.config.selfservice.methods[strategy] = {} + } + this.config.selfservice.methods[strategy].enabled = true + return this + } + + public resetCourierTemplates( + type: "recovery" | "verification" | "recovery_code" | "verification_code", + ) { + if ( + this.config.courier?.templates && + type in this.config.courier.templates + ) { + delete this.config.courier.templates[type] + } + return this + } + + public useLaxSessionAal() { + this.config.session.whoami.required_aal = "aal1" + return this + } + + public useLaxSettingsFlowAal() { + this.config.selfservice.flows.settings.required_aal = "aal1" + return this + } + + public useLaxAal() { + return this.useLaxSessionAal().useLaxSettingsFlowAal() + } + + public useHighestSessionAal() { + this.config.session.whoami.required_aal = "highest_available" + return this + } + + public useHighestSettingsFlowAal() { + this.config.selfservice.flows.settings.required_aal = "highest_available" + return this + } + + public useHighestAvailable() { + return this.useHighestSessionAal().useHighestSettingsFlowAal() + } + + public enableCode() { + this.config.selfservice.methods.code.enabled = true + return this + } + + public enableCodeMFA() { + this.config.selfservice.methods.code.mfa_enabled = true + return this + } +} diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts new file mode 100644 index 0000000..12b382a --- /dev/null +++ b/test/e2e/cypress/support/index.d.ts @@ -0,0 +1,740 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Session as KratosSession } from "@ory/kratos-client" +import { OryKratosConfiguration } from "../../shared/config" +import { ConfigBuilder } from "./configHelpers" + +export interface MailMessage { + fromAddress: string + toAddresses: Array + body: string + subject: string +} + +export type Strategy = "code" | "link" +type app = "express" | "react" + +declare global { + namespace Cypress { + interface Chainable { + /** + * Delete mails from the email server. + * + * @param options + */ + deleteMail(options: { atLeast?: boolean }): Chainable + + /** + * Adds end enables a WebAuth authenticator key. + */ + addVirtualAuthenticator(): Chainable + + /** + * Fetch the browser's Ory Session. + * + * @param opts + */ + getSession(opts?: { + expectAal?: "aal2" | "aal1" + expectMethods?: Array< + | "password" + | "webauthn" + | "lookup_secret" + | "totp" + | "code" + | "passkey" + > + token?: string + }): Chainable + + /** + * Expect that the browser has no valid Ory Kratos Cookie Session. + */ + noSession(): Chainable> + + /** + * Log a user in + * + * @param opts + */ + login(opts: { + email: string + password: string + expectSession?: boolean + cookieUrl?: string + }): Chainable> + + /** + * Sign up a user + * + * @param opts + */ + register(opts: { + email: string + password: string + query?: { [key: string]: string } + fields?: { [key: string]: any } + }): Chainable> + + /** + * Register a user with a code + * + * @param opts + */ + registerWithCode(opts: { + email: string + code?: string + traits?: { [key: string]: any } + query?: { [key: string]: string } + expectedMailCount?: number + }): Chainable> + + /** + * Updates a user's settings using an API flow + * + * @param opts + */ + settingsApi(opts: { + fields?: { [key: string]: any } + }): Chainable> + + /** + * Set the "privileged session lifespan" to a large value. + */ + longPrivilegedSessionTime(): Chainable + + /** + * Return and optionally delete an email from the fake email server. + * + * @param opts + */ + getMail(opts: { + removeMail?: boolean + expectedCount?: number + email?: string + subject?: string + body?: string + }): Chainable + + performEmailVerification(opts?: { + expect?: { email?: string; redirectTo?: string } + strategy?: Strategy + useLinkFromEmail?: boolean + }): Chainable + + /** + * Sets the Ory Kratos configuration profile. + * + * @param profile + */ + useConfigProfile(profile: string): Chainable + + /** + * Register a new user with email + password via the API. + * + * Recommended to use if you need an Ory Session Token or alternatively the user created. + * + * @param opts + */ + registerApi(opts?: { + email: string + password: string + fields: { [key: string]: string } + }): Chainable + + /** + * Submits a recovery flow via the API + * + * @param opts + */ + recoverApi(opts: { email: string; returnTo?: string }): Chainable + + /** + * Submits a verification flow via the API + * + * @param opts + */ + verificationApi(opts: { + email: string + returnTo?: string + strategy?: Strategy + }): Chainable + + /** + * Update the config file + * + * @param cb + */ + updateConfigFile(cb: (arg: OryKratosConfiguration) => any): Chainable + + /** + * Submits a verification flow via the API + * + * @param opts + */ + verificationApiExpired(opts: { + email: string + strategy?: Strategy + returnTo?: string + }): Chainable + + /** + * Sets the hook. + * + * @param hooks + */ + setupHooks( + flow: + | "registration" + | "login" + | "recovery" + | "verification" + | "settings", + phase: "before" | "after", + kind: "password" | "webauthn" | "oidc" | "code" | "passkey", + hooks: Array<{ hook: string; config?: any }>, + ): Chainable + + /** + * Sets the post registration hook. + * + * @param hooks + */ + setPostPasswordRegistrationHooks( + hooks: Array<{ hook: string; config?: any }>, + ): Chainable + + /** + * Sets the post code registration hook. + * + * @param hooks + */ + setPostCodeRegistrationHooks( + hooks: Array<{ hook: string; config?: any }>, + ): Chainable + + /** + * Submits a verification flow via the Browser + * + * @param opts + */ + verificationBrowser(opts: { + email: string + returnTo?: string + }): Chainable + + /** + * Changes the config so that the login flow lifespan is very short. + * + * + * Useful when testing expiry of login flows. + * @see longLoginLifespan() + */ + shortLoginLifespan(): Chainable + + /** + * Changes the config so that the login flow lifespan is very long. + * + * Useful when testing expiry of login flows. + * + * @see shortLoginLifespan() + */ + longLoginLifespan(): Chainable + + /** + * Change the config so that `https://www.ory.com/` is a allowed return to URL. + */ + browserReturnUrlOry(): Chainable + + /** + * Change the courier recovery invalid and valid templates to remote base64 strings + */ + remoteCourierRecoveryTemplates(): Chainable + + /** + * Resets the remote courier templates for the given template type to their default values + */ + resetCourierTemplates( + type: "recovery_code" | "recovery" | "verification", + ): Chainable + + /** + * Change the courier recovery code invalid and valid templates to remote base64 strings + */ + remoteCourierRecoveryCodeTemplates(): Chainable + + /** + * Changes the config so that the registration flow lifespan is very short. + * + * Useful when testing expiry of registration flows. + * + * @see longRegisterLifespan() + */ + shortRegisterLifespan(): Chainable + + /** + * Changes the config so that the registration flow lifespan is very long. + * + * Useful when testing expiry of registration flows. + * + * @see shortRegisterLifespan() + */ + longRegisterLifespan(): Chainable + + /** + * Changes the config so that the settings privileged lifespan is very long. + * + * Useful when testing privileged settings flows. + * + * @see longPrivilegedSessionTime() + */ + shortPrivilegedSessionTime(): Chainable + + /** + * Re-authenticates a user. + * + * @param opts + */ + reauth(opts: { + expect: { email: string; success?: boolean } + type: { email?: string; password?: string } + }): Chainable + + /** + * Change the config file to support lookup secrets + * @param value + */ + useLookupSecrets(value: boolean): Chainable + + /** + * Re-authenticates a user. + * + * @param opts + */ + reauthWithOtherAccount(opts: { + previousUrl: string + expect: { email: string; success?: boolean } + type: { email?: string; password?: string } + }): Chainable + + /** + * Do not require 2fa for /session/whoami + */ + sessionRequiresNo2fa(): Chainable + + /** + * Require 2fa for /session/whoami if available + */ + sessionRequires2fa(): Chainable + + /** + * Like sessionRequires2fa but sets this also for settings + */ + requireStrictAal(): Chainable + + /** + * Like sessionRequiresNo2fa but sets this also for settings + */ + useLaxAal(): Chainable + + /** + * Gets the lookup codes from the settings page + */ + getLookupSecrets(): Chainable> + + /** + * Expect the settings to be saved. + */ + expectSettingsSaved(): Chainable + + clearCookies( + options?: Partial, + ): Chainable + + /** + * Submits a password form by clicking the button with method=password + */ + submitPasswordForm(): Chainable + + /** + * Submits a profile form by clicking the button with method=profile + */ + submitProfileForm(app?: "mobile" | "express" | "react"): Chainable + + /** + * Submits a code form by clicking the button with method=code + */ + submitCodeForm(app: "mobile" | "express" | "react"): Chainable + + /** + * Expect a CSRF error to occur + * + * @param opts + */ + shouldHaveCsrfError(opts: { app: string }): Chainable + + /** + * Expects the app to error if a return_to is used which isn't allowed. + * + * @param init + * @param opts + */ + shouldErrorOnDisallowedReturnTo( + init: string, + opts: { app: string }, + ): Chainable + + /** + * Expect that the second factor login screen is shown + */ + shouldShow2FAScreen(): Chainable + + /** Click a webauthn button + * + * @param type + */ + clickWebAuthButton(type: "login" | "register"): Chainable + + /** + * Sign up a user using Social Sign In + * + * @param opts + */ + registerOidc(opts: { + app: app + email?: string + website?: string + scopes?: Array + rememberLogin?: boolean + rememberConsent?: boolean + acceptLogin?: boolean + acceptConsent?: boolean + expectSession?: boolean + route?: string + }): Chainable + + /** + * Sign in a user using Social Sign In + * + * @param opts + */ + loginOidc(opts: { + app: app + expectSession?: boolean + url?: string + preTriggerHook?: () => void + }): Chainable + + /** + * Triggers a Social Sign In flow for the given provider + * + * @param app + * @param provider + */ + triggerOidc(app: "react" | "express", provider?: string): Chainable + + /** + * Changes the config so that the recovery privileged lifespan is very long. + * + * Useful when testing privileged recovery flows. + * + * @see shortPrivilegedRecoveryTime() + */ + longRecoveryLifespan(): Chainable + + /** + * Changes the config so that the recovery privileged lifespan is very short. + * + * Useful when testing privileged recovery flows. + * + * @see shortPrivilegedRecoveryTime() + */ + shortRecoveryLifespan(): Chainable + + /** + * Changes the config so that the verification privileged lifespan is very long. + * + * Useful when testing recovery/verification flows. + * + * @see shortLinkLifespan() + */ + longVerificationLifespan(): Chainable + + /** + * Changes the config so that the verification privileged lifespan is very short. + * + * Useful when testing privileged verification flows. + * + * @see shortPrivilegedVerificationTime() + */ + shortVerificationLifespan(): Chainable + + /** + * Log a user out + */ + logout(): Chainable + + /** + * Deletes all mail in the mail mock server. + * + * @param opts + */ + deleteMail(opts?: { atLeast?: number }): Chainable + + /** + * Changes the config so that the link lifespan is very long. + * + * Useful when testing recovery/verification flows. + * + * @see shortLinkLifespan() + */ + longLinkLifespan(): Chainable + + /** + * Changes the config so that the link lifespan is very short. + * + * Useful when testing recovery/verification flows. + * + * @see longLinkLifespan() + */ + shortLinkLifespan(): Chainable + + /** + * Changes the config so that the code lifespan is very short. + * + * Useful when testing recovery/verification flows. + * + * @see longCodeLifespan() + */ + shortCodeLifespan(): Chainable + + /** + * Sets the `lifespan` of a strategy to 1ms (a short value) + * + * Useful to test the behavior if the subject of the strategy expired + * + * @param s the strategy + */ + shortLifespan(s: Strategy): Chainable + + /** + * Sets the `lifespan` of a strategy to 1m + * + * @param s the strategy + */ + longLifespan(s: Strategy): Chainable + + /** + * Changes the config so that the code lifespan is very long. + * + * Useful when testing recovery/verification flows. + * + * @see shortCodeLifespan() + */ + longCodeLifespan(): Chainable + + /** + * Expect a recovery email which is expired. + * + * @param opts + */ + recoverEmailButExpired(opts?: { + expect: { email: string } + }): Chainable + + /** + * Expect a recovery email with a recovery code. + * + * @param opts + */ + recoveryEmailWithCode(opts?: { + expect: { email: string; enterCode?: boolean } + }): Chainable + + /** + * Expect a verification email which is expired. + * + * @param opts + */ + verifyEmailButExpired(opts?: { + expect: { email: string } + strategy?: Strategy + }): Chainable + + /** + * Disables verification + */ + disableVerification(): Chainable + + /** + * Enables verification + */ + enableVerification(): Chainable + + /** + * Enables recovery + */ + enableRecovery(): Chainable + + /** + * Sets the recovery strategy to use + */ + useRecoveryStrategy(strategy: Strategy): Chainable + + /** + * Disables a specific recovery strategy + * + * @param strategy the recovery strategy to disable + */ + disableRecoveryStrategy(strategy: Strategy): Chainable + + /** + * Disabled recovery + */ + disableRecovery(): Chainable + + /** + * Disables registration + */ + disableRegistration(): Chainable + + /** + * Enables registration + */ + enableRegistration(): Chainable + + /** + * Expect a recovery email which is valid. + * + * @param opts + */ + recoverEmail(opts: { + expect: { email: string } + shouldVisit?: boolean + }): Chainable + + /** + * Expect a verification email which is valid. + * + * @param opts + */ + verifyEmail(opts: { + expect: { email: string; password?: string; redirectTo?: string } + strategy?: Strategy + shouldVisit?: boolean + }): Chainable + + /** + * Configures a hook which only allows verified email addresses to sign in. + */ + enableLoginForVerifiedAddressOnly(): Chainable + + /** + * Sets the value for the `notify_unknown_recipients` key for a flow + * + * @param flow the flow for which to set the config value + * @param value the value, defaults to true + */ + notifyUnknownRecipients( + flow: "recovery" | "verification", + value?: boolean, + ): Chainable + + /** + * Sign a user in via the API and return the session. + * + * @param opts + */ + loginApi(opts: { + email: string + password: string + }): Chainable<{ session: KratosSession }> + + /** + * Same as loginApi but uses dark magic to avoid cookie issues. + * + * @param opts + */ + loginApiWithoutCookies(opts: { + email: string + password: string + }): Chainable<{ session: KratosSession }> + + /** + * Which app to proxy + */ + proxy(app: "react" | "express"): Chainable + + /** + * Log a user in on mobile + * + * @param opts + */ + loginMobile(opts: { email: string; password: string }): Chainable + + /** + * Set the identity schema + * @param schema + */ + setIdentitySchema(schema: string): Chainable + + /** + * Set the default schema + * @param id + */ + setDefaultIdentitySchema(id: string): Chainable + + /** + * Remove the specified attribute from the given HTML elements + */ + removeAttribute(selectors: string[], attribute: string): Chainable + + /** + * Add an input element to the DOM as a child of the given parent + */ + addInputElement( + parent: string, + attribute: string, + value: string, + ): Chainable + + /** + * Fetches the courier messages from the admin API + */ + getCourierMessages(): Chainable< + { recipient: string; template_type: string }[] + > + + /** + * Enable the verification UI after registration hook + */ + enableVerificationUIAfterRegistration( + strategy: "password" | "oidc" | "webauthn", + ): Chainable + + /** + * Extracts a verification code from the received email + */ + getVerificationCodeFromEmail(email: string): Chainable + + /** + * Extracts a registration code from the received email + */ + getRegistrationCodeFromEmail( + email: string, + opts?: { expectedCount: number; removeMail?: boolean }, + ): Chainable + + /** + * Extracts a login code from the received email + */ + getLoginCodeFromEmail( + email: string, + opts?: { expectedCount: number }, + ): Chainable + + useConfig(cb: (config: ConfigBuilder) => ConfigBuilder): Chainable + } + } +} diff --git a/test/e2e/cypress/support/index.js b/test/e2e/cypress/support/index.js new file mode 100644 index 0000000..9f430b5 --- /dev/null +++ b/test/e2e/cypress/support/index.js @@ -0,0 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import "./commands" + +Cypress.Cookies.debug(true) diff --git a/test/e2e/cypress/tsconfig.json b/test/e2e/cypress/tsconfig.json new file mode 100644 index 0000000..259d3cf --- /dev/null +++ b/test/e2e/cypress/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es2015", "dom"], + "types": ["cypress", "node"], + "esModuleInterop": true + }, + "include": ["**/*.ts", "support/index.ts", "../shared/config.d.ts"] +} diff --git a/test/e2e/hydra-kratos-login-consent/go.mod b/test/e2e/hydra-kratos-login-consent/go.mod new file mode 100644 index 0000000..bedb3e1 --- /dev/null +++ b/test/e2e/hydra-kratos-login-consent/go.mod @@ -0,0 +1,42 @@ +module github.com/ory/kratos/test/e2e/hydra-kratos-login-consent + +go 1.26 + +require ( + github.com/ory/hydra-client-go v1.7.4 + github.com/ory/kratos-client-go v0.10.1 +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/jsonpointer v0.21.2 // 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/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test/e2e/hydra-kratos-login-consent/go.sum b/test/e2e/hydra-kratos-login-consent/go.sum new file mode 100644 index 0000000..994ed72 --- /dev/null +++ b/test/e2e/hydra-kratos-login-consent/go.sum @@ -0,0 +1,608 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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.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= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +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/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= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +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= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +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.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +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.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +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.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.21/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +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.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +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.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +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.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +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/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/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/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +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/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +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/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/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/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/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= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/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= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +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/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/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +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/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/ory/hydra-client-go v1.7.4 h1:xazbWaXCsAjRazT8EWStU6qjkT0I0EC6WtXZOGtNau4= +github.com/ory/hydra-client-go v1.7.4/go.mod h1:g1By+kj32wbTmbtBWnFV0NWDif3YBxPvse882PU912I= +github.com/ory/kratos-client-go v0.10.1 h1:kSRk+0leCJ1nPMS+FPho8b9WMzrKNpgszvta0Xo32QU= +github.com/ory/kratos-client-go v0.10.1/go.mod h1:dOQIsar76K07wMPJD/6aMhrWyY+sFGEagLDLso1CpsA= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +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/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/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= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +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= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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/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= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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/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-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-20190827160401-ba9fcec4b297/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= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +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/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-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20190215142949-d0b11bdaac8a/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-20190321052220-f7bb7a8bee54/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-20190616124812-15dcb6c0061f/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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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-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-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-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/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-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-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-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= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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-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= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +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-20200618134242-20370b0cb4b2/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/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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +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/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= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +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/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= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +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/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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +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/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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-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= +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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= +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/test/e2e/hydra-kratos-login-consent/main.go b/test/e2e/hydra-kratos-login-consent/main.go new file mode 100644 index 0000000..90b460e --- /dev/null +++ b/test/e2e/hydra-kratos-login-consent/main.go @@ -0,0 +1,190 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "os" + + "github.com/ory/hydra-client-go/client" + "github.com/ory/hydra-client-go/client/admin" + "github.com/ory/hydra-client-go/models" + kratos "github.com/ory/kratos-client-go" +) + +func check(err error) { + if err != nil { + panic(err) + } +} + +func checkReq(w http.ResponseWriter, err error) bool { + if err != nil { + http.Error(w, fmt.Sprintf("%+v", err), 500) + return false + } + return true +} + +func main() { + kratosPublicURL, err := url.Parse(cmp.Or(os.Getenv("KRATOS_PUBLIC_URL"), "http://localhost:4433")) + check(err) + adminURL, err := url.Parse(cmp.Or(os.Getenv("HYDRA_ADMIN_URL"), "http://localhost:4445")) + check(err) + hc := client.NewHTTPClientWithConfig(nil, &client.TransportConfig{Schemes: []string{adminURL.Scheme}, Host: adminURL.Host, BasePath: adminURL.Path}) + + router := http.NewServeMux() + router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`ok`)) + }) + router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { + res, err := hc.Admin.GetLoginRequest(admin.NewGetLoginRequestParams(). + WithLoginChallenge(r.URL.Query().Get("login_challenge"))) + if !checkReq(w, err) { + return + } + if *res.Payload.Skip { + res, err := hc.Admin.AcceptLoginRequest(admin.NewAcceptLoginRequestParams(). + WithLoginChallenge(r.URL.Query().Get("login_challenge")). + WithBody(&models.AcceptLoginRequest{ + Remember: true, RememberFor: 3600, + Subject: res.Payload.Subject, + })) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + return + } + + challenge := r.URL.Query().Get("login_challenge") + _, _ = fmt.Fprintf(w, ` + + + + Remember me + + + + +`, challenge) + }) + + router.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { + if !checkReq(w, r.ParseForm()) { + return + } + if r.Form.Get("action") == "accept" { + res, err := hc.Admin.AcceptLoginRequest(admin.NewAcceptLoginRequestParams(). + WithLoginChallenge(r.URL.Query().Get("login_challenge")). + WithBody(&models.AcceptLoginRequest{ + RememberFor: 3600, Remember: r.Form.Get("remember") == "true", + Subject: new(r.Form.Get("username")), + })) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + return + } + res, err := hc.Admin.RejectLoginRequest(admin.NewRejectLoginRequestParams(). + WithLoginChallenge(r.URL.Query().Get("login_challenge")). + WithBody(&models.RejectRequest{Error: "login rejected request"})) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + }) + + router.HandleFunc("GET /consent", func(w http.ResponseWriter, r *http.Request) { + res, err := hc.Admin.GetConsentRequest(admin.NewGetConsentRequestParams(). + WithConsentChallenge(r.URL.Query().Get("consent_challenge"))) + if !checkReq(w, err) { + return + } + if res.Payload.Skip { + res, err := hc.Admin.AcceptConsentRequest(admin.NewAcceptConsentRequestParams(). + WithConsentChallenge(r.URL.Query().Get("consent_challenge")). + WithBody(&models.AcceptConsentRequest{GrantScope: res.Payload.RequestedScope})) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + return + } + + checkoxes := "" + for _, s := range res.Payload.RequestedScope { + checkoxes += fmt.Sprintf(`
  • %s
  • `, s, s, s) + } + + challenge := r.URL.Query().Get("consent_challenge") + _, _ = fmt.Fprintf(w, ` + +
    +
      + %s +
    + + Remember me + + +
    + +`, challenge, checkoxes) + }) + + router.HandleFunc("POST /consent", func(w http.ResponseWriter, r *http.Request) { + _ = r.ParseForm() + if r.Form.Get("action") == "accept" { + kratosConfig := kratos.NewConfiguration() + kratosConfig.Servers = kratos.ServerConfigurations{{URL: kratosPublicURL.String()}} + kratosClient := kratos.NewAPIClient(kratosConfig) + session, _, err := kratosClient.V0alpha2Api.ToSession(r.Context()).Cookie(r.Header.Get("Cookie")).Execute() + if err != nil { + panic(err) + } + traitMap, ok := session.Identity.Traits.(map[string]interface{}) + if !ok { + panic("type assertion failed") + } + idToken := map[string]interface{}{} + // Populate ID token claims with values found in the session's traits + for _, scope := range r.Form["scope"] { + if v, ok := traitMap[scope]; ok { + idToken[scope] = v + } + } + + res, err := hc.Admin.AcceptConsentRequest(admin.NewAcceptConsentRequestParams(). + WithConsentChallenge(r.URL.Query().Get("consent_challenge")). + WithBody(&models.AcceptConsentRequest{ + Session: &models.ConsentRequestSession{IDToken: idToken}, + Remember: r.Form.Get("remember") == "true", RememberFor: 3600, + GrantScope: r.Form["scope"], + })) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + return + } + res, err := hc.Admin.RejectConsentRequest(admin.NewRejectConsentRequestParams(). + WithConsentChallenge(r.URL.Query().Get("consent_challenge")). + WithBody(&models.RejectRequest{Error: "consent rejected request"})) + if !checkReq(w, err) { + return + } + http.Redirect(w, r, *res.Payload.RedirectTo, http.StatusFound) + }) + + addr := ":" + cmp.Or(os.Getenv("PORT"), "4746") + //#nosec G112 + server := &http.Server{Addr: addr, Handler: router} + fmt.Printf("Starting web server at %s\n", addr) + check(server.ListenAndServe()) +} diff --git a/test/e2e/hydra-login-consent/go.mod b/test/e2e/hydra-login-consent/go.mod new file mode 100644 index 0000000..9bca038 --- /dev/null +++ b/test/e2e/hydra-login-consent/go.mod @@ -0,0 +1,7 @@ +module github.com/ory/kratos/test/e2e/hydra-login-consent + +go 1.26 + +require github.com/ory/hydra-client-go/v2 v2.0.3 + +require golang.org/x/oauth2 v0.34.0 // indirect diff --git a/test/e2e/hydra-login-consent/go.sum b/test/e2e/hydra-login-consent/go.sum new file mode 100644 index 0000000..ab487e3 --- /dev/null +++ b/test/e2e/hydra-login-consent/go.sum @@ -0,0 +1,359 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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.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= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +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/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= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +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= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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/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= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/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/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= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ory/hydra-client-go/v2 v2.0.3 h1:jIx968J9RBnjRuaQ21QMLCwZoa28FPvzYWAQ+88XVLw= +github.com/ory/hydra-client-go/v2 v2.0.3/go.mod h1:FRuayIF1H/HD2umlad8c3h7RuHpcmsjBDpW0/R2OQ/U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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= +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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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/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= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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-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= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +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/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-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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-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= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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/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/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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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-20190425150028-36563e24a262/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-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-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-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-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= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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-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= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +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-20200618134242-20370b0cb4b2/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/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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +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/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= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +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/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= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +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/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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +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/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= +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/test/e2e/hydra-login-consent/main.go b/test/e2e/hydra-login-consent/main.go new file mode 100644 index 0000000..158db73 --- /dev/null +++ b/test/e2e/hydra-login-consent/main.go @@ -0,0 +1,182 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "os" + + client "github.com/ory/hydra-client-go/v2" +) + +func check(err error) { + if err != nil { + panic(err) + } +} + +func checkReq(w http.ResponseWriter, err error) bool { + if err != nil { + http.Error(w, fmt.Sprintf("%+v", err), 500) + return false + } + return true +} + +func main() { + router := http.NewServeMux() + + adminURL, err := url.Parse(cmp.Or(os.Getenv("HYDRA_ADMIN_URL"), "http://localhost:4445")) + check(err) + cfg := client.NewConfiguration() + cfg.Servers = client.ServerConfigurations{ + {URL: adminURL.String()}, + } + hc := client.NewAPIClient(cfg) + + router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`ok`)) + }) + router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { + res, _, err := hc.OAuth2Api.GetOAuth2LoginRequest(r.Context()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() + if !checkReq(w, err) { + return + } + + if res.Skip { + res, _, err := hc.OAuth2Api.AcceptOAuth2LoginRequest(r.Context()). + LoginChallenge(r.URL.Query().Get("login_challenge")). + AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{ + Remember: new(true), + RememberFor: new(int64(3600)), + Subject: res.Subject, + }).Execute() + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + return + } + + challenge := r.URL.Query().Get("login_challenge") + _, _ = fmt.Fprintf(w, ` + +
    + + Remember me + + +
    + +`, challenge) + }) + + router.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { + check(r.ParseForm()) + remember := new(r.Form.Get("remember") == "true") + if r.Form.Get("action") == "accept" { + res, _, err := hc.OAuth2Api.AcceptOAuth2LoginRequest(r.Context()). + LoginChallenge(r.URL.Query().Get("login_challenge")). + AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{ + RememberFor: new(int64(3600)), + Remember: remember, + Subject: r.Form.Get("username"), + }).Execute() + + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + return + } + res, _, err := hc.OAuth2Api.RejectOAuth2LoginRequest(r.Context()).LoginChallenge(r.URL.Query().Get("login_challenge")). + RejectOAuth2Request(client.RejectOAuth2Request{Error: new("login rejected request")}).Execute() + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + }) + + router.HandleFunc("GET /consent", func(w http.ResponseWriter, r *http.Request) { + res, _, err := hc.OAuth2Api.GetOAuth2ConsentRequest(r.Context()).ConsentChallenge(r.URL.Query(). + Get("consent_challenge")).Execute() + if !checkReq(w, err) { + return + } + + if *res.Skip { + res, _, err := hc.OAuth2Api.AcceptOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.URL.Query().Get("consent_challenge")). + AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{ + GrantScope: res.RequestedScope, + }).Execute() + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + return + } + + checkoxes := "" + for _, s := range res.RequestedScope { + checkoxes += fmt.Sprintf(`
  • %s
  • `, s, s, s) + } + + challenge := r.URL.Query().Get("consent_challenge") + _, _ = fmt.Fprintf(w, ` + +
    +
      + %s +
    + + Remember me + + +
    + +`, challenge, checkoxes) + }) + + router.HandleFunc("POST /consent", func(w http.ResponseWriter, r *http.Request) { + _ = r.ParseForm() + remember := new(r.Form.Get("remember") == "true") + if r.Form.Get("action") == "accept" { + res, _, err := hc.OAuth2Api.AcceptOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.URL.Query().Get("consent_challenge")). + AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{ + Session: &client.AcceptOAuth2ConsentRequestSession{ + IdToken: map[string]interface{}{ + "website": r.Form.Get("website"), + }, + }, + RememberFor: new(int64(3600)), + Remember: remember, + GrantScope: r.Form["scope"], + }, + ).Execute() + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + return + } + res, _, err := hc.OAuth2Api.RejectOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.URL.Query().Get("consent_challenge")). + RejectOAuth2Request(client.RejectOAuth2Request{Error: new("consent rejected request")}).Execute() + if !checkReq(w, err) { + return + } + http.Redirect(w, r, res.RedirectTo, http.StatusFound) + }) + + addr := ":" + cmp.Or(os.Getenv("PORT"), "4446") + //#nosec G112 + server := &http.Server{Addr: addr, Handler: router} + fmt.Printf("Starting web server at %s\n", addr) + check(server.ListenAndServe()) +} diff --git a/test/e2e/hydra.yml b/test/e2e/hydra.yml new file mode 100644 index 0000000..d703f06 --- /dev/null +++ b/test/e2e/hydra.yml @@ -0,0 +1,17 @@ +version: v2.0.1 + +dsn: memory + +log: + level: trace + +urls: + self: + issuer: http://localhost:4444/ + public: http://localhost:4444/ + login: http://localhost:4446/login + consent: http://localhost:4446/consent + +secrets: + system: + - "1234567890123456789012345678901" diff --git a/test/e2e/mock/httptarget/go.mod b/test/e2e/mock/httptarget/go.mod new file mode 100644 index 0000000..30aeffc --- /dev/null +++ b/test/e2e/mock/httptarget/go.mod @@ -0,0 +1,3 @@ +module github.com/ory/mock + +go 1.26 diff --git a/test/e2e/mock/httptarget/go.sum b/test/e2e/mock/httptarget/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/test/e2e/mock/httptarget/main.go b/test/e2e/mock/httptarget/main.go new file mode 100644 index 0000000..72721f6 --- /dev/null +++ b/test/e2e/mock/httptarget/main.go @@ -0,0 +1,67 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "cmp" + "fmt" + "io" + "log" + "net/http" + "os" + "sync" +) + +var ( + documentsLock sync.RWMutex + documents = make(map[string][]byte) +) + +func main() { + port := cmp.Or(os.Getenv("PORT"), "4471") + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) +} + +func init() { + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + }) + + http.HandleFunc("GET /documents/{id}", func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + documentsLock.RLock() + doc, ok := documents[id] + documentsLock.RUnlock() + + if ok { + _, _ = w.Write(doc) + } else { + w.WriteHeader(http.StatusNotFound) + } + }) + + http.HandleFunc("PUT /documents/{id}", func(w http.ResponseWriter, r *http.Request) { + documentsLock.Lock() + defer documentsLock.Unlock() + id := r.PathValue("id") + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + documents[id] = body + }) + + http.HandleFunc("DELETE /documents/{id}", func(w http.ResponseWriter, r *http.Request) { + documentsLock.Lock() + defer documentsLock.Unlock() + id := r.PathValue("id") + + delete(documents, id) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/test/e2e/mock/webhook/Dockerfile b/test/e2e/mock/webhook/Dockerfile new file mode 100644 index 0000000..91faa9b --- /dev/null +++ b/test/e2e/mock/webhook/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.21-alpine AS build + +WORKDIR /build + +ADD . . + +RUN cat go.* +RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -ldflags "-s -w -extldflags '-static'" -o ./app main.go + +FROM alpine + +WORKDIR /app + +COPY --from=build /build/app ./mock + +USER 65534:65534 + +EXPOSE 4459 +ENTRYPOINT ["/app/mock"] diff --git a/test/e2e/mock/webhook/go.mod b/test/e2e/mock/webhook/go.mod new file mode 100644 index 0000000..a62e412 --- /dev/null +++ b/test/e2e/mock/webhook/go.mod @@ -0,0 +1,7 @@ +module github.com/ory/mock + +go 1.26 + +require github.com/sirupsen/logrus v1.8.3 + +require golang.org/x/sys v0.36.0 // indirect diff --git a/test/e2e/mock/webhook/go.sum b/test/e2e/mock/webhook/go.sum new file mode 100644 index 0000000..c925ce3 --- /dev/null +++ b/test/e2e/mock/webhook/go.sum @@ -0,0 +1,16 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU= +github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/e2e/mock/webhook/main.go b/test/e2e/mock/webhook/main.go new file mode 100644 index 0000000..b0487b1 --- /dev/null +++ b/test/e2e/mock/webhook/main.go @@ -0,0 +1,177 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + log "github.com/sirupsen/logrus" +) + +const AuthHeader = "X-Authorize-Request" + +type ( + // copied from selfservice/hook/web_hook.go + detailedMessage struct { + ID int `json:"id"` + Text string `json:"text"` + Type string `json:"type"` + Context json.RawMessage `json:"context,omitempty"` + } + + errorMessage struct { + InstancePtr string `json:"instance_ptr"` + Messages []detailedMessage `json:"messages"` + } + + rawHookResponse struct { + Messages []errorMessage `json:"messages"` + } + + logResponseWriter struct { + Status int + Size int + http.ResponseWriter + } +) + +// Header returns & satisfies the http.ResponseWriter interface +func (w *logResponseWriter) Header() http.Header { + return w.ResponseWriter.Header() +} + +// Write satisfies the http.ResponseWriter interface and +// captures data written, in bytes +func (w *logResponseWriter) Write(data []byte) (int, error) { + + written, err := w.ResponseWriter.Write(data) + w.Size += written + + return written, err +} + +// WriteHeader satisfies the http.ResponseWriter interface and +// allows us to catch the status code +func (w *logResponseWriter) WriteHeader(statusCode int) { + + w.Status = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +func accessLog(next http.HandlerFunc) http.HandlerFunc { + fn := func(w http.ResponseWriter, r *http.Request) { + log.WithFields(log.Fields{"application": "webhooks", "method": r.Method, "path": r.URL.Path}).Info("incoming request") + responseWriter := logResponseWriter{http.StatusOK, 0, w} + next.ServeHTTP(&responseWriter, r) + log.WithFields(log.Fields{"application": "webhooks", "status": responseWriter.Status, "size": responseWriter.Size, "path": r.URL.Path}).Info("response generated") + } + + return fn +} + +func headerAuth(next http.HandlerFunc) http.HandlerFunc { + fn := func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(AuthHeader) != "1" { + w.WriteHeader(http.StatusUnauthorized) + } else { + next.ServeHTTP(w, r) + } + } + + return fn +} + +func healthCheck(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("OK")) +} + +func webhookHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusBadRequest) + return + } + + payload := struct { + IdentityId string `json:"identity_id,omitempty"` + Email string `json:"email,omitempty"` + FlowId string `json:"flow_id"` + FlowType string `json:"flow_type"` + Context json.RawMessage `json:"ctx"` + }{} + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + log.WithError(err).Warn("could not read request body") + b := bytes.NewBufferString(fmt.Sprintf("error while reading request body: %s", err)) + _, _ = w.Write(b.Bytes()) + return + } + defer r.Body.Close() + + if err := json.Unmarshal(body, &payload); err != nil { + w.WriteHeader(http.StatusBadRequest) + log.WithError(err).Warn("could not unmarshal request JSON") + b := bytes.NewBufferString(fmt.Sprintf("error while parsing request JSON: %s", err)) + _, _ = w.Write(b.Bytes()) + return + } + + log.WithField("payload", string(body)).Info("unmarshalled request") + + if !strings.Contains(payload.Email, "_blocked@ory.sh") || payload.FlowType == "api" { + w.WriteHeader(http.StatusOK) + return + } + + w.WriteHeader(http.StatusConflict) + + detail := detailedMessage{ + ID: 1234, + Type: "error", + Text: "email could not be validated", + } + msg := errorMessage{InstancePtr: "#/traits/email", Messages: []detailedMessage{detail}} + resp := rawHookResponse{Messages: []errorMessage{msg}} + err = json.NewEncoder(w).Encode(&resp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + buff := bytes.NewBufferString(err.Error()) + _, _ = w.Write(buff.Bytes()) + return + } +} + +func writeWebhookHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusBadRequest) + return + } + + log.WithFields(log.Fields{"application": "webhooks", "path": r.URL.Path, "response": r.URL.Query().Get("response")}).Info("sending response") + _, _ = w.Write([]byte(r.URL.Query().Get("response"))) +} + +func main() { + mux := http.NewServeMux() + + mux.HandleFunc("/health", healthCheck) + mux.HandleFunc("/webhook", accessLog(headerAuth(webhookHandler))) + mux.HandleFunc("/webhook/write", accessLog(writeWebhookHandler)) + + s := http.Server{ + Addr: ":4459", + Handler: mux, + } + + err := s.ListenAndServe() + if err != nil { + panic(err) + } +} diff --git a/test/e2e/modd.conf b/test/e2e/modd.conf new file mode 100644 index 0000000..3d9b684 --- /dev/null +++ b/test/e2e/modd.conf @@ -0,0 +1,11 @@ +test/e2e/mock/webhook/*.go { + indir: ./test/e2e/mock/webhook + prep: go build -o ../../.bin/webhook . + daemon +sigterm: ../../.bin/webhook +} + +**/*.go !**/*_test.go { + prep: go build -o test/e2e/.bin/kratos . + prep: test/e2e/.bin/kratos migrate sql -e --yes + daemon +sigterm: test/e2e/.bin/kratos serve --watch-courier --dev -c test/e2e/kratos.generated.yml +} diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json new file mode 100644 index 0000000..4b4fb37 --- /dev/null +++ b/test/e2e/package-lock.json @@ -0,0 +1,3347 @@ +{ + "name": "@ory/kratos-e2e-suite", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ory/kratos-e2e-suite", + "version": "0.0.1", + "dependencies": { + "@faker-js/faker": "10.1.0", + "@types/promise-retry": "1.1.6", + "async-retry": "1.3.3", + "mailhog": "4.16.0", + "promise-retry": "2.0.1" + }, + "devDependencies": { + "@ory/kratos-client": "25.4.0", + "@playwright/test": "1.56.1", + "@types/async-retry": "1.4.9", + "@types/node": "24.10.0", + "@types/yamljs": "0.2.34", + "chrome-remote-interface": "0.33.3", + "cypress": "14.4.0", + "dayjs": "1.11.19", + "dotenv": "17.2.3", + "got": "14.6.3", + "json-schema-to-typescript": "15.0.4", + "otpauth": "9.4.1", + "phone-number-generator-js": "1.2.16", + "process": "0.11.10", + "typescript": "5.9.3", + "wait-on": "9.0.3", + "yamljs": "0.3.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@faker-js/faker": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz", + "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ory/kratos-client": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-25.4.0.tgz", + "integrity": "sha512-D2r1oXwqH+UtplbZb2ib8boutc42JFy0+HqetbS7Ao/YIcTqgOAb+2zC3bMAkyDQr+HUfxlWzlf2TNUJa8PuOg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/async-retry": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/async-retry/-/async-retry-1.4.9.tgz", + "integrity": "sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/promise-retry": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/promise-retry/-/promise-retry-1.1.6.tgz", + "integrity": "sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA==", + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "license": "MIT" + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yamljs": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.34.tgz", + "integrity": "sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.18", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz", + "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.5", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chrome-remote-interface": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", + "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "2.11.x", + "ws": "^7.2.0" + }, + "bin": { + "chrome-remote-interface": "bin/client.js" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, + "node_modules/class-validator/node_modules/libphonenumber-js": { + "version": "1.12.42", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.42.tgz", + "integrity": "sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.4.0.tgz", + "integrity": "sha512-/I59Fqxo7fqdiDi3IM2QKA65gZ7+PVejXg404/I8ZSq+NOnrmw+2pnMUJzpoNyg7KABcEBmgpkfAqhV98p7wJA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.8", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.7.1", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "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==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/form-data/node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "14.6.3", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.3.tgz", + "integrity": "sha512-u1nNamHo7oAkyTkMD7g0F0pIvW6vNAF9CCmtkh8ZQrlToKJZ5wEPQHFqt6RzaZC0rJMzH+rnjIElyTsIkT0wQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/joi": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz", + "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.10.30", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.30.tgz", + "integrity": "sha512-PLGc+xfrQrkya/YK2/5X+bPpxRmyJBHM+xxz9krUdSgk4Vs2ZwxX5/Ow0lv3r9PDlDtNWb4u+it8MY5rZ0IyGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mailhog": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/mailhog/-/mailhog-4.16.0.tgz", + "integrity": "sha512-wXrGik+0MaAy4dbYTImxa8niX9a4aRpZTzC/b1GzCvQs09khhs0aKZgHjgScakI4Y18WInDvvF48hhEz9ifN4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/otpauth": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.4.1.tgz", + "integrity": "sha512-+iVvys36CFsyXEqfNftQm1II7SW23W1wx9RwNk0Cd97lbvorqAhBDksb/0bYry087QMxjiuBS0wokdoZ0iUeAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/phone-number-generator-js": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/phone-number-generator-js/-/phone-number-generator-js-1.2.16.tgz", + "integrity": "sha512-QYbF5MtrA7N8ia+owkF220bNenxT/Uv8XcPPtKxPfWeJZMX/1Ndt8fuXMrdAlWYAU5KizS+dz4jYGvAS09A4GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-validator": "0.14.1", + "libphonenumber-js": "1.10.30", + "lodash": "4.17.21" + } + }, + "node_modules/phone-number-generator-js/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/wait-on": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", + "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.2", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/test/e2e/package.json b/test/e2e/package.json new file mode 100644 index 0000000..3eb06d8 --- /dev/null +++ b/test/e2e/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ory/kratos-e2e-suite", + "version": "0.0.1", + "scripts": { + "openapi-generator-cli": "openapi-generator-cli", + "playwright": "playwright test", + "playwright:ui": "playwright test --ui", + "test": "cypress run --browser chrome", + "test:watch": "cypress open --browser chrome", + "text-run": "exit 0", + "wait-on": "wait-on" + }, + "dependencies": { + "@faker-js/faker": "10.1.0", + "@types/promise-retry": "1.1.6", + "async-retry": "1.3.3", + "mailhog": "4.16.0", + "promise-retry": "2.0.1" + }, + "devDependencies": { + "@ory/kratos-client": "25.4.0", + "@playwright/test": "1.56.1", + "@types/async-retry": "1.4.9", + "@types/node": "24.10.0", + "@types/yamljs": "0.2.34", + "chrome-remote-interface": "0.33.3", + "cypress": "14.4.0", + "dayjs": "1.11.19", + "dotenv": "17.2.3", + "got": "14.6.3", + "json-schema-to-typescript": "15.0.4", + "otpauth": "9.4.1", + "phone-number-generator-js": "1.2.16", + "process": "0.11.10", + "typescript": "5.9.3", + "wait-on": "9.0.3", + "yamljs": "0.3.0" + }, + "overrides": { + "uuid": "14.0.0", + "qs": "6.15.2", + "axios": "1.18.1", + "brace-expansion": "1.1.16" + } +} diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts new file mode 100644 index 0000000..796de0a --- /dev/null +++ b/test/e2e/playwright.config.ts @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig, devices } from "@playwright/test" +import * as dotenv from "dotenv" + +dotenv.config({ path: __dirname + "/playwright/playwright.env" }) + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./playwright/tests", + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 1, + workers: 1, + reporter: process.env.CI ? [["github"], ["html"], ["list"]] : "html", + globalSetup: "./playwright/setup/hydra-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", + }, + + /* Configure projects for major browsers */ + projects: [ + { + 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", + }, + }, + ], + + webServer: [ + { + command: [ + "cp test/e2e/playwright/kratos.base-config.json test/e2e/playwright/kratos.config.json", + "go run . migrate sql -e --yes", + "go run . serve --watch-courier --dev -c test/e2e/playwright/kratos.config.json", + ].join(" && "), + cwd: "../..", + url: "http://localhost:4433/health/ready", + env: { + DSN: dbToDsn(), + COURIER_SMTP_CONNECTION_URI: + "smtp://localhost:8026/?disable_starttls=true", + }, + timeout: 7 * 60 * 1000, // 7 minutes + }, + { + command: "go tool MailHog -smtp-bind-addr=localhost:8026", + cwd: "../..", + reuseExistingServer: false, + url: "http://localhost:8025/", + }, + { + command: "go run test/e2e/mock/httptarget/main.go", + cwd: "../..", + reuseExistingServer: false, + url: "http://localhost:4471/health", + }, + { + // env -u DB: Hydra v2 maps the DB env var to its `db` config key (expects an + // object), so any DB=sqlite from the shell would fail Hydra config validation. + command: "env -u DB .bin/hydra serve all -c test/e2e/hydra.yml --dev", + cwd: "../..", + reuseExistingServer: true, + url: "http://localhost:4444/health/ready", + }, + { + command: "test/e2e/hydra-login-consent/hydra-login-consent", + cwd: "../..", + reuseExistingServer: true, + url: "http://localhost:4446/", + env: { + PORT: "4446", + HYDRA_ADMIN_URL: "http://localhost:4445", + }, + }, + ], +}) + +function dbToDsn(): string { + switch (process.env.DB) { + case "postgres": + return process.env.TEST_DATABASE_POSTGRESQL! + case "cockroach": + return process.env.TEST_DATABASE_COCKROACHDB! + case "mysql": + return process.env.TEST_DATABASE_MYSQL! + case "sqlite": + return process.env.TEST_DATABASE_SQLITE! + default: + return "memory" + } +} diff --git a/test/e2e/playwright/actions/db.ts b/test/e2e/playwright/actions/db.ts new file mode 100644 index 0000000..019eceb --- /dev/null +++ b/test/e2e/playwright/actions/db.ts @@ -0,0 +1,62 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { execSync } from "child_process" + +/** + * Extracts the SQLite file path from the TEST_DATABASE_SQLITE env var. + * + * Expected format: sqlite:///path/to/db.sqlite?_fk=true + */ +export function getSqliteDbPath(): string { + const dsn = process.env["TEST_DATABASE_SQLITE"] + if (!dsn) { + throw new Error( + "TEST_DATABASE_SQLITE is not set — cannot manipulate legacy data", + ) + } + + // Strip the sqlite:// prefix and any query parameters + const withoutScheme = dsn.replace(/^sqlite:\/\//, "") + const pathOnly = withoutScheme.split("?")[0] + return pathOnly +} + +/** + * Downgrades an identity created via admin API to simulate a legacy (pre-normalization) + * record in the database. This reverts the E.164 normalization that the admin API applies + * on creation, making the DB state match what an old Kratos version would have stored. + * + * Updates: + * - identity_credential_identifiers: replaces normalized identifier with legacy format + * - identity_credentials: sets version to 0 (pre-normalization) + * - identity_verifiable_addresses: replaces normalized value with legacy format + * - identity_recovery_addresses: replaces normalized value with legacy format + */ +export function downgradeLegacyIdentity( + identityId: string, + legacyPhone: string, + normalizedPhone: string, +) { + const dbPath = getSqliteDbPath() + const escapedLegacy = legacyPhone.replace(/'/g, "''") + const escapedNormalized = normalizedPhone.replace(/'/g, "''") + + const queries = [ + // Revert credential identifiers to legacy format + `UPDATE identity_credential_identifiers SET identifier = '${escapedLegacy}' WHERE identifier = '${escapedNormalized}' AND identity_credential_id IN (SELECT id FROM identity_credentials WHERE identity_id = '${identityId}');`, + // Set credential version to 0 (pre-normalization) + `UPDATE identity_credentials SET version = 0 WHERE identity_id = '${identityId}';`, + // Revert verifiable addresses + `UPDATE identity_verifiable_addresses SET value = '${escapedLegacy}' WHERE value = '${escapedNormalized}' AND identity_id = '${identityId}';`, + // Revert recovery addresses + `UPDATE identity_recovery_addresses SET value = '${escapedLegacy}' WHERE value = '${escapedNormalized}' AND identity_id = '${identityId}';`, + ] + + for (const query of queries) { + execSync(`sqlite3 "${dbPath}" "${query}"`, { + encoding: "utf-8", + timeout: 5000, + }) + } +} diff --git a/test/e2e/playwright/actions/identity.ts b/test/e2e/playwright/actions/identity.ts new file mode 100644 index 0000000..a170c7b --- /dev/null +++ b/test/e2e/playwright/actions/identity.ts @@ -0,0 +1,100 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { faker } from "@faker-js/faker" +import { APIRequestContext } from "@playwright/test" +import { CreateIdentityBody } from "@ory/kratos-client" +import { generatePhoneNumber, CountryNames } from "phone-number-generator-js" +import { expect } from "../fixtures" + +export async function createIdentity( + request: APIRequestContext, + data: Partial, +) { + const resp = await request.post("http://localhost:4434/admin/identities", { + data, + }) + expect(resp.status()).toBe(201) + return await resp.json() +} + +export async function createIdentityWithPhoneNumber( + request: APIRequestContext, +) { + const phone = generatePhoneNumber({ + countryName: CountryNames.Germany, + withoutCountryCode: false, + }) + return { + identity: await createIdentity(request, { + schema_id: "sms", + traits: { + phone, + }, + }), + phone, + } +} + +export async function createIdentityWithEmail(request: APIRequestContext) { + const email = faker.internet.email({ provider: "ory.sh" }) + return { + identity: await createIdentity(request, { + schema_id: "email", + traits: { + email, + website: faker.internet.url(), + }, + }), + email, + } +} + +export async function createIdentityWithPhoneAndPassword( + request: APIRequestContext, + phone: string, + password: string, + schemaId = "sms-password", +) { + return { + identity: await createIdentity(request, { + schema_id: schemaId, + traits: { + phone, + }, + credentials: { + password: { + config: { + password, + }, + }, + }, + }), + phone, + password, + } +} + +export async function createIdentityWithPassword(request: APIRequestContext) { + const email = faker.internet.email({ provider: "ory.sh" }) + const password = faker.internet.password() + return { + identity: await createIdentity(request, { + schema_id: "email", + traits: { + email, + website: faker.internet.url(), + }, + + credentials: { + password: { + config: { + password, + }, + }, + }, + }), + email, + password, + } +} diff --git a/test/e2e/playwright/actions/login.ts b/test/e2e/playwright/actions/login.ts new file mode 100644 index 0000000..f26cb7e --- /dev/null +++ b/test/e2e/playwright/actions/login.ts @@ -0,0 +1,61 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIRequestContext } from "@playwright/test" +import { findCsrfToken } from "../lib/helper" +import { LoginFlow, LogoutFlow, 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() +} + +export async function logoutUrl(r: APIRequestContext, baseUrl: string) { + const { logout_url } = await expectJSONResponse( + await r.get(baseUrl + "/self-service/logout/browser", { + headers: { + Accept: "application/json", + }, + }), + { + message: "Logout failed", + }, + ) + return logout_url +} diff --git a/test/e2e/playwright/actions/mail.ts b/test/e2e/playwright/actions/mail.ts new file mode 100644 index 0000000..172c6d8 --- /dev/null +++ b/test/e2e/playwright/actions/mail.ts @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import mailhog from "mailhog" +import retry from "async-retry" + +const mh = mailhog({ + basePath: "http://localhost:8025/api", +}) + +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(query, kind) + if (res.total === 0) { + throw new Error("no emails found") + } + 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 0000000..5d77b6d --- /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/actions/webhook.ts b/test/e2e/playwright/actions/webhook.ts new file mode 100644 index 0000000..82b2511 --- /dev/null +++ b/test/e2e/playwright/actions/webhook.ts @@ -0,0 +1,57 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { request } from "@playwright/test" +import retry from "promise-retry" +import { retryOptions } from "../lib/config" + +export const WEBHOOK_TARGET = "http://127.0.0.1:4471" + +const baseUrl = WEBHOOK_TARGET + +/** + * Fetches a documented (hopefully) created by web hook + * + * @param key + */ +export async function fetchDocument(key: string) { + const r = await request.newContext() + + return retry(async (retry) => { + const res = await r.get(documentUrl(key)) + if (res.status() !== 200) { + const body = await res.text() + const message = `Expected response code 200 but received ${res.status()}: ${body}` + return retry(message) + } + return await res.json() + }, retryOptions) +} + +/** + * Fetches a documented (hopefully) created by web hook + * + * @param key + */ +export async function deleteDocument(key: string) { + const r = await request.newContext() + + return retry(async (retry) => { + const res = await r.delete(documentUrl(key)) + if (res.status() !== 204) { + const body = await res.text() + const message = `Expected response code 204 but received ${res.status()}: ${body}` + return retry(message) + } + return + }, retryOptions) +} + +/** + * Returns the URL for a specific document + * + * @param key + */ +export function documentUrl(key: string) { + return `${baseUrl}/documents/${key}` +} diff --git a/test/e2e/playwright/fixtures/index.ts b/test/e2e/playwright/fixtures/index.ts new file mode 100644 index 0000000..e407874 --- /dev/null +++ b/test/e2e/playwright/fixtures/index.ts @@ -0,0 +1,241 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Identity } from "@ory/kratos-client" +import { + APIRequestContext, + CDPSession, + expect as baseExpect, + Page, + test as base, +} 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 { 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" +import { + createIdentityWithEmail, + createIdentityWithPassword, +} from "../actions/identity" +import { randomBytes } from "crypto" + +// from https://stackoverflow.com/questions/61132262/typescript-deep-partial +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial + } + : T + +type TestFixtures = { + identity: { oryIdentity: Identity; email: string; password: string } + identityWithoutPassword: { oryIdentity: Identity; email: string } + configOverride: DeepPartial + config: OryKratosConfiguration + virtualAuthenticatorOptions: Partial + pageCDPSession: CDPSession + virtualAuthenticator: Protocol.WebAuthn.addVirtualAuthenticatorReturnValue +} + +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: {}, + config: [ + async ({ request, configOverride }, use) => { + const configToWrite = merge(default_config, configOverride) + + const revision = randomBytes(16).toString("hex") + const fileDirectory = __dirname + "/../.." + await writeFile( + fileDirectory + "/playwright/kratos.config.json", + JSON.stringify( + { + ...configToWrite, + // Forces a new hash, even if the config was not changed. + revision, + }, + null, + 2, + ), + ) + + await expect(async () => { + const resp = await request.get( + "http://localhost:4434/admin/health/config", + ) + const updatedRevision = (await resp.body()).toString() + expect(updatedRevision).toBe(revision) + }).toPass() + + await use(configToWrite) + }, + { auto: true }, + ], + 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 { + identity: oryIdentity, + password, + email, + } = await createIdentityWithPassword(request) + i.attach("identity", { + body: JSON.stringify(oryIdentity, null, 2), + contentType: "application/json", + }) + await use({ + oryIdentity, + email, + password, + }) + }, + identityWithoutPassword: async ({ request }, use, i) => { + const { identity: oryIdentity, email } = + await createIdentityWithEmail(request) + i.attach("identity", { + body: JSON.stringify(oryIdentity, null, 2), + contentType: "application/json", + }) + await use({ + oryIdentity, + email, + }) + }, + 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/fixtures/mappers/hydra-phone.jsonnet b/test/e2e/playwright/fixtures/mappers/hydra-phone.jsonnet new file mode 100644 index 0000000..7039a33 --- /dev/null +++ b/test/e2e/playwright/fixtures/mappers/hydra-phone.jsonnet @@ -0,0 +1,12 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + phone: claims.sub, + }, + }, + } diff --git a/test/e2e/playwright/fixtures/schemas/sms-oidc.ts b/test/e2e/playwright/fixtures/schemas/sms-oidc.ts new file mode 100644 index 0000000..191b794 --- /dev/null +++ b/test/e2e/playwright/fixtures/schemas/sms-oidc.ts @@ -0,0 +1,35 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export default { + $id: "https://schemas.ory.sh/presets/kratos/test/sms-oidc/identity.schema.json", + $schema: "http://json-schema.org/draft-07/schema#", + title: "Person", + type: "object", + properties: { + traits: { + type: "object", + properties: { + phone: { + type: "string", + format: "tel", + title: "Your Phone Number", + minLength: 3, + "ory.sh/kratos": { + credentials: { + code: { + identifier: true, + via: "sms", + }, + }, + verification: { + via: "sms", + }, + }, + }, + }, + required: ["phone"], + additionalProperties: false, + }, + }, +} diff --git a/test/e2e/playwright/fixtures/schemas/sms-password.ts b/test/e2e/playwright/fixtures/schemas/sms-password.ts new file mode 100644 index 0000000..f84163e --- /dev/null +++ b/test/e2e/playwright/fixtures/schemas/sms-password.ts @@ -0,0 +1,41 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export default { + $id: "https://schemas.ory.sh/presets/kratos/test/sms-password/identity.schema.json", + $schema: "http://json-schema.org/draft-07/schema#", + title: "Person", + type: "object", + properties: { + traits: { + type: "object", + properties: { + phone: { + type: "string", + format: "tel", + title: "Your Phone Number", + minLength: 3, + "ory.sh/kratos": { + credentials: { + password: { + identifier: true, + }, + code: { + identifier: true, + via: "sms", + }, + }, + verification: { + via: "sms", + }, + recovery: { + via: "sms", + }, + }, + }, + }, + required: ["phone"], + additionalProperties: false, + }, + }, +} diff --git a/test/e2e/playwright/fixtures/schemas/sms.ts b/test/e2e/playwright/fixtures/schemas/sms.ts new file mode 100644 index 0000000..0e51866 --- /dev/null +++ b/test/e2e/playwright/fixtures/schemas/sms.ts @@ -0,0 +1,35 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export default { + $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: { + phone: { + type: "string", + format: "tel", + title: "Your Phone Number", + minLength: 3, + "ory.sh/kratos": { + credentials: { + code: { + identifier: true, + via: "sms", + }, + }, + verification: { + via: "sms", + }, + }, + }, + }, + required: ["phone"], + additionalProperties: false, + }, + }, +} diff --git a/test/e2e/playwright/kratos.base-config.json b/test/e2e/playwright/kratos.base-config.json new file mode 100644 index 0000000..e9abcfd --- /dev/null +++ b/test/e2e/playwright/kratos.base-config.json @@ -0,0 +1,120 @@ +{ + "identity": { + "schemas": [ + { + "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": { + "public": { + "base_url": "http://localhost:4455/", + "cors": { + "enabled": true, + "allowed_origins": ["http://localhost:3000", "http://localhost:19006"], + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"] + } + } + }, + "log": { + "level": "trace", + "leak_sensitive_values": true + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"], + "cipher": ["secret-thirty-two-character-long"] + }, + "selfservice": { + "default_browser_return_url": "http://localhost:4455/", + "allowed_return_urls": [ + "http://localhost:4455", + "http://localhost:19006", + "https://www.ory.sh/", + "https://example.org/", + "https://www.example.org/", + "exp://example.com/my-app", + "https://example.com/my-app" + ], + "methods": { + "link": { + "config": { + "lifespan": "1h" + } + }, + "code": { + "config": { + "lifespan": "1h" + } + }, + "oidc": { + "enabled": true, + "config": { + "providers": [ + { + "id": "hydra", + "label": "Ory", + "provider": "generic", + "client_id": "client_id", + "client_secret": "client_secret", + "issuer_url": "http://localhost:4444/", + "scope": ["offline"], + "mapper_url": "file://test/e2e/profiles/oidc/hydra.jsonnet" + } + ] + } + } + }, + "flows": { + "settings": { + "privileged_session_max_age": "5m", + "ui_url": "http://localhost:4455/settings" + }, + "logout": { + "after": { + "default_browser_return_url": "http://localhost:4455/login" + } + }, + "registration": { + "ui_url": "http://localhost:4455/registration", + "after": { + "password": { + "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": { + "ui_url": "http://localhost:4455/recovery" + } + } + }, + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@localhost:8026/?skip_ssl_verify=true" + } + } +} diff --git a/test/e2e/playwright/lib/config.ts b/test/e2e/playwright/lib/config.ts new file mode 100644 index 0000000..4584d51 --- /dev/null +++ b/test/e2e/playwright/lib/config.ts @@ -0,0 +1,14 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import type { OperationOptions } from "retry" + +export type RetryOptions = OperationOptions + +export const retryOptions: RetryOptions = { + retries: 20, + factor: 1, + maxTimeout: 500, + minTimeout: 250, + randomize: false, +} diff --git a/test/e2e/playwright/lib/helper.ts b/test/e2e/playwright/lib/helper.ts new file mode 100644 index 0000000..6a654bf --- /dev/null +++ b/test/e2e/playwright/lib/helper.ts @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// 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})/ + +/** + * Extracts the recovery or verification code from a mail + * + * @param mail the mail to extract the code from + * @returns the code or null if no code was found + */ +export function extractCode(mail: Message) { + const result = codeRegex.exec(mail.html || mail.text) + if (result != null && result.length > 0) { + return result[0] + } + 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, + featureFlags, +}: { + style?: LoginFlowStyle + mitigateEnumeration?: boolean + selfservice?: Partial + featureFlags?: OryKratosConfiguration["feature_flags"] +}) => ({ + selfservice: { + default_browser_return_url: "http://localhost:4455/welcome", + ...selfservice, + flows: { + login: { + ...selfservice?.flows?.login, + style, + }, + ...selfservice?.flows, + }, + }, + security: { + account_enumeration: { + mitigate: mitigateEnumeration, + }, + }, + ...(featureFlags ? { feature_flags: featureFlags } : {}), +}) diff --git a/test/e2e/playwright/lib/request.ts b/test/e2e/playwright/lib/request.ts new file mode 100644 index 0000000..5732e5a --- /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 0000000..029458a --- /dev/null +++ b/test/e2e/playwright/models/elements/login.ts @@ -0,0 +1,269 @@ +// 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"][name="method"][value="code"]', + ) + public codeSubmitMfa = this.page.locator( + 'button[type="submit"][name="address"]', + ) + 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) + const enabled = [ + this.config.selfservice.methods.passkey?.enabled, + this.config.selfservice.methods.password?.enabled, + this.config.selfservice.methods.code?.passwordless_enabled, + this.config.selfservice.methods.webauthn?.enabled && + this.config.selfservice.methods.webauthn?.config?.passwordless, + this.config.selfservice.methods.oidc?.enabled && + this.config.selfservice.methods.oidc.config.providers.length > 0, + ].filter((e) => e) + + if (enabled.length > 1) { + await this.codeSubmit.click() + } + break + case LoginStyle.Unified: + await this.inputField("identifier").fill(identifier) + await this.codeSubmit.click() + break + } + } + + 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/models/elements/registration.ts b/test/e2e/playwright/models/elements/registration.ts new file mode 100644 index 0000000..06c9f2a --- /dev/null +++ b/test/e2e/playwright/models/elements/registration.ts @@ -0,0 +1,48 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect, Page } from "@playwright/test" +import { createInputLocator, InputLocator } from "../../selectors/input" +import { OryKratosConfiguration } from "../../../shared/config" + +export class RegistrationPage { + public identifier: InputLocator + + constructor( + readonly page: Page, + readonly config: OryKratosConfiguration, + ) { + this.identifier = createInputLocator(page, "identifier") + } + + async open() { + await Promise.all([ + this.page.goto(this.config.selfservice.flows.registration.ui_url), + this.isReady(), + this.page.waitForURL((url) => + url + .toString() + .includes(this.config.selfservice.flows.registration.ui_url), + ), + ]) + await this.isReady() + } + + inputField(name: string) { + return this.page.locator(`input[name="${name}"]`) + } + + submitField(name: string) { + return this.page.locator(`[type="submit"][name="method"][value="${name}"]`) + } + + async isReady() { + await expect(this.inputField("csrf_token").nth(0)).toBeHidden() + } + + async triggerRegistrationWithCode(identifier: string) { + await this.inputField("traits.phone").fill(identifier) + await this.submitField("profile").click() + await this.submitField("code").click() + } +} diff --git a/test/e2e/playwright/models/elements/settings.ts b/test/e2e/playwright/models/elements/settings.ts new file mode 100644 index 0000000..8229770 --- /dev/null +++ b/test/e2e/playwright/models/elements/settings.ts @@ -0,0 +1,41 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect, Page } from "@playwright/test" +import { TOTP } from "otpauth" +import { OryKratosConfiguration } from "../../../shared/config" + +export class SettingsPage { + constructor( + readonly page: Page, + readonly config: OryKratosConfiguration, + ) {} + + async isReady() { + for (const csrfInput of await this.page + .locator(`input[name="csrf_token"]`) + .all()) { + await expect(csrfInput).toHaveValue(/.+/) + } + } + + async open() { + await this.page.goto(this.config.selfservice.flows.settings.ui_url) + await this.isReady() + } + + async setupTotp() { + const totpSecret = await this.page + .getByTestId("node/text/totp_secret_key/text") + .locator("code") + .textContent() + await expect(totpSecret).toMatch(/^[A-Z2-7]{32}$/) + const totpCode = () => { + return new TOTP({ secret: totpSecret }).generate() + } + await this.page.fill("input[name=totp_code]", totpCode()) + await this.page.locator("button[name=method][value=totp]").click() + + return { totpCode } + } +} diff --git a/test/e2e/playwright/selectors/input.ts b/test/e2e/playwright/selectors/input.ts new file mode 100644 index 0000000..c4f1f35 --- /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 new file mode 100644 index 0000000..5e4f5d1 --- /dev/null +++ b/test/e2e/playwright/setup/default_config.ts @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { OryKratosConfiguration } from "../../shared/config" + +export const default_config: OryKratosConfiguration = { + dsn: "", + identity: { + schemas: [ + { + 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: { + public: { + base_url: "http://localhost:4455/", + cors: { + enabled: true, + allowed_origins: ["http://localhost:3000", "http://localhost:19006"], + allowed_headers: [ + "Authorization", + "Content-Type", + "Max-Age", + "X-Session-Token", + "X-XSRF-TOKEN", + "X-CSRF-TOKEN", + ], + }, + }, + }, + + log: { + level: "trace", + leak_sensitive_values: true, + }, + secrets: { + cookie: ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"], + cipher: ["secret-thirty-two-character-long"], + }, + selfservice: { + default_browser_return_url: "http://localhost:4455/welcome", + allowed_return_urls: [ + "http://localhost:4455", + "http://localhost:19006", + "https://www.ory.sh/", + "https://example.org/", + "https://www.example.org/", + "exp://example.com/my-app", + "https://example.com/my-app", + ], + methods: { + link: { + config: { + lifespan: "1h", + }, + }, + code: { + config: { + lifespan: "1h", + }, + }, + oidc: { + enabled: true, + config: { + providers: [ + { + id: "hydra", + label: "Ory", + provider: "generic", + client_id: process.env["OIDC_HYDRA_CLIENT_ID"], + client_secret: process.env["OIDC_HYDRA_CLIENT_SECRET"], + issuer_url: "http://localhost:4444/", + scope: ["offline"], + mapper_url: "file://test/e2e/profiles/oidc/hydra.jsonnet", + }, + ], + }, + }, + }, + + flows: { + settings: { + privileged_session_max_age: "5m", + ui_url: "http://localhost:4455/settings", + }, + logout: { + after: { + default_browser_return_url: "http://localhost:4455/login", + }, + }, + registration: { + ui_url: "http://localhost:4455/registration", + after: { + password: { + 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, + ui_url: "http://localhost:4455/recovery", + }, + }, + }, + + courier: { + smtp: { + connection_uri: "smtp://localhost:8026/?disable_starttls=true", + }, + }, +} diff --git a/test/e2e/playwright/setup/hydra-global-setup.ts b/test/e2e/playwright/setup/hydra-global-setup.ts new file mode 100644 index 0000000..9c12a5e --- /dev/null +++ b/test/e2e/playwright/setup/hydra-global-setup.ts @@ -0,0 +1,40 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { request } from "@playwright/test" + +/** + * Creates a Hydra OAuth2 client and exposes its credentials via process.env so + * that test workers see the generated client_id / client_secret when they load + * the oidcConfig constants. Playwright propagates process.env mutations from + * globalSetup to all worker processes. + */ +async function globalSetup() { + const context = await request.newContext() + try { + const resp = await context.post("http://localhost:4445/admin/clients", { + data: { + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + scope: "openid offline", + redirect_uris: [ + "http://localhost:4455/self-service/methods/oidc/callback/hydra", + ], + }, + }) + + if (resp.status() !== 201) { + throw new Error( + `Failed to create Hydra OAuth2 client: ${resp.status()} ${await resp.text()}`, + ) + } + + const client = await resp.json() + process.env["OIDC_HYDRA_CLIENT_ID"] = client.client_id + process.env["OIDC_HYDRA_CLIENT_SECRET"] = client.client_secret + } finally { + await context.dispose() + } +} + +export default globalSetup diff --git a/test/e2e/playwright/tests/desktop/code/sms.spec.ts b/test/e2e/playwright/tests/desktop/code/sms.spec.ts new file mode 100644 index 0000000..8d374c2 --- /dev/null +++ b/test/e2e/playwright/tests/desktop/code/sms.spec.ts @@ -0,0 +1,116 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import smsSchema from "../../../fixtures/schemas/sms" +import { LoginPage } from "../../../models/elements/login" +import { hasSession } from "../../../actions/session" +import { createIdentityWithPhoneNumber } from "../../../actions/identity" +import { + deleteDocument, + documentUrl, + fetchDocument, +} from "../../../actions/webhook" +import { RegistrationPage } from "../../../models/elements/registration" +import { CountryNames, generatePhoneNumber } from "phone-number-generator-js" + +const documentId = "doc-" + Math.random().toString(36).substring(7) + +test.describe("account enumeration protection off", () => { + test.use({ + configOverride: { + security: { + account_enumeration: { + enabled: false, + }, + }, + selfservice: { + flows: { + login: { + style: "unified", + }, + registration: { + after: { + code: { + hooks: [ + { + hook: "session", + }, + ], + }, + }, + }, + }, + methods: { + code: { + passwordless_enabled: true, + }, + password: { + enabled: false, + }, + }, + }, + courier: { + channels: [ + { + id: "sms", + type: "http", + request_config: { + body: "base64://ZnVuY3Rpb24oY3R4KSB7DQpjdHg6IGN0eCwNCn0=", + method: "PUT", + url: documentUrl(documentId), + }, + }, + ], + }, + identity: { + default_schema_id: "sms", + schemas: [ + { + id: "sms", + url: + "base64://" + + Buffer.from(JSON.stringify(smsSchema), "ascii").toString( + "base64", + ), + }, + ], + }, + }, + }) + + test.afterEach(async () => { + await deleteDocument(documentId) + }) + + test("login succeeds", async ({ page, config, kratosPublicURL }) => { + const identity = await createIdentityWithPhoneNumber(page.request) + + const login = new LoginPage(page, config) + await login.open() + await login.triggerLoginWithCode(identity.phone) + + const result = await fetchDocument(documentId) + await login.codeInput.input.fill(result.ctx.template_data.login_code) + await login.codeSubmit.getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) + + test("registration succeeds", async ({ page, config, kratosPublicURL }) => { + const phone = generatePhoneNumber({ + countryName: CountryNames.Germany, + withoutCountryCode: false, + }) + + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(phone) + + const result = await fetchDocument(documentId) + const code = result.ctx.template_data.registration_code + await registration.inputField("code").fill(code) + await registration.submitField("code").getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) +}) 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 0000000..c579b8a --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts @@ -0,0 +1,636 @@ +// 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" +import { SettingsPage } from "../../../models/elements/settings" +import { logoutUrl } from "../../../actions/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) + }) +}) + +for (const tc of [ + { + name: "do fast login when only code method enabled and configured, mitigation off", + methodsEnabled: ["code"], + methodsConfigured: "code", + mitigation: false, + expectFastLogin: true, + }, + { + name: "do fast login when only code method enabled and configured, mitigation on", + methodsEnabled: ["code"], + methodsConfigured: "code", + mitigation: true, + expectFastLogin: true, + }, + { + name: "do not fast login when multiple methods enabled, all configured, mitigation off", + methodsEnabled: ["password", "code"], + methodsConfigured: "all", + mitigation: false, + expectFastLogin: false, + }, + { + name: "do not fast login when multiple methods enabled, all configured, mitigation on", + methodsEnabled: ["password", "code"], + methodsConfigured: "all", + mitigation: true, + expectFastLogin: false, + }, + { + name: "do fast login when multiple methods enabled, only code configured, mitigation off", + methodsEnabled: ["password", "code"], + methodsConfigured: "code", + mitigation: false, + expectFastLogin: true, + }, + { + name: "do not fast login when multiple methods enabled, only code configured, mitigation on", + methodsEnabled: ["password", "code"], + methodsConfigured: "code", + mitigation: true, + expectFastLogin: false, + }, +]) { + test.describe(`account enumeration protection ${ + tc.mitigation ? "on" : "off" + }`, () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: tc.mitigation, + selfservice: { + methods: { + oidc: { + enabled: false, + }, + password: { + enabled: tc.methodsEnabled.includes("password"), + }, + code: { + passwordless_enabled: tc.methodsEnabled.includes("code"), + }, + }, + }, + }), + }) + + test( + tc.name, + async ({ page, config, identity, identityWithoutPassword }) => { + const id = + tc.methodsConfigured === "all" ? identity : identityWithoutPassword + + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst(id.email) + + if (tc.expectFastLogin) { + await expect(login.submitPassword).toBeHidden() + await expect( + page.locator('[data-testid="ui/message/1010014"]'), + "expect code sent message to be shown", + ).toBeVisible() + await expect(login.codeSubmit).toBeVisible() + } else { + await expect(login.submitPassword).toBeVisible() + await expect(login.codeSubmit).toBeVisible() + } + }, + ) + }) +} + +test.describe(() => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: false, + selfservice: { + methods: { + password: { + enabled: false, + }, + code: { + passwordless_enabled: true, + }, + }, + }, + featureFlags: { + refresh_login_choose_address: 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, + }) + // On a refresh login the identity is fixed by the session, so the screen + // shows a "Send code to
    " button instead of an identifier field + // (ory/kratos#4194). + await expect(login.inputField("identifier")).toBeHidden() + await login.codeSubmitMfa.click() + + 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) + }) +}) + +test.describe("second factor", () => { + for (const tc of [ + { + name: "do fast login when only code method enabled and configured", + methodsEnabled: ["code"], + methodsConfigured: "code", + expectFastLogin: true, + }, + { + name: "do not fast login when multiple methods enabled, all configured", + methodsEnabled: ["totp", "code"], + methodsConfigured: "all", + expectFastLogin: false, + }, + { + name: "do fast login when multiple methods enabled, only code configured", + methodsEnabled: ["totp", "code"], + methodsConfigured: "code", + expectFastLogin: true, + }, + ]) { + test.describe(`code mfa ${ + tc.expectFastLogin ? "with" : "without" + } fast login`, () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + selfservice: { + methods: { + oidc: { + enabled: false, + }, + password: { + enabled: true, + }, + totp: { + enabled: tc.methodsEnabled.includes("totp"), + }, + code: { + passwordless_enabled: false, + mfa_enabled: tc.methodsEnabled.includes("code"), + }, + }, + }, + }), + }) + + test(tc.name, async ({ page, config, identity, kratosPublicURL }) => { + const login = new LoginPage(page, config) + + if (tc.methodsConfigured === "all") { + await test.step("setup TOTP", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + + 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 settings = new SettingsPage(page, config) + await settings.open() + + await settings.setupTotp() + + await page.goto(await logoutUrl(page.request, kratosPublicURL)) + }) + } + + test.step("first factor authentication", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + }) + + if (tc.expectFastLogin) { + await expect(login.totpInput.input).toBeHidden() + await expect( + page.locator('[data-testid="ui/message/1010025"]'), + "expect code sent message to be shown", + ).toBeVisible() + await expect(login.codeSubmit).toBeVisible() + } else { + await expect(login.totpInput.input).toBeVisible() + await expect(login.codeSubmitMfa).toBeVisible() + } + }) + }) + } +}) + +test.describe("second factor refresh", () => { + // TOTP is enabled alongside code so the code method does not fast-login as the + // only second factor. That keeps the flow on the regular handler path, which + // renders the "Send code to
    " button this test exercises. + test.use({ + configOverride: toConfig({ + style: "identifier_first", + selfservice: { + methods: { + oidc: { + enabled: false, + }, + password: { + enabled: true, + }, + totp: { + enabled: true, + }, + code: { + passwordless_enabled: false, + mfa_enabled: true, + }, + }, + }, + }), + }) + + test("refreshes an aal2 session through the code address button", async ({ + page, + config, + identity, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + const seenCodes: string[] = [] + + await test.step("set up TOTP as a second 2FA method", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + + // The verified email already works as a code second factor, so with + // `highest_available` the flow fast-logs in to aal2 with code. Complete + // that before opening settings to set up TOTP. + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !seenCodes.some((c) => m.html.includes(c)), + }) + const code = extractCode(mails[0]) + seenCodes.push(code) + await login.codeInput.input.fill(code) + await login.codeSubmit.getByText("Continue").click() + + const settings = new SettingsPage(page, config) + await settings.open() + await settings.setupTotp() + + await page.goto(await logoutUrl(page.request, kratosPublicURL)) + }) + + const initialSession = + await test.step("log in to aal2 with the code second factor", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + + // With TOTP and code both configured, the flow escalates to aal2 and + // renders both factors instead of fast-logging in. + await expect(login.totpInput.input).toBeVisible() + await expect(login.codeSubmitMfa).toBeVisible() + + await login.codeSubmitMfa.click() + + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !seenCodes.some((c) => m.html.includes(c)), + }) + const code = extractCode(mails[0]) + seenCodes.push(code) + + await login.codeInput.input.fill(code) + await login.codeSubmit.getByText("Continue").click() + + const session = await getSession(page.request, kratosPublicURL) + expect(session.active).toBe(true) + expect(session.authenticator_assurance_level).toBe("aal2") + return session + }) + + await test.step("refresh the aal2 session", async () => { + await login.open({ + refresh: true, + aal: "aal2", + }) + + // On a refresh + aal2 flow the second-factor hydrator wins over the + // first-factor refresh branch: it renders "Send code to
    " + // buttons and never the hidden first-factor identifier field + // (ory/kratos#4194). + await expect(login.codeSubmitMfa).toBeVisible() + await expect(login.inputField("identifier")).toBeHidden() + + await login.codeSubmitMfa.click() + + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !seenCodes.some((c) => m.html.includes(c)), + }) + 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 refreshedSession = await getSession(page.request, kratosPublicURL) + expect(refreshedSession.active).toBe(true) + expect(refreshedSession.authenticator_assurance_level).toBe("aal2") + + const initialAuthAt = Date.parse(initialSession.authenticated_at) + const refreshedAuthAt = Date.parse(refreshedSession.authenticated_at) + expect(refreshedAuthAt).toBeGreaterThanOrEqual(initialAuthAt) + }) + }) + + test("rejects a foreign address on an aal2 refresh", async ({ + page, + config, + identity, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + const seenCodes: string[] = [] + + await test.step("set up TOTP as a second 2FA method", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + + // Complete the code second factor that fast-login auto-sends, then set + // up TOTP so later flows render the address button instead of fast-login. + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !seenCodes.some((c) => m.html.includes(c)), + }) + const code = extractCode(mails[0]) + seenCodes.push(code) + await login.codeInput.input.fill(code) + await login.codeSubmit.getByText("Continue").click() + + const settings = new SettingsPage(page, config) + await settings.open() + await settings.setupTotp() + + await page.goto(await logoutUrl(page.request, kratosPublicURL)) + }) + + const aal2Session = + await test.step("log in to aal2 with the code second factor", async () => { + await login.open() + await login.loginWithPassword(identity.email, identity.password) + + await login.codeSubmitMfa.click() + + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !seenCodes.some((c) => m.html.includes(c)), + }) + const code = extractCode(mails[0]) + seenCodes.push(code) + await login.codeInput.input.fill(code) + await login.codeSubmit.getByText("Continue").click() + + const session = await getSession(page.request, kratosPublicURL) + expect(session.authenticator_assurance_level).toBe("aal2") + return session + }) + + await test.step("tampering the address with a foreign identifier is rejected", async () => { + await login.open({ + refresh: true, + aal: "aal2", + }) + + // Tamper the server-rendered address button so it posts a foreign + // identifier. A refresh login must not send a code to it + // (ory/kratos#4194). + const foreignEmail = "not-" + identity.email + await login.codeSubmitMfa.evaluate( + (el, value) => el.setAttribute("value", value), + foreignEmail, + ) + await login.codeSubmitMfa.click() + + await expect( + page.locator('[data-testid="ui/message/4000035"]'), + "expect the no-code-credentials error to be shown", + ).toBeVisible() + await expect(login.codeInput.input).toBeHidden() + + // The active session is untouched: still aal2, same authentication time. + const session = await getSession(page.request, kratosPublicURL) + expect(session.active).toBe(true) + expect(session.authenticator_assurance_level).toBe("aal2") + expect(session.authenticated_at).toBe(aal2Session.authenticated_at) + }) + }) +}) 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 0000000..5568b88 --- /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 0000000..ecd4fe0 --- /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/passkeys.options.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.options.spec.ts new file mode 100644 index 0000000..904b20d --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.options.spec.ts @@ -0,0 +1,243 @@ +// 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, hasNoSession } from "../../../actions/session" +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" + +async function toggleAutomaticPresenceSimulation( + cdpSession: CDPSession, + authenticatorId: string, + enabled: boolean, +) { + await cdpSession.send("WebAuthn.setAutomaticPresenceSimulation", { + authenticatorId, + enabled, + }) +} + +async function startPasskeyRegistration(page: Page) { + 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() + return identifier +} + +async function registerWithPasskey( + page: Page, + pageCDPSession: CDPSession, + config: OryKratosConfiguration, + authenticatorId: string, +) { + return await test.step("create passkey identity", async () => { + const identifier = await startPasskeyRegistration(page) + await toggleAutomaticPresenceSimulation( + pageCDPSession, + authenticatorId, + true, + ) + await page.locator("button[name=passkey_register_trigger]").click() + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + return identifier + }) +} + +test.describe("passkey options - cross-platform attachment", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + selfservice: { + methods: { + passkey: { + enabled: true, + config: { + rp: { + display_name: "ORY", + id: "localhost", + origins: ["http://localhost:4455"], + }, + authenticator_selection: { + attachment: "cross-platform", + }, + }, + }, + }, + flows: { + registration: { + after: { + passkey: { + hooks: [{ hook: "session" }], + }, + }, + }, + }, + }, + }), + virtualAuthenticatorOptions: { + automaticPresenceSimulation: true, + hasResidentKey: true, + transport: "usb", + }, + }) + + test("registration with cross-platform authenticator succeeds", async ({ + config, + page, + pageCDPSession, + virtualAuthenticator, + kratosPublicURL, + }) => { + const identifier = await registerWithPasskey( + page, + pageCDPSession, + config, + virtualAuthenticator.authenticatorId, + ) + + await expect( + getSession(page.request, kratosPublicURL), + ).resolves.toMatchObject({ + active: true, + identity: { traits: { email: identifier } }, + }) + }) +}) + +test.describe("passkey options - user verification required", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + selfservice: { + methods: { + passkey: { + enabled: true, + config: { + rp: { + display_name: "ORY", + id: "localhost", + origins: ["http://localhost:4455"], + }, + authenticator_selection: { + user_verification: "required", + }, + }, + }, + }, + flows: { + registration: { + after: { + passkey: { + hooks: [{ hook: "session" }], + }, + }, + }, + }, + }, + }), + virtualAuthenticatorOptions: { + automaticPresenceSimulation: true, + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }) + + test("registration with user verification required succeeds", async ({ + config, + page, + pageCDPSession, + virtualAuthenticator, + kratosPublicURL, + }) => { + const identifier = await registerWithPasskey( + page, + pageCDPSession, + config, + virtualAuthenticator.authenticatorId, + ) + + await expect( + getSession(page.request, kratosPublicURL), + ).resolves.toMatchObject({ + active: true, + identity: { traits: { email: identifier } }, + }) + }) +}) + +test.describe("passkey options - user verification required, authenticator cannot verify", () => { + // Authenticator reports no UV capability, so the browser refuses to + // create a credential when the RP sets user_verification=required. + test.use({ + configOverride: toConfig({ + style: "identifier_first", + selfservice: { + methods: { + passkey: { + enabled: true, + config: { + rp: { + display_name: "ORY", + id: "localhost", + origins: ["http://localhost:4455"], + }, + authenticator_selection: { + user_verification: "required", + }, + }, + }, + }, + flows: { + registration: { + after: { + passkey: { + hooks: [{ hook: "session" }], + }, + }, + }, + }, + }, + }), + virtualAuthenticatorOptions: { + automaticPresenceSimulation: true, + hasResidentKey: true, + hasUserVerification: false, + isUserVerified: false, + }, + }) + + test("registration fails and leaves the user on the registration page", async ({ + page, + pageCDPSession, + virtualAuthenticator, + kratosPublicURL, + }) => { + await startPasskeyRegistration(page) + await toggleAutomaticPresenceSimulation( + pageCDPSession, + virtualAuthenticator.authenticatorId, + true, + ) + await page.locator("button[name=passkey_register_trigger]").click() + + // Credential creation must fail; the flow must not finalize a session. + // Give the browser time to reject the ceremony, then assert we stay on + // /registration and no session exists. + await page.waitForTimeout(1000) + await expect(page).toHaveURL(/\/registration/) + await hasNoSession(page.request, kratosPublicURL) + + // The passkey trigger must remain visible so the user can retry. + await expect( + page.locator("button[name=passkey_register_trigger]"), + ).toBeVisible() + }) +}) 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 0000000..fd6026c --- /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/desktop/profile_first/everything.registration.spec.ts b/test/e2e/playwright/tests/desktop/profile_first/everything.registration.spec.ts new file mode 100644 index 0000000..01ddab1 --- /dev/null +++ b/test/e2e/playwright/tests/desktop/profile_first/everything.registration.spec.ts @@ -0,0 +1,147 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from "@playwright/test" +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import { RegistrationPage } from "../../../models/elements/registration" +import { + OryKratosConfiguration, + RegistrationFlowStyle, + RegistrationNodeGroup, +} from "../../../../shared/config" + +const selfservice: Partial = { + methods: { + code: { + passwordless_enabled: true, + }, + password: { + enabled: true, + }, + webauthn: { + enabled: true, + config: { + passwordless: true, + rp: { id: "localhost", display_name: "Ory Kratos" }, + }, + }, + passkey: { + enabled: true, + config: { + rp: { id: "localhost", display_name: "Ory Kratos" }, + }, + }, + totp: { + enabled: true, + }, + lookup_secret: { + enabled: true, + }, + oidc: { + enabled: true, + config: { + providers: [ + { + id: "github", + provider: "github", + label: "GitHub", + client_id: "1", + client_secret: "1", + mapper_url: "base64://", + }, + { + id: "google", + provider: "google", + label: "Google", + client_id: "1", + client_secret: "1", + mapper_url: "base64://e30=", + }, + ], + }, + }, + }, +} + +test.describe("profile_first strategy with all methods enabled", () => { + ;["default", "password"].forEach((group: RegistrationNodeGroup) => { + test.describe(`password group behavior is ${group}`, () => { + ;["profile_first", "unified"].forEach((style: RegistrationFlowStyle) => { + test.describe(`registration with ${style} enabled`, () => { + ;[ + ["password"], + ["password", "webauthn"], + ["password", "code"], + ["password", "code", "webauthn"], + ["password", "code", "passkey"], + ["password", "code", "passkey", "webauthn"], + ].forEach((methods) => { + test.describe(`methods ${methods.join(", ")} enabled`, () => { + test.use({ + configOverride: { + ...toConfig({ + style: "identifier_first", + mitigateEnumeration: false, + selfservice: { + ...selfservice, + methods: { + password: { + enabled: methods.includes("password"), + }, + webauthn: { + enabled: methods.includes("webauthn"), + config: { + passwordless: true, + rp: { id: "localhost", display_name: "Ory Kratos" }, + }, + }, + passkey: { + enabled: methods.includes("passkey"), + config: { + rp: { id: "localhost", display_name: "Ory Kratos" }, + }, + }, + code: { + enabled: methods.includes("code"), + passwordless_enabled: methods.includes("code"), + }, + totp: { + enabled: false, + }, + lookup_secret: { + enabled: false, + }, + oidc: { + enabled: false, + }, + }, + flows: { + registration: { style }, + }, + }, + }), + feature_flags: { + password_profile_registration_node_group: group, + }, + }, + }) + test("registration does not have any duplicated fields when using profile first", async ({ + page, + config, + }) => { + const registration = new RegistrationPage(page, config) + await registration.open() + + await expect( + page.locator('[name="traits.email"]'), + "expect the profile form fields to not be duplicated", + ).toHaveCount(style === "profile_first" ? 1 : methods.length) + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/test/e2e/playwright/tests/desktop/regression/phone-normalization.spec.ts b/test/e2e/playwright/tests/desktop/regression/phone-normalization.spec.ts new file mode 100644 index 0000000..2f95a8a --- /dev/null +++ b/test/e2e/playwright/tests/desktop/regression/phone-normalization.spec.ts @@ -0,0 +1,769 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIRequestContext } from "@playwright/test" +import type { OryKratosConfiguration } from "../../../../shared/config" +import { downgradeLegacyIdentity } from "../../../actions/db" +import { + createIdentity, + createIdentityWithPhoneAndPassword, +} from "../../../actions/identity" +import { hasNoSession, hasSession } from "../../../actions/session" +import { + deleteDocument, + documentUrl, + fetchDocument, +} from "../../../actions/webhook" +import { expect, test } from "../../../fixtures" +import smsSchema from "../../../fixtures/schemas/sms" +import smsOidcSchema from "../../../fixtures/schemas/sms-oidc" +import smsPasswordSchema from "../../../fixtures/schemas/sms-password" +import { LoginPage } from "../../../models/elements/login" +import { RegistrationPage } from "../../../models/elements/registration" + +test.skip( + process.env.DB !== "sqlite", + "These tests rely on a SQL downgrade that only works with SQLite.", +) + +type DeepPartial = T extends object + ? { [P in keyof T]?: DeepPartial } + : T + +// Test phone numbers — deterministic so the SQL downgrade can target the known E.164 form +const DE_LEGACY = "+49 176 671 11 638" +const DE_E164 = "+4917667111638" +const DE_DASHED = "+49-176-671-11-638" + +const US_LEGACY = "+1 (415) 555-2671" +const US_E164 = "+14155552671" + +const smsDocumentId = "doc-sms-" + Math.random().toString(36).substring(7) + +function smsSchemaBase64(schema: object) { + return ( + "base64://" + + Buffer.from(JSON.stringify(schema), "ascii").toString("base64") + ) +} + +// Always register all three SMS schemas so identities from any test group +// can be listed and deleted via the admin API regardless of which schema +// is "active". Without this, cleanIdentities() silently fails when the +// current config doesn't know the schema of an existing identity. +function allSmsSchemas() { + return [ + { id: "sms" as const, url: smsSchemaBase64(smsSchema) }, + { id: "sms-password" as const, url: smsSchemaBase64(smsPasswordSchema) }, + { id: "sms-oidc" as const, url: smsSchemaBase64(smsOidcSchema) }, + // Include default and email schemas so cleanIdentities can list identities + // left over from other test specs that use these schemas. + { + id: "default" as const, + url: "file://test/e2e/profiles/oidc/identity.traits.schema.json", + }, + { + id: "email" as const, + url: "file://test/e2e/profiles/email/identity.traits.schema.json", + }, + ] +} + +function smsCourierChannels() { + return [ + { + id: "sms" as const, + type: "http" as const, + request_config: { + body: "base64://ZnVuY3Rpb24oY3R4KSB7DQpjdHg6IGN0eCwNCn0=", + method: "PUT", + url: documentUrl(smsDocumentId), + }, + }, + ] +} + +/** + * Config for code-only login flows (A, B, F tests). + * Password is disabled so the unified login page renders a single identifier + * input, avoiding the strict-mode violation caused by two `input[name=identifier]` + * elements when both code and password methods are active. + */ +function smsCodeConfig(schemaId: string): DeepPartial { + return { + security: { account_enumeration: { enabled: false } }, + selfservice: { + flows: { + login: { style: "unified" }, + registration: { + after: { code: { hooks: [{ hook: "session" }] } }, + }, + recovery: { enabled: true, use: "code" }, + }, + methods: { + code: { passwordless_enabled: true }, + password: { enabled: false }, + }, + }, + courier: { channels: smsCourierChannels() }, + identity: { default_schema_id: schemaId, schemas: allSmsSchemas() }, + } +} + +/** + * Config for password+recovery flows (C, D, E tests). + * Code is enabled (for recovery) but passwordless_enabled is false so the + * login page shows only the password form (single identifier input). + * Recovery V2 (choose_recovery_address) is enabled so that SMS phone addresses + * are supported — Recovery V1 hardcodes AddressTypeEmail and never sends SMS. + */ +function smsPasswordConfig( + schemaId: string, +): DeepPartial { + return { + security: { account_enumeration: { enabled: false } }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + feature_flags: { + // choose_recovery_address enables Recovery V2 which supports SMS phone + // addresses — Recovery V1 hardcodes AddressTypeEmail and never sends SMS. + choose_recovery_address: true, + use_continue_with_transitions: true, + } as any, + selfservice: { + flows: { + login: { style: "unified" }, + registration: { + after: { password: { hooks: [{ hook: "session" }] } }, + }, + recovery: { enabled: true, use: "code" }, + }, + methods: { + code: { enabled: true, passwordless_enabled: false }, + password: { enabled: true }, + }, + }, + courier: { channels: smsCourierChannels() }, + identity: { default_schema_id: schemaId, schemas: allSmsSchemas() }, + } +} + +/** + * Deletes all identities via the admin API. + * Called in beforeEach to ensure a clean DB state for every test, + * preventing UNIQUE constraint violations from previous test runs or retries. + */ +async function cleanIdentities(request: APIRequestContext) { + const resp = await request.get( + "http://localhost:4434/admin/identities?per_page=250", + ) + if (!resp.ok()) { + console.error( + `cleanIdentities: list failed with status ${resp.status()}: ${await resp.text()}`, + ) + return + } + const identities: Array<{ id: string }> = await resp.json() + for (const identity of identities) { + const del = await request.delete( + `http://localhost:4434/admin/identities/${identity.id}`, + ) + if (!del.ok()) { + console.error( + `cleanIdentities: delete ${identity.id} failed with status ${del.status()}`, + ) + } + } +} + +// ──────────────────────────────────────────────────────────────────── +// A. Code login (SMS) — legacy data +// ──────────────────────────────────────────────────────────────────── +test.describe("A: code login with legacy phone data", () => { + test.use({ + configOverride: smsCodeConfig("sms"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("A1: login succeeds with exact legacy format", async ({ + page, + config, + kratosPublicURL, + }) => { + // Create identity with DE_LEGACY via admin API — Kratos normalizes and stores DE_E164 + const identity = await createIdentity(page.request, { + schema_id: "sms", + traits: { phone: DE_LEGACY }, + }) + // Revert DE_E164 → DE_LEGACY to simulate a pre-normalization DB record + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + const login = new LoginPage(page, config) + await login.open() + await login.triggerLoginWithCode(DE_LEGACY) + + const result = await fetchDocument(smsDocumentId) + await login.codeInput.input.fill(result.ctx.template_data.login_code) + await Promise.all([ + page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ), + login.codeSubmit.getByText("Continue").click(), + ]) + await hasSession(page.request, kratosPublicURL) + }) + + test("A2: login fails with E.164 when DB has legacy format (known limitation)", async ({ + page, + config, + kratosPublicURL, + }) => { + // Create with legacy, store as DE_E164, then downgrade to legacy + const identity = await createIdentity(page.request, { + schema_id: "sms", + traits: { phone: DE_LEGACY }, + }) + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + const login = new LoginPage(page, config) + await login.open() + // Enter E.164 format — IN query becomes IN(DE_E164, DE_E164), no match on legacy DB row + await login.inputField("identifier").fill(DE_E164) + await Promise.all([ + page.waitForLoadState("networkidle"), + login.codeSubmit.click(), + ]) + + // No SMS code is sent because no identity was found + await hasNoSession(page.request, kratosPublicURL) + }) + + test("A3: login fails with dashed format when DB has legacy format", async ({ + page, + config, + kratosPublicURL, + }) => { + const identity = await createIdentity(page.request, { + schema_id: "sms", + traits: { phone: DE_LEGACY }, + }) + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + const login = new LoginPage(page, config) + await login.open() + await login.inputField("identifier").fill(DE_DASHED) + await Promise.all([ + page.waitForLoadState("networkidle"), + login.codeSubmit.click(), + ]) + + await hasNoSession(page.request, kratosPublicURL) + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// B. Code login (SMS) — new data (control group) +// ──────────────────────────────────────────────────────────────────── +test.describe("B: code login with newly registered phone data", () => { + test.use({ + configOverride: smsCodeConfig("sms"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("B1: register with spaces, login with E.164", async ({ + page, + config, + kratosPublicURL, + }) => { + // Register with non-normalized phone — new Kratos normalizes and stores DE_E164 + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(DE_LEGACY) + + let result = await fetchDocument(smsDocumentId) + const regCode = result.ctx.template_data.registration_code + await registration.inputField("code").fill(regCode) + await Promise.all([ + page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ), + registration.submitField("code").getByText("Continue").click(), + ]) + await hasSession(page.request, kratosPublicURL) + + // Clear session for login test + await page.context().clearCookies() + await deleteDocument(smsDocumentId) + + // Login with E.164 — should succeed because DB now stores E.164 (new normalization) + const login = new LoginPage(page, config) + await login.open() + await login.triggerLoginWithCode(DE_E164) + + result = await fetchDocument(smsDocumentId) + await login.codeInput.input.fill(result.ctx.template_data.login_code) + await login.codeSubmit.getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) + + test("B2: register with spaces, login with same spaces", async ({ + page, + config, + kratosPublicURL, + }) => { + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(DE_LEGACY) + + let result = await fetchDocument(smsDocumentId) + const regCode = result.ctx.template_data.registration_code + await registration.inputField("code").fill(regCode) + await Promise.all([ + page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ), + registration.submitField("code").getByText("Continue").click(), + ]) + await hasSession(page.request, kratosPublicURL) + + await page.context().clearCookies() + await deleteDocument(smsDocumentId) + + // Login with same spaced format — IN query's 2nd value (raw input) matches + const login = new LoginPage(page, config) + await login.open() + await login.triggerLoginWithCode(DE_LEGACY) + + result = await fetchDocument(smsDocumentId) + await login.codeInput.input.fill(result.ctx.template_data.login_code) + await login.codeSubmit.getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// C. Password login with phone — legacy data +// ──────────────────────────────────────────────────────────────────── +test.describe("C: password login with legacy phone data", () => { + const password = "vErY-s3cur3-p@ss!" + + test.use({ + configOverride: smsPasswordConfig("sms-password"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("C1: password login succeeds with exact legacy phone format", async ({ + page, + config, + kratosPublicURL, + }) => { + // Create via admin API — Kratos stores DE_E164, then downgrade to legacy + const { identity } = await createIdentityWithPhoneAndPassword( + page.request, + DE_LEGACY, + password, + "sms-password", + ) + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + const login = new LoginPage(page, config) + await login.open() + await login.loginWithPassword(DE_LEGACY, password) + await hasSession(page.request, kratosPublicURL) + }) + + test("C2: password login fails with E.164 when DB has legacy format (known limitation)", async ({ + page, + config, + kratosPublicURL, + }) => { + const { identity } = await createIdentityWithPhoneAndPassword( + page.request, + DE_LEGACY, + password, + "sms-password", + ) + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + const login = new LoginPage(page, config) + await login.open() + // E.164 input — IN query produces IN(DE_E164, DE_E164) — no match on legacy value + await login.loginWithPassword(DE_E164, password) + await page.waitForLoadState("networkidle") + await hasNoSession(page.request, kratosPublicURL) + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// D. Recovery via SMS — legacy data +// ──────────────────────────────────────────────────────────────────── +test.describe("D: SMS recovery with legacy phone data", () => { + const password = "vErY-s3cur3-p@ss!" + + test.use({ + configOverride: smsPasswordConfig("sms-password"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("D1: recovery succeeds with exact legacy phone format", async ({ + page, + config, + }) => { + const { identity } = await createIdentityWithPhoneAndPassword( + page.request, + US_LEGACY, + password, + "sms-password", + ) + downgradeLegacyIdentity(identity.id, US_LEGACY, US_E164) + + await page.goto(config.selfservice.flows!.recovery!.ui_url!) + await page.locator('input[name="recovery_address"]').fill(US_LEGACY) + await page.locator('button[name="method"][value="code"]').click() + + // Recovery code should be sent via SMS webhook + // RecoveryCodeValidModel.RecoveryCode is marshalled as "verification_code" + const result = await fetchDocument(smsDocumentId) + expect(result.ctx.template_data).toBeDefined() + expect(result.ctx.template_data.verification_code).toBeDefined() + }) + + test("D2: recovery fails with E.164 when DB has legacy format (known limitation)", async ({ + page, + config, + }) => { + const { identity } = await createIdentityWithPhoneAndPassword( + page.request, + US_LEGACY, + password, + "sms-password", + ) + downgradeLegacyIdentity(identity.id, US_LEGACY, US_E164) + + await page.goto(config.selfservice.flows!.recovery!.ui_url!) + await page.locator('input[name="recovery_address"]').fill(US_E164) + await page.locator('button[name="method"][value="code"]').click() + + // Wait for Kratos to process, then verify no SMS was sent + await page.waitForTimeout(2000) + try { + const result = await fetchDocument(smsDocumentId) + // If a document exists, recovery was triggered — unexpected for this known-limitation case + expect(result.ctx?.template_data?.verification_code).toBeUndefined() + } catch { + // fetchDocument retries and throws on 404 — no document means no SMS, which is expected + } + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// E. Verification — legacy data +// ──────────────────────────────────────────────────────────────────── +test.describe("E: verification address integrity with legacy data", () => { + test.use({ + configOverride: smsPasswordConfig("sms-password"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test("E1: legacy identity retains verification address", async ({ page }) => { + const { identity } = await createIdentityWithPhoneAndPassword( + page.request, + DE_LEGACY, + "some-password-123!", + "sms-password", + ) + downgradeLegacyIdentity(identity.id, DE_LEGACY, DE_E164) + + // Fetch the identity via admin API and verify the address is still present + const resp = await page.request.get( + `http://localhost:4434/admin/identities/${identity.id}?include_credential=password`, + ) + expect(resp.status()).toBe(200) + const fetched = await resp.json() + + // The verifiable address should still exist (even if value was reverted to legacy) + expect(fetched.verifiable_addresses).toBeDefined() + expect(fetched.verifiable_addresses.length).toBeGreaterThan(0) + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// F. Duplicate identity detection (ConflictingIdentity) +// ──────────────────────────────────────────────────────────────────── +test.describe("F: duplicate identity detection with phone normalization", () => { + test.use({ + configOverride: smsCodeConfig("sms"), + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("F1: legacy phone in DB, register with E.164 creates duplicate (known gap)", async ({ + page, + config, + kratosPublicURL, + }) => { + // Create identity with DE_LEGACY → stored as DE_E164 → then downgrade to DE_LEGACY + const legacyIdentity = await createIdentity(page.request, { + schema_id: "sms", + traits: { phone: DE_LEGACY }, + }) + downgradeLegacyIdentity(legacyIdentity.id, DE_LEGACY, DE_E164) + + // Register a new identity with DE_E164 (the E.164 form of the same number) + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(DE_E164) + + // The IN query becomes IN(DE_E164, DE_E164) which does NOT match legacy "+49 176 671 11 638" + // So no conflict is detected and a registration code is sent + const result = await fetchDocument(smsDocumentId) + expect(result.ctx.template_data.registration_code).toBeDefined() + + // Complete registration — this proves the system allowed a duplicate + await registration + .inputField("code") + .fill(result.ctx.template_data.registration_code) + await Promise.all([ + page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ), + registration.submitField("code").getByText("Continue").click(), + ]) + await hasSession(page.request, kratosPublicURL) + + // Verify two distinct identities exist for the same logical phone number — BUG + const listResp = await page.request.get( + "http://localhost:4434/admin/identities?per_page=250", + ) + const identities = await listResp.json() + const matching = identities.filter( + (i: { traits: { phone: string } }) => + i.traits.phone === DE_E164 || i.traits.phone === DE_LEGACY, + ) + // BUG: Two identities exist for the same phone number + expect(matching.length).toBe(2) + }) + + test("F2: new E.164 in DB, register with spaced format detects conflict", async ({ + page, + config, + }) => { + // Create identity with E.164 directly (modern Kratos stores this as-is) + await createIdentity(page.request, { + schema_id: "sms", + traits: { phone: DE_E164 }, + }) + + // Attempt to register with the spaced variant of the same number. + // The IN query becomes IN(DE_E164, DE_LEGACY) — DE_E164 matches the DB record. + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(DE_LEGACY) + + const result = await fetchDocument(smsDocumentId) + expect(result.ctx.template_data.registration_code).toBeDefined() + + await registration + .inputField("code") + .fill(result.ctx.template_data.registration_code) + await registration.submitField("code").getByText("Continue").click() + + // Conflict is detected: only one identity should exist + const listResp = await page.request.get( + "http://localhost:4434/admin/identities?per_page=250", + ) + const identities = await listResp.json() + const matching = identities.filter( + (i: { traits: { phone: string } }) => + i.traits.phone === DE_E164 || i.traits.phone === DE_LEGACY, + ) + expect(matching.length).toBe(1) + }) +}) + +// ──────────────────────────────────────────────────────────────────── +// G. OIDC account linking — legacy data +// ──────────────────────────────────────────────────────────────────── +const oidcConfig: DeepPartial = { + security: { + account_enumeration: { + enabled: false, + }, + }, + selfservice: { + flows: { + login: { + style: "unified", + }, + registration: { + after: { + oidc: { + hooks: [{ hook: "session" }], + }, + code: { + hooks: [{ hook: "session" }], + }, + }, + }, + }, + methods: { + code: { + passwordless_enabled: true, + }, + password: { + enabled: false, + }, + oidc: { + enabled: true, + config: { + providers: [ + { + id: "hydra", + label: "Ory", + provider: "generic", + client_id: process.env["OIDC_HYDRA_CLIENT_ID"], + client_secret: process.env["OIDC_HYDRA_CLIENT_SECRET"], + issuer_url: "http://localhost:4444/", + scope: ["offline"], + mapper_url: + "file://test/e2e/playwright/fixtures/mappers/hydra-phone.jsonnet", + }, + ], + }, + }, + }, + }, + courier: { channels: smsCourierChannels() }, + identity: { default_schema_id: "sms-oidc", schemas: allSmsSchemas() }, +} + +test.describe("G: OIDC account linking with legacy phone data", () => { + test.use({ + configOverride: oidcConfig, + }) + + test.beforeEach(async ({ page }) => { + await cleanIdentities(page.request) + }) + + test.afterEach(async () => { + await deleteDocument(smsDocumentId) + }) + + test("G1: legacy phone in DB, OIDC returns E.164 creates duplicate (known gap)", async ({ + page, + config, + kratosPublicURL, + }) => { + // Create identity with legacy phone via admin API, then downgrade to legacy format + const legacyIdentity = await createIdentity(page.request, { + schema_id: "sms-oidc", + traits: { phone: DE_LEGACY }, + }) + downgradeLegacyIdentity(legacyIdentity.id, DE_LEGACY, DE_E164) + + // Initiate OIDC registration — Hydra returns DE_E164 as sub claim + await page.goto(config.selfservice.flows!.registration!.ui_url!) + await page.locator('button[name="provider"][value="hydra"]').click() + + // Hydra login: use E.164 phone as the username (sub claim) + await page.locator("input[name=username]").fill(DE_E164) + await page.locator("button[name=action][value=accept]").click() + + // Consent screen + await page.locator("#offline").check() + await page.locator("#openid").check() + await page.locator("button[name=action][value=accept]").click() + + // ConflictingIdentity uses IN(DE_E164, DE_E164) which does NOT match legacy record + // BUG: A second identity is created instead of linking to the existing one + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + await hasSession(page.request, kratosPublicURL) + + const listResp = await page.request.get( + "http://localhost:4434/admin/identities?per_page=250", + ) + const identities = await listResp.json() + const matching = identities.filter( + (i: { traits: { phone: string } }) => + i.traits.phone === DE_E164 || i.traits.phone === DE_LEGACY, + ) + // BUG: Two identities exist for the same phone number + expect(matching.length).toBe(2) + }) + + test("G2: new E.164 in DB, OIDC registration detects conflict (no duplicate)", async ({ + page, + config, + }) => { + // Create identity with E.164 phone (modern format — no downgrade needed) + await createIdentity(page.request, { + schema_id: "sms-oidc", + traits: { phone: DE_E164 }, + }) + + // Initiate OIDC registration — Hydra returns DE_E164 as sub claim + await page.goto(config.selfservice.flows!.registration!.ui_url!) + await page.locator('button[name="provider"][value="hydra"]').click() + + await page.locator("input[name=username]").fill(DE_E164) + await page.locator("button[name=action][value=accept]").click() + + await page.locator("#offline").check() + await page.locator("#openid").check() + await page.locator("button[name=action][value=accept]").click() + + // ConflictingIdentity uses IN(DE_E164, DE_E164) which DOES match DE_E164 in DB. + // Conflict is detected → Kratos shows duplicate-credentials / account-linking + // page instead of completing registration. No session is created. + await page.waitForLoadState("networkidle") + + // Key assertion: conflict was detected, so NO duplicate identity was created. + const listResp = await page.request.get( + "http://localhost:4434/admin/identities?per_page=250", + ) + const identities = await listResp.json() + const matching = identities.filter( + (i: { traits: { phone: string } }) => i.traits.phone === DE_E164, + ) + expect(matching.length).toBe(1) + }) +}) diff --git a/test/e2e/playwright/tests/mobile/app_login.spec.ts b/test/e2e/playwright/tests/mobile/app_login.spec.ts new file mode 100644 index 0000000..6dd6a93 --- /dev/null +++ b/test/e2e/playwright/tests/mobile/app_login.spec.ts @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect, Page } from "@playwright/test" +import { test } from "../../fixtures" + +async function performOidcLogin(popup: Page, username: string) { + await popup.waitForLoadState() + + await popup.locator("#username").fill(username) + await popup.getByRole("button", { name: "login" }).click() + + await popup.locator("#offline").click() + await popup.locator("#openid").click() + await popup.locator("#website").fill("https://example.com") + await popup.getByRole("button", { name: "login" }).click() +} + +async function rejectOidcLogin(popup: Page) { + await popup.waitForLoadState() + await popup.getByRole("button", { name: "reject" }).click() + await popup.close() +} + +async function testRegistrationOrLogin(page: Page, username: string) { + const popupPromise = page.waitForEvent("popup") + await page.getByText(/sign (up|in) with ory/i).click() + const popup = await popupPromise + + await performOidcLogin(popup, username) + + await page.waitForURL("Home") + expect(popup.isClosed()).toBeTruthy() + await expect(page.getByText("Welcome back")).toBeVisible() +} + +async function logout(page: Page) { + return page.getByTestId("logout").click() +} + +async function testRegistration(page: Page, username: string) { + await page.goto("Registration") + return testRegistrationOrLogin(page, username) +} + +async function testLogin(page: Page, username: string) { + await page.goto("Login") + return testRegistrationOrLogin(page, username) +} + +test.describe("Registration", () => { + test("register twice", async ({ page }) => { + await testRegistration(page, "registration@example.com") + await logout(page) + await testRegistration(page, "registration@example.com") + }) + + test("register, then login", async ({ page }) => { + await testRegistration(page, "registration-login@example.com") + await logout(page) + await testLogin(page, "registration-login@example.com") + }) + + test("register, cancel, register", async ({ page }) => { + await page.goto("Registration") + + let popupPromise = page.waitForEvent("popup") + await page.getByText(/sign (up|in) with ory/i).click() + let popup = await popupPromise + + await rejectOidcLogin(popup) + + await expect(page.getByText("login rejected request")).toBeVisible() + popupPromise = page.waitForEvent("popup") + await page.getByText(/continue/i).click() + popup = await popupPromise + await performOidcLogin(popup, "register-reject-then-accept@example.com") + + await page.waitForURL("Home") + expect(popup.isClosed()).toBeTruthy() + await expect(page.getByText("Welcome back")).toBeVisible() + }) +}) + +test.describe("Login", () => { + test("login twice", async ({ page }) => { + await testLogin(page, "login@example.com") + await logout(page) + await testLogin(page, "login@example.com") + }) + + test("login, then register", async ({ page }) => { + await testLogin(page, "login-registration@example.com") + await logout(page) + await testRegistration(page, "login-registration@example.com") + }) + + test("login, cancel, login", async ({ page }) => { + await page.goto("Login") + + let popupPromise = page.waitForEvent("popup") + await page.getByText(/sign (up|in) with ory/i).click() + let popup = await popupPromise + + await rejectOidcLogin(popup) + + await expect(page.getByText("login rejected request")).toBeVisible() + popupPromise = page.waitForEvent("popup") + await page.getByText(/sign in with ory/i).click() + popup = await popupPromise + await performOidcLogin(popup, "login-reject-then-accept@example.com") + + await page.waitForURL("Home") + expect(popup.isClosed()).toBeTruthy() + await expect(page.getByText("Welcome back")).toBeVisible() + }) +}) diff --git a/test/e2e/playwright/tests/mobile/app_recovery.spec.ts b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts new file mode 100644 index 0000000..a14975c --- /dev/null +++ b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts @@ -0,0 +1,140 @@ +// Copyright © 2023 Ory Corp +// 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" + +const schemaConfig = { + default_schema_id: "email", + schemas: [ + { + id: "email", + url: "file://test/e2e/profiles/email/identity.traits.schema.json", + }, + ], +} + +test.describe("Recovery", () => { + test.use({ + configOverride: { + identity: { + ...schemaConfig, + }, + feature_flags: { + use_continue_with_transitions: true, + }, + }, + }) + + test("succeeds with a valid email address", async ({ page, identity }) => { + await page.goto("/Recovery") + + 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({ query: identity.email, kind: "to" }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + const wrongCode = "0" + code + + await test.step("enter wrong code", async () => { + await page.getByTestId("code").fill(wrongCode) + 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("Continue").click() + await page.waitForURL(/Settings/) + await expect(page.getByTestId("ui/message/1060001").first()).toBeVisible() + }) + }) + + test("wrong email address does not get sent", async ({ page, identity }) => { + await page.goto("/Recovery") + + 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({ query: identity.email, kind: "to" }) + expect(false).toBeTruthy() + } catch (e) { + // this is expected + } + }) + + test("fails with an invalid code", async ({ page, identity }) => { + await page.goto("/Recovery") + + await page.getByTestId("email").fill(identity.email) + await page.getByTestId("submit-form").click() + await page.getByTestId("ui/message/1060003").isVisible() + + const mails = await search({ query: identity.email, kind: "to" }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + const wrongCode = "0" + code + + await test.step("enter wrong repeatedly", async () => { + for (let i = 0; i < 10; i++) { + await page.getByTestId("code").fill(wrongCode) + 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("Continue", { exact: true }).click() + await expect(page.getByTestId("ui/message/4060006")).toBeVisible() + }) + }) + + test.describe("with short code expiration", () => { + test.use({ + configOverride: { + identity: { + ...schemaConfig, + }, + selfservice: { + methods: { + code: { + config: { + lifespan: "1ms", + }, + }, + }, + }, + feature_flags: { + use_continue_with_transitions: true, + }, + }, + }) + + test("fails with an expired code", async ({ page, identity }) => { + await page.goto("/Recovery") + + await page.getByTestId("email").fill(identity.email) + await page.getByTestId("submit-form").click() + await page.getByTestId("ui/message/1060003").isVisible() + + 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("Continue", { exact: true }).click() + await expect(page.getByTestId("ui/message/4060006")).toBeVisible() + }) + }) +}) diff --git a/test/e2e/playwright/types/index.ts b/test/e2e/playwright/types/index.ts new file mode 100644 index 0000000..ddb3431 --- /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 new file mode 100644 index 0000000..a98e497 --- /dev/null +++ b/test/e2e/profiles/code/.kratos.yml @@ -0,0 +1,48 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + logout: + after: + 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: + ui_url: http://localhost:4455/login + after: + code: + hooks: + - hook: require_verified_address + error: + ui_url: http://localhost:4455/error + verification: + enabled: true + use: code + ui_url: http://localhost:4455/verification + recovery: + enabled: true + use: code + ui_url: http://localhost:4455/recovery + methods: + password: + enabled: true + code: + passwordless_enabled: true + enabled: true + config: + lifespan: 1h + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/code/identity.traits.schema.json diff --git a/test/e2e/profiles/code/identity.code.only.traits.schema.json b/test/e2e/profiles/code/identity.code.only.traits.schema.json new file mode 100644 index 0000000..97573e0 --- /dev/null +++ b/test/e2e/profiles/code/identity.code.only.traits.schema.json @@ -0,0 +1,31 @@ +{ + "$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" + } + } + } + } + }, + "required": ["email"] + } + } +} diff --git a/test/e2e/profiles/code/identity.complex.traits.schema.json b/test/e2e/profiles/code/identity.complex.traits.schema.json new file mode 100644 index 0000000..9eda337 --- /dev/null +++ b/test/e2e/profiles/code/identity.complex.traits.schema.json @@ -0,0 +1,77 @@ +{ + "$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": { + "username": { + "type": "string", + "title": "Your Username", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "email2": { + "type": "string", + "format": "email", + "title": "Your Second E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ] + } + } +} diff --git a/test/e2e/profiles/code/identity.traits.schema.json b/test/e2e/profiles/code/identity.traits.schema.json new file mode 100644 index 0000000..ef8f80f --- /dev/null +++ b/test/e2e/profiles/code/identity.traits.schema.json @@ -0,0 +1,42 @@ +{ + "$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" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "tos": { + "type": "boolean", + "title": "Accept Terms of Service", + "description": "In order to sign up, you have to accept our terms of service." + } + }, + "required": ["email", "tos"] + } + } +} diff --git a/test/e2e/profiles/email/.kratos.yml b/test/e2e/profiles/email/.kratos.yml new file mode 100644 index 0000000..b1d62a3 --- /dev/null +++ b/test/e2e/profiles/email/.kratos.yml @@ -0,0 +1,31 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + 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 + + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verification + recovery: + ui_url: http://localhost:4455/recovery + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/email/identity.traits.schema.json diff --git a/test/e2e/profiles/email/identity.traits.schema.json b/test/e2e/profiles/email/identity.traits.schema.json new file mode 100644 index 0000000..e5d40a4 --- /dev/null +++ b/test/e2e/profiles/email/identity.traits.schema.json @@ -0,0 +1,67 @@ +{ + "$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 + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/kratos.base.yml b/test/e2e/profiles/kratos.base.yml new file mode 100644 index 0000000..1479e7f --- /dev/null +++ b/test/e2e/profiles/kratos.base.yml @@ -0,0 +1,59 @@ +log: + level: trace + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +selfservice: + default_browser_return_url: http://localhost:4455/ + 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 + +serve: + public: + base_url: http://localhost:4455/ + cors: + enabled: true + allowed_origins: + - http://localhost:3000 + - http://localhost:19006 + allowed_headers: + - Authorization + - Content-Type + - X-Session-Token + admin: + base_url: http://kratos:4434/ + +hashers: + algorithm: argon2 + argon2: + memory: 1KB + iterations: 1 + parallelism: 1 + +courier: + smtp: + connection_uri: smtps://test:test@localhost:1025/?skip_ssl_verify=true + +session: + whoami: + required_aal: aal1 + +feature_flags: + legacy_continue_with_verification_ui: true + legacy_require_verified_login_error: true + legacy_oidc_registration_node_group: true diff --git a/test/e2e/profiles/mfa-optional/.kratos.yml b/test/e2e/profiles/mfa-optional/.kratos.yml new file mode 100644 index 0000000..061f9a5 --- /dev/null +++ b/test/e2e/profiles/mfa-optional/.kratos.yml @@ -0,0 +1,53 @@ +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 + + 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: + code: + mfa_enabled: true + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + rp: + id: localhost + origin: http://localhost:4455 + display_name: Ory + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/mfa-optional/identity.traits.schema.json + +session: + whoami: + required_aal: highest_available diff --git a/test/e2e/profiles/mfa-optional/identity.traits.schema.json b/test/e2e/profiles/mfa-optional/identity.traits.schema.json new file mode 100644 index 0000000..4a8d866 --- /dev/null +++ b/test/e2e/profiles/mfa-optional/identity.traits.schema.json @@ -0,0 +1,45 @@ +{ + "$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 + } + } + } + }, + "optionalMfaEmail": { + "type": "string", + "format": "email", + "title": "Your E-Mail for MFA (Optional)", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/mfa/.kratos.yml b/test/e2e/profiles/mfa/.kratos.yml new file mode 100644 index 0000000..081b3bb --- /dev/null +++ b/test/e2e/profiles/mfa/.kratos.yml @@ -0,0 +1,53 @@ +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: true + ui_url: http://localhost:4455/registration + after: + password: + 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: + code: + mfa_enabled: true + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + rp: + id: localhost + origin: http://localhost:4455 + display_name: Ory + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/mfa/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/mfa/identity.traits.schema.json b/test/e2e/profiles/mfa/identity.traits.schema.json new file mode 100644 index 0000000..36aef36 --- /dev/null +++ b/test/e2e/profiles/mfa/identity.traits.schema.json @@ -0,0 +1,44 @@ +{ + "$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 + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml new file mode 100644 index 0000000..632badc --- /dev/null +++ b/test/e2e/profiles/mobile/.kratos.yml @@ -0,0 +1,80 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 5m + required_aal: aal1 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: true + after: + password: + hooks: + - hook: session + oidc: + hooks: + - hook: session + recovery: + enabled: true + + verification: + enabled: false + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + rp: + id: localhost + origin: http://localhost:4455 + display_name: Ory + 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/email/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/mobile/identity.traits.schema.json b/test/e2e/profiles/mobile/identity.traits.schema.json new file mode 100644 index 0000000..73971b8 --- /dev/null +++ b/test/e2e/profiles/mobile/identity.traits.schema.json @@ -0,0 +1,48 @@ +{ + "$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 + } + } + } + }, + "website": { + "type": "string", + "format": "uri", + "minLength": 10 + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/network/.kratos.yml b/test/e2e/profiles/network/.kratos.yml new file mode 100644 index 0000000..c3c8b3d --- /dev/null +++ b/test/e2e/profiles/network/.kratos.yml @@ -0,0 +1,52 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: true + ui_url: http://localhost:4455/registration + after: + hooks: + - hook: web_hook + config: + method: GET + url: https://www.google.com/login/hook + body: http://192.168.178.3:4455/body + + login: + ui_url: http://localhost:4455/login + before: + hooks: + - hook: web_hook + config: + method: GET + url: http://192.168.178.2:4455/login/hook + body: file://test/e2e/profiles/network/webhook.jsonnet + + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verification + enabled: true + recovery: + ui_url: http://localhost:4455/recovery + +identity: + default_schema_id: localhost + schemas: + - id: localhost + url: http://localhost/ + - id: ref + url: file://test/e2e/profiles/network/identity.traits.schema.json + - id: working + url: file://test/e2e/profiles/email/identity.traits.schema.json + +clients: + http: + disallow_private_ip_ranges: true diff --git a/test/e2e/profiles/network/identity.traits.schema.json b/test/e2e/profiles/network/identity.traits.schema.json new file mode 100644 index 0000000..fdf1742 --- /dev/null +++ b/test/e2e/profiles/network/identity.traits.schema.json @@ -0,0 +1,60 @@ +{ + "$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 + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "website": { + "$ref": "http://192.168.178.1/" + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/network/webhook.jsonnet b/test/e2e/profiles/network/webhook.jsonnet new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/e2e/profiles/network/webhook.jsonnet @@ -0,0 +1 @@ +{} diff --git a/test/e2e/profiles/oidc-provider-mfa/.kratos.yml b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml new file mode 100644 index 0000000..ac577ce --- /dev/null +++ b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml @@ -0,0 +1,54 @@ +clients: + http: + disallow_private_ip_ranges: false +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: + ui_url: http://localhost:4455/registration + after: + password: + 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 + webauthn: + enabled: true + config: + rp: + id: localhost + origin: http://localhost:4455 + display_name: Ory + +oauth2_provider: + url: "http://localhost:4745" + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/oidc-provider-mfa/identity.traits.schema.json + +session: + whoami: + required_aal: highest_available diff --git a/test/e2e/profiles/oidc-provider-mfa/identity.traits.schema.json b/test/e2e/profiles/oidc-provider-mfa/identity.traits.schema.json new file mode 100644 index 0000000..e88020f --- /dev/null +++ b/test/e2e/profiles/oidc-provider-mfa/identity.traits.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 + } + } + } + }, + "website": { + "title": "Website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/oidc-provider/.kratos.yml b/test/e2e/profiles/oidc-provider/.kratos.yml new file mode 100644 index 0000000..09b2c99 --- /dev/null +++ b/test/e2e/profiles/oidc-provider/.kratos.yml @@ -0,0 +1,61 @@ +clients: + http: + disallow_private_ip_ranges: false +oauth2_provider: + url: "http://localhost:4745" +selfservice: + methods: + password: + enabled: true + code: + enabled: true + oidc: + enabled: true + config: + providers: + - 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-provider/hydra.jsonnet + flows: + settings: + privileged_session_max_age: 5m + ui_url: http://localhost:4455/settings + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: true + ui_url: http://localhost:4455/registration + after: + oidc: + hooks: + - hook: session + password: + 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 + enabled: true + lifespan: 5m + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/oidc-provider/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long diff --git a/test/e2e/profiles/oidc-provider/hydra.jsonnet b/test/e2e/profiles/oidc-provider/hydra.jsonnet new file mode 100644 index 0000000..a0e1425 --- /dev/null +++ b/test/e2e/profiles/oidc-provider/hydra.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + email: claims.sub, + [if "website" in claims then "website" else null]: claims.website + }, + }, + } diff --git a/test/e2e/profiles/oidc-provider/identity.traits.schema.json b/test/e2e/profiles/oidc-provider/identity.traits.schema.json new file mode 100644 index 0000000..8b6cd43 --- /dev/null +++ b/test/e2e/profiles/oidc-provider/identity.traits.schema.json @@ -0,0 +1,60 @@ +{ + "$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 + } + }, + "recovery": { + "via": "email" + } + } + }, + "website": { + "title": "Website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml new file mode 100644 index 0000000..b0a327b --- /dev/null +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -0,0 +1,72 @@ +selfservice: + methods: + 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 + + flows: + settings: + privileged_session_max_age: 5m + ui_url: http://localhost:4455/settings + + 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 + 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: + ui_url: http://localhost:4455/recovery + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/oidc/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/oidc/hydra.jsonnet b/test/e2e/profiles/oidc/hydra.jsonnet new file mode 100644 index 0000000..a0e1425 --- /dev/null +++ b/test/e2e/profiles/oidc/hydra.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + email: claims.sub, + [if "website" in claims then "website" else null]: claims.website + }, + }, + } diff --git a/test/e2e/profiles/oidc/identity-required.traits.schema.json b/test/e2e/profiles/oidc/identity-required.traits.schema.json new file mode 100644 index 0000000..20b2b50 --- /dev/null +++ b/test/e2e/profiles/oidc/identity-required.traits.schema.json @@ -0,0 +1,80 @@ +{ + "$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" + } + } + } + }, + "website": { + "title": "Website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + }, + "nested": { + "type": "object", + "properties": { + "b": { + "title": " optional B", + "type": "string" + } + } + }, + "requirednested": { + "title": "required nested", + "type": "object", + "properties": { + "a": { + "title": "Required A", + "const": true + }, + "b": { + "title": "Required optional B", + "type": "string" + } + }, + "required": [ + "a" + ] + } + }, + "required": [ + "email", + "website", + "requirednested", + "consent" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/oidc/identity.traits.schema.json b/test/e2e/profiles/oidc/identity.traits.schema.json new file mode 100644 index 0000000..67857c8 --- /dev/null +++ b/test/e2e/profiles/oidc/identity.traits.schema.json @@ -0,0 +1,52 @@ +{ + "$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" + } + }, + "verification": { + "via": "email" + } + } + }, + "website": { + "title": "Website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": ["email", "website"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml new file mode 100644 index 0000000..26f8abb --- /dev/null +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -0,0 +1,56 @@ +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 + - http://localhost:19006 + 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 0000000..8dbece1 --- /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 new file mode 100644 index 0000000..b3582a6 --- /dev/null +++ b/test/e2e/profiles/passwordless/.kratos.yml @@ -0,0 +1,68 @@ +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: true + 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 + + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + passwordless: true + rp: + 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: + - id: default + url: file://test/e2e/profiles/passwordless/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/passwordless/identity.traits.schema.json b/test/e2e/profiles/passwordless/identity.traits.schema.json new file mode 100644 index 0000000..9218e71 --- /dev/null +++ b/test/e2e/profiles/passwordless/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", "website"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/recovery-mfa/.kratos.yml b/test/e2e/profiles/recovery-mfa/.kratos.yml new file mode 100644 index 0000000..03b0337 --- /dev/null +++ b/test/e2e/profiles/recovery-mfa/.kratos.yml @@ -0,0 +1,47 @@ +selfservice: + methods: + password: + enabled: true + link: + enabled: true + + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 1m + required_aal: highest_available + + recovery: + ui_url: http://localhost:4455/recovery + enabled: true + lifespan: 5m + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + registration: + ui_url: http://localhost:4455/registration + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/recovery/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long diff --git a/test/e2e/profiles/recovery-mfa/identity.traits.schema.json b/test/e2e/profiles/recovery-mfa/identity.traits.schema.json new file mode 100644 index 0000000..4c8165c --- /dev/null +++ b/test/e2e/profiles/recovery-mfa/identity.traits.schema.json @@ -0,0 +1,36 @@ +{ + "$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 + } + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/recovery/.kratos.yml b/test/e2e/profiles/recovery/.kratos.yml new file mode 100644 index 0000000..3d3ca8f --- /dev/null +++ b/test/e2e/profiles/recovery/.kratos.yml @@ -0,0 +1,38 @@ +selfservice: + methods: + password: + enabled: true + link: + enabled: true + + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 1m + + recovery: + ui_url: http://localhost:4455/recovery + enabled: true + lifespan: 5m + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + registration: + ui_url: http://localhost:4455/registration + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/recovery/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long diff --git a/test/e2e/profiles/recovery/identity.traits.schema.json b/test/e2e/profiles/recovery/identity.traits.schema.json new file mode 100644 index 0000000..4c8165c --- /dev/null +++ b/test/e2e/profiles/recovery/identity.traits.schema.json @@ -0,0 +1,36 @@ +{ + "$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 + } + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml new file mode 100644 index 0000000..6d5eb44 --- /dev/null +++ b/test/e2e/profiles/spa/.kratos.yml @@ -0,0 +1,91 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 5m + ui_url: http://localhost:4455/settings + required_aal: aal1 + + 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 + oidc: + hooks: + - + hook: session + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + enabled: true + lifespan: 5m + ui_url: http://localhost:4455/verify + recovery: + enabled: true + lifespan: 5m + ui_url: http://localhost:4455/recovery + + methods: + 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 + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + rp: + id: localhost + origin: http://localhost:4455 + display_name: Ory + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/spa/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/spa/identity.traits.schema.json b/test/e2e/profiles/spa/identity.traits.schema.json new file mode 100644 index 0000000..f5b9f9b --- /dev/null +++ b/test/e2e/profiles/spa/identity.traits.schema.json @@ -0,0 +1,66 @@ +{ + "$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 + }, + "totp": { + "account_name": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "tos": { + "title": "Accept Terms of Service", + "type": "boolean" + }, + "age": { + "title": "Age", + "type": "number", + "maximum": 300 + }, + "consent": { + "title": "Consent", + "type": "boolean" + }, + "newsletter": { + "title": "Newsletter", + "type": "boolean" + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml new file mode 100644 index 0000000..f527179 --- /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 0000000..51b2a1c --- /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/verification/.kratos.yml b/test/e2e/profiles/verification/.kratos.yml new file mode 100644 index 0000000..ca4932f --- /dev/null +++ b/test/e2e/profiles/verification/.kratos.yml @@ -0,0 +1,43 @@ +selfservice: + methods: + password: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 1m + + verification: + ui_url: http://localhost:4455/verification + enabled: true + lifespan: 5m + 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 + registration: + ui_url: http://localhost:4455/registration + error: + ui_url: http://localhost:4455/error + recovery: + ui_url: http://localhost:4455/recovery + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/verification/identity.traits.schema.json + +secrets: + cipher: + - secret-thirty-two-character-long diff --git a/test/e2e/profiles/verification/identity.traits.schema.json b/test/e2e/profiles/verification/identity.traits.schema.json new file mode 100644 index 0000000..c1e81b0 --- /dev/null +++ b/test/e2e/profiles/verification/identity.traits.schema.json @@ -0,0 +1,36 @@ +{ + "$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 + } + }, + "verification": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/webhooks/.kratos.yml b/test/e2e/profiles/webhooks/.kratos.yml new file mode 100644 index 0000000..00eb105 --- /dev/null +++ b/test/e2e/profiles/webhooks/.kratos.yml @@ -0,0 +1,58 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + 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 + - hook: web_hook + config: + url: http://127.0.0.1:4459/webhook + method: POST + body: file://test/e2e/profiles/webhooks/webhook_body.jsonnet + can_interrupt: true + auth: + type: api_key + config: + name: X-Authorize-Request + value: "1" + in: header + + login: + ui_url: http://localhost:4455/login + after: + password: + hooks: + - hook: web_hook + config: + url: http://127.0.0.1:4459/webhook + method: POST + body: file://test/e2e/profiles/webhooks/webhook_body.jsonnet + can_interrupt: true + auth: + type: api_key + config: + name: X-Authorize-Request + value: "1" + in: header + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + ui_url: http://localhost:4455/recovery + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/webhooks/identity.traits.schema.json diff --git a/test/e2e/profiles/webhooks/identity.traits.schema.json b/test/e2e/profiles/webhooks/identity.traits.schema.json new file mode 100644 index 0000000..6972a26 --- /dev/null +++ b/test/e2e/profiles/webhooks/identity.traits.schema.json @@ -0,0 +1,42 @@ +{ + "$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 + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/webhooks/webhook_body.jsonnet b/test/e2e/profiles/webhooks/webhook_body.jsonnet new file mode 100644 index 0000000..696e1f0 --- /dev/null +++ b/test/e2e/profiles/webhooks/webhook_body.jsonnet @@ -0,0 +1,8 @@ +function(ctx) { + identity_id: if std.objectHas(ctx, "identity") then ctx.identity.id else null, + email: if std.objectHas(ctx, "identity") then ctx.identity.traits.email else null, + flow_id: ctx.flow.id, + flow_type: ctx.flow.type, + identity: if std.objectHas(ctx, "identity") then ctx.identity else null, + ctx: ctx +} diff --git a/test/e2e/proxy/package-lock.json b/test/e2e/proxy/package-lock.json new file mode 100644 index 0000000..31e2b2f --- /dev/null +++ b/test/e2e/proxy/package-lock.json @@ -0,0 +1,1707 @@ +{ + "name": "proxy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "proxy", + "version": "1.0.0", + "dependencies": { + "express": "4.21.2", + "nodemon": "3.1.10", + "request": "2.88.2", + "url-join": "5.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "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", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "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/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tldts": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.28" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/test/e2e/proxy/package.json b/test/e2e/proxy/package.json new file mode 100644 index 0000000..e1d256c --- /dev/null +++ b/test/e2e/proxy/package.json @@ -0,0 +1,24 @@ +{ + "name": "proxy", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "index.js", + "overrides": { + "tough-cookie": "6.0.1", + "path-to-regexp": "0.1.13", + "qs": "6.15.2", + "form-data": "2.5.6", + "body-parser": "1.20.6", + "brace-expansion": "1.1.16" + }, + "scripts": { + "start": "nodemon ./proxy.js" + }, + "dependencies": { + "express": "4.21.2", + "nodemon": "3.1.10", + "request": "2.88.2", + "url-join": "5.0.0" + } +} diff --git a/test/e2e/proxy/proxy.js b/test/e2e/proxy/proxy.js new file mode 100644 index 0000000..ffddc3d --- /dev/null +++ b/test/e2e/proxy/proxy.js @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import request from "request" +import { resolve } from "path" +import urljoin from "url-join" +import express from "express" +import fs from "fs" + +const app = express() + +const proxy = + (base, prefix = null) => + (req, res, next) => { + let url = urljoin(base, req.url) + + // we need to tell Krato we are behind a reverse proxy + req.headers["x-forwarded-host"] = req.get("host") + + if (prefix) { + url = urljoin(base, prefix, req.url) + } + req + .pipe(request(url, { followRedirect: false }).on("error", next)) + .pipe(res) + } + +app.use( + "/self-service/", + proxy(process.env.KRATOS_PUBLIC_URL, "/self-service/"), +) +app.use("/schemas/", proxy(process.env.KRATOS_PUBLIC_URL, "/schemas/")) +app.use("/.well-known/", proxy(process.env.KRATOS_PUBLIC_URL, "/.well-known/")) + +app.use("/", (req, res, next) => { + const pc = JSON.parse(fs.readFileSync(resolve("../proxy.json")).toString()) + switch (pc) { + case "react": + proxy(process.env.KRATOS_UI_REACT_URL)(req, res, next) + return + case "react-native": + proxy(process.env.KRATOS_UI_REACT_NATIVE_URL)(req, res, next) + return + } + proxy(process.env.KRATOS_UI_URL)(req, res, next) +}) + +const port = parseInt(process.env.PORT) || 4455 + +let listener = () => { + console.log(`Listening on http://0.0.0.0:${port}`) +} + +app.listen(port, "0.0.0.0", listener) diff --git a/test/e2e/render-kratos-config.sh b/test/e2e/render-kratos-config.sh new file mode 100755 index 0000000..46d5ea2 --- /dev/null +++ b/test/e2e/render-kratos-config.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# Renders the config schema to a TypeScript file that can be used in Cypress tests. + +set -euxo pipefail + +dir=$(realpath $(dirname "${BASH_SOURCE[0]}")) + +ory_x_version="$(go -C $dir/../.. list -f '{{.Version}}' -m github.com/ory/x)" + +curl --retry 7 --retry-connrefused -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/shared/config.d.ts) + +rm $dir/.tracing-config.schema.json + +(cd $dir/../..; make format) diff --git a/test/e2e/run.sh b/test/e2e/run.sh new file mode 100755 index 0000000..0905ccf --- /dev/null +++ b/test/e2e/run.sh @@ -0,0 +1,432 @@ +#!/usr/bin/env bash + +echo "Running Ory Kratos E2E Tests..." +echo "" +set -euxo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." + +make .bin/hydra + +export PATH=.bin:$PATH +export KRATOS_PUBLIC_URL=http://localhost:4433/ +export KRATOS_BROWSER_URL=http://localhost:4433/ +export KRATOS_ADMIN_URL=http://localhost:4434/ +export KRATOS_UI_URL=http://localhost:4456/ +export KRATOS_UI_REACT_URL=http://localhost:4458/ +export KRATOS_UI_REACT_NATIVE_URL=http://localhost:19006/ +export LOG_LEAK_SENSITIVE_VALUES=true +export DEV_DISABLE_API_FLOW_ENFORCEMENT=true +export COOKIE_SECRET=kweifawskf23weas +export CSRF_COOKIE_NAME=node_csrf_token +export CSRF_COOKIE_SECRET=lkaw9oe8isedrhq2 + +base=$(pwd) + +setup=yes +dev=no +nokill=no +cleanup=no +for i in "$@"; do + case $i in + --no-kill) + nokill=yes + shift # past argument=value + ;; + --only-setup) + setup=only + shift # past argument=value + ;; + --no-setup) + setup=no + shift # past argument=value + ;; + --dev) + dev=yes + shift # past argument=value + ;; + --cleanup) + cleanup=yes + shift # past argument=value + ;; + esac +done + +cleanup() { + # Temporarily disable exit on error for cleanup (services may not be running) + set +e + + echo "=== E2E Cleanup ===" + + # Step 1: Kill process supervisors FIRST (they respawn children) + echo "Killing process supervisors (modd, nodemon)..." + pkill -9 -f "modd" 2>/dev/null + pkill -9 -f "nodemon" 2>/dev/null + sleep 2 + + # Step 2: Kill Kratos and webhook (were managed by modd) + echo "Killing Kratos and webhook..." + pkill -9 -f "kratos serve" 2>/dev/null + pkill -9 -f "\.bin/kratos" 2>/dev/null + pkill -9 -f "\.bin/webhook" 2>/dev/null + killall kratos 2>/dev/null + killall webhook 2>/dev/null + + # Step 3: Kill Node.js services + echo "Killing Node.js services..." + pkill -9 -f "node.*proxy" 2>/dev/null + pkill -9 -f "kratos-selfservice-ui-node" 2>/dev/null + pkill -9 -f "kratos-selfservice-ui-react-nextjs" 2>/dev/null + pkill -9 -f "expo" 2>/dev/null + pkill -9 -f "webpack.*19006" 2>/dev/null + + # Step 4: Kill Hydra processes + echo "Killing Hydra processes..." + killall hydra 2>/dev/null + killall hydra-login-consent 2>/dev/null + killall hydra-kratos-login-consent 2>/dev/null + pkill -9 -f "hydra serve" 2>/dev/null + + # Step 5: Kill any remaining processes on known ports + echo "Cleaning up any remaining port listeners..." + for port in 4433 4434 4444 4445 4446 4455 4456 4458 4744 4745 4746 19006; do + pid=$(lsof -ti:$port 2>/dev/null) + if [ -n "$pid" ]; then + echo " Killing PID $pid on port $port" + kill -9 $pid 2>/dev/null + fi + done + + # Step 6: Stop and remove Docker containers + echo "Stopping Docker containers..." + docker stop kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach mailslurper kratos_test_hydra 2>/dev/null + docker rm kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach mailslurper kratos_test_hydra 2>/dev/null + + echo "=== Cleanup Complete ===" + + # Re-enable exit on error + set -e +} + +prepare() { + echo "::group::prepare" + if [[ "${nokill}" == "no" ]]; then + cleanup + fi + + if [ -z ${TEST_DATABASE_POSTGRESQL-} ]; then + docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach || true + 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:latest-v25.4 start-single-node --insecure + + export TEST_DATABASE_MYSQL="mysql://root:secret@(localhost:3444)/mysql?parseTime=true&multiStatements=true" + export TEST_DATABASE_POSTGRESQL="postgres://postgres:secret@localhost:3445/postgres?sslmode=disable" + export TEST_DATABASE_COCKROACHDB="cockroach://root@localhost:3446/defaultdb?sslmode=disable" + fi + + if [ -z "${NODE_UI_PATH-}" ]; then + node_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/kratos-selfservice-ui-node" + git clone --depth 1 --branch master https://github.com/ory/kratos-selfservice-ui-node.git "$node_ui_dir" + (cd "$node_ui_dir" && npm i --legacy-peer-deps && npm run build) + else + node_ui_dir="${NODE_UI_PATH}" + fi + + if [ -z "${RN_UI_PATH-}" ]; then + rn_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/kratos-selfservice-ui-react-native" + git clone --depth 1 --branch master https://github.com/ory/kratos-selfservice-ui-react-native.git "$rn_ui_dir" + (cd "$rn_ui_dir" && npm i) + else + rn_ui_dir="${RN_UI_PATH}" + fi + + if [ -z "${REACT_UI_PATH-}" ]; then + react_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/ory/kratos-selfservice-ui-react-nextjs" + git clone --depth 1 --branch master https://github.com/ory/kratos-selfservice-ui-react-nextjs.git "$react_ui_dir" + (cd "$react_ui_dir" && npm i) + else + react_ui_dir="${REACT_UI_PATH}" + fi + + ( + rm test/e2e/proxy.json || true + echo '"express"' > test/e2e/proxy.json + cd test/e2e/proxy + npm i + ) + + if [ -z ${CI-} ]; then + docker rm mailslurper hydra hydra-ui -f || true + docker run --name mailslurper -p 4436:4436 -p 4437:4437 -p 1025:1025 oryd/mailslurper:latest-smtps > "${base}/test/e2e/mailslurper.e2e.log" 2>&1 & + fi + + # Check if any ports that we need are open already + nc -zv localhost 4444 && exit 1 + nc -zv localhost 4445 && exit 1 + nc -zv localhost 4446 && exit 1 + nc -zv localhost 4455 && exit 1 + nc -zv localhost 19006 && exit 1 + nc -zv localhost 4456 && exit 1 + nc -zv localhost 4458 && exit 1 + nc -zv localhost 4744 && exit 1 + nc -zv localhost 4745 && exit 1 + + ( + cd "$rn_ui_dir" + KRATOS_URL=http://localhost:4433 CI=1 npm run web \ + >"${base}/test/e2e/rn-profile-app.e2e.log" 2>&1 & + ) + + hydra serve all -c test/e2e/hydra.yml --dev >"${base}/test/e2e/hydra.e2e.log" 2>&1 & + + (cd test/e2e; npm run wait-on -- -l -t 300000 http-get://localhost:4445/health/alive) + + hydra_client=$(hydra create oauth2-client \ + --endpoint http://localhost:4445 \ + --grant-type authorization_code --grant-type refresh_token \ + --response-type code --response-type id_token \ + --scope openid --scope offline \ + --redirect-uri http://localhost:4455/self-service/methods/oidc/callback/hydra \ + --format json) + export OIDC_HYDRA_CLIENT_ID=$(jq -r '.client_id' <<< "$hydra_client" ) + export OIDC_HYDRA_CLIENT_SECRET=$(jq -r '.client_secret' <<< "$hydra_client" ) + + google_client=$(hydra create oauth2-client \ + --endpoint http://localhost:4445 \ + --grant-type authorization_code --grant-type refresh_token \ + --response-type code --response-type id_token \ + --scope openid --scope offline \ + --redirect-uri http://localhost:4455/self-service/methods/oidc/callback/google \ + --format json) + export OIDC_GOOGLE_CLIENT_ID=$(jq -r '.client_id' <<< "$google_client" ) + export OIDC_GOOGLE_CLIENT_SECRET=$(jq -r '.client_secret' <<< "$google_client" ) + + github_client=$(hydra create oauth2-client \ + --endpoint http://localhost:4445 \ + --grant-type authorization_code --grant-type refresh_token \ + --response-type code --response-type id_token \ + --scope openid --scope offline \ + --redirect-uri http://localhost:4455/self-service/methods/oidc/callback/github \ + --format json) + export OIDC_GITHUB_CLIENT_ID=$(jq -r '.client_id' <<< "$github_client" ) + export OIDC_GITHUB_CLIENT_SECRET=$(jq -r '.client_secret' <<< "$github_client" ) + + ( + cd test/e2e/hydra-login-consent + go build . + PORT=4446 HYDRA_ADMIN_URL=http://localhost:4445 ./hydra-login-consent >"${base}/test/e2e/hydra-ui.e2e.log" 2>&1 & + ) + + # Spin up another Hydra instance with the express node app used as the login UI for kratos-hydra OIDC provider tests + DSN=memory SERVE_PUBLIC_PORT=4744 \ + SERVE_ADMIN_PORT=4745 \ + URLS_SELF_ISSUER=http://localhost:4744 \ + LOG_LEVEL=trace \ + URLS_LOGIN=http://localhost:4455/login \ + URLS_CONSENT=http://localhost:4746/consent \ + SECRETS_SYSTEM="[\"1234567890123456789012345678901\"]" \ + hydra serve all --dev >"${base}/test/e2e/hydra-kratos.e2e.log" 2>&1 & + + (cd test/e2e; npm run wait-on -- -l -t 300000 http-get://127.0.0.1:4745/health/alive) + + dummy_client=$(hydra create oauth2-client \ + --endpoint http://localhost:4745 \ + --token-endpoint-auth-method client_secret_basic \ + --grant-type authorization_code --grant-type refresh_token \ + --response-type code --response-type id_token \ + --scope openid --scope offline --scope email --scope website \ + --redirect-uri http://localhost:5555/callback \ + --redirect-uri https://ory-network-httpbin-ijakee5waq-ez.a.run.app/anything \ + --format json) + export CYPRESS_OIDC_DUMMY_CLIENT_ID=$(jq -r '.client_id' <<< "$dummy_client" ) + export CYPRESS_OIDC_DUMMY_CLIENT_SECRET=$(jq -r '.client_secret' <<< "$dummy_client" ) + + ( + cd test/e2e/hydra-kratos-login-consent + go build . + PORT=4746 HYDRA_ADMIN_URL=http://localhost:4745 ./hydra-kratos-login-consent >"${base}/test/e2e/hydra-kratos-ui.e2e.log" 2>&1 & + ) + + ( + cd "$node_ui_dir" + PORT=4456 SECURITY_MODE=cookie npm run start \ + >"${base}/test/e2e/ui-node.e2e.log" 2>&1 & + ) + + if [ -z "${REACT_UI_PATH-}" ]; then + ( + cd "$react_ui_dir" + NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://localhost:4433 npm run build + NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://localhost:4433 npm run start -- --hostname 127.0.0.1 --port 4458 \ + >"${base}/test/e2e/react-iu.e2e.log" 2>&1 & + ) + else + ( + cd "$react_ui_dir" + PORT=4458 NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://localhost:4433 npm run dev \ + >"${base}/test/e2e/react-iu.e2e.log" 2>&1 & + ) + fi + + ( + cd test/e2e/proxy + PORT=4455 npm run start \ + >"${base}/test/e2e/proxy.e2e.log" 2>&1 & + ) + + # Make the environment available to Playwright + env | grep KRATOS_ > test/e2e/playwright/playwright.env + env | grep TEST_DATABASE_ >> test/e2e/playwright/playwright.env + env | grep OIDC_ >> test/e2e/playwright/playwright.env + env | grep CYPRESS_ >> test/e2e/playwright/playwright.env + echo LOG_LEAK_SENSITIVE_VALUES=true >> test/e2e/playwright/playwright.env + echo DEV_DISABLE_API_FLOW_ENFORCEMENT=true >> test/e2e/playwright/playwright.env + + echo "::endgroup::" +} + +run() { + echo "::group::run-prep" + killall modd || true + killall kratos || true + + export DSN=${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 mfa-optional spa network passwordless passkey webhooks oidc-provider oidc-provider-mfa two-steps; do + go tool 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 + cp test/e2e/kratos.email.yml test/e2e/kratos.generated.yml + + (go tool modd -f test/e2e/modd.conf >"${base}/test/e2e/kratos.e2e.log" 2>&1 &) + + # Having to wait 10 minutes for cockroach to apply the migrations is ridiculous but sometimes it takes that long in CI + npm run wait-on -- -l -t 10m http-get://127.0.0.1:4434/health/ready \ + http-get://127.0.0.1:4444/.well-known/openid-configuration \ + http-get://127.0.0.1:4455/health/ready \ + http-get://127.0.0.1:4445/health/ready \ + http-get://127.0.0.1:4446/ \ + http-get://127.0.0.1:4456/health/alive \ + http-get://127.0.0.1:19006/ \ + http-get://127.0.0.1:4437/mail \ + http-get://127.0.0.1:4458/ \ + http-get://127.0.0.1:4459/health + echo "::endgroup::" + + if [[ $dev == "yes" ]]; then + (cd test/e2e; npm run test:watch --) + else + if [ -z "${CYPRESS_RECORD_KEY-}" ]; then + (cd test/e2e; npx cypress run --browser chrome ${CYPRESS_OPTS-}) + else + (cd test/e2e; npx cypress run --browser chrome ${CYPRESS_OPTS-} --record --tag "${2}" ) + fi + fi +} + +usage() { + echo $"This script runs the e2e tests. + +To run the tests just pick a database name: + + $0 + + Supported databases are 'sqlite', 'mysql', 'postgres', 'cockroach': + + $0 sqlite + $0 mysql + $0 postgres + $0 cockroach + ... + + If you are using a database other than SQLite, you need to set + an environment variable that points to it: + + export TEST_DATABASE_MYSQL=... + export TEST_DATABASE_POSTGRESQL=... + export TEST_DATABASE_COCKROACHDB=... + $0 + + The Makefile has a helper for that which uses Docker to start the + databases: + + make test-resetdb + source script/test-envs.sh + $0 + +To run e2e tests in dev mode (useful for writing them), run: + + $0 --dev + +To set up all the services without running the tests, use: + + $0 --only-setup + +To then run the tests without the set up steps, use: + + $0 --no-setup + +To prevent processes from being killed during set up phase, use: + $0 --no-kill + +If you are making changes to the kratos-selfservice-ui-node +project as well, point the 'NODE_UI_PATH' environment variable to +the path where the kratos-selfservice-ui-node project is checked out: + + export NODE_UI_PATH=$HOME/workspace/kratos-selfservice-ui-node + export RN_UI_PATH=$HOME/workspace/kratos-selfservice-ui-react-native + export REACT_UI_PATH=$HOME/workspace/kratos-selfservice-ui-react-nextjs + $0 ..." +} + +if [[ "${cleanup}" == "yes" ]]; then + cleanup + exit 0 +fi + +export TEST_DATABASE_SQLITE="sqlite:///$(mktemp -d -t ci-XXXXXXXXXX)/db.sqlite?_fk=true&_journal=WAL" +export TEST_DATABASE_MEMORY="memory" + +case "${1:-default}" in +sqlite) + echo "Database set up at: $TEST_DATABASE_SQLITE" + dsn="${TEST_DATABASE_SQLITE}" + db="sqlite" + ;; + +mysql) + dsn="${TEST_DATABASE_MYSQL}" + db="mysql" + ;; + +postgres) + dsn="${TEST_DATABASE_POSTGRESQL}" + db="postgres" + ;; + +cockroach) + dsn="${TEST_DATABASE_COCKROACHDB}" + db="cockroach" + ;; + +*) + usage + if [[ "${setup}" == "only" ]]; then + prepare + exit 0 + else + exit 1 + fi + ;; +esac + +if [[ "${setup}" == "yes" ]]; then + prepare +fi + +run "${dsn}" "${db}" diff --git a/test/e2e/shared/config.d.ts b/test/e2e/shared/config.d.ts new file mode 100644 index 0000000..6b0ad37 --- /dev/null +++ b/test/e2e/shared/config.d.ts @@ -0,0 +1,1599 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type OryKratosConfiguration = { + [k: string]: unknown | undefined +} & { + selfservice: { + default_browser_return_url: RedirectBrowsersToSetURLPerDefault + allowed_return_urls?: AllowedReturnToURLs + flows?: { + settings?: { + ui_url?: URLOfTheSettingsPage + lifespan?: string + privileged_session_max_age?: string + required_aal?: RequiredAuthenticatorAssuranceLevel + after?: SelfServiceAfterSettings + before?: SelfServiceBeforeSettings + } + logout?: { + clear_browser_data?: ClearBrowserDataOnLogout + after?: { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + } + } + registration?: { + enabled?: EnableUserRegistration + login_hints?: ProvideLoginHintsOnFailedRegistration + ui_url?: RegistrationUIURL + lifespan?: string + before?: SelfServiceBeforeRegistration + after?: SelfServiceAfterRegistration + enable_legacy_one_step?: DisableTwoStepRegistration + style?: RegistrationFlowStyle + } + login?: { + ui_url?: LoginUIURL + lifespan?: string + style?: LoginFlowStyle + before?: SelfServiceBeforeLogin + after?: SelfServiceAfterLogin + } + verification?: EmailAndPhoneVerificationAndAccountActivationConfiguration + recovery?: AccountRecoveryConfiguration + error?: { + ui_url?: OryKratosErrorUIURL + } + } + methods?: { + b2b?: SingleSignOnForB2B + profile?: { + enabled?: EnablesProfileManagementMethod + } + link?: { + enabled?: EnablesLinkMethod + config?: LinkConfiguration + } + 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 + } + totp?: { + enabled?: EnablesTheTOTPMethod + config?: TOTPConfiguration + } + lookup_secret?: { + enabled?: EnablesTheLookupSecretMethod + } + webauthn?: { + enabled?: EnablesTheWebAuthnMethod + config?: WebAuthnConfiguration + } + passkey?: { + enabled?: EnablesThePasskeyMethod + config?: PasskeyConfiguration + } + oidc?: SpecifyOpenIDConnectAndOAuth2Configuration + } + } + database?: DatabaseRelatedConfiguration + dsn: DataSourceName + courier?: CourierConfiguration + oauth2_provider?: OAuth2ProviderConfiguration + preview?: ConfigurePreviewFeatures + serve?: { + admin?: { + request_log?: { + disable_for_health?: DisableHealthEndpointsRequestLogging + } + base_url?: AdminBaseURL + host?: AdminHost + port?: AdminPort + socket?: Socket + tls?: HTTPS + } + public?: { + request_log?: { + disable_for_health?: DisableHealthEndpointsRequestLogging1 + } + /** + * Configures Cross Origin Resource Sharing for public endpoints. + */ + cors?: { + /** + * Sets whether CORS is enabled. + */ + enabled?: boolean + /** + * A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin. + */ + allowed_origins?: ((string | "*") & string)[] + /** + * A list of HTTP methods the user agent is allowed to use with cross-domain requests. + */ + allowed_methods?: ( + | "POST" + | "GET" + | "PUT" + | "PATCH" + | "DELETE" + | "CONNECT" + | "HEAD" + | "OPTIONS" + | "TRACE" + )[] + /** + * A list of non simple headers the client is allowed to use with cross-domain requests. + */ + allowed_headers?: string[] + /** + * Sets which headers are safe to expose to the API of a CORS API specification. + */ + exposed_headers?: string[] + /** + * Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. + */ + allow_credentials?: boolean + /** + * TODO + */ + options_passthrough?: boolean + /** + * Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request. + */ + max_age?: number + /** + * Adds additional log output to debug server side CORS issues. + */ + debug?: boolean + } + base_url?: BaseURL + host?: PublicHost + port?: PublicPort + socket?: Socket + tls?: HTTPS + } + } + tracing?: { + [k: string]: unknown | undefined + } + log?: Log + identity: { + default_schema_id?: TheDefaultIdentitySchema + schemas: AllJSONSchemasForIdentityTraits + } + secrets?: { + default?: DefaultEncryptionSigningSecrets + cookie?: SigningKeysForCookies + pagination?: SecretsToEncryptThePaginationToken + cipher?: SecretsToUseForEncryptionByCipher + } + hashers?: HashingAlgorithmConfiguration + ciphers?: CipherAlgorithmConfiguration + cookies?: HTTPCookieConfiguration + session?: { + whoami?: WhoAmIToSessionSettings + lifespan?: SessionLifespan + cookie?: { + domain?: SessionCookieDomain + name?: SessionCookieName + persistent?: MakeSessionCookiePersistent + path?: SessionCookiePath + secure?: SessionCookieSecureFlag1 + same_site?: SessionCookieSameSiteConfiguration + } + 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 + } + disallow_ref_in_identity_schemas?: DisallowExternal$RefResolutionInIdentitySchemas + [k: string]: unknown | undefined + } + version?: TheKratosVersionThisConfigIsWrittenFor + dev?: boolean + help?: boolean + /** + * This is a CLI flag and environment variable and can not be set using the config file. + */ + "sqa-opt-out"?: boolean + /** + * This is a CLI flag and environment variable and can not be set using the config file. + */ + "watch-courier"?: boolean + "expose-metrics-port"?: MetricsPort + /** + * This is a CLI flag and environment variable and can not be set using the config file. + */ + config?: string[] + clients?: GlobalOutgoingNetworkSettings + feature_flags?: FeatureFlags + organizations?: Organizations + enterprise?: EnterpriseFeatures + revision?: ConfigRevision +} +/** + * Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.com/kratos/docs/concepts/browser-redirect-flow-completion). + */ +export type RedirectBrowsersToSetURLPerDefault = string +/** + * List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows. + */ +export type AllowedReturnToURLs = string[] +/** + * URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). + */ +export type URLOfTheSettingsPage = string +/** + * Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA. + */ +export type RequiredAuthenticatorAssuranceLevel = "aal1" | "highest_available" +/** + * Define what the hook should do + */ +export type WebHookConfiguration = + | { + [k: string]: unknown | undefined + } + | { + can_interrupt?: false + [k: string]: unknown | undefined + } +export type SelfServiceHooks = (SelfServiceWebHook | B2BSSOHook)[] +/** + * If set to true, a successful browser logout sends the `Clear-Site-Data` response header over HTTPS, instructing the browser to clear cookies, storage, and caches for the origin. This is off by default to avoid clearing data that other applications on the same origin may rely on. + */ +export type ClearBrowserDataOnLogout = boolean +/** + * If set to true will enable [User Registration](https://www.ory.com/kratos/docs/self-service/flows/user-registration/). + */ +export type EnableUserRegistration = boolean +/** + * When registration fails because an account with the given credentials or addresses previously signed up, provide login hints about available methods to sign in to the user. + */ +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 +/** + * Deprecated, please use `style` instead. + */ +export type DisableTwoStepRegistration = boolean +/** + * The style of the registration flow. If set to `unified` the login flow will be a one-step process. If set to `profile_first` the registration flow will first ask for the profile information first, and then the credentials. + */ +export type RegistrationFlowStyle = "unified" | "profile_first" +/** + * 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" +export type SelfServiceAfterDefaultLoginMethodHooks = ( + | SelfServiceSessionRevokerHook + | SelfServiceRequireVerifiedAddressHook + | SelfServiceWebHook + | SelfServiceVerificationHook + | SelfServiceShowVerificationUIHook + | B2BSSOHook +)[] +/** + * If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.com/kratos/docs/self-service/flows/verify-email-account-activation/). + */ +export type EnableEmailPhoneVerification = boolean +/** + * URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). + */ +export type VerifyUIURL = string +/** + * Sets how long the verification request (for the UI interaction) is valid. + */ +export type SelfServiceVerificationRequestLifespan = string +/** + * The strategy to use for verification requests + */ +export type VerificationStrategy = "link" | "code" +/** + * Whether to notify recipients, if verification was requested for their address. + */ +export type NotifyUnknownRecipients = boolean +/** + * If set to true will enable [Account Recovery](https://www.ory.com/kratos/docs/self-service/flows/password-reset-account-recovery/). + */ +export type EnableAccountRecovery = boolean +/** + * URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). + */ +export type RecoveryUIURL = string +export type SelfServiceAfterRecoveryHooks = ( + | SelfServiceWebHook + | SelfServiceSessionRevokerHook +)[] +/** + * Sets how long the recovery request is valid. If expired, the user has to redo the flow. + */ +export type SelfServiceRecoveryRequestLifespan = string +/** + * The strategy to use for recovery requests + */ +export type RecoveryStrategy = "link" | "code" +/** + * Whether to notify recipients, if recovery was requested for their account. + */ +export type NotifyUnknownRecipients1 = boolean +/** + * URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). + */ +export type OryKratosErrorUIURL = string +/** + * Overrides session.lifespan for sessions issued for this organization. Unset means inherit the project default. + */ +export type SessionLifespanOverride = string +export type EnablesProfileManagementMethod = boolean +export type EnablesLinkMethod = boolean +export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks = + string +export type HowLongALinkIsValidFor = string +export type EnablesUsernameEmailAndPasswordMethod = boolean +/** + * Allows changing the default HIBP host to a self hosted version. + */ +export type CustomHaveibeenpwnedHost = string +/** + * If set to false the password validation does not utilize the Have I Been Pwnd API. + */ +export type EnableTheHaveIBeenPwnedAPI = boolean +/** + * Defines how often a password may have been breached before it is rejected. + */ +export type AllowPasswordBreaches = number +/** + * If set to false the password validation fails when the network or the Have I Been Pwnd API is down. + */ +export type IgnoreLookupNetworkErrors = boolean +/** + * Defines the minimum length of the password. + */ +export type MinimumPasswordLength = 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. + */ +export type TOTPIssuer = string +export type EnablesTheLookupSecretMethod = boolean +export type EnablesTheWebAuthnMethod = boolean +/** + * If enabled will have the effect that WebAuthn is used for passwordless flows (as a first factor) and not for multi-factor set ups. With this set to true, users will see an option to sign up with WebAuthn on the registration screen. + */ +export type UseForPasswordlessFlows = boolean +export type RelyingPartyRPConfig = + | { + origin?: { + [k: string]: unknown | undefined + } + origins?: { + [k: string]: unknown | undefined + } + [k: string]: unknown | undefined + } + | { + origin: string + origins?: { + [k: string]: unknown | undefined + } + [k: string]: unknown | undefined + } + | { + origin?: { + [k: string]: unknown | undefined + } + 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[] +/** + * Constrains eligible authenticators to a specific attachment modality. Use 'platform' for built-in authenticators (Touch ID, Windows Hello) or 'cross-platform' for roaming authenticators (HID tokens, Yubikeys). Omit this field to let users register either type (WebAuthn 'no preference'), which is the recommended default for passkey deployments. + */ +export type AuthenticatorAttachment = "platform" | "cross-platform" +/** + * Controls whether the authenticator must create a client-side discoverable credential (historically called a resident key). Maps to the WebAuthn authenticatorSelection.residentKey member, whose name the spec retains for compatibility. See https://pkg.go.dev/github.com/go-webauthn/webauthn/protocol#ResidentKeyRequirement. + */ +export type ClientSideDiscoverableCredentialRequirement = + | "required" + | "preferred" + | "discouraged" +/** + * Controls whether biometrics or PIN are required for the passkey ceremony. + */ +export type UserVerificationRequirement = + | "required" + | "preferred" + | "discouraged" +/** + * Tells the authenticator what attestation statement to provide. 'none' is the WebAuthn default and tells the client not to provide an attestation statement, which is what most consumer passkeys (e.g., iOS, Android) support today. Use 'direct' to request the raw attestation statement, 'indirect' to allow the client to decide, or 'enterprise' for uniquely identifying attestation. + */ +export type AttestationConveyancePreference = + | "none" + | "indirect" + | "direct" + | "enterprise" +/** + * Timeout for the passkey registration ceremony. Uses Go duration format. If empty, library defaults are used (5m with user verification, 2m without). + */ +export type RegistrationTimeout = string +/** + * Timeout for the passkey login ceremony. Uses Go duration format. If empty, library defaults are used (5m with user verification, 2m without). + */ +export type LoginTimeout = 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. + */ +export type BaseURLForOAuth2RedirectURIs = string +export type SelfServiceOIDCProvider = { + [k: string]: unknown | undefined +} & { + id: string + provider: Provider + label?: OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons + client_id: string + client_secret?: string + issuer_url?: string + auth_url?: string + token_url?: string + mapper_url: JsonnetMapperURL + scope?: string[] + microsoft_tenant?: AzureADTenant + subject_source?: MicrosoftSubjectSource + apple_team_id?: AppleDeveloperTeamID + apple_private_key_id?: ApplePrivateKeyIdentifier + apple_private_key?: ApplePrivateKey + requested_claims?: OpenIDConnectClaims + organization_id?: OrganizationID + additional_id_token_audiences?: AdditionalClientIdsAllowedWhenUsingIDTokenSubmission + aal2_acr_values?: UpstreamAcrValuesThatMarkTheSessionAsAAL2 + aal2_amr_values?: UpstreamAmrValuesThatMarkTheSessionAsAAL2 + claims_source?: ClaimsSource + pkce?: ProofKeyForCodeExchange + fedcm_config_url?: FederationConfigurationURL + net_id_token_origin_header?: NetIDTokenOriginHeader + account_linking_mode?: AccountLinkingMode + update_identity_on_login?: UpdateIdentityOnLogin +} +/** + * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, amazon. + */ +export type Provider = + | "github" + | "github-app" + | "gitlab" + | "generic" + | "google" + | "microsoft" + | "discord" + | "salesforce" + | "slack" + | "facebook" + | "auth0" + | "vk" + | "yandex" + | "apple" + | "spotify" + | "netid" + | "dingtalk" + | "patreon" + | "line" + | "linkedin" + | "linkedin_v2" + | "lark" + | "x" + | "fedcm-test" + | "amazon" + | "uaepass" +export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons = + string +/** + * The URL where the jsonnet source is located for mapping the provider's data to Ory Kratos data. + */ +export type JsonnetMapperURL = string +/** + * The Azure AD Tenant to use for authentication. + */ +export type AzureADTenant = string +/** + * Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier. If the value is `oid` then the the oid (Object ID) is taken to identify users across different services. + */ +export type MicrosoftSubjectSource = "userinfo" | "me" | "oid" +/** + * Apple Developer Team ID needed for generating a JWT token for client secret + */ +export type AppleDeveloperTeamID = string +/** + * Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret + */ +export type ApplePrivateKeyIdentifier = string +/** + * Sign In with Apple Private Key needed for generating a JWT token for client secret + */ +export type ApplePrivateKey = string +/** + * The ID of the organization that this provider belongs to. Only effective in the Ory Network. + */ +export type OrganizationID = string +export type AdditionalClientIdsAllowedWhenUsingIDTokenSubmission = string[] +/** + * List of upstream OIDC `acr` claim values that should elevate the resulting Kratos session to AAL2. If the ID token returned by the upstream provider contains an `acr` claim matching any of these values, the session is marked AAL2. Leave empty to always issue AAL1 sessions for this provider. + */ +export type UpstreamAcrValuesThatMarkTheSessionAsAAL2 = string[] +/** + * List of upstream OIDC `amr` claim values that should elevate the resulting Kratos session to AAL2. If the ID token returned by the upstream provider contains an `amr` array including any of these values, the session is marked AAL2. Values follow RFC 8176 (for example `mfa`, `otp`, `hwk`, `fpt`). + */ +export type UpstreamAmrValuesThatMarkTheSessionAsAAL2 = 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" +/** + * 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 + */ +export type ProofKeyForCodeExchange = "auto" | "never" | "force" +/** + * The URL where the FedCM IdP configuration is located for the provider. This is only effective in the Ory Network. + */ +export type FederationConfigurationURL = string +/** + * Contains the orgin header to be used when exchanging a NetID FedCM token for an ID token + */ +export type NetIDTokenOriginHeader = string +/** + * Controls how account conflicts are resolved for this provider. `confirm_with_existing_credential` (default) requires the user to verify their identity with an existing credential. `automatic` silently links accounts if the provider verifies email ownership. This is only effective in the Ory Network. + */ +export type AccountLinkingMode = + | "confirm_with_existing_credential" + | "automatic" +/** + * Controls whether the identity is updated from OIDC claims on each login. `never` (default) does not update the identity. `automatic` re-runs the Jsonnet claims mapper on every OIDC login and updates the identity's traits and metadata if they changed. + */ +export type UpdateIdentityOnLogin = "never" | "automatic" +/** + * A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with. + */ +export type OpenIDConnectAndOAuth2Providers = SelfServiceOIDCProvider[] +/** + * Controls how many records should be purged from one table during database cleanup task + */ +export type NumberOfRecordsToCleanInOneIteration = number +/** + * Controls the delay time between cleaning each table in one cleanup iteration + */ +export type DelayBetweenEachTableCleanups = string +/** + * Controls how old records do we want to leave + */ +export type RemoveRecordsOlderThan = string +/** + * DSN is used to specify the database credentials as a connection URI. + */ +export type DataSourceName = string +/** + * You can override certain or all message templates by pointing this key to the path where the templates are located. + */ +export type OverrideMessageTemplates = string +/** + * Defines how emails will be sent, either through SMTP (default) or HTTP. + */ +export type DeliveryStrategy = "smtp" | "http" +/** + * This URL will be used to send the emails to. + */ +export type HTTPAddressOfAPIEndpoint = string +/** + * Define which auth mechanism to use for auth with the HTTP email provider + */ +export type AuthMechanisms1 = + | WebHookAuthApiKeyProperties + | WebHookAuthBasicAuthProperties +/** + * This URI will be used to connect to the SMTP server. Use the scheme smtps for implicit TLS sessions or smtp for explicit StartTLS/cleartext sessions. Please note that TLS is always enforced with certificate trust verification by default for security reasons on both schemes. With the smtp scheme you can use the query parameter (`?disable_starttls=true`) to allow cleartext sessions or (`?disable_starttls=false`) to enforce StartTLS (default behaviour). Additionally, use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates (default behaviour) on both implicit and explicit TLS sessions. + */ +export type SMTPConnectionString = string +/** + * Path of the client X.509 certificate, in case of certificate based client authentication to the SMTP server. + */ +export type SMTPClientCertificatePath = string +/** + * Path of the client certificate private key, in case of certificate based client authentication to the SMTP server + */ +export type SMTPClientPrivateKeyPath = string +/** + * The recipient of an email will see this as the sender address. + */ +export type SMTPSenderAddress = string +/** + * The recipient of an email will see this as the sender name. + */ +export type SMTPSenderName = string +/** + * Identifier used in the SMTP HELO/EHLO command. Some SMTP relays require a unique identifier. + */ +export type SMTPHELOEHLOName = string +/** + * The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only sms is supported. + */ +export type ChannelId = "sms" +/** + * The channel type. Currently only http is supported. + */ +export type ChannelType = "http" +/** + * If set, the login and registration flows will handle the Ory OAuth 2.0 & OpenID `login_challenge` query parameter to serve as an OpenID Connect Provider. This URL should point to Ory Hydra when you are not running on the Ory Network and be left untouched otherwise. + */ +export type OAuth20ProviderURL = string +/** + * Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow. + */ +export type PersistOAuth2RequestBetweenFlows = boolean +/** + * The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network. + */ +export type DefaultReadConsistencyLevel = "strong" | "eventual" +/** + * Disable request logging for /health/alive and /health/ready endpoints + */ +export type DisableHealthEndpointsRequestLogging = boolean +/** + * The URL where the admin endpoint is exposed at. + */ +export type AdminBaseURL = string +/** + * The host (interface) kratos' admin endpoint listens on. + */ +export type AdminHost = string +/** + * The port kratos' admin endpoint listens on. + */ +export type AdminPort = number +export type PrivateKeyPEM = TlsxSource +export type PathToPEMEncodedFle = string +/** + * The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`. + */ +export type Base64EncodedInline = string +export type TLSCertificatePEM = TlsxSource +/** + * Disable request logging for /health/alive and /health/ready endpoints + */ +export type DisableHealthEndpointsRequestLogging1 = boolean +/** + * The URL where the endpoint is exposed at. This domain is used to generate redirects, form URLs, and more. + */ +export type BaseURL = string +/** + * The host (interface) kratos' public endpoint listens on. + */ +export type PublicHost = string +/** + * The port kratos' public endpoint listens on. + */ +export type PublicPort = number +/** + * If set will leak sensitive values (e.g. emails) in the logs. + */ +export type LeakSensitiveLogValues = boolean +/** + * Text to use, when redacting sensitive log value. + */ +export type SensitiveLogValueRedactionText = string +/** + * This Identity Schema will be used as the default for self-service flows. Its ID needs to exist in the "schemas" list. + */ +export type TheDefaultIdentitySchema = string +/** + * Note that identities that used the "default_schema_url" field in older kratos versions will be corrupted unless you specify their schema url with the id "default" in this list. + * + * @minItems 1 + */ +export type AllJSONSchemasForIdentityTraits = [ + { + id: TheSchemaSID + url: JSONSchemaURLForIdentityTraitsSchema + selfservice_selectable?: IsTheSchemaEnabledInSelfServiceFlows + [k: string]: unknown | undefined + }, + ...{ + id: TheSchemaSID + url: JSONSchemaURLForIdentityTraitsSchema + selfservice_selectable?: IsTheSchemaEnabledInSelfServiceFlows + [k: string]: unknown | undefined + }[], +] +export type TheSchemaSID = string +/** + * URL for JSON Schema which describes a identity's traits. Can be a file path, a https URL, or a base64 encoded string. + */ +export type JSONSchemaURLForIdentityTraitsSchema = string +/** + * If set to true, this schema can be used explicity in self-service flows by setting `identity_schema` query parameter to the schema's ID. + */ +export type IsTheSchemaEnabledInSelfServiceFlows = boolean +/** + * The first secret in the array is used for signing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret. + */ +export type DefaultEncryptionSigningSecrets = string[] +/** + * The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret. + */ +export type SigningKeysForCookies = string[] +/** + * To avoid clients reverse-engineering and relying on the implementation details of the pagination token, it is encrypted with these keys + * + * @minItems 1 + */ +export type SecretsToEncryptThePaginationToken = [string, ...string[]] +/** + * The first secret in the array is used for encryption data while all other keys are used to decrypt older data that were signed with. + * + * @minItems 1 + */ +export type SecretsToUseForEncryptionByCipher = [string, ...string[]] +/** + * One of the values: argon2, bcrypt. + * Any other hashes will be migrated to the set algorithm once an identity authenticates using their password. + */ +export type PasswordHashingAlgorithm = "argon2" | "bcrypt" +/** + * One of the values: noop, aes, xchacha20-poly1305 + */ +export type CipheringAlgorithm = "noop" | "aes" | "xchacha20-poly1305" +/** + * Sets the cookie domain for session and CSRF cookies. Useful when dealing with subdomains. Use with care! + */ +export type HTTPCookieDomain = string +/** + * Sets the session and CSRF cookie path. Use with care! + */ +export type HTTPCookiePath = string +/** + * Sets the session secure flag. If unset, defaults to !dev mode. + */ +export type SessionCookieSecureFlag = string +/** + * Sets the session and CSRF cookie SameSite. + */ +export type HTTPCookieSameSiteConfiguration = "Strict" | "Lax" | "None" +export type TokenTimeToLive = string +export type JsonnetMapperURL1 = string +export type JSONWebKeySetURL = string +/** + * The source of the subject claim in the token. Can be one of: `id`, or `external_id`. + */ +export type SubjectSource = "id" | "external_id" +/** + * Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again. + */ +export type SessionLifespan = string +/** + * Sets the session cookie domain. Useful when dealing with subdomains. Use with care! Overrides `cookies.domain`. + */ +export type SessionCookieDomain = string +/** + * Sets the session cookie name. Use with care! + */ +export type SessionCookieName = string +/** + * If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4). This option affects the Ory OAuth2 and OpenID Provider's remember feature as well. + */ +export type MakeSessionCookiePersistent = boolean +/** + * Sets the session cookie path. Use with care! Overrides `cookies.path`. + */ +export type SessionCookiePath = string +/** + * Sets the session secure flag. If unset, defaults to !dev mode. + */ +export type SessionCookieSecureFlag1 = string +/** + * Sets the session cookie SameSite. Overrides `cookies.same_site`. + */ +export type SessionCookieSameSiteConfiguration = "Strict" | "Lax" | "None" +/** + * Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value. + */ +export type EarliestPossibleSessionExtension = string +/** + * If true, `$ref` URLs inside identity schemas may not resolve to `file://`, `http://`, or `https://` sources. This blocks server-side file reads (`file://`) and server-side request forgery (`http(s)://`) via malicious identity schemas. Internal JSON-pointer refs (`#/definitions/...`) and self-contained `base64://` refs remain allowed. Leave at the default (false) to preserve existing behavior for operators who intentionally reference external schemas. Ory Network forces this to true. + */ +export type DisallowExternal$RefResolutionInIdentitySchemas = boolean +/** + * SemVer according to https://semver.org/ prefixed with `v` as in our releases. + */ +export type TheKratosVersionThisConfigIsWrittenFor = string +/** + * The port the courier's metrics endpoint listens on (0/disabled by default). This is a CLI flag and environment variable and can not be set using the config file. + */ +export type MetricsPort = number +/** + * Disallow all outgoing HTTP calls to private IP ranges. This feature can help protect against SSRF attacks. + */ +export type DisallowPrivateIPRanges = boolean +/** + * Allows the given URLs to be called despite them being in the private IP range. URLs need to have an exact and case-sensitive match to be exempt. + */ +export type AddExemptURLsToPrivateIPRanges = string[] +/** + * Disallow all outgoing SMTP connections to private IP ranges. This feature can help protect against SSRF attacks. + */ +export type DisallowPrivateIPRanges1 = boolean +/** + * List of request headers that are forwarded to the web hook target in canonical form. + */ +export type AllowedRequestHeaders = 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 +/** + * If enabled, enable new recovery screens to pick which address to send a recovery code to, and can send a code via SMS. It is safe to toggle it back and forth, existing recovery flows will be handled with their respective logic. That is because it is decided at creation time whether a recovery flow is V1 or V2 and this cannot be changed afterwards. Thus, if a recovery flow is created with this flag enabled, it will be created as a recovery v2 flow. If this flag is disabled while this flow is still active, this flow will still be handled with the correct logic (v2). + */ +export type EnableNewRecoveryScreensToPickWhichAddressToSendARecoveryCodeLinkTo = + boolean +/** + * If true, restores the legacy behavior of always including `show_verification_ui` in the registration flow's `continue_with` when verification is enabled. If set to false, `show_verification_ui` is only set in `continue_with` if the `show_verification_ui` hook is used. This flag will be removed in the future. + */ +export type AlwaysIncludeShowVerificationUiInContinueWith = boolean +/** + * If true, the login flow will return a form error if the login identifier is not verified, which restores legacy behavior. If this value is false, the `continue_with` array will contain a `show_verification_ui` hook instead. + */ +export type ReturnAFormErrorIfTheLoginIdentifierIsNotVerified = boolean +/** + * If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future. + */ +export type EnableFasterSessionExtension = boolean +/** + * The node group to use for registration flows. Previously, the node group for the password method's profile fields was `password`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility. + */ +export type RegistrationNodeGroup = "password" | "default" +/** + * The node group to use for registration flows. Previously, the node group for the oidc method's profile fields was `oidc`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility and will be removed in the future. + */ +export type RegistrationNodeGroupForOIDC = boolean +/** + * If enabled, a code-strategy refresh (privileged re-authentication) login renders a "Send code to
    " button per available code address instead of re-asking for the identifier. The identity is fixed by the active session, so re-entering the identifier is unnecessary and makes the privileged login look like a fresh login. It is safe to toggle this back and forth. + */ +export type EnableTheAddressPickerOnTheCodeRefreshLoginScreen = 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 +/** + * Set a recognizable revision. This could be the commit time or a random value. This value is exposed at the `/health/config` endpoint and allows you to ensure that the correct config is loaded. + */ +export type ConfigRevision = string + +export interface SelfServiceAfterSettings { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + password?: SelfServiceAfterSettingsAuthMethod + totp?: SelfServiceAfterSettingsAuthMethod + oidc?: SelfServiceAfterSettingsAuthMethod + webauthn?: SelfServiceAfterSettingsAuthMethod + passkey?: SelfServiceAfterSettingsAuthMethod + lookup_secret?: SelfServiceAfterSettingsAuthMethod + profile?: SelfServiceAfterSettingsProfileMethod + hooks?: SelfServiceHooks +} +export interface SelfServiceAfterSettingsAuthMethod { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: (SelfServiceWebHook | SelfServiceSessionRevokerHook)[] +} +export interface SelfServiceWebHook { + hook: "web_hook" + config: WebHookConfiguration +} +export interface SelfServiceSessionRevokerHook { + hook: "revoke_active_sessions" +} +export interface SelfServiceAfterSettingsProfileMethod { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: ( + | SelfServiceWebHook + | SelfServiceShowVerificationUIHook + | B2BSSOHook + | VerifyNewAddressSettingsHook + | SelfServiceNotifyPreviousAddressesHook + )[] +} +export interface SelfServiceShowVerificationUIHook { + hook: "show_verification_ui" +} +export interface B2BSSOHook { + hook: "b2b_sso" | "organization" + [k: string]: unknown | undefined +} +/** + * Defers email or phone changes made through the settings profile flow until the new address is verified. The identity's traits are applied only after the user completes a verification flow for the new address. + * + * Configuration note: do not configure `show_verification_ui` on the same `selfservice.flows.settings.after.profile.hooks` list. `verify_new_address` aborts the settings flow and triggers its own verification, so a `show_verification_ui` hook on the profile list would run twice: once when the settings flow is aborted and again after verification completes. Keep `show_verification_ui` off the `profile` hook list when `verify_new_address` is active. + * + * When the pending change is applied, the remaining post-persist hooks on the `profile` list (for example, `web_hook`) run against the updated identity. The pending change is rejected if the session that initiated it has been revoked, expired, or deleted before verification completes. + */ +export interface VerifyNewAddressSettingsHook { + hook: "verify_new_address" +} +export interface SelfServiceNotifyPreviousAddressesHook { + hook: "notify_previous_addresses" + config?: { + /** + * Which previous addresses receive the change notification. 'removed' (default): only addresses that no longer exist after the change. 'all_verified': all addresses that were verified before the change. 'all': all addresses on the identity before the change. + */ + recipients?: "removed" | "all_verified" | "all" + } +} +export interface SelfServiceBeforeSettings { + hooks?: SelfServiceHooks +} +export interface SelfServiceBeforeRegistration { + hooks?: SelfServiceHooks +} +export interface SelfServiceAfterRegistration { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + password?: SelfServiceAfterRegistrationMethod + webauthn?: SelfServiceAfterRegistrationMethod + passkey?: SelfServiceAfterRegistrationMethod + oidc?: SelfServiceAfterRegistrationMethod + code?: SelfServiceAfterRegistrationMethod + hooks?: SelfServiceHooks +} +export interface SelfServiceAfterRegistrationMethod { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: ( + | SelfServiceSessionIssuerHook + | SelfServiceWebHook + | SelfServiceShowVerificationUIHook + | B2BSSOHook + )[] +} +export interface SelfServiceSessionIssuerHook { + hook: "session" +} +export interface SelfServiceBeforeLogin { + hooks?: SelfServiceHooks +} +export interface SelfServiceAfterLogin { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + password?: SelfServiceAfterDefaultLoginMethod + webauthn?: SelfServiceAfterDefaultLoginMethod + passkey?: SelfServiceAfterDefaultLoginMethod + oidc?: SelfServiceAfterOIDCLoginMethod + code?: SelfServiceAfterDefaultLoginMethod + totp?: SelfServiceAfterDefaultLoginMethod + lookup_secret?: SelfServiceAfterDefaultLoginMethod + hooks?: SelfServiceAfterDefaultLoginMethodHooks +} +export interface SelfServiceAfterDefaultLoginMethod { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: SelfServiceAfterDefaultLoginMethodHooks +} +export interface SelfServiceRequireVerifiedAddressHook { + hook: "require_verified_address" +} +export interface SelfServiceVerificationHook { + hook: "verification" +} +export interface SelfServiceAfterOIDCLoginMethod { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: ( + | SelfServiceSessionRevokerHook + | SelfServiceWebHook + | SelfServiceRequireVerifiedAddressHook + | B2BSSOHook + )[] +} +export interface EmailAndPhoneVerificationAndAccountActivationConfiguration { + enabled?: EnableEmailPhoneVerification + ui_url?: VerifyUIURL + after?: SelfServiceAfterVerification + lifespan?: SelfServiceVerificationRequestLifespan + before?: SelfServiceBeforeVerification + use?: VerificationStrategy + notify_unknown_recipients?: NotifyUnknownRecipients +} +export interface SelfServiceAfterVerification { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: SelfServiceHooks +} +export interface SelfServiceBeforeVerification { + hooks?: SelfServiceHooks +} +export interface AccountRecoveryConfiguration { + enabled?: EnableAccountRecovery + ui_url?: RecoveryUIURL + after?: SelfServiceAfterRecovery + lifespan?: SelfServiceRecoveryRequestLifespan + before?: SelfServiceBeforeRecovery + use?: RecoveryStrategy + notify_unknown_recipients?: NotifyUnknownRecipients1 +} +export interface SelfServiceAfterRecovery { + default_browser_return_url?: RedirectBrowsersToSetURLPerDefault + hooks?: SelfServiceAfterRecoveryHooks +} +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[] + session_lifespan?: SessionLifespanOverride + [k: string]: unknown | undefined + }[] + } +} +/** + * Additional configuration for the link strategy. + */ +export interface LinkConfiguration { + base_url?: OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks + lifespan?: HowLongALinkIsValidFor + [k: string]: unknown | undefined +} +/** + * Define how passwords are validated. + */ +export interface PasswordConfiguration { + haveibeenpwned_host?: CustomHaveibeenpwnedHost + haveibeenpwned_enabled?: EnableTheHaveIBeenPwnedAPI + max_breaches?: AllowPasswordBreaches + ignore_network_errors?: IgnoreLookupNetworkErrors + min_password_length?: MinimumPasswordLength + 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 + /** + * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads + */ + body?: string + additionalProperties?: never + } + } +} +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 +} +export interface WebAuthnConfiguration { + passwordless?: UseForPasswordlessFlows + rp?: RelyingPartyRPConfig +} +export interface PasskeyConfiguration { + rp?: RelyingPartyRPConfig1 + authenticator_selection?: AuthenticatorSelectionCriteria + attestation?: AttestationConfiguration + timeouts?: TimeoutConfiguration +} +export interface RelyingPartyRPConfig1 { + display_name: RelyingPartyDisplayName + id: RelyingPartyIdentifier + origins?: RelyingPartyOrigins + [k: string]: unknown | undefined +} +/** + * Controls which authenticators are eligible for passkey registration. + */ +export interface AuthenticatorSelectionCriteria { + attachment?: AuthenticatorAttachment + resident_key?: ClientSideDiscoverableCredentialRequirement + user_verification?: UserVerificationRequirement +} +/** + * Controls the attestation conveyance preference sent to the authenticator during passkey registration. + */ +export interface AttestationConfiguration { + preference?: AttestationConveyancePreference +} +/** + * Controls timeouts for passkey registration and login ceremonies. + */ +export interface TimeoutConfiguration { + registration?: RegistrationTimeout + login?: LoginTimeout +} +export interface SpecifyOpenIDConnectAndOAuth2Configuration { + enabled?: EnablesOpenIDConnectMethod + config?: { + base_redirect_uri?: BaseURLForOAuth2RedirectURIs + providers?: OpenIDConnectAndOAuth2Providers + } +} +/** + * The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint. + */ +export interface OpenIDConnectClaims { + /** + * This interface was referenced by `OpenIDConnectClaims`'s JSON-Schema definition + * via the `patternProperty` "^userinfo$|^id_token$". + */ + [k: string]: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ + [k: string]: null | { + /** + * Indicates whether the Claim being requested is an Essential Claim. + */ + essential?: boolean + /** + * Requests that the Claim be returned with a particular value. + */ + value?: { + [k: string]: unknown | undefined + } + /** + * Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference. + */ + values?: { + [k: string]: unknown | undefined + }[] + } + } +} +/** + * Miscellaneous settings used in database related tasks (cleanup, etc.) + */ +export interface DatabaseRelatedConfiguration { + cleanup?: DatabaseCleanupSettings +} +/** + * Settings that controls how the database cleanup process is configured (delays, batch size, etc.) + */ +export interface DatabaseCleanupSettings { + batch_size?: NumberOfRecordsToCleanInOneIteration + sleep?: DelaysBetweenVariousDatabaseCleanupPhases + older_than?: RemoveRecordsOlderThan + [k: string]: unknown | undefined +} +/** + * Configures delays between each step of the cleanup process. It is useful to tune the process so it will be efficient and performant. + */ +export interface DelaysBetweenVariousDatabaseCleanupPhases { + tables?: DelayBetweenEachTableCleanups + [k: string]: unknown | undefined +} +/** + * The courier is responsible for sending and delivering messages over email, sms, and other means. + */ +export interface CourierConfiguration { + templates?: { + recovery?: CourierTemplates + recovery_code?: CourierTemplates + verification?: CourierTemplates + verification_code?: CourierTemplates + registration_code?: { + valid?: { + email: EmailCourierTemplate + sms?: SmsCourierTemplate + } + } + login_code?: { + valid?: { + email: EmailCourierTemplate + sms?: SmsCourierTemplate + } + } + verifiable_address_changed?: { + email?: EmailCourierTemplate + sms?: SmsCourierTemplate + } + } + template_override_path?: OverrideMessageTemplates + /** + * Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned + */ + message_retries?: number + /** + * Configures the dispatch worker. + */ + worker?: { + /** + * Defines how many messages are pulled from the queue at once. + */ + pull_count?: number + /** + * Defines how long the worker waits before pulling messages from the queue again. + */ + pull_wait?: string + [k: string]: unknown | undefined + } + delivery_strategy?: DeliveryStrategy + http?: HTTPConfiguration + smtp?: SMTPConfiguration + channels?: CourierChannelConfiguration[] +} +export interface CourierTemplates { + invalid?: { + email: EmailCourierTemplate + } + valid?: { + email: EmailCourierTemplate + sms?: SmsCourierTemplate + } +} +export interface EmailCourierTemplate { + body?: { + /** + * The fallback template for email clients that do not support html. + */ + plaintext?: string + /** + * The default template used for sending out emails. The template can contain HTML + */ + html?: string + } + subject?: string +} +export interface SmsCourierTemplate { + body?: { + /** + * A template send to the SMS provider. + */ + plaintext?: string + } +} +/** + * Configures outgoing emails using HTTP. + */ +export interface HTTPConfiguration { + request_config?: HttpRequestConfig +} +export interface HttpRequestConfig { + url?: HTTPAddressOfAPIEndpoint + /** + * The HTTP method to use (GET, POST, etc). Defaults to POST. + */ + method?: string + /** + * The HTTP headers that must be applied to request + */ + headers?: { + [k: string]: string | undefined + } + /** + * 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 + additionalProperties?: never +} +/** + * Configures outgoing emails using the SMTP protocol. + */ +export interface SMTPConfiguration { + connection_uri?: SMTPConnectionString + client_cert_path?: SMTPClientCertificatePath + client_key_path?: SMTPClientPrivateKeyPath + from_address?: SMTPSenderAddress + from_name?: SMTPSenderName + headers?: SMTPHeaders + local_name?: SMTPHELOEHLOName +} +/** + * These headers will be passed in the SMTP conversation -- e.g. when using the AWS SES SMTP interface for cross-account sending. + */ +export interface SMTPHeaders { + [k: string]: string | undefined +} +export interface CourierChannelConfiguration { + id: ChannelId + type?: ChannelType + request_config: HttpRequestConfig +} +export interface OAuth2ProviderConfiguration { + url?: OAuth20ProviderURL + headers?: HTTPRequestHeaders + override_return_to?: PersistOAuth2RequestBetweenFlows +} +/** + * These headers will be passed in HTTP request to the OAuth2 Provider. + */ +export interface HTTPRequestHeaders { + [k: string]: string | undefined +} +export interface ConfigurePreviewFeatures { + default_read_consistency_level?: DefaultReadConsistencyLevel + [k: string]: unknown | undefined +} +/** + * Sets the permissions of the unix socket + */ +export interface Socket { + /** + * Owner of unix socket. If empty, the owner will be the user running Kratos. + */ + owner?: string + /** + * Group of unix socket. If empty, the group will be the primary group of the user running Kratos. + */ + group?: string + /** + * Mode of unix socket in numeric form + */ + mode?: number +} +/** + * Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled. + */ +export interface HTTPS { + key?: PrivateKeyPEM + cert?: TLSCertificatePEM +} +export interface TlsxSource { + path?: PathToPEMEncodedFle + base64?: Base64EncodedInline +} +/** + * Configure logging using the following options. Logging will always be sent to stdout and stderr. + */ +export interface Log { + /** + * Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL. + */ + level?: "trace" | "debug" | "info" | "warning" | "error" | "fatal" | "panic" + leak_sensitive_values?: LeakSensitiveLogValues + redaction_text?: SensitiveLogValueRedactionText + /** + * The log format can either be text or JSON. + */ + format?: "json" | "text" +} +export interface HashingAlgorithmConfiguration { + algorithm?: PasswordHashingAlgorithm + argon2?: ConfigurationForTheArgon2IdHasher + bcrypt?: ConfigurationForTheBcryptHasherMinimumIs4WhenDevFlagIsUsedAnd12Otherwise +} +export interface ConfigurationForTheArgon2IdHasher { + memory?: string + iterations?: number + /** + * Number of parallel workers, defaults to 2*runtime.NumCPU(). + */ + parallelism?: number + salt_length?: number + key_length?: number + /** + * The time a hashing operation (~login latency) should take. + */ + expected_duration?: string + /** + * The standard deviation expected for hashing operations. If this value is exceeded you will be warned in the logs to adjust the parameters. + */ + expected_deviation?: string + /** + * The memory dedicated for Kratos. As password hashing is very resource intense, Kratos will monitor the memory consumption and warn about high values. + */ + dedicated_memory?: string +} +export interface ConfigurationForTheBcryptHasherMinimumIs4WhenDevFlagIsUsedAnd12Otherwise { + cost: number +} +export interface CipherAlgorithmConfiguration { + algorithm?: CipheringAlgorithm + [k: string]: unknown | undefined +} +/** + * Configure the HTTP Cookies. Applies to both CSRF and session cookies. + */ +export interface HTTPCookieConfiguration { + domain?: HTTPCookieDomain + path?: HTTPCookiePath + secure?: SessionCookieSecureFlag + same_site?: HTTPCookieSameSiteConfiguration +} +/** + * Control how the `/sessions/whoami` endpoint is behaving. + */ +export interface WhoAmIToSessionSettings { + required_aal?: RequiredAuthenticatorAssuranceLevel + tokenizer?: TokenizerConfiguration +} +/** + * Configure the tokenizer, responsible for converting a session into a token format such as JWT. + */ +export interface TokenizerConfiguration { + templates?: TokenizerTemplates + [k: string]: unknown | undefined +} +/** + * A list of different templates that govern how a session is converted to a token format. + */ +export interface TokenizerTemplates { + /** + * This interface was referenced by `TokenizerTemplates`'s JSON-Schema definition + * via the `patternProperty` "[a-zA-Z0-9-_.]+". + */ + [k: string]: { + ttl?: TokenTimeToLive + claims_mapper_url?: JsonnetMapperURL1 + jwks_url: JSONWebKeySetURL + subject_source?: SubjectSource + [k: string]: unknown | undefined + } +} +/** + * Configure how outgoing network calls behave. + */ +export interface GlobalOutgoingNetworkSettings { + http?: GlobalHTTPClientConfiguration + smtp?: SMTPClientConfiguration + web_hook?: GlobalWebHookHTTPClientConfiguration + [k: string]: unknown | undefined +} +/** + * Configure how outgoing HTTP calls behave. + */ +export interface GlobalHTTPClientConfiguration { + disallow_private_ip_ranges?: DisallowPrivateIPRanges + private_ip_exception_urls?: AddExemptURLsToPrivateIPRanges + [k: string]: unknown | undefined +} +/** + * Configure how outgoing SMTP calls behave. + */ +export interface SMTPClientConfiguration { + disallow_private_ip_ranges?: DisallowPrivateIPRanges1 + [k: string]: unknown | undefined +} +/** + * Configure the global HTTP client of the web_hook action. + */ +export interface GlobalWebHookHTTPClientConfiguration { + header_allowlist?: AllowedRequestHeaders + [k: string]: unknown | undefined +} +export interface FeatureFlags { + cacheable_sessions?: EnableOrySessionsCaching + cacheable_sessions_max_age?: SetOrySessionEdgeCachingMaximumAge + use_continue_with_transitions?: EnableNewFlowTransitionsUsingContinueWithItems + choose_recovery_address?: EnableNewRecoveryScreensToPickWhichAddressToSendARecoveryCodeLinkTo + legacy_continue_with_verification_ui?: AlwaysIncludeShowVerificationUiInContinueWith + legacy_require_verified_login_error?: ReturnAFormErrorIfTheLoginIdentifierIsNotVerified + faster_session_extend?: EnableFasterSessionExtension + password_profile_registration_node_group?: RegistrationNodeGroup + legacy_oidc_registration_node_group?: RegistrationNodeGroupForOIDC + refresh_login_choose_address?: EnableTheAddressPickerOnTheCodeRefreshLoginScreen +} +/** + * 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/test/e2e/tsconfig.json b/test/e2e/tsconfig.json new file mode 100644 index 0000000..a80bccc --- /dev/null +++ b/test/e2e/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "es6", + "dom" + ], + "types": [ + "cypress", + "node" + ], + "esModuleInterop": true + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/test/schema/README.md b/test/schema/README.md new file mode 100644 index 0000000..3f66926 --- /dev/null +++ b/test/schema/README.md @@ -0,0 +1,17 @@ +# Schema Tests + +This test works by validating cases of the payload against a schema. +To add a case, place a file in the `${schemaName}.schema.test.${success|failure}` folder. +A success case should validate, a failure case should throw a validation error. + +## Handling of "$ref" + +To allow testing definitions on their own, and reduce the size of individual cases, +every `"$ref"` in the schema will be replaced with `"const"` before validation. +This means that a case only has to define the pointer as a string. Example: + +```yaml +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: + - "#/definitions/selfServiceSessionRevokerHook" +``` diff --git a/test/schema/fixtures/config.schema.test.failure/OIDCClaims.malformed.yaml b/test/schema/fixtures/config.schema.test.failure/OIDCClaims.malformed.yaml new file mode 100644 index 0000000..3b171b6 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/OIDCClaims.malformed.yaml @@ -0,0 +1,18 @@ +userinfo: + given_name: + essential: true + someInvalidProperty: 'invalid' # Invalid property + nickname: null + email: + essential: InvalidValueForEssential + email_verified: + essentials: true # key typo + picture: null + http://example/info/claims/groups: null +id_token: + auth_time: + essential: true + acr: + values: ['urn:mace:incommon:iap:silver'] + sub: + value: '248289761001' diff --git a/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml b/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml new file mode 100644 index 0000000..8a8f9c5 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml @@ -0,0 +1,5 @@ +valid: + email: + body: + plaintext: "https://some-url" + subject: "https://some-url" diff --git a/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml b/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml new file mode 100644 index 0000000..9ed67f4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml @@ -0,0 +1,4 @@ +invalid: + another-courier: +valid: + email: diff --git a/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml b/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml new file mode 100644 index 0000000..b9ac44d --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml @@ -0,0 +1,4 @@ +body: + plaintext: "http://call-something" + html: "malformed uri" +subject: "something" diff --git a/test/schema/fixtures/config.schema.test.failure/root.SMSConfigmalformedURL.yaml b/test/schema/fixtures/config.schema.test.failure/root.SMSConfigmalformedURL.yaml new file mode 100644 index 0000000..f5b1003 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.SMSConfigmalformedURL.yaml @@ -0,0 +1,5 @@ +sms: + request_config: + url: "malformed uri" + method: POST + body: "malformed uri" diff --git a/test/schema/fixtures/config.schema.test.failure/root.invalidTypes.yaml b/test/schema/fixtures/config.schema.test.failure/root.invalidTypes.yaml new file mode 100644 index 0000000..239169f --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.invalidTypes.yaml @@ -0,0 +1,81 @@ +selfservice: + + strategies: + password: + enabled: 0 + oidc: + enabled: 0 + config: + providers: + - foo + + logout: + redirect_to: 0 + + profile: + lifespan: 10 + + login: + lifespan: 10 + before: foo + after: foo + + registration: + lifespan: 10 + before: + - foo + after: foo + +dsn: foo + +courier: + template_override_path: foo + smtp: + connection_uri: 0 + from_address: 0 + message_retries: invalid-value + +serve: + admin: + host: 0 + port: foo + public: + host: 0 + port: foo + +urls: + self: + public: 1 + admin: 1 + mfa_ui: 1 + login_ui: 1 + settings_ui: 1 + default_return_to: 1 + registration_ui: 1 + error_ui: 1 + allowed_return_urls: + - 1 + - 1 + +log: + level: 1 + format: 1 + +identity: + traits: + default_schema_id: 1123 + schemas: + - id: 1 + url: 1 + +secrets: + session: + - 1 + +hashers: + argon2: + memory: foo + iterations: foo + parallelism: foo + salt_length: foo + key_length: foo diff --git a/test/schema/fixtures/config.schema.test.failure/root.invalidVersion.yaml b/test/schema/fixtures/config.schema.test.failure/root.invalidVersion.yaml new file mode 100644 index 0000000..0571e3a --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.invalidVersion.yaml @@ -0,0 +1 @@ +version: x1.0.0 diff --git a/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml b/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml new file mode 100644 index 0000000..2f8b1b0 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml @@ -0,0 +1,20 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +courier: + template_override_path: foo + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + templates: + recovery: "#/definitions/courierTemplates" + verification: + valid: + email: "#/definitions/emailCourierTemplate" diff --git a/test/schema/fixtures/config.schema.test.failure/root.requiresCourierRecovery.yml b/test/schema/fixtures/config.schema.test.failure/root.requiresCourierRecovery.yml new file mode 100644 index 0000000..2d82d7f --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.requiresCourierRecovery.yml @@ -0,0 +1,12 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + flows: + recovery: + enabled: true + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com diff --git a/test/schema/fixtures/config.schema.test.failure/root.requiresCourierVerification.yml b/test/schema/fixtures/config.schema.test.failure/root.requiresCourierVerification.yml new file mode 100644 index 0000000..fe6a640 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.requiresCourierVerification.yml @@ -0,0 +1,12 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + flows: + verification: + enabled: true + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.invalidTypes.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.invalidTypes.yaml new file mode 100644 index 0000000..b8c6fd9 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.invalidTypes.yaml @@ -0,0 +1,11 @@ +id: 1 +provider: unknown +client_id: 1 +client_secret: 1 +issuer_url: not an URL +auth_url: not an URL +token_url: not an URL +mapper_url: not an URL +scope: + - 1 +microsoft_tenant: 1 diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdNotWithGithub.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdNotWithGithub.yaml new file mode 100644 index 0000000..6c9bc69 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdNotWithGithub.yaml @@ -0,0 +1,6 @@ +id: foo +provider: github +client_id: asdf +client_secret: asdf +mapper_url: file://./mapper_file +apple_private_key_id: org diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdRequiredWithApple.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdRequiredWithApple.yaml new file mode 100644 index 0000000..ceba719 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyIdRequiredWithApple.yaml @@ -0,0 +1,10 @@ +id: foo +provider: apple +client_id: foo +team_id: foo +private_key: foo +mapper_url: https://example.com +scope: + - foo + - bar +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyNotWithGithub.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyNotWithGithub.yaml new file mode 100644 index 0000000..249d926 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyNotWithGithub.yaml @@ -0,0 +1,6 @@ +id: foo +provider: github +client_id: asdf +client_secret: asdf +mapper_url: file://./mapper_file +private_key: org diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyRequiredWithApple.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyRequiredWithApple.yaml new file mode 100644 index 0000000..6110617 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.privateKeyRequiredWithApple.yaml @@ -0,0 +1,10 @@ +id: foo +provider: apple +client_id: foo +apple_team_id: foo +apple_private_key_id: foo +mapper_url: https://example.com +scope: + - foo + - bar +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdNotWithGithub.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdNotWithGithub.yaml new file mode 100644 index 0000000..8696d56 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdNotWithGithub.yaml @@ -0,0 +1,6 @@ +id: foo +provider: github +client_id: asdf +client_secret: asdf +mapper_url: file://./mapper_file +team_id: org diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdRequiredWithApple.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdRequiredWithApple.yaml new file mode 100644 index 0000000..eef15c3 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.teamIdRequiredWithApple.yaml @@ -0,0 +1,10 @@ +id: foo +provider: apple +client_id: foo +apple_private_key_id: foo +apple_private_key: foo +mapper_url: https://example.com +scope: + - foo + - bar +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantNotWithGithub.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantNotWithGithub.yaml new file mode 100644 index 0000000..8f61577 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantNotWithGithub.yaml @@ -0,0 +1,6 @@ +id: foo +provider: github +client_id: asdf +client_secret: asdf +mapper_url: file://./mapper_file +microsoft_tenant: org diff --git a/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantRequiredWithMicrosoft.yaml b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantRequiredWithMicrosoft.yaml new file mode 100644 index 0000000..af34dcf --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/selfServiceOIDCProvider.tenantRequiredWithMicrosoft.yaml @@ -0,0 +1,5 @@ +id: foo +provider: microsoft +client_id: asdf +client_secret: asdf +mapper_url: file://./mapper_file diff --git a/test/schema/fixtures/config.schema.test.success/OIDCClaims.full.yaml b/test/schema/fixtures/config.schema.test.success/OIDCClaims.full.yaml new file mode 100644 index 0000000..9f37b14 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/OIDCClaims.full.yaml @@ -0,0 +1,18 @@ +userinfo: + given_name: + essential: true + nickname: null + email: + essential: true + email_verified: + essential: true + picture: null + http://example/info/claims/groups: null +id_token: + auth_time: + essential: true + acr: + values: ['urn:mace:incommon:iap:silver'] + sub: + essential: true + value: 248289761001 diff --git a/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml b/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml new file mode 100644 index 0000000..8f0ae48 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml @@ -0,0 +1,4 @@ +invalid: + email: "#/definitions/emailCourierTemplate" +valid: + email: "#/definitions/emailCourierTemplate" diff --git a/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URI-Reference.yaml b/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URI-Reference.yaml new file mode 100644 index 0000000..ad6ac43 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URI-Reference.yaml @@ -0,0 +1 @@ +/dashboard diff --git a/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URL.yaml b/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URL.yaml new file mode 100644 index 0000000..1880bd4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/defaultReturnTo.URL.yaml @@ -0,0 +1 @@ +https://my-app.com/dashboard diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml new file mode 100644 index 0000000..0229871 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml @@ -0,0 +1,4 @@ +body: + plaintext: "base64://" + html: "base64://" +subject: "http://" diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml new file mode 100644 index 0000000..1de8bac --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml @@ -0,0 +1 @@ +subject: "http://" diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml new file mode 100644 index 0000000..7a802c8 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml @@ -0,0 +1,3 @@ +body: + plaintext: "base64://" + html: "base64://" diff --git a/test/schema/fixtures/config.schema.test.success/root.courierSMS.yaml b/test/schema/fixtures/config.schema.test.success/root.courierSMS.yaml new file mode 100644 index 0000000..dc015e0 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.courierSMS.yaml @@ -0,0 +1,19 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +courier: + message_retries: 50 + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + channels: + - id: sms + type: http + request_config: "#/definitions/httpRequestConfig" diff --git a/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml b/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml new file mode 100644 index 0000000..026d96c --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml @@ -0,0 +1,18 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +courier: + template_override_path: foo + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + templates: + recovery: "#/definitions/courierTemplates" + verification: "#/definitions/courierTemplates" diff --git a/test/schema/fixtures/config.schema.test.success/root.full.yml b/test/schema/fixtures/config.schema.test.success/root.full.yml new file mode 100644 index 0000000..1d4ca48 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.full.yml @@ -0,0 +1,109 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + + allowed_return_urls: + - https://foo.bar + - /dashboard + + flows: + settings: + ui_url: /settings + lifespan: 1h5s + privileged_session_max_age: 1h + after: "#/definitions/selfServiceAfterSettings" + + logout: + after: + default_browser_return_url: "#/definitions/defaultReturnTo" + + registration: + ui_url: /registration + lifespan: 1h + after: "#/definitions/selfServiceAfterRegistration" + + login: + ui_url: /login + lifespan: 1h + after: "#/definitions/selfServiceAfterLogin" + + verification: + enabled: true + ui_url: /verification + lifespan: 1h + after: "#/definitions/selfServiceAfterVerification" + + recovery: + enabled: true + ui_url: /recovery + lifespan: 1h + after: "#/definitions/selfServiceAfterRecovery" + + error: + ui_url: /error + + methods: + profile: + enabled: true + link: + enabled: true + password: + enabled: true + totp: + enabled: true + config: + issuer: "foobar" + oidc: + enabled: true + config: + providers: + - "#/definitions/selfServiceOIDCProvider" + +dsn: foo + +courier: + template_override_path: foo + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + +serve: + admin: + host: foo + port: 4434 + public: + host: foo + port: 4433 + +log: + level: trace + format: json + +identity: + default_schema_id: foo + schemas: + - id: foo + url: https://example.com + +secrets: + default: + - default secret with a lot of chars + cookie: + - cookie secret with a lot of chars + cipher: + - secret-thirty-two-character-long + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + argon2: + memory: 128MB + iterations: 1 + parallelism: 1 + salt_length: 16 + key_length: 16 + dedicated_memory: 1GB + expected_deviation: 500ms + expected_duration: 500ms + +version: v1.0.0 diff --git a/test/schema/fixtures/config.schema.test.success/root.no_aliases.yaml b/test/schema/fixtures/config.schema.test.success/root.no_aliases.yaml new file mode 100644 index 0000000..0c22110 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.no_aliases.yaml @@ -0,0 +1,17 @@ +serve: + public: + base_url: "#/definitions/baseUrl" + +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +session: + cookie: + domain: foobar diff --git a/test/schema/fixtures/config.schema.test.success/root.required.yml b/test/schema/fixtures/config.schema.test.success/root.required.yml new file mode 100644 index 0000000..6e0eb24 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.required.yml @@ -0,0 +1,9 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.full.yaml new file mode 100644 index 0000000..43e5f67 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.full.yaml @@ -0,0 +1,2 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: "#/definitions/selfServiceAfterDefaultLoginMethodHooks" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethod.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.full.yaml new file mode 100644 index 0000000..603fba6 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.full.yaml @@ -0,0 +1,2 @@ +- "#/definitions/selfServiceSessionRevokerHook" +- "#/definitions/selfServiceRequireVerifiedAddressHook" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.required.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterDefaultLoginMethodHooks.required.yaml @@ -0,0 +1 @@ +[] diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.full.yaml new file mode 100644 index 0000000..49b2e4c --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.full.yaml @@ -0,0 +1,4 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +password: "#/definitions/selfServiceAfterDefaultLoginMethod" +webauthn: "#/definitions/selfServiceAfterDefaultLoginMethod" +oidc: "#/definitions/selfServiceAfterOIDCLoginMethod" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterLogin.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.full.yaml new file mode 100644 index 0000000..efb00bd --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.full.yaml @@ -0,0 +1,3 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: + - "#/definitions/selfServiceSessionRevokerHook" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterOIDCLoginMethod.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.full.yaml new file mode 100644 index 0000000..51291b7 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.full.yaml @@ -0,0 +1,4 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +password: "#/definitions/selfServiceAfterRegistrationMethod" +webauthn: "#/definitions/selfServiceAfterRegistrationMethod" +oidc: "#/definitions/selfServiceAfterRegistrationMethod" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistration.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.full.yaml new file mode 100644 index 0000000..1291f7f --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.full.yaml @@ -0,0 +1,3 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: + - "#/definitions/selfServiceSessionIssuerHook" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterRegistrationMethod.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.full.yaml new file mode 100644 index 0000000..55a737b --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.full.yaml @@ -0,0 +1,3 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +password: "#/definitions/selfServiceAfterSettingsAuthMethod" +profile: "#/definitions/selfServiceAfterSettingsProfileMethod" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettings.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsAuthMethod.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsAuthMethod.full.yaml new file mode 100644 index 0000000..5e3a24e --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsAuthMethod.full.yaml @@ -0,0 +1,4 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: + - "#/definitions/selfServiceWebHook" + - "#/definitions/selfServiceSessionRevokerHook" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.full.yaml new file mode 100644 index 0000000..9938abc --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.full.yaml @@ -0,0 +1,3 @@ +default_browser_return_url: "#/definitions/defaultReturnTo" +hooks: + - "#/definitions/selfServiceWebHook" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.required.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceAfterSettingsProfileMethod.required.yaml @@ -0,0 +1 @@ +{} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml new file mode 100644 index 0000000..1f0ee5f --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml @@ -0,0 +1,11 @@ +id: foo +provider: apple +client_id: foo +apple_team_id: foo +apple_private_key_id: foo +apple_private_key: foo +mapper_url: https://example.com +scope: + - foo + - bar +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml new file mode 100644 index 0000000..78940d8 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml @@ -0,0 +1,12 @@ +id: foo +provider: github +client_id: foo +client_secret: foo +issuer_url: https://example.com +auth_url: https://example.com +token_url: https://example.com +mapper_url: https://example.com +scope: + - foo + - bar +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml new file mode 100644 index 0000000..d17290f --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml @@ -0,0 +1,13 @@ +id: foo +provider: microsoft +client_id: foo +client_secret: foo +issuer_url: https://example.com +auth_url: https://example.com +token_url: https://example.com +mapper_url: https://example.com +scope: + - foo + - bar +microsoft_tenant: org +requested_claims: "#/definitions/OIDCClaims" diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.required.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.required.yaml new file mode 100644 index 0000000..f1f726b --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.required.yaml @@ -0,0 +1,5 @@ +id: foo +provider: github +client_id: foo +client_secret: foo +mapper_url: https://example.com diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceSessionIssuerHook.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceSessionIssuerHook.full.yaml new file mode 100644 index 0000000..61d4585 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceSessionIssuerHook.full.yaml @@ -0,0 +1 @@ +hook: session diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceSessionRevokerHook.full.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceSessionRevokerHook.full.yaml new file mode 100644 index 0000000..02c717d --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceSessionRevokerHook.full.yaml @@ -0,0 +1 @@ +hook: revoke_active_sessions diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceWebHook.noauth.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceWebHook.noauth.yaml new file mode 100644 index 0000000..328d561 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/selfServiceWebHook.noauth.yaml @@ -0,0 +1,7 @@ +hook: web_hook +config: + url: https://foo/bar + method: POST + body: base64://asdf + response: + ignore: false diff --git a/test/schema/fixtures/config.schema.test.success/webHookAuthApiKeyProperties.full.yaml b/test/schema/fixtures/config.schema.test.success/webHookAuthApiKeyProperties.full.yaml new file mode 100644 index 0000000..a17a06e --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/webHookAuthApiKeyProperties.full.yaml @@ -0,0 +1,5 @@ +type: api_key +config: + name: foo + value: bar + in: header diff --git a/test/schema/fixtures/config.schema.test.success/webHookAuthBasicAuthProperties.full.yaml b/test/schema/fixtures/config.schema.test.success/webHookAuthBasicAuthProperties.full.yaml new file mode 100644 index 0000000..7804c4b --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/webHookAuthBasicAuthProperties.full.yaml @@ -0,0 +1,4 @@ +type: basic_auth +config: + user: foo + password: bar diff --git a/test/schema/schema_test.go b/test/schema/schema_test.go new file mode 100644 index 0000000..c109cbe --- /dev/null +++ b/test/schema/schema_test.go @@ -0,0 +1,153 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pkg/errors" + + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" + _ "github.com/ory/jsonschema/v3/fileloader" +) + +type schema struct { + name string + raw string + s *jsonschema.Schema +} + +type schemas []schema + +type result int + +const ( + success result = iota + failure +) + +func (r result) String() string { + return []string{"success", "failure"}[r] +} + +func (s schema) validate(t *testing.T, path string) error { + t.Helper() + if s.s == nil { + compiler := jsonschema.NewCompiler() + if err := compiler.AddResource(s.name, strings.NewReader(s.raw)); err != nil { + return errors.WithStack(err) + } + + sx, err := compiler.Compile(context.Background(), s.name) + if err != nil { + return errors.WithStack(err) + } + + s.s = sx + } + + var doc io.Reader + y, err := os.ReadFile(path) // #nosec G304 test code + if err != nil { + return errors.WithStack(err) + } + + j, err := yaml.YAMLToJSON(y) + if err != nil { + return errors.WithStack(err) + } + + doc = bytes.NewBuffer(j) + + if err := s.s.Validate(doc); err != nil { + return errors.Errorf("there were validation errors: %s", err) + } + + return nil +} + +func (ss *schemas) getByName(n string) (*schema, error) { + for _, s := range *ss { + if s.name == n { + return &s, nil + } + } + + return nil, errors.Errorf("could not find schema with name %s", n) +} + +func TestSchemas(t *testing.T) { + t.Run("test embedx/config.schema.json", SchemaTestRunner("../../embedx", "config")) +} + +func SchemaTestRunner(spath string, sname string) func(*testing.T) { + return func(t *testing.T) { + sb, err := os.ReadFile(fmt.Sprintf("%s/%s.schema.json", spath, sname)) + require.NoError(t, err) + + // To test refs independently and reduce test case size we replace every "$ref" with "const". + // That way refs will not be resolved but we still make sure that they are pointing to the right definition. + // Changing a definition will result in just changing test cases for that definition. + s := strings.ReplaceAll(string(sb), `"$ref":`, `"const":`) + schemas := schemas{{ + name: "root", + raw: s, + }} + def := gjson.Get(s, "definitions") + if def.Exists() { + require.True(t, def.IsObject()) + def.ForEach(func(key, value gjson.Result) bool { + require.Equal(t, gjson.String, key.Type) + schemas = append(schemas, schema{ + name: key.String(), + raw: value.Raw, + }) + return true + }) + } + + RunCases(t, schemas, fmt.Sprintf("./fixtures/%s.schema.test.success", sname), success) + RunCases(t, schemas, fmt.Sprintf("./fixtures/%s.schema.test.failure", sname), failure) + } +} + +func RunCases(t *testing.T, ss schemas, dir string, expected result) { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + require.NoError(t, err) + if info.IsDir() { + return nil + } + + parts := strings.Split(info.Name(), ".") + require.Equal(t, 3, len(parts)) + sName, tc := parts[0], parts[1] + + s, err := ss.getByName(sName) + require.NoError(t, err) + + t.Run(fmt.Sprintf("case=schema %s test case %s expects %s", sName, tc, expected), func(t *testing.T) { + err := s.validate(t, path) + if expected == success { + assert.NoError(t, err, "path: %s", path) + } else { + assert.Error(t, err, "path: %s", path) + } + }) + + return nil + }) + require.NoError(t, err) +} diff --git a/test/stub/identity/empty.schema.json b/test/stub/identity/empty.schema.json new file mode 100644 index 0000000..923aa99 --- /dev/null +++ b/test/stub/identity/empty.schema.json @@ -0,0 +1,10 @@ +{ + "$id": "https://example.com/stubs/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/text/context.go b/text/context.go new file mode 100644 index 0000000..a0f304b --- /dev/null +++ b/text/context.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "encoding/json" +) + +func context(ctx map[string]any) []byte { + if len(ctx) == 0 { + panic("context must not be empty") + } + res, err := json.Marshal(ctx) + if err != nil { + panic(err) + } + return res +} diff --git a/text/id.go b/text/id.go new file mode 100644 index 0000000..8952aa2 --- /dev/null +++ b/text/id.go @@ -0,0 +1,233 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +// This file MUST not have any imports to modules that are not in the standard library. +// Otherwise, `make docs/cli` will fail. + +type ID int + +const ( + InfoSelfServiceLoginRoot ID = 1010000 + iota // 1010000 + InfoSelfServiceLogin // 1010001 + InfoSelfServiceLoginWith // 1010002 + InfoSelfServiceLoginReAuth // 1010003 + InfoSelfServiceLoginMFA // 1010004 + InfoSelfServiceLoginVerify // 1010005 + InfoSelfServiceLoginTOTPLabel // 1010006 + InfoLoginLookupLabel // 1010007 + InfoSelfServiceLoginWebAuthn // 1010008 + InfoLoginTOTP // 1010009 + InfoLoginLookup // 1010010 + InfoSelfServiceLoginContinueWebAuthn // 1010011 + InfoSelfServiceLoginWebAuthnPasswordless // 1010012 + InfoSelfServiceLoginContinue // 1010013 + InfoSelfServiceLoginCodeSent // 1010014 + InfoSelfServiceLoginCode // 1010015 + InfoSelfServiceLoginLink // 1010016 + InfoSelfServiceLoginAndLink // 1010017 + InfoSelfServiceLoginWithAndLink // 1010018 + InfoSelfServiceLoginCodeMFA // 1010019 + InfoSelfServiceLoginCodeMFAHint // 1010020 + InfoSelfServiceLoginPasskey // 1010021 + InfoSelfServiceLoginPassword // 1010022 + InfoSelfServiceLoginAAL2CodeAddress // 1010023 + InfoSelfServiceLoginDeviceAuthn // 1010024 + InfoSelfServiceLoginCodeSentForAuthenticatedUser // 1010025 +) + +const ( + InfoSelfServiceLogout ID = 1020000 + iota +) + +const ( + InfoSelfServiceMFA ID = 1030000 + iota +) + +const ( + InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 + InfoSelfServiceRegistration // 1040001 + InfoSelfServiceRegistrationWith // 1040002 + InfoSelfServiceRegistrationContinue // 1040003 + InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 + InfoSelfServiceRegistrationRegisterCode // 1040006 + InfoSelfServiceRegistrationRegisterPasskey // 1040007 + InfoSelfServiceRegistrationBack // 1040008 + InfoSelfServiceRegistrationChooseCredentials // 1040009 +) + +const ( + InfoSelfServiceSettings ID = 1050000 + iota + InfoSelfServiceSettingsUpdateSuccess + InfoSelfServiceSettingsUpdateLinkOidc + InfoSelfServiceSettingsUpdateUnlinkOidc + InfoSelfServiceSettingsUpdateUnlinkTOTP + InfoSelfServiceSettingsTOTPQRCode + InfoSelfServiceSettingsTOTPSecret + InfoSelfServiceSettingsRevealLookup + InfoSelfServiceSettingsRegenerateLookup + InfoSelfServiceSettingsLookupSecret + InfoSelfServiceSettingsLookupSecretLabel + InfoSelfServiceSettingsLookupConfirm + InfoSelfServiceSettingsRegisterWebAuthn + InfoSelfServiceSettingsRegisterWebAuthnDisplayName + InfoSelfServiceSettingsLookupSecretUsed + InfoSelfServiceSettingsLookupSecretList + InfoSelfServiceSettingsDisableLookup + InfoSelfServiceSettingsTOTPSecretLabel + InfoSelfServiceSettingsRemoveWebAuthn + InfoSelfServiceSettingsRegisterPasskey + InfoSelfServiceSettingsRemovePasskey + InfoSelfServiceSettingsRemoveDeviceAuthnKey + InfoSelfServiceSettingsDeviceAuthnNonce + InfoSelfServiceSettingsManagedByOrganization +) + +const ( + InfoSelfServiceRecovery ID = 1060000 + iota // 1060000 + InfoSelfServiceRecoverySuccessful // 1060001 + InfoSelfServiceRecoveryEmailSent // 1060002 + InfoSelfServiceRecoveryEmailWithCodeSent // 1060003 + InfoSelfServiceRecoveryMessageMaskedWithCodeSent // 1060004 + InfoSelfServiceRecoveryAskForFullAddress // 1060005 + InfoSelfServiceRecoveryAskToChooseAddress // 1060006 + InfoSelfServiceRecoveryBack // 1060007 +) + +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 // 1070014 + InfoNodeLabelCaptcha // 1070015 + InfoNodeLabelRecoveryAddress // 1070016 + InfoNodeLabelPhoneNumber // 1070017 + InfoNodeLabelEmailOrPhone // 1070018 +) + +const ( + InfoSelfServiceVerification ID = 1080000 + iota // 1080000 + InfoSelfServiceVerificationEmailSent // 1080001 + InfoSelfServiceVerificationSuccessful // 1080002 + InfoSelfServiceVerificationEmailWithCodeSent // 1080003 + InfoSelfServiceVerificationPhoneWithCodeSent // 1080004 + InfoSelfServiceVerificationPhoneSuccessful // 1080005 +) + +const ( + ErrorValidation ID = 4000000 + iota + ErrorValidationGeneric + ErrorValidationRequired + ErrorValidationMinLength + ErrorValidationInvalidFormat + ErrorValidationPasswordPolicyViolationGeneric + ErrorValidationInvalidCredentials + ErrorValidationDuplicateCredentials + ErrorValidationTOTPVerifierWrong + ErrorValidationIdentifierMissing + ErrorValidationAddressNotVerified + ErrorValidationNoTOTPDevice + ErrorValidationLookupAlreadyUsed + ErrorValidationNoWebAuthnDevice + ErrorValidationNoLookup + ErrorValidationSuchNoWebAuthnUser + ErrorValidationLookupInvalid + ErrorValidationMaxLength + ErrorValidationMinimum + ErrorValidationExclusiveMinimum + ErrorValidationMaximum + ErrorValidationExclusiveMaximum + ErrorValidationMultipleOf + ErrorValidationMaxItems + ErrorValidationMinItems + ErrorValidationUniqueItems + ErrorValidationWrongType + ErrorValidationDuplicateCredentialsOnOIDCLink + ErrorValidationDuplicateCredentialsWithHints + ErrorValidationConst + ErrorValidationConstGeneric + ErrorValidationPasswordIdentifierTooSimilar + ErrorValidationPasswordMinLength + ErrorValidationPasswordMaxLength + ErrorValidationPasswordTooManyBreaches + ErrorValidationNoCodeUser + ErrorValidationTraitsMismatch + ErrorValidationAccountNotFound + ErrorValidationCaptchaError + ErrorValidationPasswordNewSameAsOld + ErrorValidationEmail + ErrorValidationPhone + ErrorValidationNoDeviceAuthnDevice + ErrorValidationWebAuthnVerifierWrong + ErrorValidationDeviceAuthnVerifierWrong + ErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid + ErrorValidationDeviceAuthnKeyReenrollmentRequired +) + +const ( + ErrorValidationLogin ID = 4010000 + iota // 4010000 + ErrorValidationLoginFlowExpired // 4010001 + ErrorValidationLoginNoStrategyFound // 4010002 + ErrorValidationRegistrationNoStrategyFound // 4010003 + ErrorValidationSettingsNoStrategyFound // 4010004 + ErrorValidationRecoveryNoStrategyFound // 4010005 + ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLoginRetrySuccess // 4010007 + ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 + ErrorValidationLoginLinkedCredentialsDoNotMatch // 4010009 + ErrorValidationLoginAddressUnknown // 4010010 + ErrorValidationIdentityDisabled // 4010011 +) + +const ( + ErrorValidationRegistration ID = 4040000 + iota + ErrorValidationRegistrationFlowExpired // 4040001 + ErrorValidateionRegistrationRetrySuccess // 4040002 + ErrorValidationRegistrationCodeInvalidOrAlreadyUsed // 4040003 +) + +const ( + ErrorValidationSettings ID = 4050000 + iota + ErrorValidationSettingsFlowExpired // 4050001 + ErrorValidationSettingsTooManyAddressChanges // 4050002 +) + +const ( + ErrorValidationRecovery ID = 4060000 + iota // 4060000 + ErrorValidationRecoveryRetrySuccess // 4060001 + ErrorValidationRecoveryStateFailure // 4060002 + ErrorValidationRecoveryMissingRecoveryToken // 4060003 + ErrorValidationRecoveryTokenInvalidOrAlreadyUsed // 4060004 + ErrorValidationRecoveryFlowExpired // 4060005 + ErrorValidationRecoveryCodeInvalidOrAlreadyUsed // 4060006 +) + +const ( + ErrorValidationVerification ID = 4070000 + iota // 4070000 + ErrorValidationVerificationTokenInvalidOrAlreadyUsed // 4070001 + ErrorValidationVerificationRetrySuccess // 4070002 + ErrorValidationVerificationStateFailure // 4070003 + ErrorValidationVerificationMissingVerificationToken // 4070004 + ErrorValidationVerificationFlowExpired // 4070005 + ErrorValidationVerificationCodeInvalidOrAlreadyUsed // 4070006 +) + +const ( + ErrorSystem ID = 5000000 + iota // 5000000 + ErrorSystemGeneric // 5000001 + ErrorSystemNoAuthenticationMethodsAvailable // 5000002 + ErrorSystemOrganizationNoSSOProvidersAvailable // 5000003 +) diff --git a/text/id_test.go b/text/id_test.go new file mode 100644 index 0000000..f888a01 --- /dev/null +++ b/text/id_test.go @@ -0,0 +1,86 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIDs(t *testing.T) { + assert.Equal(t, 1010000, int(InfoSelfServiceLoginRoot)) + + assert.Equal(t, 1020000, int(InfoSelfServiceLogout)) + + assert.Equal(t, 1030000, int(InfoSelfServiceMFA)) + + assert.Equal(t, 1040000, int(InfoSelfServiceRegistrationRoot)) + assert.Equal(t, 1040001, int(InfoSelfServiceRegistration)) + + assert.Equal(t, 1050000, int(InfoSelfServiceSettings)) + assert.Equal(t, 1050001, int(InfoSelfServiceSettingsUpdateSuccess)) + + assert.Equal(t, 1060000, int(InfoSelfServiceRecovery)) + assert.Equal(t, 1060001, int(InfoSelfServiceRecoverySuccessful)) + assert.Equal(t, 1060002, int(InfoSelfServiceRecoveryEmailSent)) + assert.Equal(t, 1060003, int(InfoSelfServiceRecoveryEmailWithCodeSent)) + + assert.Equal(t, 1070000, int(InfoNodeLabel)) + assert.Equal(t, 1070001, int(InfoNodeLabelInputPassword)) + assert.Equal(t, 1070002, int(InfoNodeLabelGenerated)) + assert.Equal(t, 1070003, int(InfoNodeLabelSave)) + assert.Equal(t, 1070004, int(InfoNodeLabelID)) + assert.Equal(t, 1070005, int(InfoNodeLabelSubmit)) + assert.Equal(t, 1070006, int(InfoNodeLabelVerifyOTP)) + assert.Equal(t, 1070007, int(InfoNodeLabelEmail)) + assert.Equal(t, 1070008, int(InfoNodeLabelResendOTP)) + assert.Equal(t, 1070009, int(InfoNodeLabelContinue)) + assert.Equal(t, 1070010, int(InfoNodeLabelRecoveryCode)) + assert.Equal(t, 1070011, int(InfoNodeLabelVerificationCode)) + assert.Equal(t, 1070018, int(InfoNodeLabelEmailOrPhone)) + + assert.Equal(t, 1080000, int(InfoSelfServiceVerification)) + + assert.Equal(t, 4000000, int(ErrorValidation)) + assert.Equal(t, 4000001, int(ErrorValidationGeneric)) + assert.Equal(t, 4000002, int(ErrorValidationRequired)) + + assert.Equal(t, 4010000, int(ErrorValidationLogin)) + assert.Equal(t, 4010001, int(ErrorValidationLoginFlowExpired)) + + assert.Equal(t, 4040000, int(ErrorValidationRegistration)) + assert.Equal(t, 4040001, int(ErrorValidationRegistrationFlowExpired)) + + assert.Equal(t, 4050000, int(ErrorValidationSettings)) + assert.Equal(t, 4050001, int(ErrorValidationSettingsFlowExpired)) + + assert.Equal(t, 4060000, int(ErrorValidationRecovery)) + assert.Equal(t, 4060001, int(ErrorValidationRecoveryRetrySuccess)) + assert.Equal(t, 4060002, int(ErrorValidationRecoveryStateFailure)) + + assert.Equal(t, 4070000, int(ErrorValidationVerification)) + assert.Equal(t, 4070001, int(ErrorValidationVerificationTokenInvalidOrAlreadyUsed)) + + assert.Equal(t, 5000000, int(ErrorSystem)) + assert.Equal(t, 5000001, int(ErrorSystemGeneric)) + assert.Equal(t, 5000002, int(ErrorSystemNoAuthenticationMethodsAvailable)) + assert.Equal(t, 5000003, int(ErrorSystemOrganizationNoSSOProvidersAvailable)) + + assert.Equal(t, 4060006, int(ErrorValidationRecoveryCodeInvalidOrAlreadyUsed)) + assert.Equal(t, 4070006, int(ErrorValidationVerificationCodeInvalidOrAlreadyUsed)) + + assert.Equal(t, 1080000, int(InfoSelfServiceVerification)) + assert.Equal(t, 1080001, int(InfoSelfServiceVerificationEmailSent)) + assert.Equal(t, 1080002, int(InfoSelfServiceVerificationSuccessful)) + assert.Equal(t, 1080003, int(InfoSelfServiceVerificationEmailWithCodeSent)) + assert.Equal(t, 1080004, int(InfoSelfServiceVerificationPhoneWithCodeSent)) + assert.Equal(t, 1080005, int(InfoSelfServiceVerificationPhoneSuccessful)) + + assert.Equal(t, 1070015, int(InfoNodeLabelCaptcha)) + assert.Equal(t, 4000038, int(ErrorValidationCaptchaError)) + + assert.Equal(t, 4000040, int(ErrorValidationEmail)) + assert.Equal(t, 4000041, int(ErrorValidationPhone)) +} diff --git a/text/message.go b/text/message.go new file mode 100644 index 0000000..e0d7360 --- /dev/null +++ b/text/message.go @@ -0,0 +1,89 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "database/sql/driver" + "encoding/json" + + "github.com/ory/x/sqlxx" +) + +// swagger:model uiTexts +type Messages []Message + +func (h Messages) MarshalJSON() ([]byte, error) { + if len(h) == 0 { + return []byte("[]"), nil + } + + return json.Marshal([]Message(h)) +} + +func (h *Messages) Scan(value interface{}) error { + return sqlxx.JSONScan(h, value) +} + +func (h Messages) Value() (driver.Value, error) { + if h == nil { + return []byte("[]"), nil + } + return json.Marshal(h) +} + +func (h *Messages) Add(m *Message) Messages { + *h = append(*h, *m) + return *h +} + +func (h *Messages) Set(m *Message) Messages { + *h = Messages{*m} + return *h +} + +func (h *Messages) Clear() Messages { + *h = *new(Messages) + return *h +} + +// swagger:model uiText +type Message struct { + // The message ID. + // + // required: true + ID ID `json:"id"` + + // The message text. Written in american english. + // + // required: true + Text string `json:"text"` + + // The message type. + // + // required: true + Type UITextType `json:"type"` + + // The message's context. Useful when customizing messages. + Context json.RawMessage `json:"context,omitempty" faker:"-"` +} + +func (m *Message) Scan(value interface{}) error { + return sqlxx.JSONScan(m, value) +} + +func (m Message) Value() (driver.Value, error) { + return json.Marshal(&m) +} + +func (m *Message) Error() string { + return m.Text +} + +func (m *Message) Is(err error) bool { + em, ok := err.(*Message) + if !ok { + return false + } + return m.ID == em.ID +} diff --git a/text/message_error.go b/text/message_error.go new file mode 100644 index 0000000..597d1b6 --- /dev/null +++ b/text/message_error.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +// This file contains error IDs for all system errors / JSON errors + +const ( + ErrIDNeedsPrivilegedSession = "session_refresh_required" + ErrIDSelfServiceFlowExpired = "self_service_flow_expired" + ErrIDSelfServiceFlowDisabled = "self_service_flow_disabled" + ErrIDSelfServiceBrowserLocationChangeRequiredError = "browser_location_change_required" + ErrIDSelfServiceFlowReplaced = "self_service_flow_replaced" + + ErrIDAlreadyLoggedIn = "session_already_available" + ErrIDAddressNotVerified = "session_verified_address_required" + ErrIDSessionHasAALAlready = "session_aal_already_fulfilled" + ErrIDSessionRequiredForHigherAAL = "session_aal1_required" + ErrIDHigherAALRequired = "session_aal2_required" + ErrIDNoActiveSession = "session_inactive" + ErrIDRedirectURLNotAllowed = "self_service_flow_return_to_forbidden" + ErrIDInitiatedBySomeoneElse = "security_identity_mismatch" + + ErrIDIdentityDisabled = "identity_disabled" + + ErrIDCSRF = "security_csrf_violation" +) diff --git a/text/message_login.go b/text/message_login.go new file mode 100644 index 0000000..8fe3ba5 --- /dev/null +++ b/text/message_login.go @@ -0,0 +1,324 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "time" + + "github.com/ory/x/clock" +) + +func NewInfoLoginReAuth() *Message { + return &Message{ + ID: InfoSelfServiceLoginReAuth, + Type: Info, + Text: "Please confirm this action by verifying that it is you.", + } +} + +func NewInfoLoginMFA() *Message { + return &Message{ + ID: InfoSelfServiceLoginMFA, + Type: Info, + Text: "Please complete the second authentication challenge.", + } +} + +func NewInfoLoginWebAuthnPasswordless() *Message { + return &Message{ + ID: InfoSelfServiceLoginWebAuthnPasswordless, + Type: Info, + Text: "Prepare your WebAuthn device (e.g. security key, biometrics scanner, ...) and press continue.", + } +} + +func NewInfoLoginTOTPLabel() *Message { + return &Message{ + ID: InfoSelfServiceLoginTOTPLabel, + Type: Info, + Text: "Authentication code", + } +} + +func NewInfoLoginLookupLabel() *Message { + return &Message{ + ID: InfoLoginLookupLabel, + Type: Info, + Text: "Backup recovery code", + } +} + +func NewInfoLogin() *Message { + return &Message{ + ID: InfoSelfServiceLogin, + Text: "Sign in", + Type: Info, + } +} + +func NewInfoLoginLinkMessage(dupIdentifier, provider, newLoginURL string, availableCredentials, availableProviders []string) *Message { + return &Message{ + ID: InfoSelfServiceLoginLink, + Type: Info, + Text: fmt.Sprintf( + "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, + "duplicate_identifier": dupIdentifier, + "new_login_url": newLoginURL, + "available_credential_types": availableCredentials, + "available_providers": availableProviders, + }), + } +} + +func NewInfoLoginAndLink() *Message { + return &Message{ + ID: InfoSelfServiceLoginAndLink, + Text: "Sign in and link", + Type: Info, + } +} + +func NewInfoLoginTOTP() *Message { + return &Message{ + ID: InfoLoginTOTP, + Text: "Use Authenticator", + Type: Info, + } +} + +func NewInfoLoginPassword() *Message { + return &Message{ + ID: InfoSelfServiceLoginPassword, + Text: "Sign in with password", + Type: Info, + } +} + +func NewInfoLoginLookup() *Message { + return &Message{ + ID: InfoLoginLookup, + Text: "Use backup recovery code", + Type: Info, + } +} + +func NewInfoLoginVerify() *Message { + return &Message{ + ID: InfoSelfServiceLoginVerify, + Text: "Verify", + Type: Info, + } +} + +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_id": providerId, + }), + } +} + +func NewInfoLoginWithAndLink(provider string) *Message { + return &Message{ + ID: InfoSelfServiceLoginWithAndLink, + Text: fmt.Sprintf("Confirm with %s", provider), + Type: Info, + Context: context(map[string]any{ + "provider": provider, + }), + } +} + +func NewErrorValidationLoginFlowExpired(c clock.Clock, expiredAt time.Time) *Message { + return &Message{ + ID: ErrorValidationLoginFlowExpired, + Text: fmt.Sprintf("The login flow expired %.2f minutes ago, please try again.", c.Now().Sub(expiredAt).Minutes()), + Type: Error, + Context: context(map[string]any{ + "expired_at": expiredAt, + "expired_at_unix": expiredAt.Unix(), + }), + } +} + +func NewErrorValidationLoginNoStrategyFound() *Message { + return &Message{ + ID: ErrorValidationLoginNoStrategyFound, + Text: "Could not find a strategy to log you in with. Did you fill out the form correctly?", + Type: Error, + } +} + +func NewErrorValidationRegistrationNoStrategyFound() *Message { + return &Message{ + ID: ErrorValidationRegistrationNoStrategyFound, + Text: "Could not find a strategy to sign you up with. Did you fill out the form correctly?", + Type: Error, + } +} + +func NewErrorValidationSettingsNoStrategyFound() *Message { + return &Message{ + ID: ErrorValidationSettingsNoStrategyFound, + Text: "Could not find a strategy to update your settings. Did you fill out the form correctly?", + Type: Error, + } +} + +func NewErrorValidationRecoveryNoStrategyFound() *Message { + return &Message{ + ID: ErrorValidationRecoveryNoStrategyFound, + Text: "Could not find a strategy to recover your account with. Did you fill out the form correctly?", + Type: Error, + } +} + +func NewErrorValidationVerificationNoStrategyFound() *Message { + return &Message{ + ID: ErrorValidationVerificationNoStrategyFound, + Text: "Could not find a strategy to verify your account with. Did you fill out the form correctly?", + Type: Error, + } +} + +func NewInfoSelfServiceLoginWebAuthn() *Message { + return &Message{ + ID: InfoSelfServiceLoginWebAuthn, + Text: "Sign in with a hardware key", + Type: Info, + } +} + +func NewInfoSelfServiceLoginDeviceAuthn() *Message { + return &Message{ + ID: InfoSelfServiceLoginDeviceAuthn, + Text: "Sign in with a hardware key", + Type: Info, + } +} + +func NewInfoSelfServiceLoginPasskey() *Message { + return &Message{ + ID: InfoSelfServiceLoginPasskey, + Text: "Sign in with passkey", + Type: Info, + } +} + +func NewInfoSelfServiceContinueLoginWebAuthn() *Message { + return &Message{ + ID: InfoSelfServiceLoginContinueWebAuthn, + Text: "Sign in with a hardware key", + Type: Info, + } +} + +func NewInfoSelfServiceLoginContinue() *Message { + return &Message{ + ID: InfoSelfServiceLoginContinue, + Text: "Continue", + Type: Info, + } +} + +func NewLoginCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceLoginCodeSent, + Type: Info, + Text: "A code was sent to the address you provided. If you didn't receive it, please check the spelling of the address and try again.", + } +} + +// NewLoginCodeSentForAuthenticatedUser is shown on refresh and second factor +// login flows where the address is bound to the authenticated identity rather +// than typed by the user. The wording avoids the inaccurate "address you +// provided" phrasing and the unhelpful "check the spelling" guidance, since +// the user cannot change the recipient mid-flow. +func NewLoginCodeSentForAuthenticatedUser() *Message { + return &Message{ + ID: InfoSelfServiceLoginCodeSentForAuthenticatedUser, + Type: Info, + Text: "A code was sent to your address. If you didn't receive it, please try again.", + } +} + +func NewErrorValidationLoginCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationLoginCodeInvalidOrAlreadyUsed, + Text: "The login code is invalid or has already been used. Please try again.", + Type: Error, + } +} + +func NewErrorValidationLoginRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidationLoginRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + } +} + +func NewInfoSelfServiceLoginCode() *Message { + return &Message{ + ID: InfoSelfServiceLoginCode, + Type: Info, + Text: "Send sign in code", + } +} + +func NewErrorValidationLoginLinkedCredentialsDoNotMatch() *Message { + return &Message{ + ID: ErrorValidationLoginLinkedCredentialsDoNotMatch, + Text: "Linked credentials do not match.", + Type: Error, + } +} + +func NewErrorValidationAddressUnknown() *Message { + return &Message{ + ID: ErrorValidationLoginAddressUnknown, + Text: "The address you entered does not match any known addresses in the current account.", + Type: Error, + } +} + +func NewInfoSelfServiceLoginCodeMFA() *Message { + return &Message{ + ID: InfoSelfServiceLoginCodeMFA, + Type: Info, + Text: "Request code to continue", + } +} + +func NewInfoSelfServiceLoginAAL2CodeAddress(channel string, to string) *Message { + return &Message{ + ID: InfoSelfServiceLoginAAL2CodeAddress, + Type: Info, + Text: fmt.Sprintf("Send code to %s", to), + Context: context(map[string]any{ + "address": to, + "channel": channel, + }), + } +} + +func NewErrorValidationIdentityDisabled() *Message { + return &Message{ + ID: ErrorValidationIdentityDisabled, + Text: "This account has been disabled. Please contact support for assistance.", + Type: Error, + } +} diff --git a/text/message_node.go b/text/message_node.go new file mode 100644 index 0000000..d0919de --- /dev/null +++ b/text/message_node.go @@ -0,0 +1,136 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +func NewInfoNodeLabelVerifyOTP() *Message { + return &Message{ + ID: InfoNodeLabelVerifyOTP, + Text: "Verify code", + Type: Info, + } +} + +func NewInfoNodeLabelVerificationCode() *Message { + return &Message{ + ID: InfoNodeLabelVerificationCode, + Text: "Verification code", + Type: Info, + } +} + +func NewInfoNodeLabelRecoveryCode() *Message { + return &Message{ + ID: InfoNodeLabelRecoveryCode, + Text: "Recovery code", + Type: Info, + } +} + +func NewInfoNodeLabelRegistrationCode() *Message { + return &Message{ + ID: InfoNodeLabelRegistrationCode, + Text: "Registration code", + Type: Info, + } +} + +func NewInfoNodeLabelLoginCode() *Message { + return &Message{ + ID: InfoNodeLabelLoginCode, + Text: "Login code", + Type: Info, + } +} + +func NewInfoNodeInputPassword() *Message { + return &Message{ + ID: InfoNodeLabelInputPassword, + Text: "Password", + Type: Info, + } +} + +func NewInfoNodeLabelGenerated(title string, name string) *Message { + return &Message{ + ID: InfoNodeLabelGenerated, + Text: title, + Type: Info, + Context: context(map[string]any{ + "title": title, + "name": name, + }), + } +} + +func NewInfoNodeLabelSave() *Message { + return &Message{ + ID: InfoNodeLabelSave, + Text: "Save", + Type: Info, + } +} + +func NewInfoNodeLabelSubmit() *Message { + return &Message{ + ID: InfoNodeLabelSubmit, + Text: "Submit", + Type: Info, + } +} + +func NewInfoNodeLabelContinue() *Message { + return &Message{ + ID: InfoNodeLabelContinue, + Text: "Continue", + Type: Info, + } +} + +func NewInfoNodeLabelID() *Message { + return &Message{ + ID: InfoNodeLabelID, + Text: "ID", + Type: Info, + } +} + +func NewInfoNodeInputEmail() *Message { + return &Message{ + ID: InfoNodeLabelEmail, + Text: "Email", + Type: Info, + } +} + +func NewInfoNodeInputPhoneNumber() *Message { + return &Message{ + ID: InfoNodeLabelPhoneNumber, + Text: "Phone number", + Type: Info, + } +} + +func NewInfoNodeInputEmailOrPhone() *Message { + return &Message{ + ID: InfoNodeLabelEmailOrPhone, + Text: "Email or phone number", + Type: Info, + } +} + +func NewInfoNodeResendOTP() *Message { + return &Message{ + ID: InfoNodeLabelResendOTP, + Text: "Resend code", + Type: Info, + } +} + +func NewInfoNodeLoginAndLinkCredential() *Message { + return &Message{ + ID: InfoNodeLabelLoginAndLinkCredential, + Text: "Login and link credential", + Type: Info, + } +} diff --git a/text/message_recovery.go b/text/message_recovery.go new file mode 100644 index 0000000..e5682c8 --- /dev/null +++ b/text/message_recovery.go @@ -0,0 +1,128 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "time" + + "github.com/ory/x/clock" +) + +func NewErrorValidationRecoveryFlowExpired(c clock.Clock, expiredAt time.Time) *Message { + return &Message{ + ID: ErrorValidationRecoveryFlowExpired, + Text: fmt.Sprintf("The recovery flow expired %.2f minutes ago, please try again.", c.Now().Sub(expiredAt).Minutes()), + Type: Error, + Context: context(map[string]any{ + "expired_at": expiredAt, + "expired_at_unix": expiredAt.Unix(), + }), + } +} + +func NewRecoverySuccessful(c clock.Clock, privilegedSessionExpiresAt time.Time) *Message { + hasLeft := privilegedSessionExpiresAt.Sub(c.Now()) + return &Message{ + ID: InfoSelfServiceRecoverySuccessful, + Type: Success, + Text: fmt.Sprintf("You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next %.2f minutes.", hasLeft.Minutes()), + Context: context(map[string]any{ + "privilegedSessionExpiresAt": privilegedSessionExpiresAt, + "privileged_session_expires_at": privilegedSessionExpiresAt, + "privileged_session_expires_at_unix": privilegedSessionExpiresAt.Unix(), + }), + } +} + +func NewRecoveryEmailSent() *Message { + return &Message{ + ID: InfoSelfServiceRecoveryEmailSent, + Type: Info, + Text: "An email containing a recovery link has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + } +} + +func NewRecoveryEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRecoveryEmailWithCodeSent, + Type: Info, + Text: "An email containing a recovery code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + } +} + +func NewRecoveryAskAnyRecoveryAddress() *Message { + return &Message{ + ID: InfoNodeLabelRecoveryAddress, + Text: "Recovery address", + Type: Info, + } +} + +func NewRecoveryCodeRecoverySelectAddressSent(maskedAddress string) *Message { + return &Message{ + ID: InfoSelfServiceRecoveryMessageMaskedWithCodeSent, + Type: Info, + Text: fmt.Sprintf("A recovery code has been sent to %s. If you have not received it, check the spelling of the address and make sure to use the address you registered with.", maskedAddress), + Context: context(map[string]any{ + "masked_address": maskedAddress, + }), + } +} + +func NewRecoveryAskForFullAddress() *Message { + return &Message{ + ID: InfoSelfServiceRecoveryAskForFullAddress, + Type: Info, + Text: "Recover access to your account by providing your recovery address in full.", + } +} + +func NewRecoveryAskToChooseAddress() *Message { + return &Message{ + ID: InfoSelfServiceRecoveryAskToChooseAddress, + Type: Info, + Text: "How do you want to recover your account?", + } +} + +func NewRecoveryBack() *Message { + return &Message{ + ID: InfoSelfServiceRecoveryBack, + Type: Info, + Text: "Back", + } +} + +func NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRecoveryTokenInvalidOrAlreadyUsed, + Text: "The recovery token is invalid or has already been used. Please retry the flow.", + Type: Error, + } +} + +func NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRecoveryCodeInvalidOrAlreadyUsed, + Text: "The recovery code is invalid or has already been used. Please try again.", + Type: Error, + } +} + +func NewErrorValidationRecoveryRetrySuccess() *Message { + return &Message{ + ID: ErrorValidationRecoveryRetrySuccess, + Text: "The request was already completed successfully and can not be retried.", + Type: Error, + } +} + +func NewErrorValidationRecoveryStateFailure() *Message { + return &Message{ + ID: ErrorValidationRecoveryStateFailure, + Text: "The recovery flow reached a failure state and must be retried.", + Type: Error, + } +} diff --git a/text/message_registration.go b/text/message_registration.go new file mode 100644 index 0000000..3efe4c9 --- /dev/null +++ b/text/message_registration.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "time" + + "github.com/ory/x/clock" +) + +func NewInfoRegistration() *Message { + return &Message{ + ID: InfoSelfServiceRegistration, + Text: "Sign up", + Type: Info, + } +} + +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_id": providerID, + }), + } +} + +func NewInfoRegistrationContinue() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationContinue, + Text: "Continue", + Type: Info, + } +} + +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(c clock.Clock, expiredAt time.Time) *Message { + return &Message{ + ID: ErrorValidationRegistrationFlowExpired, + Text: fmt.Sprintf("The registration flow expired %.2f minutes ago, please try again.", c.Now().Sub(expiredAt).Minutes()), + Type: Error, + Context: context(map[string]any{ + "expired_at": expiredAt, + "expired_at_unix": expiredAt.Unix(), + }), + } +} + +func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterWebAuthn, + Text: "Sign up with security key", + Type: Info, + } +} + +func NewInfoSelfServiceRegistrationRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterPasskey, + Text: "Sign up with passkey", + Type: Info, + } +} + +func NewRegistrationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationEmailWithCodeSent, + Type: Info, + Text: "A code has been sent to the address(es) you provided. If you have not received a message, check the spelling of the address and retry the registration.", + } +} + +func NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRegistrationCodeInvalidOrAlreadyUsed, + Text: "The registration code is invalid or has already been used. Please try again.", + Type: Error, + } +} + +func NewErrorValidationRegistrationRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidateionRegistrationRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + } +} + +func NewInfoSelfServiceRegistrationRegisterCode() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterCode, + Text: "Send sign up code", + Type: Info, + } +} diff --git a/text/message_settings.go b/text/message_settings.go new file mode 100644 index 0000000..ecc44a2 --- /dev/null +++ b/text/message_settings.go @@ -0,0 +1,227 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "strings" + "time" + + "github.com/ory/x/clock" +) + +func NewErrorValidationSettingsFlowExpired(c clock.Clock, expiredAt time.Time) *Message { + return &Message{ + ID: ErrorValidationSettingsFlowExpired, + Text: fmt.Sprintf("The settings flow expired %.2f minutes ago, please try again.", c.Now().Sub(expiredAt).Minutes()), + Type: Error, + Context: context(map[string]any{ + "expired_at": expiredAt, + "expired_at_unix": expiredAt.Unix(), + }), + } +} + +func NewErrorValidationSettingsTooManyAddressChanges() *Message { + return &Message{ + ID: ErrorValidationSettingsTooManyAddressChanges, + Text: "You can only change one address at a time. Please update each address separately.", + Type: Error, + } +} + +func NewInfoSelfServiceSettingsTOTPQRCode() *Message { + return &Message{ + ID: InfoSelfServiceSettingsTOTPQRCode, + Text: "Authenticator app QR code", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsTOTPSecret(secret string) *Message { + return &Message{ + ID: InfoSelfServiceSettingsTOTPSecret, + Text: secret, + Type: Info, + Context: context(map[string]any{ + "secret": secret, + }), + } +} +func NewInfoSelfServiceSettingsTOTPSecretLabel() *Message { + return &Message{ + ID: InfoSelfServiceSettingsTOTPSecretLabel, + Text: "This is your authenticator app secret. Use it if you can not scan the QR code.", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsUpdateSuccess() *Message { + return &Message{ + ID: InfoSelfServiceSettingsUpdateSuccess, + Text: "Your changes have been saved!", + Type: Success, + } +} + +func NewInfoSelfServiceSettingsUpdateUnlinkTOTP() *Message { + return &Message{ + ID: InfoSelfServiceSettingsUpdateUnlinkTOTP, + Text: "Unlink TOTP Authenticator App", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsRevealLookup() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRevealLookup, + Text: "Reveal backup recovery codes", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsRegenerateLookup() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegenerateLookup, + Text: "Generate new backup recovery codes", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsDisableLookup() *Message { + return &Message{ + ID: InfoSelfServiceSettingsDisableLookup, + Text: "Disable this method", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsLookupConfirm() *Message { + return &Message{ + ID: InfoSelfServiceSettingsLookupConfirm, + Text: "Confirm backup recovery codes", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsLookupSecretList(secrets []string, raw any) *Message { + return &Message{ + ID: InfoSelfServiceSettingsLookupSecretList, + Text: strings.Join(secrets, ", "), + Type: Info, + Context: context(map[string]any{ + "secrets": raw, + }), + } +} +func NewInfoSelfServiceSettingsLookupSecret(secret string) *Message { + return &Message{ + ID: InfoSelfServiceSettingsLookupSecret, + Text: secret, + Type: Info, + Context: context(map[string]any{ + "secret": secret, + }), + } +} + +func NewInfoSelfServiceSettingsLookupSecretUsed(usedAt time.Time) *Message { + return &Message{ + ID: InfoSelfServiceSettingsLookupSecretUsed, + Text: fmt.Sprintf("Secret was used at %s", usedAt), + Type: Info, + Context: context(map[string]any{ + "used_at": usedAt, + "used_at_unix": usedAt.Unix(), + }), + } +} + +func NewInfoSelfServiceSettingsLookupSecretsLabel() *Message { + return &Message{ + ID: InfoSelfServiceSettingsLookupSecretLabel, + Text: "These are your back up recovery codes. Please keep them in a safe place!", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsUpdateLinkOIDC(provider string) *Message { + return &Message{ + ID: InfoSelfServiceSettingsUpdateLinkOidc, + Text: fmt.Sprintf("Link %s", provider), + Type: Info, + Context: context(map[string]any{ + "provider": provider, + }), + } +} + +func NewInfoSelfServiceSettingsUpdateUnlinkOIDC(provider string) *Message { + return &Message{ + ID: InfoSelfServiceSettingsUpdateUnlinkOidc, + Text: fmt.Sprintf("Unlink %s", provider), + Type: Info, + Context: context(map[string]any{ + "provider": provider, + }), + } +} + +func NewInfoSelfServiceSettingsRegisterWebAuthn() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegisterWebAuthn, + Text: "Add security key", + Type: Info, + } +} + +func NewInfoSelfServiceSettingsRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegisterPasskey, + Text: "Add passkey", + Type: Info, + } +} + +func NewInfoSelfServiceRegisterWebAuthnDisplayName() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegisterWebAuthnDisplayName, + Text: "Name of the security key", + Type: Info, + } +} + +func NewInfoSelfServiceRemoveWebAuthn(name string, createdAt time.Time) *Message { + return &Message{ + ID: InfoSelfServiceSettingsRemoveWebAuthn, + Text: fmt.Sprintf("Remove security key \"%s\"", name), + Type: Info, + Context: context(map[string]any{ + "display_name": name, + "added_at": createdAt, + "added_at_unix": createdAt.Unix(), + }), + } +} + +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(), + }), + } +} + +func NewInfoSelfServiceSettingsManagedByOrganization() *Message { + return &Message{ + ID: InfoSelfServiceSettingsManagedByOrganization, + Text: "Your account is managed by your organization. To change these settings, contact your organization administrator.", + Type: Info, + } +} diff --git a/text/message_system.go b/text/message_system.go new file mode 100644 index 0000000..dbb9244 --- /dev/null +++ b/text/message_system.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +func NewErrorSystemGeneric(reason string) *Message { + return &Message{ + ID: ErrorSystemGeneric, + Text: reason, + Type: Error, + Context: context(map[string]any{ + "reason": reason, + }), + } +} + +func NewErrorSystemNoAuthenticationMethodsAvailable() *Message { + return &Message{ + ID: ErrorSystemNoAuthenticationMethodsAvailable, + Text: "No authentication methods are available. Please contact the system administrator.", + Type: Error, + } +} + +func NewErrorSystemOrganizationNoSSOProvidersAvailable() *Message { + return &Message{ + ID: ErrorSystemOrganizationNoSSOProvidersAvailable, + Text: "Your organization requires SSO authentication, but no SSO provider is configured. Please contact the system administrator.", + Type: Error, + } +} + +func NewCaptchaContainerMessage() *Message { + return &Message{ + ID: InfoNodeLabelCaptcha, + Text: "Please complete the captcha challenge to continue.", + Type: Info, + } +} diff --git a/text/message_test.go b/text/message_test.go new file mode 100644 index 0000000..307f6b7 --- /dev/null +++ b/text/message_test.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessage(t *testing.T) { + expected := &Message{ID: InfoSelfServiceSettingsUpdateSuccess, Text: "foo", Type: Info} + + v, err := expected.Value() + require.NoError(t, err) + + var actual Message + require.NoError(t, actual.Scan(v)) + + assert.EqualValues(t, expected, &actual, v) +} + +func TestNewErrorValidationDuplicateCredentialsWithHints(t *testing.T) { + t.Run("title-cases real provider names", func(t *testing.T) { + msg := NewErrorValidationDuplicateCredentialsWithHints(nil, []string{"google", "github"}, "user@example.com") + assert.Contains(t, msg.Text, "Google, Github") + }) + + t.Run("preserves placeholder templates without title-casing", func(t *testing.T) { + msg := NewErrorValidationDuplicateCredentialsWithHints( + []string{"{available_credential_types_list}"}, + []string{"{available_oidc_providers_list}"}, + "{credential_identifier_hint}", + ) + assert.Contains(t, msg.Text, "{available_oidc_providers_list}") + assert.NotContains(t, msg.Text, "{Available_oidc_providers_list}") + }) +} + +func TestMessages(t *testing.T) { + expected := Messages{{ID: InfoSelfServiceSettingsUpdateSuccess, Text: "foo", Type: Info}} + + v, err := expected.Value() + require.NoError(t, err) + + var actual Messages + require.NoError(t, actual.Scan(v)) + + assert.EqualValues(t, expected, actual, v) +} diff --git a/text/message_validation.go b/text/message_validation.go new file mode 100644 index 0000000..010f690 --- /dev/null +++ b/text/message_validation.go @@ -0,0 +1,511 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "strings" + + "github.com/ory/x/stringslice" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func NewValidationErrorGeneric(reason string) *Message { + return &Message{ + ID: ErrorValidationGeneric, + Text: reason, + Type: Error, + Context: context(map[string]any{ + "reason": reason, + }), + } +} + +func NewValidationErrorRequired(missing string) *Message { + return &Message{ + ID: ErrorValidationRequired, + Text: fmt.Sprintf("Property %s is missing.", missing), + Type: Error, + Context: context(map[string]any{ + "property": missing, + }), + } +} + +func NewErrorValidationMinLength(minLength, actualLength int) *Message { + return &Message{ + ID: ErrorValidationMinLength, + Text: fmt.Sprintf("length must be >= %d, but got %d", minLength, actualLength), + Type: Error, + Context: context(map[string]any{ + "min_length": minLength, + "actual_length": actualLength, + }), + } +} + +func NewErrorValidationMaxLength(maxLength, actualLength int) *Message { + return &Message{ + ID: ErrorValidationMaxLength, + Text: fmt.Sprintf("length must be <= %d, but got %d", maxLength, actualLength), + Type: Error, + Context: context(map[string]any{ + "max_length": maxLength, + "actual_length": actualLength, + }), + } +} + +func NewErrorValidationInvalidFormat(pattern string) *Message { + return &Message{ + ID: ErrorValidationInvalidFormat, + Text: fmt.Sprintf("does not match pattern %q", pattern), + Type: Error, + Context: context(map[string]any{ + "pattern": pattern, + }), + } +} + +func NewErrorValidationMinimum(minimum, actual float64) *Message { + return &Message{ + ID: ErrorValidationMinimum, + Text: fmt.Sprintf("must be >= %v but found %v", minimum, actual), + Type: Error, + Context: context(map[string]any{ + "minimum": minimum, + "actual": actual, + }), + } +} + +func NewErrorValidationExclusiveMinimum(minimum, actual float64) *Message { + return &Message{ + ID: ErrorValidationExclusiveMinimum, + Text: fmt.Sprintf("must be > %v but found %v", minimum, actual), + Type: Error, + Context: context(map[string]any{ + "minimum": minimum, + "actual": actual, + }), + } +} + +func NewErrorValidationMaximum(maximum, actual float64) *Message { + return &Message{ + ID: ErrorValidationMaximum, + Text: fmt.Sprintf("must be <= %v but found %v", maximum, actual), + Type: Error, + Context: context(map[string]any{ + "maximum": maximum, + "actual": actual, + }), + } +} + +func NewErrorValidationExclusiveMaximum(maximum, actual float64) *Message { + return &Message{ + ID: ErrorValidationExclusiveMaximum, + Text: fmt.Sprintf("must be < %v but found %v", maximum, actual), + Type: Error, + Context: context(map[string]any{ + "maximum": maximum, + "actual": actual, + }), + } +} + +func NewErrorValidationMultipleOf(base, actual float64) *Message { + return &Message{ + ID: ErrorValidationMultipleOf, + Text: fmt.Sprintf("%v not multipleOf %v", actual, base), + Type: Error, + Context: context(map[string]any{ + "base": base, + "actual": actual, + }), + } +} + +func NewErrorValidationMaxItems(maxItems, actualItems int) *Message { + return &Message{ + ID: ErrorValidationMaxItems, + Text: fmt.Sprintf("maximum %d items allowed, but found %d items", maxItems, actualItems), + Type: Error, + Context: context(map[string]any{ + "max_items": maxItems, + "actual_items": actualItems, + }), + } +} + +func NewErrorValidationMinItems(minItems, actualItems int) *Message { + return &Message{ + ID: ErrorValidationMinItems, + Text: fmt.Sprintf("minimum %d items allowed, but found %d items", minItems, actualItems), + Type: Error, + Context: context(map[string]any{ + "min_items": minItems, + "actual_items": actualItems, + }), + } +} + +func NewErrorValidationUniqueItems(indexA, indexB int) *Message { + return &Message{ + ID: ErrorValidationUniqueItems, + Text: fmt.Sprintf("items at index %d and %d are equal", indexA, indexB), + Type: Error, + Context: context(map[string]any{ + "index_a": indexA, + "index_b": indexB, + }), + } +} + +func NewErrorValidationWrongType(allowedTypes []string, actualType string) *Message { + return &Message{ + ID: ErrorValidationWrongType, + Text: fmt.Sprintf("expected %s, but got %s", strings.Join(allowedTypes, " or "), actualType), + Type: Error, + Context: context(map[string]any{ + "allowed_types": allowedTypes, + "actual_type": actualType, + }), + } +} + +func NewErrorValidationConst(expected any) *Message { + return &Message{ + ID: ErrorValidationConst, + Text: fmt.Sprintf("must be equal to constant %v", expected), + Type: Error, + Context: context(map[string]any{ + "expected": expected, + }), + } +} + +func NewErrorValidationConstGeneric() *Message { + return &Message{ + ID: ErrorValidationConstGeneric, + Text: "const failed", + Type: Error, + } +} + +func NewErrorValidationEmail(value string) *Message { + return &Message{ + ID: ErrorValidationEmail, + Text: "Enter a valid email address", + Type: Error, + Context: context(map[string]any{ + "value": value, + }), + } +} + +func NewErrorValidationPhone(value string) *Message { + return &Message{ + ID: ErrorValidationPhone, + Text: "Enter a valid phone number", + Type: Error, + Context: context(map[string]any{ + "value": value, + }), + } +} + +func NewErrorValidationPasswordPolicyViolationGeneric(reason string) *Message { + return &Message{ + ID: ErrorValidationPasswordPolicyViolationGeneric, + Text: fmt.Sprintf("The password can not be used because %s.", reason), + Type: Error, + Context: context(map[string]any{ + "reason": reason, + }), + } +} + +func NewErrorValidationPasswordIdentifierTooSimilar() *Message { + return &Message{ + ID: ErrorValidationPasswordIdentifierTooSimilar, + Text: "The password can not be used because it is too similar to the identifier.", + Type: Error, + } +} + +func NewErrorValidationPasswordMinLength(minLength, actualLength int) *Message { + return &Message{ + ID: ErrorValidationPasswordMinLength, + Text: fmt.Sprintf("The password must be at least %d characters long, but got %d.", minLength, actualLength), + Type: Error, + Context: context(map[string]any{ + "min_length": minLength, + "actual_length": actualLength, + }), + } +} + +func NewErrorValidationPasswordMaxLength(maxLength, actualLength int) *Message { + return &Message{ + ID: ErrorValidationPasswordMaxLength, + Text: fmt.Sprintf("The password must be at most %d characters long, but got %d.", maxLength, actualLength), + Type: Error, + Context: context(map[string]any{ + "max_length": maxLength, + "actual_length": actualLength, + }), + } +} + +func NewErrorValidationPasswordNewSameAsOld() *Message { + return &Message{ + ID: ErrorValidationPasswordNewSameAsOld, + Text: "The new password must be different from the old password.", + Type: Error, + } +} + +func NewErrorValidationPasswordTooManyBreaches(breaches int64) *Message { + return &Message{ + ID: ErrorValidationPasswordTooManyBreaches, + Text: "The password has been found in data breaches and must no longer be used.", + Type: Error, + Context: context(map[string]any{ + "breaches": breaches, + }), + } +} + +func NewErrorValidationInvalidCredentials() *Message { + return &Message{ + ID: ErrorValidationInvalidCredentials, + Text: "The provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number.", + Type: Error, + } +} + +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, + Text: "An account with the same identifier (email, phone, username, ...) exists already.", + Type: Error, + } +} + +func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes []string, availableOIDCProviders []string, credentialIdentifierHint string) *Message { + identifier := credentialIdentifierHint + if identifier == "" { + identifier = "an email, phone, or username" + } + oidcProviders := make([]string, 0, len(availableOIDCProviders)) + for _, provider := range availableOIDCProviders { + if strings.ContainsAny(provider, "{}") { + oidcProviders = append(oidcProviders, provider) + } else { + oidcProviders = append(oidcProviders, cases.Title(language.English).String(provider)) + } + } + + reason := fmt.Sprintf("You tried signing in with %s which is already in use by another account.", identifier) + if len(availableCredentialTypes) > 0 { + humanReadable := make([]string, 0, len(availableCredentialTypes)) + for _, cred := range availableCredentialTypes { + switch cred { + case "password": + humanReadable = append(humanReadable, "your password") + case "oidc", "saml": + humanReadable = append(humanReadable, "social sign in") + case "webauthn": + 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 + humanReadable = append(humanReadable, availableCredentialTypes...) + } + + humanReadable = stringslice.Unique(humanReadable) + + // 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, ", ")) + } + } + if len(oidcProviders) > 0 { + reason += fmt.Sprintf(" You can sign in using one of the following social sign in providers: %s.", strings.Join(oidcProviders, ", ")) + } + + return &Message{ + ID: ErrorValidationDuplicateCredentialsWithHints, + Text: reason, + Type: Error, + Context: context(map[string]any{ + "available_credential_types": availableCredentialTypes, + "available_oidc_providers": availableOIDCProviders, + "credential_identifier_hint": credentialIdentifierHint, + }), + } +} + +func NewErrorValidationDuplicateCredentialsOnOIDCLink() *Message { + return &Message{ + ID: ErrorValidationDuplicateCredentialsOnOIDCLink, + Text: "An account with the same identifier (email, phone, username, ...) exists already. " + + "Please sign in to your existing account to link your social profile.", + Type: Error, + } +} + +func NewErrorValidationTOTPVerifierWrong() *Message { + return &Message{ + ID: ErrorValidationTOTPVerifierWrong, + Text: "The provided authentication code is invalid, please try again.", + Type: Error, + } +} + +func NewErrorValidationWebAuthnVerifierWrong() *Message { + return &Message{ + ID: ErrorValidationWebAuthnVerifierWrong, + Text: "The provided web authn login is invalid, please try again.", + Type: Error, + } +} +func NewErrorValidationDeviceAuthnVerifierWrong() *Message { + return &Message{ + ID: ErrorValidationDeviceAuthnVerifierWrong, + Text: "The provided DeviceAuthn signature is invalid.", + Type: Error, + } +} + +func NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid() *Message { + return &Message{ + ID: ErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid, + Text: "This DeviceAuthn key can no longer be used because relaxed attestation is expired or disabled. Please enroll your device again.", + Type: Error, + } +} + +func NewErrorValidationDeviceAuthnKeyReenrollmentRequired() *Message { + return &Message{ + ID: ErrorValidationDeviceAuthnKeyReenrollmentRequired, + Text: "This DeviceAuthn key was enrolled before user verification was introduced and can no longer be used. Please enroll your device again.", + Type: Error, + } +} + +func NewErrorValidationLookupAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationLookupAlreadyUsed, + Text: "This backup recovery code has already been used.", + Type: Error, + } +} + +func NewErrorValidationLookupInvalid() *Message { + return &Message{ + ID: ErrorValidationLookupInvalid, + Text: "The backup recovery code is not valid.", + Type: Error, + } +} + +func NewErrorValidationIdentifierMissing() *Message { + return &Message{ + ID: ErrorValidationIdentifierMissing, + Text: "Could not find any login identifiers. Did you forget to set them? This could also be caused by a server misconfiguration.", + Type: Error, + } +} + +func NewErrorValidationAddressNotVerified() *Message { + return &Message{ + ID: ErrorValidationAddressNotVerified, + Text: "Account not active yet. Did you forget to verify your email address?", + Type: Error, + } +} + +func NewErrorValidationNoTOTPDevice() *Message { + return &Message{ + ID: ErrorValidationNoTOTPDevice, + Text: "You have no TOTP device set up.", + Type: Error, + } +} + +func NewErrorValidationNoLookup() *Message { + return &Message{ + ID: ErrorValidationNoLookup, + Text: "You have no backup recovery codes set up.", + Type: Error, + } +} + +func NewErrorValidationNoWebAuthnDevice() *Message { + return &Message{ + ID: ErrorValidationNoWebAuthnDevice, + Text: "You have no WebAuthn device set up.", + Type: Error, + } +} + +func NewErrorValidationSuchNoWebAuthnUser() *Message { + return &Message{ + ID: ErrorValidationSuchNoWebAuthnUser, + Text: "This account does not exist or has no security key set up.", + Type: Error, + } +} + +func NewErrorValidationNoDeviceAuthnDevice() *Message { + return &Message{ + ID: ErrorValidationNoDeviceAuthnDevice, + Text: "You have no DeviceAuthn device set up.", + Type: Error, + } +} + +func NewErrorValidationNoCodeUser() *Message { + return &Message{ + ID: ErrorValidationNoCodeUser, + Text: "This account does not exist or has not setup sign in with code.", + Type: Error, + } +} + +func NewErrorValidationTraitsMismatch() *Message { + return &Message{ + ID: ErrorValidationTraitsMismatch, + Text: "The provided traits do not match the traits previously associated with this flow.", + Type: Error, + } +} + +func NewErrorCaptchaFailed() *Message { + return &Message{ + ID: ErrorValidationCaptchaError, + Text: "Captcha verification failed, please try again.", + Type: Error, + } +} diff --git a/text/message_verification.go b/text/message_verification.go new file mode 100644 index 0000000..f0b5714 --- /dev/null +++ b/text/message_verification.go @@ -0,0 +1,105 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "fmt" + "time" + + "github.com/ory/x/clock" +) + +func NewErrorValidationVerificationFlowExpired(c clock.Clock, expiredAt time.Time) *Message { + return &Message{ + ID: ErrorValidationVerificationFlowExpired, + Text: fmt.Sprintf("The verification flow expired %.2f minutes ago, please try again.", c.Now().Sub(expiredAt).Minutes()), + Type: Error, + Context: context(map[string]any{ + "expired_at": expiredAt, + "expired_at_unix": expiredAt.Unix(), + }), + } +} + +func NewInfoSelfServiceVerificationSuccessful() *Message { + return &Message{ + ID: InfoSelfServiceVerificationSuccessful, + Type: Success, + Text: "You successfully verified your email address.", + } +} + +func NewInfoSelfServiceVerificationPhoneSuccessful() *Message { + return &Message{ + ID: InfoSelfServiceVerificationPhoneSuccessful, + Type: Success, + Text: "You successfully verified your phone number.", + } +} + +func NewVerificationEmailSent() *Message { + return &Message{ + ID: InfoSelfServiceVerificationEmailSent, + Type: Info, + Text: "An email containing a verification link has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + } +} + +func NewErrorValidationVerificationTokenInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationVerificationTokenInvalidOrAlreadyUsed, + Text: "The verification token is invalid or has already been used. Please retry the flow.", + Type: Error, + } +} + +func NewErrorValidationVerificationRetrySuccess() *Message { + return &Message{ + ID: ErrorValidationVerificationRetrySuccess, + Text: "The request was already completed successfully and can not be retried.", + Type: Error, + } +} + +func NewErrorValidationVerificationStateFailure() *Message { + return &Message{ + ID: ErrorValidationVerificationStateFailure, + Text: "The verification flow reached a failure state and must be retried.", + Type: Error, + } +} + +func NewErrorValidationVerificationCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationVerificationCodeInvalidOrAlreadyUsed, + Text: "The verification code is invalid or has already been used. Please try again.", + Type: Error, + } +} + +func NewVerificationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceVerificationEmailWithCodeSent, + Type: Info, + Text: "An email containing a verification code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and make sure to use the address you registered with.", + } +} + +func NewVerificationPhoneWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceVerificationPhoneWithCodeSent, + Type: Info, + Text: "A text message containing a verification code has been sent to the phone number you provided. If you have not received a text message, check the spelling of the number and make sure to use the number you registered with.", + } +} + +// VerificationCodeSentMessage returns the "code has been sent" message for the +// channel the code was delivered over. SMS verifications get the phone wording; +// every other channel falls back to the email wording. +func VerificationCodeSentMessage(via string) *Message { + if via == "sms" { + return NewVerificationPhoneWithCodeSent() + } + return NewVerificationEmailWithCodeSent() +} diff --git a/text/message_verification_test.go b/text/message_verification_test.go new file mode 100644 index 0000000..6874682 --- /dev/null +++ b/text/message_verification_test.go @@ -0,0 +1,45 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/ory/x/clock" +) + +func TestNewErrorValidationVerificationFlowExpired(t *testing.T) { + t.Parallel() + + expiredAt := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + // "Now" is exactly two minutes after the flow expired. + c := clock.NewMock(expiredAt.Add(2 * time.Minute)) + + msg := NewErrorValidationVerificationFlowExpired(c, expiredAt) + + assert.Equal(t, ErrorValidationVerificationFlowExpired, msg.ID) + assert.Equal(t, "The verification flow expired 2.00 minutes ago, please try again.", msg.Text) +} + +func TestVerificationCodeSentMessage(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + via string + wantID ID + }{ + {via: "sms", wantID: InfoSelfServiceVerificationPhoneWithCodeSent}, + {via: "email", wantID: InfoSelfServiceVerificationEmailWithCodeSent}, + // Any unknown channel falls back to the email message. + {via: "", wantID: InfoSelfServiceVerificationEmailWithCodeSent}, + } { + t.Run("via="+tc.via, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.wantID, VerificationCodeSentMessage(tc.via).ID) + }) + } +} diff --git a/text/type.go b/text/type.go new file mode 100644 index 0000000..138fa28 --- /dev/null +++ b/text/type.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package text + +// swagger:enum UITextType +type UITextType string + +// aligned with https://github.com/ory/elements/blob/main/src/theme/message.css.ts +const ( + Info UITextType = "info" + Error UITextType = "error" + Success UITextType = "success" +) diff --git a/ui/container/container.go b/ui/container/container.go new file mode 100644 index 0000000..9444877 --- /dev/null +++ b/ui/container/container.go @@ -0,0 +1,394 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "context" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "net/http" + "regexp" + "slices" + "strings" + + "github.com/ory/kratos/ui/node" + "github.com/ory/x/sqlxx" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/x/decoderx" + "github.com/ory/x/jsonschemax" + "github.com/ory/x/jsonx" + + "github.com/ory/kratos/schema" + "github.com/ory/kratos/text" +) + +var ( + _ ErrorParser = (*Container)(nil) + _ ValueSetter = (*Container)(nil) + _ Resetter = (*Container)(nil) + _ CSRFSetter = (*Container)(nil) + _ NodeGetter = (*Container)(nil) +) + +// Container represents a HTML Form. The container can work with both HTTP Form and JSON requests +// +// swagger:model uiContainer +type Container struct { + // Action should be used as the form action URL `
    `. + // + // required: true + Action string `json:"action" faker:"url"` + + // Method is the form method (e.g. POST) + // + // required: true + Method string `json:"method" faker:"http_method"` + + // Nodes contains the form's nodes + // + // The form's nodes can be input fields, text, images, and other UI elements. + // + // required: true + Nodes node.Nodes `json:"nodes"` + + // Messages contains all global form messages and errors. + Messages text.Messages `json:"messages,omitempty"` +} + +// New returns an empty container. +func New(action string) *Container { + return &Container{ + Action: action, + Method: "POST", + Nodes: node.Nodes{}, + } +} + +// NewFromHTTPRequest creates a new Container and populates fields by parsing the HTTP Request body. +// A jsonSchemaRef needs to be added to allow HTTP Form Post Body parsing. +func NewFromHTTPRequest(r *http.Request, group node.UiNodeGroup, action string, compiler decoderx.HTTPDecoderOption) (*Container, error) { + c := New(action) + raw := json.RawMessage(`{}`) + if err := decoderx.Decode(r, &raw, compiler); err != nil { + if err := c.ParseError(group, err); err != nil { + return nil, err + } + } + + c.UpdateNodeValuesFromJSON(raw, "", group) + return c, nil +} + +// NewFromJSON creates a UI Container based on the provided JSON struct. +func NewFromJSON(action string, group node.UiNodeGroup, raw json.RawMessage, prefix string) *Container { + c := New(action) + c.UpdateNodeValuesFromJSON(raw, prefix, group) + return c +} + +// NewFromStruct creates a UI Container based on serialized contents of the provided struct. +func NewFromStruct(action string, group node.UiNodeGroup, v interface{}, prefix string) (*Container, error) { + c := New(action) + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + c.UpdateNodeValuesFromJSON(data, prefix, group) + return c, nil +} + +// NewFromJSONSchema creates a new Container and populates the fields +// using the provided JSON Schema. When compiler is nil, disallowRefs toggles +// the `$ref` scheme restriction on the default compiler; otherwise the +// caller is responsible for constructing a compiler with the desired +// restriction. +func NewFromJSONSchema(ctx context.Context, action string, group node.UiNodeGroup, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler, disallowRefs bool) (*Container, error) { + c := New(action) + nodes, err := NodesFromJSONSchema(ctx, group, jsonSchemaRef, prefix, compiler, disallowRefs) + if err != nil { + return nil, err + } + + c.Nodes = nodes + return c, nil +} + +func NodesFromJSONSchema(ctx context.Context, group node.UiNodeGroup, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler, disallowRefs bool) (node.Nodes, error) { + if compiler == nil { + // When the caller doesn't supply a compiler, we build a default one + // ourselves. Apply the caller's disallowRefs preference so the default + // doesn't silently downgrade to legacy behavior. + var err error + compiler, err = schema.NewCompilerWithURL(ctx, jsonSchemaRef, disallowRefs) + if err != nil { + return nil, err + } + } + paths, err := jsonschemax.ListPaths(ctx, jsonSchemaRef, compiler) + if err != nil { + return nil, err + } + + nodes := node.Nodes{} + for _, value := range paths { + if value.TypeHint == jsonschemax.JSON { + continue + } + + name := addPrefix(value.Name, prefix, ".") + nodes = append(nodes, node.NewInputFieldFromSchema(name, group, value)) + } + + return nodes, nil +} + +func (c *Container) GetNodes() *node.Nodes { + return &c.Nodes +} + +func (c *Container) SortNodes(ctx context.Context, opts ...node.SortOption) error { + return c.Nodes.SortBySchema(ctx, opts...) +} + +// ResetMessages resets the container's own and its node's messages. +func (c *Container) ResetMessages(exclude ...string) { + c.Messages = nil + for k, n := range c.Nodes { + if !slices.Contains(exclude, n.ID()) { + n.Messages = nil + } + c.Nodes[k] = n + } +} + +// Reset resets the container's errors as well as each field's value and errors. +func (c *Container) Reset(exclude ...string) { + c.Messages = nil + c.Nodes.Reset(exclude...) +} + +// ParseError type asserts the given error and sets the container's errors or a +// field's errors and if the error is not something to be handled by the +// formUI Container, the error is returned. +// +// This method DOES NOT touch the values of the node values/names, only its errors. +func (c *Container) ParseError(group node.UiNodeGroup, err error) error { + if e := richError(nil); errors.As(err, &e) { + switch e.ID() { + case text.ErrIDIdentityDisabled: + c.AddMessage(group, text.NewErrorValidationIdentityDisabled()) + return nil + default: + if e.StatusCode() == http.StatusBadRequest { + c.AddMessage(group, text.NewValidationErrorGeneric(e.Reason())) + return nil + } + } + + return err + } else if e := new(schema.ValidationError); errors.As(err, &e) { + pointer, _ := jsonschemax.JSONPointerToDotNotation(e.InstancePtr) + for i := range e.Messages { + c.AddMessage(group, &e.Messages[i], pointer) + } + return nil + } else if e := new(jsonschema.ValidationError); errors.As(err, &e) { + switch ctx := e.Context.(type) { + case *jsonschema.ValidationErrorContextRequired: + for _, required := range ctx.Missing { + // The pointer can be ignored because if there is an error, we'll just use + // the empty field (global error). + pointer, _ := jsonschemax.JSONPointerToDotNotation(required) + segments := strings.Split(required, "/") + c.AddMessage(group, text.NewValidationErrorRequired(segments[len(segments)-1]), pointer) + } + default: + // The pointer can be ignored because if there is an error, we'll just use + // the empty field (global error). + causes := e.Causes + if len(e.Causes) == 0 { + pointer, _ := jsonschemax.JSONPointerToDotNotation(e.InstancePtr) + c.AddMessage(group, translateValidationError(e), pointer) + return nil + } + + for _, ee := range causes { + if err := c.ParseError(group, ee); err != nil { + return err + } + } + } + return nil + } else if e := new(schema.ValidationListError); errors.As(err, &e) { + for _, ee := range e.Validations { + if err := c.ParseError(group, ee); err != nil { + return err + } + } + return nil + } + return err +} + +var formatErrorMatcher = regexp.MustCompile(`"([^"]+)"\s+is not valid\s+"([^"]+)"`) + +func translateValidationError(err *jsonschema.ValidationError) *text.Message { + segments := strings.Split(err.SchemaPtr, "/") + switch segments[len(segments)-1] { + case "minLength": + minLength, actual := -1, -1 + _, _ = fmt.Sscanf(err.Message, "length must be >= %d, but got %d", &minLength, &actual) + return text.NewErrorValidationMinLength(minLength, actual) + case "maxLength": + maxLength, actual := -1, -1 + _, _ = fmt.Sscanf(err.Message, "length must be <= %d, but got %d", &maxLength, &actual) + return text.NewErrorValidationMaxLength(maxLength, actual) + case "pattern": + pattern := "" + _, _ = fmt.Sscanf(err.Message, "does not match pattern %q", &pattern) + return text.NewErrorValidationInvalidFormat(pattern) + case "minimum": + minimum, actual := -1.0, -1.0 + _, _ = fmt.Sscanf(err.Message, "must be >= %v but found %v", &minimum, &actual) + return text.NewErrorValidationMinimum(minimum, actual) + case "exclusiveMinimum": + minimum, actual := -1.0, -1.0 + _, _ = fmt.Sscanf(err.Message, "must be > %v but found %v", &minimum, &actual) + return text.NewErrorValidationExclusiveMinimum(minimum, actual) + case "maximum": + maximum, actual := -1.0, -1.0 + _, _ = fmt.Sscanf(err.Message, "must be <= %v but found %v", &maximum, &actual) + return text.NewErrorValidationMaximum(maximum, actual) + case "exclusiveMaximum": + maximum, actual := -1.0, -1.0 + _, _ = fmt.Sscanf(err.Message, "must be < %v but found %v", &maximum, &actual) + return text.NewErrorValidationExclusiveMaximum(maximum, actual) + case "multipleOf": + base, actual := -1.0, -1.0 + _, _ = fmt.Sscanf(err.Message, "%v not multipleOf %v", &actual, &base) + return text.NewErrorValidationMultipleOf(base, actual) + case "maxItems": + maxItems, actual := -1, -1 + _, _ = fmt.Sscanf(err.Message, "maximum %d items allowed, but found %d items", &maxItems, &actual) + return text.NewErrorValidationMaxItems(maxItems, actual) + case "minItems": + minItems, actual := -1, -1 + _, _ = fmt.Sscanf(err.Message, "minimum %d items allowed, but found %d items", &minItems, &actual) + return text.NewErrorValidationMinItems(minItems, actual) + case "uniqueItems": + indexA, indexB := -1, -1 + _, _ = fmt.Sscanf(err.Message, "items at index %d and %d are equal", &indexA, &indexB) + return text.NewErrorValidationUniqueItems(indexA, indexB) + case "type": + allowedTypes, actualType, _ := strings.Cut(strings.TrimPrefix(err.Message, "expected "), ", but got ") + return text.NewErrorValidationWrongType(strings.Split(allowedTypes, " or "), actualType) + case "const": + if err.Message != "const failed" { + expectedValue := strings.TrimPrefix(err.Message, "value must be ") + return text.NewErrorValidationConst(expectedValue) + } + return text.NewErrorValidationConstGeneric() + case "format": + m := formatErrorMatcher.FindStringSubmatch(err.Message) + if len(m) != 3 { + return text.NewValidationErrorGeneric(err.Message) + } + + value := m[1] + format := m[2] + + switch format { + case "email", "email_loose": + return text.NewErrorValidationEmail(value) + case "tel": + return text.NewErrorValidationPhone(value) + default: + return text.NewValidationErrorGeneric(err.Message) + } + default: + return text.NewValidationErrorGeneric(err.Message) + } +} + +// UpdateNodeValuesFromJSON sets the container's fields to the provided values. +func (c *Container) UpdateNodeValuesFromJSON(raw json.RawMessage, prefix string, group node.UiNodeGroup) { + for k, v := range jsonx.Flatten(raw) { + k = addPrefix(k, prefix, ".") + + if n := c.Nodes.Find(k); n != nil { + n.Attributes.SetValue(v) + n.Group = group + continue + } + + c.Nodes.Upsert(node.NewInputFieldFromJSON(k, v, group)) + } +} + +// Unset removes a field from the container. +func (c *Container) UnsetNode(id string) { + c.Nodes.Remove(id) +} + +// SetCSRF sets the CSRF value using e.g. nosurf.Token(r). +func (c *Container) SetCSRF(token string) { + c.SetNode(node.NewCSRFNode(token)) +} + +// SetNode sets a field. +func (c *Container) SetNode(n *node.Node) { + c.Nodes.Upsert(n) +} + +// SetValue sets a container's field to the provided name and value. +func (c *Container) SetValue(id string, n *node.Node) { + if f := c.Nodes.Find(id); f != nil { + f.Attributes.SetValue(n.GetValue()) + return + } + + c.Nodes.Upsert(n) +} + +// AddMessage adds the provided error, and if a non-empty names list is set, +// adds the error on the corresponding field. +func (c *Container) AddMessage(group node.UiNodeGroup, err *text.Message, setForFields ...string) { + if !slices.ContainsFunc(setForFields, func(s string) bool { return strings.TrimSpace(s) != "" }) { + c.Messages = append(c.Messages, *err) + return + } + + for _, name := range setForFields { + if ff := c.Nodes.Find(name); ff != nil { + ff.Messages = append(ff.Messages, *err) + continue + } + + n := node.NewInputField(name, nil, node.DefaultGroup, node.InputAttributeTypeText) + n.Messages = text.Messages{*err} + c.Nodes = append(c.Nodes, n) + } +} + +func (c *Container) Scan(value interface{}) error { + return sqlxx.JSONScan(c, value) +} + +func (c *Container) Value() (driver.Value, error) { + if c == nil { + return nil, nil + } + return json.Marshal(c) +} + +func addPrefix(name, prefix, separator string) string { + if prefix == "" { + return name + } + return fmt.Sprintf("%s%s%s", prefix, separator, name) +} diff --git a/ui/container/container_test.go b/ui/container/container_test.go new file mode 100644 index 0000000..3a0d5f1 --- /dev/null +++ b/ui/container/container_test.go @@ -0,0 +1,335 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "sort" + "testing" + + "github.com/ory/kratos/ui/node" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/herodot" + "github.com/ory/x/decoderx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/schema" + "github.com/ory/kratos/text" +) + +func newJSONRequest(t *testing.T, j string) *http.Request { + req, err := http.NewRequest("POST", "/", bytes.NewBufferString(j)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + return req +} + +func newFormRequest(t *testing.T, values url.Values) *http.Request { + req, err := http.NewRequest("POST", "/", bytes.NewBufferString(values.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req +} + +func TestContainer(t *testing.T) { + ctx := context.Background() + t.Run("method=NewFromJSON", func(t *testing.T) { + for k, tc := range []struct { + r string + prefix string + expect *Container + }{ + { + r: `{"numby":1.5,"stringy":"foobar","objy":{"objy":{},"numby":1.5,"stringy":"foobar"}}`, + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputFieldFromJSON("numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.stringy", "foobar", node.DefaultGroup), + node.NewInputFieldFromJSON("stringy", "foobar", node.DefaultGroup), + }, + }, + }, + { + r: `{"numby":1.5,"stringy":"foobar","objy":{"objy":{},"numby":1.5,"stringy":"foobar"}}`, + prefix: "traits", + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputFieldFromJSON("traits.numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("traits.objy.numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("traits.objy.stringy", "foobar", node.DefaultGroup), + node.NewInputFieldFromJSON("traits.stringy", "foobar", node.DefaultGroup), + }, + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + actual := NewFromJSON("action", node.DefaultGroup, json.RawMessage(tc.r), tc.prefix) + + // sort actual.fields lexicographically to have a deterministic order + sort.SliceStable(actual.Nodes, func(i, j int) bool { + return actual.Nodes[i].ID() < actual.Nodes[j].ID() + }) + + assert.Equal(t, "action", actual.Action) + assert.EqualValues(t, tc.expect.Nodes, actual.Nodes) + }) + } + }) + + t.Run("method=NewFromHTTPRequest", func(t *testing.T) { + for k, tc := range []struct { + ref string + r *http.Request + expect *Container + }{ + { + ref: "./stub/simple.schema.json", + r: newJSONRequest(t, `{"numby":1.5,"stringy":"foobar","objy":{"objy":{},"numby":1.5,"stringy":"foobar"}}`), + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputFieldFromJSON("numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.stringy", "foobar", node.DefaultGroup), + node.NewInputFieldFromJSON("stringy", "foobar", node.DefaultGroup), + }, + }, + }, + { + ref: "./stub/simple.schema.json", + r: newFormRequest(t, url.Values{ + "numby": {"1.5"}, + "stringy": {"foobar"}, + "objy.numby": {"1.5"}, + "objy.stringy": {"foobar"}, + }), + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputFieldFromJSON("numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.numby", 1.5, node.DefaultGroup), + node.NewInputFieldFromJSON("objy.stringy", "foobar", node.DefaultGroup), + node.NewInputFieldFromJSON("stringy", "foobar", node.DefaultGroup), + }, + }, + }, + // FIXME https://github.com/ory/kratos/issues/1316 + // + //{ + // ref: "./stub/complex.schema.json", + // r: newFormRequest(t, url.Values{ + // "meal.chef": {"aeneas"}, + // }), + // expect: &Container{ + // Nodes: node.Nodes{ + // node.NewInputFieldFromJSON("meal.chef", "aeneas", node.DefaultGroup), + // &node.Node{Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "meal.name", Type: node.InputAttributeTypeText}, Messages: text.Messages{*text.NewValidationErrorRequired("name")}, Meta: &node.Meta{}}, + // }, + // }, + //}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + actual, err := NewFromHTTPRequest(tc.r, node.DefaultGroup, "action", decoderx.HTTPJSONSchemaCompiler(tc.ref, nil)) + require.NoError(t, err) + // sort actual.fields lexicographically to have a deterministic order + sort.SliceStable(actual.Nodes, func(i, j int) bool { + return actual.Nodes[i].ID() < actual.Nodes[j].ID() + }) + assert.Equal(t, "action", actual.Action) + assert.EqualValues(t, tc.expect.Nodes, actual.Nodes) + }) + } + }) + + t.Run("method=NewFromJSONSchema", func(t *testing.T) { + for k, tc := range []struct { + ref string + prefix string + expect *Container + }{ + { + ref: "./stub/simple.schema.json", + prefix: "", + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputField("numby", nil, node.DefaultGroup, node.InputAttributeTypeNumber), + node.NewInputField("objy.numby", nil, node.DefaultGroup, node.InputAttributeTypeNumber), + node.NewInputField("objy.stringy", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("stringy", nil, node.DefaultGroup, node.InputAttributeTypeText), + }, + }, + }, + { + ref: "./stub/simple.schema.json", + prefix: "traits", + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputField("traits.numby", nil, node.DefaultGroup, node.InputAttributeTypeNumber), + node.NewInputField("traits.objy.numby", nil, node.DefaultGroup, node.InputAttributeTypeNumber), + node.NewInputField("traits.objy.stringy", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("traits.stringy", nil, node.DefaultGroup, node.InputAttributeTypeText), + }, + }, + }, + { + ref: "./stub/complex.schema.json", + expect: &Container{ + Nodes: node.Nodes{ + node.NewInputField("fruits", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("meal.chef", nil, node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("meal.name", nil, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute), + + // FIXME https://github.com/ory/kratos/issues/1316 + // + //node.NewInputField("vegetables.veggieName", nil, node.DefaultGroup, node.InputAttributeTypeText), + //node.NewInputField("vegetables.veggieLike", nil, node.DefaultGroup, node.InputAttributeTypeText), + //node.NewInputField("vegetables.veggieAmount", nil, node.DefaultGroup, node.InputAttributeTypeText), + }, + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + actual, err := NewFromJSONSchema(ctx, "action", + node.DefaultGroup, tc.ref, tc.prefix, nil, false) + require.NoError(t, err) + assert.Equal(t, "action", actual.Action) + assert.EqualValues(t, tc.expect.Messages, actual.Messages) + assert.EqualValues(t, tc.expect.Nodes, actual.Nodes) + }) + } + }) + + t.Run("method=ParseError", func(t *testing.T) { + for k, tc := range []struct { + err error + expectErr bool + expect Container + }{ + {err: errors.New("foo"), expectErr: true}, + {err: herodot.ErrNotFound(), expectErr: true}, + {err: herodot.ErrBadRequest().WithReason("tests"), expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewValidationErrorGeneric("tests")}}}, + {err: schema.NewInvalidCredentialsError(), expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewErrorValidationInvalidCredentials()}}}, + {err: &jsonschema.ValidationError{Message: "test", InstancePtr: "#/foo/bar/baz"}, expect: Container{Nodes: node.Nodes{ + &node.Node{Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "foo.bar.baz", Type: node.InputAttributeTypeText}, Messages: text.Messages{*text.NewValidationErrorGeneric("test")}, Meta: new(node.Meta)}, + }}}, + {err: &jsonschema.ValidationError{Message: "test", InstancePtr: ""}, expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewValidationErrorGeneric("test")}}}, + {err: &jsonschema.ValidationError{Message: `"asd" is not valid "email"`, InstancePtr: "", SchemaPtr: "#/email/format"}, expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewErrorValidationEmail("asd")}}}, + {err: &jsonschema.ValidationError{Message: `"asd" is not valid "tel"`, InstancePtr: "", SchemaPtr: "#/phone/format"}, expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewErrorValidationPhone("asd")}}}, + {err: &jsonschema.ValidationError{Message: `"asd" is not valid "unknown-format"`, InstancePtr: "", SchemaPtr: "#/unknown/format"}, expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewValidationErrorGeneric(`"asd" is not valid "unknown-format"`)}}}, + // Malformed format error message + {err: &jsonschema.ValidationError{Message: `"asd" is not valid "unknown-format`, InstancePtr: "", SchemaPtr: "#/unknown/format"}, expect: Container{Nodes: node.Nodes{}, Messages: text.Messages{*text.NewValidationErrorGeneric(`"asd" is not valid "unknown-format`)}}}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + for _, in := range []error{tc.err, errors.WithStack(tc.err)} { + c := New("") + err := c.ParseError(node.DefaultGroup, in) + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.EqualValues(t, tc.expect.Messages, c.Messages) + assert.EqualValues(t, tc.expect.Nodes, c.Nodes) + } + }) + } + }) + + t.Run("method=SetValue", func(t *testing.T) { + c := Container{ + Nodes: node.Nodes{ + node.NewInputField("1", "foo", node.DefaultGroup, node.InputAttributeTypeText), + node.NewInputField("2", "", node.DefaultGroup, node.InputAttributeTypeText), + }, + } + + assert.Len(t, c.Nodes, 2) + + c.SetValue("1", node.NewInputFieldFromJSON("1", "baz1", node.DefaultGroup)) + c.SetValue("2", node.NewInputFieldFromJSON("2", "baz2", node.DefaultGroup)) + c.SetValue("3", node.NewInputFieldFromJSON("3", "baz3", node.DefaultGroup)) + + assert.Len(t, c.Nodes, 3) + for _, k := range []string{"1", "2", "3"} { + assert.EqualValues(t, fmt.Sprintf("baz%s", k), c.Nodes.Find(k).Attributes.GetValue(), "%+v", c) + } + }) + + t.Run("method=SetCSRF", func(t *testing.T) { + f := &Container{Nodes: node.Nodes{}} + f.SetCSRF("csrf-token") + assert.Contains(t, f.Nodes, node.NewCSRFNode("csrf-token")) + }) + + t.Run("method=AddMessage", func(t *testing.T) { + c := Container{ + Nodes: node.Nodes{ + &node.Node{Messages: text.Messages{*text.NewValidationErrorGeneric("foo")}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "1", Type: node.InputAttributeTypeText, FieldValue: ""}}, + &node.Node{Messages: text.Messages{}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "2", Type: node.InputAttributeTypeText, FieldValue: ""}}, + }, + } + assert.Len(t, c.Nodes, 2) + c.AddMessage(node.DefaultGroup, &text.Message{Text: "baz1"}, "1") + c.AddMessage(node.DefaultGroup, &text.Message{Text: "baz2"}, "2") + c.AddMessage(node.DefaultGroup, &text.Message{Text: "baz3"}, "3") + c.AddMessage(node.DefaultGroup, &text.Message{Text: "baz"}, "4", "5", "6") + c.AddMessage(node.DefaultGroup, &text.Message{Text: "rootbar"}) + + assert.Len(t, c.Nodes, 6) + + for _, k := range []string{"1", "2", "3"} { + n := c.Nodes.Find(k) + assert.EqualValues(t, fmt.Sprintf("baz%s", k), n.Messages[len(n.Messages)-1].Text, "%+v", c) + } + + for _, k := range []string{"4", "5", "6"} { + assert.EqualValues(t, "baz", c.Nodes.Find(k).Messages[0].Text, "%+v", c) + } + + assert.Len(t, c.Messages, 1) + assert.Equal(t, "rootbar", c.Messages[0].Text) + }) + + t.Run("method=Reset", func(t *testing.T) { + c := Container{ + Nodes: node.Nodes{ + &node.Node{Messages: text.Messages{{Text: "foo"}}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "1", Type: node.InputAttributeTypeText, FieldValue: "foo"}}, + &node.Node{Messages: text.Messages{{Text: "bar"}}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "2", Type: node.InputAttributeTypeText, FieldValue: "bar"}}, + }, + Messages: text.Messages{{Text: ""}}, + } + c.Reset() + + assert.Empty(t, c.Messages) + assert.Empty(t, c.Nodes.Find("1").Messages) + assert.Empty(t, c.Nodes.Find("1").Attributes.(*node.InputAttributes).FieldValue) + assert.Empty(t, c.Nodes.Find("2").Messages) + assert.Empty(t, c.Nodes.Find("2").Attributes.(*node.InputAttributes).FieldValue) + }) + + t.Run("method=remove", func(t *testing.T) { + c := Container{ + Nodes: node.Nodes{ + &node.Node{Messages: text.Messages{{Text: "foo"}}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "1", Type: node.InputAttributeTypeText, FieldValue: "foo"}}, + &node.Node{Messages: text.Messages{{Text: "bar"}}, Group: node.DefaultGroup, Type: node.Input, Attributes: &node.InputAttributes{Name: "2", Type: node.InputAttributeTypeText, FieldValue: "bar"}}, + }, + Messages: text.Messages{{Text: ""}}, + } + + require.Len(t, c.Nodes, 2) + c.GetNodes().Remove("1") + require.Len(t, c.Nodes, 1) + require.EqualValues(t, "bar", c.Nodes[0].Attributes.GetValue()) + }) +} diff --git a/ui/container/error.go b/ui/container/error.go new file mode 100644 index 0000000..aee5cdd --- /dev/null +++ b/ui/container/error.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package container + +type ( + richError interface { + StatusCode() int + Reason() string + ID() string + } +) diff --git a/ui/container/stub/all_formats.schema.json b/ui/container/stub/all_formats.schema.json new file mode 100644 index 0000000..6333b70 --- /dev/null +++ b/ui/container/stub/all_formats.schema.json @@ -0,0 +1,58 @@ +{ + "$id": "https://example.com/all_formats.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "password": { + "type": "string", + "test_expected_type": "password" + }, + "csrf_token": { + "type": "string", + "test_expected_type": "hidden" + }, + "simpleString": { + "type": "string", + "test_expected_type": "text" + }, + "simpleNumber": { + "type": "number", + "test_expected_type": "number" + }, + "simpleBoolean": { + "type": "boolean", + "test_expected_type": "checkbox" + }, + "emailString": { + "type": "string", + "format": "email", + "test_expected_type": "email" + }, + "dateTimeString": { + "type": "string", + "format": "date-time", + "test_expected_type": "datetime-local" + }, + "regexString": { + "type": "string", + "format": "regex", + "test_expected_type": "text" + }, + "dateString": { + "type": "string", + "format": "date", + "test_expected_type": "date" + }, + "uriString": { + "type": "string", + "format": "uri", + "test_expected_type": "url" + }, + "patternString": { + "type": "string", + "pattern": "foo.*", + "test_expected_type": "text", + "test_expected_pattern": true + } + } +} diff --git a/ui/container/stub/complex.schema.json b/ui/container/stub/complex.schema.json new file mode 100644 index 0000000..989823b --- /dev/null +++ b/ui/container/stub/complex.schema.json @@ -0,0 +1,52 @@ +{ + "$id": "https://example.com/complex.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["meal"], + "properties": { + "meal": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "chef": { + "type": "string" + } + } + }, + "fruits": { + "type": "array", + "items": { + "type": "string" + } + }, + "vegetables": { + "type": "array", + "items": { + "$ref": "#/definitions/veggie" + } + } + }, + "definitions": { + "veggie": { + "type": "object", + "required": [ + "veggieName", + "veggieLike" + ], + "properties": { + "veggieName": { + "type": "string" + }, + "veggieLike": { + "type": "boolean" + }, + "veggieAmount": { + "type": "number" + } + } + } + } +} diff --git a/ui/container/stub/identity.schema.json b/ui/container/stub/identity.schema.json new file mode 100644 index 0000000..6bdf636 --- /dev/null +++ b/ui/container/stub/identity.schema.json @@ -0,0 +1,43 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + } + } +} diff --git a/ui/container/stub/simple.schema.json b/ui/container/stub/simple.schema.json new file mode 100644 index 0000000..7a0b1fa --- /dev/null +++ b/ui/container/stub/simple.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": {} + } + } + } + } +} diff --git a/ui/container/types.go b/ui/container/types.go new file mode 100644 index 0000000..90e10b9 --- /dev/null +++ b/ui/container/types.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "github.com/ory/kratos/ui/node" +) + +// ErrorParser is capable of parsing and processing errors. +type ErrorParser interface { + // ParseError type asserts the given error and sets the forms's errors or a + // field's errors and if the error is not something to be handled by the + // formUI Container itself, the error is returned for further propagation (e.g. showing a 502 status code). + ParseError(group node.UiNodeGroup, err error) error +} + +type NodeSetter interface { + // SetNode sets (adds / replaces) a node. + SetNode(field node.Node) +} + +type NodeUnsetter interface { + // UnsetFields removes a node. + UnsetNode(name string) +} + +type ValueSetter interface { + // SetValue sets a value the passed node. + SetValue(name string, value *node.Node) +} + +type CSRFSetter interface { + // SetCSRF sets the CSRF value. + SetCSRF(string) +} + +type Resetter interface { + // Resets all values and messages recursively. + Reset(exclude ...string) +} + +type MessageResetter interface { + // ResetMessages resets the messages recursively. + ResetMessages(exclude ...string) +} + +type FieldSorter interface { + SortNodes(schemaRef string, prefix string, keysInOrder []string) error +} + +type NodeGetter interface { + GetNodes() *node.Nodes +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-anchor_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-anchor_node.json new file mode 100644 index 0000000..832c3b2 --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-anchor_node.json @@ -0,0 +1,12 @@ +{ + "type": "a", + "group": "default", + "attributes": { + "href": "https://example.com", + "title": null, + "id": "", + "node_type": "a" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-division_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-division_node.json new file mode 100644 index 0000000..661e956 --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-division_node.json @@ -0,0 +1,10 @@ +{ + "type": "div", + "group": "default", + "attributes": { + "id": "", + "node_type": "div" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-empty_type_inferred_from_attributes.json b/ui/node/.snapshots/TestNodeMarshalJSON-empty_type_inferred_from_attributes.json new file mode 100644 index 0000000..588fb45 --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-empty_type_inferred_from_attributes.json @@ -0,0 +1,12 @@ +{ + "type": "input", + "group": "default", + "attributes": { + "name": "email", + "type": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-image_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-image_node.json new file mode 100644 index 0000000..d43efb9 --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-image_node.json @@ -0,0 +1,13 @@ +{ + "type": "img", + "group": "default", + "attributes": { + "src": "image.jpg", + "id": "", + "width": 0, + "height": 0, + "node_type": "img" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-input_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-input_node.json new file mode 100644 index 0000000..5ac946f --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-input_node.json @@ -0,0 +1,13 @@ +{ + "type": "input", + "group": "default", + "attributes": { + "name": "password", + "type": "password", + "value": "secret", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-script_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-script_node.json new file mode 100644 index 0000000..75468d5 --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-script_node.json @@ -0,0 +1,17 @@ +{ + "type": "script", + "group": "default", + "attributes": { + "src": "script.js", + "async": false, + "referrerpolicy": "", + "crossorigin": "", + "integrity": "", + "type": "", + "id": "", + "nonce": "", + "node_type": "script" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/.snapshots/TestNodeMarshalJSON-text_node.json b/ui/node/.snapshots/TestNodeMarshalJSON-text_node.json new file mode 100644 index 0000000..f83802d --- /dev/null +++ b/ui/node/.snapshots/TestNodeMarshalJSON-text_node.json @@ -0,0 +1,11 @@ +{ + "type": "text", + "group": "default", + "attributes": { + "text": null, + "id": "", + "node_type": "text" + }, + "messages": [], + "meta": {} +} diff --git a/ui/node/attributes.go b/ui/node/attributes.go new file mode 100644 index 0000000..bcef89b --- /dev/null +++ b/ui/node/attributes.go @@ -0,0 +1,542 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "fmt" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/x/webauthnx/js" +) + +const ( + InputAttributeTypeText UiNodeInputAttributeType = "text" + InputAttributeTypePassword UiNodeInputAttributeType = "password" + InputAttributeTypeNumber UiNodeInputAttributeType = "number" + InputAttributeTypeCheckbox UiNodeInputAttributeType = "checkbox" + InputAttributeTypeHidden UiNodeInputAttributeType = "hidden" + InputAttributeTypeEmail UiNodeInputAttributeType = "email" + InputAttributeTypeTel UiNodeInputAttributeType = "tel" + InputAttributeTypeSubmit UiNodeInputAttributeType = "submit" + InputAttributeTypeButton UiNodeInputAttributeType = "button" + InputAttributeTypeDateTimeLocal UiNodeInputAttributeType = "datetime-local" + InputAttributeTypeDate UiNodeInputAttributeType = "date" + InputAttributeTypeURI UiNodeInputAttributeType = "url" +) + +const ( + InputAttributeAutocompleteEmail UiNodeInputAttributeAutocomplete = "email" + InputAttributeAutocompleteTel UiNodeInputAttributeAutocomplete = "tel" + InputAttributeAutocompleteUrl UiNodeInputAttributeAutocomplete = "url" + InputAttributeAutocompleteCurrentPassword UiNodeInputAttributeAutocomplete = "current-password" + InputAttributeAutocompleteNewPassword UiNodeInputAttributeAutocomplete = "new-password" + InputAttributeAutocompleteOneTimeCode UiNodeInputAttributeAutocomplete = "one-time-code" + InputAttributeAutocompleteUsername UiNodeInputAttributeAutocomplete = "username" + InputAttributeAutocompleteUsernameWebauthn UiNodeInputAttributeAutocomplete = "username webauthn" +) + +// swagger:enum UiNodeInputAttributeType +type UiNodeInputAttributeType string + +// swagger:enum UiNodeInputAttributeAutocomplete +type UiNodeInputAttributeAutocomplete string + +// Attributes represents a list of attributes (e.g. `href="foo"` for links). +// +// swagger:model uiNodeAttributes +type Attributes interface { + // swagger:ignore + ID() string + + // swagger:ignore + Reset() + + // swagger:ignore + SetValue(value interface{}) + + // swagger:ignore + GetValue() interface{} + + // swagger:ignore + GetNodeType() UiNodeType + + // swagger:ignore + Matches(other Attributes) bool +} + +// InputAttributes represents the attributes of an input node +// +// swagger:model uiNodeInputAttributes +type InputAttributes struct { + // The input's element name. + // + // required: true + Name string `json:"name"` + + // The input's element type. + // + // required: true + Type UiNodeInputAttributeType `json:"type" faker:"-"` + + // The input's value. + FieldValue interface{} `json:"value,omitempty" faker:"string"` + + // Mark this input field as required. + Required bool `json:"required,omitempty"` + + // The autocomplete attribute for the input. + Autocomplete UiNodeInputAttributeAutocomplete `json:"autocomplete,omitempty"` + + // The input's label text. + Label *text.Message `json:"label,omitempty"` + + // The input's pattern. + Pattern string `json:"pattern,omitempty"` + + // Sets the input's disabled field to true or false. + // + // required: true + Disabled bool `json:"disabled"` + + // 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"` + + // The allowed values for the input when the underlying JSON schema + // defines an `enum`. When present, clients should render the field as a + // select/dropdown rather than a free-form text input. When absent, + // clients continue to render a plain text input, so this field is + // backward compatible with UIs that do not look at it. + Options []InputAttributesOption `json:"options,omitempty" faker:"-"` + + // 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 UiNodeType `json:"node_type"` +} + +// Represents a single selectable value for an input whose JSON schema +// defined an `enum`. The value is always a scalar JSON type (string, number, +// or boolean) serialized verbatim from the schema. When present, clients +// should render the parent input as a select/dropdown. +// +// swagger:model uiNodeInputAttributesOption +type InputAttributesOption struct { + // The value that will be submitted when this option is picked. + // It is serialized verbatim from the JSON schema `enum` entry, so it is + // always a scalar JSON value (string, number, or boolean). + // + // required: true + Value any `json:"value"` +} + +// ImageAttributes represents the attributes of an image node. +// +// swagger:model uiNodeImageAttributes +type ImageAttributes struct { + // The image's source URL. + // + // format: uri + // required: true + Source string `json:"src"` + + // A unique identifier + // + // required: true + Identifier string `json:"id"` + + // Width of the image + // + // required: true + Width int `json:"width"` + + // Height of the image + // + // required: true + Height int `json:"height"` + + // 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". + // + // required: true + NodeType UiNodeType `json:"node_type"` +} + +// AnchorAttributes represents the attributes of an anchor node. +// +// swagger:model uiNodeAnchorAttributes +type AnchorAttributes struct { + // The link's href (destination) URL. + // + // format: uri + // required: true + HREF string `json:"href"` + + // The link's title. + // + // required: true + Title *text.Message `json:"title"` + + // A unique identifier + // + // required: true + Identifier 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". + // + // required: true + NodeType UiNodeType `json:"node_type"` +} + +// TextAttributes represents the attributes of a text node. +// +// swagger:model uiNodeTextAttributes +type TextAttributes struct { + // The text of the text node. + // + // required: true + Text *text.Message `json:"text"` + + // A unique identifier + // + // required: true + Identifier 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". + // + // required: true + NodeType UiNodeType `json:"node_type"` +} + +// ScriptAttributes represent script nodes which load javascript. +// +// swagger:model uiNodeScriptAttributes +type ScriptAttributes struct { + // The script source + // + // required: true + Source string `json:"src"` + + // The script async type + // + // required: true + Async bool `json:"async"` + + // The script referrer policy + // + // required: true + ReferrerPolicy string `json:"referrerpolicy"` + + // The script cross origin policy + // + // required: true + CrossOrigin string `json:"crossorigin"` + + // The script's integrity hash + // + // required: true + Integrity string `json:"integrity"` + + // The script MIME type + // + // required: true + Type string `json:"type"` + + // A unique identifier + // + // required: true + Identifier string `json:"id"` + + // 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! + // + // required: true + Nonce string `json:"nonce"` + + // 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". + // + // required: true + NodeType UiNodeType `json:"node_type"` +} + +// DivisionAttributes represent a division section. +// +// Division sections are used for interactive widgets that require a hook in the DOM / view. +// +// swagger:model uiNodeDivisionAttributes +type DivisionAttributes struct { + // A classname that should be rendered into the DOM. + Classname string `json:"class,omitzero"` + + // A unique identifier + // + // required: true + Identifier string `json:"id"` + + // Data is a map of key-value pairs that are passed to the division. + // + // They may be used for `data-...` attributes. + Data map[string]string `json:"data,omitzero"` + + // NodeType represents this node's type. 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". + // + // required: true + NodeType UiNodeType `json:"node_type"` +} + +func (d DivisionAttributes) ID() string { + return d.Identifier +} + +func (d DivisionAttributes) Reset() {} + +func (d DivisionAttributes) SetValue(_ interface{}) {} + +func (d DivisionAttributes) GetValue() interface{} { + return nil +} + +func (d DivisionAttributes) GetNodeType() UiNodeType { + return d.NodeType +} + +func (d DivisionAttributes) Matches(other Attributes) bool { + ot, ok := other.(*DivisionAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && d.ID() != ot.ID() { + return false + } + + return true +} + +var ( + _ Attributes = new(InputAttributes) + _ Attributes = new(ImageAttributes) + _ Attributes = new(AnchorAttributes) + _ Attributes = new(TextAttributes) + _ Attributes = new(ScriptAttributes) + _ Attributes = new(DivisionAttributes) +) + +func (a *InputAttributes) ID() string { + return a.Name +} + +func (a *ImageAttributes) ID() string { + return a.Identifier +} + +func (a *AnchorAttributes) ID() string { + return a.Identifier +} + +func (a *TextAttributes) ID() string { + return a.Identifier +} + +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 +} + +func (a *ImageAttributes) SetValue(value interface{}) { + a.Source, _ = value.(string) +} + +func (a *AnchorAttributes) SetValue(value interface{}) { + a.HREF, _ = value.(string) +} + +func (a *TextAttributes) SetValue(value interface{}) { + a.Text, _ = value.(*text.Message) +} + +func (a *ScriptAttributes) SetValue(value interface{}) { + a.Source, _ = value.(string) +} + +func (a *InputAttributes) GetValue() interface{} { + return a.FieldValue +} + +func (a *ImageAttributes) GetValue() interface{} { + return a.Source +} + +func (a *AnchorAttributes) GetValue() interface{} { + return a.HREF +} + +func (a *TextAttributes) GetValue() interface{} { + return a.Text +} + +func (a *ScriptAttributes) GetValue() interface{} { + return a.Source +} + +func (a *InputAttributes) Reset() { + a.FieldValue = nil +} + +func (a *ImageAttributes) Reset() { +} + +func (a *AnchorAttributes) Reset() { +} + +func (a *TextAttributes) Reset() { +} + +func (a *ScriptAttributes) Reset() { +} + +func (a *InputAttributes) GetNodeType() UiNodeType { + return UiNodeType(a.NodeType) +} + +func (a *ImageAttributes) GetNodeType() UiNodeType { + return UiNodeType(a.NodeType) +} + +func (a *AnchorAttributes) GetNodeType() UiNodeType { + return UiNodeType(a.NodeType) +} + +func (a *TextAttributes) GetNodeType() UiNodeType { + return UiNodeType(a.NodeType) +} + +func (a *ScriptAttributes) GetNodeType() UiNodeType { + return UiNodeType(a.NodeType) +} diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go new file mode 100644 index 0000000..1eac8b8 --- /dev/null +++ b/ui/node/attributes_input.go @@ -0,0 +1,251 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "encoding/json" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/nosurfx" + "github.com/ory/x/jsonschemax" +) + +const DisableFormField = "disableFormField" + +// maxEnumOptions caps how many `enum` values a single identity-schema +// property may surface as a selectable option on a UI node. The cap bounds +// the size of flow responses when an operator (accidentally or otherwise) +// declares a very large enum, so a single misconfigured schema cannot +// inflate every active registration/settings/login flow on a tenant. +const maxEnumOptions = 500 + +func toFormType(n string, i interface{}) UiNodeInputAttributeType { + switch n { + case nosurfx.CSRFTokenName: + return InputAttributeTypeHidden + case "password": + return InputAttributeTypePassword + } + + switch i.(type) { + case float64, int64, int32, float32, json.Number: + return InputAttributeTypeNumber + case bool: + return InputAttributeTypeCheckbox + } + + return InputAttributeTypeText +} + +type ( + InputAttributesModifier func(attributes *InputAttributes) + InputAttributesModifiers []InputAttributesModifier +) + +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) + } +} + +func applyInputAttributes(opts []InputAttributesModifier, attributes *InputAttributes) *InputAttributes { + for _, f := range opts { + f(attributes) + } + return attributes +} + +type ( + ImageAttributesModifier func(attributes *ImageAttributes) + ImageAttributesModifiers []ImageAttributesModifier +) + +func WithImageAttributes(f func(a *ImageAttributes)) func(a *ImageAttributes) { + return func(a *ImageAttributes) { + f(a) + } +} + +func applyImageAttributes(opts ImageAttributesModifiers, attributes *ImageAttributes) *ImageAttributes { + for _, f := range opts { + f(attributes) + } + return attributes +} + +type ( + ScriptAttributesModifier func(attributes *ScriptAttributes) + ScriptAttributesModifiers []ScriptAttributesModifier +) + +func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAttributes) *ScriptAttributes { + for _, f := range opts { + f(attributes) + } + return attributes +} + +type ( + DivisionAttributesModifier func(attributes *DivisionAttributes) + DivisionAttributesModifiers []DivisionAttributesModifier +) + +func WithDivisionAttributes(f func(a *DivisionAttributes)) func(a *DivisionAttributes) { + return func(a *DivisionAttributes) { + f(a) + } +} + +func applyDivisionAttributes(opts DivisionAttributesModifiers, attributes *DivisionAttributes) *DivisionAttributes { + for _, f := range opts { + f(attributes) + } + return attributes +} + +func NewInputFieldFromJSON(name string, value interface{}, group UiNodeGroup, opts ...InputAttributesModifier) *Node { + return &Node{ + Type: Input, + Group: group, + Attributes: applyInputAttributes(opts, &InputAttributes{Name: name, Type: toFormType(name, value), FieldValue: value}), + Meta: &Meta{}, + } +} + +func NewInputField(name string, value interface{}, group UiNodeGroup, inputType UiNodeInputAttributeType, opts ...InputAttributesModifier) *Node { + return &Node{ + Type: Input, + Group: group, + Attributes: applyInputAttributes(opts, &InputAttributes{Name: name, Type: inputType, FieldValue: value}), + Meta: &Meta{}, + } +} + +func NewImageField(id string, src string, group UiNodeGroup, opts ...ImageAttributesModifier) *Node { + return &Node{ + Type: Image, + Group: group, + Attributes: applyImageAttributes(opts, &ImageAttributes{Source: src, Identifier: id}), + Meta: &Meta{}, + } +} + +func NewTextField(id string, text *text.Message, group UiNodeGroup) *Node { + return &Node{ + Type: Text, + Group: group, + Attributes: &TextAttributes{Text: text, Identifier: id}, + Meta: &Meta{}, + } +} + +func NewDivisionField(id string, group UiNodeGroup, opts ...DivisionAttributesModifier) *Node { + return &Node{ + Type: Division, + Group: group, + Attributes: applyDivisionAttributes(opts, &DivisionAttributes{Identifier: id}), + Meta: &Meta{}, + } +} + +func NewAnchorField(id string, href string, group UiNodeGroup, title *text.Message) *Node { + return &Node{ + Type: Anchor, + Group: group, + Attributes: &AnchorAttributes{Title: title, HREF: href, Identifier: id}, + Meta: &Meta{}, + } +} + +func NewScriptField(name string, src string, group UiNodeGroup, integrity string, opts ...ScriptAttributesModifier) *Node { + return &Node{ + Type: Script, + Group: group, + Attributes: applyScriptAttributes(opts, &ScriptAttributes{ + Identifier: name, + Type: "text/javascript", + Source: src, + Async: true, + ReferrerPolicy: "no-referrer", + CrossOrigin: "anonymous", + Integrity: integrity, + Nonce: x.NewUUID().String(), + }), + Meta: &Meta{}, + } +} + +func NewInputFieldFromSchema(name string, group UiNodeGroup, p jsonschemax.Path, opts ...InputAttributesModifier) *Node { + attr := &InputAttributes{ + Name: name, + Type: toFormType(p.Name, p.Type), + Required: p.Required, + } + + // If format is set, we can make a more distinct decision: + switch p.Format { + case "date-time": + attr.Type = InputAttributeTypeDateTimeLocal + case "email": + attr.Type = InputAttributeTypeEmail + attr.Autocomplete = InputAttributeAutocompleteEmail + case "tel": + attr.Type = InputAttributeTypeTel + attr.Autocomplete = InputAttributeAutocompleteTel + case "date": + attr.Type = InputAttributeTypeDate + case "uri": + attr.Type = InputAttributeTypeURI + attr.Autocomplete = InputAttributeAutocompleteUrl + case "regex": + attr.Type = InputAttributeTypeText + } + + // Enum values become selectable options. The presence of options is the + // signal for consumers to render a dropdown instead of a free-form input. + // Cap the number of options to keep a pathological schema from inflating + // every flow response. + if len(p.Enum) > 0 { + n := min(len(p.Enum), maxEnumOptions) + attr.Options = make([]InputAttributesOption, n) + for i, v := range p.Enum[:n] { + attr.Options[i] = InputAttributesOption{Value: v} + } + } + + // Other properties + if p.Pattern != nil { + attr.Pattern = p.Pattern.String() + } + + // Set disabled if the custom property is set + if isDisabled, ok := p.CustomProperties[DisableFormField]; ok { + if isDisabled, ok := isDisabled.(bool); ok { + attr.Disabled = isDisabled + } + } + + var meta Meta + if len(p.Title) > 0 { + meta.Label = text.NewInfoNodeLabelGenerated(p.Title, name) + } + + return &Node{ + Type: Input, + Attributes: applyInputAttributes(opts, attr), + Group: group, + Meta: &meta, + } +} diff --git a/ui/node/attributes_input_csrf.go b/ui/node/attributes_input_csrf.go new file mode 100644 index 0000000..67ca3dd --- /dev/null +++ b/ui/node/attributes_input_csrf.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import "github.com/ory/kratos/x/nosurfx" + +func NewCSRFNode(token string) *Node { + return &Node{ + Type: Input, + Group: DefaultGroup, + Attributes: &InputAttributes{ + Name: nosurfx.CSRFTokenName, + Type: InputAttributeTypeHidden, + FieldValue: token, + Required: true, + }, + } +} diff --git a/ui/node/attributes_input_test.go b/ui/node/attributes_input_test.go new file mode 100644 index 0000000..95f2386 --- /dev/null +++ b/ui/node/attributes_input_test.go @@ -0,0 +1,100 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +func TestFieldFromPath(t *testing.T) { + t.Parallel() + + ctx := t.Context() + t.Run("all properties are properly transferred", func(t *testing.T) { + t.Parallel() + + schema, err := os.ReadFile("./fixtures/all_formats.schema.json") + require.NoError(t, err) + + c := jsonschema.NewCompiler() + require.NoError(t, c.AddResource("test.json", bytes.NewBuffer(schema))) + + paths, err := jsonschemax.ListPaths(ctx, "test.json", c) + require.NoError(t, err) + + for _, path := range paths { + node := NewInputFieldFromSchema(path.Name, DefaultGroup, path) + assert.EqualValues(t, "input", node.Type) + require.IsType(t, new(InputAttributes), node.Attributes) + attr := node.Attributes.(*InputAttributes) + + assert.EqualValues(t, gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_type", path.Name)).String(), attr.Type) + assert.True(t, !gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_pattern", path.Name)).Exists() || + (gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_pattern", path.Name)).Bool() && attr.Pattern != "")) + + expectedAutocomplete := gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_autocomplete", path.Name)) + + if expectedAutocomplete.Exists() { + assert.EqualValues(t, expectedAutocomplete.String(), attr.Autocomplete) + } + + expectedOptions := gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_options", path.Name)) + if expectedOptions.Exists() { + // Compare through JSON so string, number, and boolean + // option values all survive the round-trip without relying + // on a lossy `%v` coercion. + var got []any + for _, o := range attr.Options { + got = append(got, o.Value) + } + gotJSON, err := json.Marshal(got) + require.NoError(t, err) + assert.JSONEq(t, expectedOptions.Raw, string(gotJSON), "field %s", path.Name) + } else { + assert.Empty(t, attr.Options, "field %s should have no options", path.Name) + } + } + }) + + t.Run("caps enum options at maxEnumOptions", func(t *testing.T) { + t.Parallel() + + enum := make([]any, maxEnumOptions+50) + for i := range enum { + enum[i] = fmt.Sprintf("v%d", i) + } + node := NewInputFieldFromSchema("field", DefaultGroup, jsonschemax.Path{ + Name: "field", + Type: "", + Enum: enum, + }) + attr := node.Attributes.(*InputAttributes) + assert.Len(t, attr.Options, maxEnumOptions) + assert.Equal(t, "v0", attr.Options[0].Value) + assert.Equal(t, fmt.Sprintf("v%d", maxEnumOptions-1), attr.Options[maxEnumOptions-1].Value) + }) + + t.Run("empty enum produces no options", func(t *testing.T) { + t.Parallel() + + node := NewInputFieldFromSchema("field", DefaultGroup, jsonschemax.Path{ + Name: "field", + Type: "", + Enum: []any{}, + }) + attr := node.Attributes.(*InputAttributes) + assert.Empty(t, attr.Options) + }) +} diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go new file mode 100644 index 0000000..e73133d --- /dev/null +++ b/ui/node/attributes_test.go @@ -0,0 +1,124 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "encoding/json" + "testing" + + "github.com/tidwall/gjson" + + "github.com/ory/x/jsonx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIDs(t *testing.T) { + assert.EqualValues(t, "foo", (&AnchorAttributes{Identifier: "foo"}).ID()) + assert.EqualValues(t, "foo", (&ImageAttributes{Identifier: "foo"}).ID()) + assert.EqualValues(t, "foo", (&TextAttributes{Identifier: "foo"}).ID()) + assert.EqualValues(t, "foo", (&InputAttributes{Name: "foo"}).ID()) + 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()) + assert.EqualValues(t, Script, gjson.Get(script, "type").String()) + + text := jsonx.TestMarshalJSONString(t, &Node{Attributes: &TextAttributes{}}) + assert.EqualValues(t, Text, gjson.Get(text, "attributes.node_type").String()) + assert.EqualValues(t, Text, gjson.Get(text, "type").String()) + + image := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ImageAttributes{}}) + assert.EqualValues(t, Image, gjson.Get(image, "attributes.node_type").String()) + assert.EqualValues(t, Image, gjson.Get(image, "type").String()) + + input := jsonx.TestMarshalJSONString(t, &Node{Attributes: &InputAttributes{}}) + assert.EqualValues(t, Input, gjson.Get(input, "attributes.node_type").String()) + assert.EqualValues(t, Input, gjson.Get(input, "type").String()) + + anchor := jsonx.TestMarshalJSONString(t, &Node{Attributes: &AnchorAttributes{}}) + assert.EqualValues(t, Anchor, gjson.Get(anchor, "attributes.node_type").String()) + assert.EqualValues(t, Anchor, gjson.Get(anchor, "type").String()) +} + +func TestNodeDecode(t *testing.T) { + for _, kind := range []UiNodeType{ + Text, + Input, + Image, + Anchor, + Script, + } { + var n Node + require.NoError(t, json.Unmarshal([]byte(`{"type":"`+kind+`"}`), &n)) + assert.EqualValues(t, kind, n.Attributes.GetNodeType()) + } +} diff --git a/ui/node/fixtures/all_formats.schema.json b/ui/node/fixtures/all_formats.schema.json new file mode 100644 index 0000000..063b017 --- /dev/null +++ b/ui/node/fixtures/all_formats.schema.json @@ -0,0 +1,92 @@ +{ + "$id": "https://example.com/all_formats.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "password": { + "type": "string", + "test_expected_type": "password" + }, + "csrf_token": { + "type": "string", + "test_expected_type": "hidden" + }, + "simpleString": { + "type": "string", + "test_expected_type": "text" + }, + "simpleNumber": { + "type": "number", + "test_expected_type": "number" + }, + "simpleBoolean": { + "type": "boolean", + "test_expected_type": "checkbox" + }, + "emailString": { + "type": "string", + "format": "email", + "test_expected_type": "email", + "test_expected_autocomplete": "email" + }, + "phoneString": { + "type": "string", + "format": "tel", + "test_expected_type": "tel", + "test_expected_autocomplete": "tel" + }, + "dateTimeString": { + "type": "string", + "format": "date-time", + "test_expected_type": "datetime-local" + }, + "regexString": { + "type": "string", + "format": "regex", + "test_expected_type": "text" + }, + "dateString": { + "type": "string", + "format": "date", + "test_expected_type": "date" + }, + "uriString": { + "type": "string", + "format": "uri", + "test_expected_type": "url", + "test_expected_autocomplete": "url" + }, + "patternString": { + "type": "string", + "pattern": "foo.*", + "test_expected_type": "text", + "test_expected_pattern": true + }, + "enumString": { + "type": "string", + "enum": ["one", "two", "three"], + "test_expected_type": "text", + "test_expected_options": ["one", "two", "three"] + }, + "enumNumber": { + "type": "number", + "enum": [1, 2.5, 3], + "test_expected_type": "number", + "test_expected_options": [1, 2.5, 3] + }, + "enumBoolean": { + "type": "boolean", + "enum": [true, false], + "test_expected_type": "checkbox", + "test_expected_options": [true, false] + }, + "enumEmail": { + "type": "string", + "format": "email", + "enum": ["admin@example.com", "user@example.com"], + "test_expected_type": "email", + "test_expected_autocomplete": "email", + "test_expected_options": ["admin@example.com", "user@example.com"] + } + } +} diff --git a/ui/node/fixtures/complex.schema.json b/ui/node/fixtures/complex.schema.json new file mode 100644 index 0000000..989823b --- /dev/null +++ b/ui/node/fixtures/complex.schema.json @@ -0,0 +1,52 @@ +{ + "$id": "https://example.com/complex.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["meal"], + "properties": { + "meal": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "chef": { + "type": "string" + } + } + }, + "fruits": { + "type": "array", + "items": { + "type": "string" + } + }, + "vegetables": { + "type": "array", + "items": { + "$ref": "#/definitions/veggie" + } + } + }, + "definitions": { + "veggie": { + "type": "object", + "required": [ + "veggieName", + "veggieLike" + ], + "properties": { + "veggieName": { + "type": "string" + }, + "veggieLike": { + "type": "boolean" + }, + "veggieAmount": { + "type": "number" + } + } + } + } +} diff --git a/ui/node/fixtures/identity.schema.json b/ui/node/fixtures/identity.schema.json new file mode 100644 index 0000000..6bdf636 --- /dev/null +++ b/ui/node/fixtures/identity.schema.json @@ -0,0 +1,43 @@ +{ + "$id": "https://example.com/registration.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" + } + } + }, + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "booly": { + "type": "boolean" + }, + "should_big_number": { + "type": "number", + "minimum": 1200 + }, + "should_long_string": { + "type": "string", + "minLength": 25 + } + } + } + } +} diff --git a/ui/node/fixtures/simple.schema.json b/ui/node/fixtures/simple.schema.json new file mode 100644 index 0000000..7a0b1fa --- /dev/null +++ b/ui/node/fixtures/simple.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": { + "stringy": { + "type": "string" + }, + "numby": { + "type": "number" + }, + "objy": { + "type": "object", + "properties": {} + } + } + } + } +} diff --git a/ui/node/fixtures/sort/expected/1.json b/ui/node/fixtures/sort/expected/1.json new file mode 100644 index 0000000..4ab9da2 --- /dev/null +++ b/ui/node/fixtures/sort/expected/1.json @@ -0,0 +1,108 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "Sk3WySQDVpL5V9IuQelZTXcRVbBE1STI0HAJEcHOnM0/xPz5p1piBzuBMKZWxgYJc5TN8k8BTM9Hazrf/n76pg==", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password_identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/ui/node/fixtures/sort/expected/2.json b/ui/node/fixtures/sort/expected/2.json new file mode 100644 index 0000000..a271696 --- /dev/null +++ b/ui/node/fixtures/sort/expected/2.json @@ -0,0 +1,118 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.abcd", + "type": "input", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.email", + "type": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/ui/node/fixtures/sort/expected/3.json b/ui/node/fixtures/sort/expected/3.json new file mode 100644 index 0000000..416b314 --- /dev/null +++ b/ui/node/fixtures/sort/expected/3.json @@ -0,0 +1,105 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.abcd", + "type": "input", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.email", + "type": "email", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/ui/node/fixtures/sort/expected/4.json b/ui/node/fixtures/sort/expected/4.json new file mode 100644 index 0000000..ae0f68d --- /dev/null +++ b/ui/node/fixtures/sort/expected/4.json @@ -0,0 +1,164 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.email", + "type": "email", + "value": "john@doe.com", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "traits.name", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "profile", + "attributes": { + "name": "method", + "type": "submit", + "value": "profile", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "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": 1070003, + "text": "Save", + "type": "info" + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "github", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link github", + "type": "info", + "context": { + "provider": "github" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "unlink", + "type": "submit", + "value": "google", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050003, + "text": "Unlink google", + "type": "info", + "context": { + "provider": "google" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "link", + "type": "submit", + "value": "ory", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050002, + "text": "Link ory", + "type": "info", + "context": { + "provider": "ory" + } + } + } + } +] diff --git a/ui/node/fixtures/sort/input/1.json b/ui/node/fixtures/sort/input/1.json new file mode 100644 index 0000000..74eb0b6 --- /dev/null +++ b/ui/node/fixtures/sort/input/1.json @@ -0,0 +1,92 @@ +[ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password_identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "Sk3WySQDVpL5V9IuQelZTXcRVbBE1STI0HAJEcHOnM0/xPz5p1piBzuBMKZWxgYJc5TN8k8BTM9Hazrf/n76pg==", + "required": true, + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "disabled": false + }, + "messages": null + } +] diff --git a/ui/node/fixtures/sort/input/2.json b/ui/node/fixtures/sort/input/2.json new file mode 100644 index 0000000..fd76887 --- /dev/null +++ b/ui/node/fixtures/sort/input/2.json @@ -0,0 +1,100 @@ +[ + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==", + "required": true, + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.email", + "type": "email", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.abcd", + "type": "input", + "disabled": false + }, + "messages": null + } +] diff --git a/ui/node/fixtures/sort/input/3.json b/ui/node/fixtures/sort/input/3.json new file mode 100644 index 0000000..a69c7dc --- /dev/null +++ b/ui/node/fixtures/sort/input/3.json @@ -0,0 +1,89 @@ +[ + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "method", + "type": "submit", + "value": "oidc", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==", + "required": true, + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.email", + "type": "email", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "github", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "google", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "oidc.provider", + "type": "submit", + "value": "hydra", + "disabled": false + }, + "messages": null + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "traits.abcd", + "type": "input", + "disabled": false + }, + "messages": null + } +] diff --git a/ui/node/fixtures/sort/input/4.json b/ui/node/fixtures/sort/input/4.json new file mode 100644 index 0000000..98ea857 --- /dev/null +++ b/ui/node/fixtures/sort/input/4.json @@ -0,0 +1,155 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "required": true, + "type": "hidden", + "value": "" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "type": "email", + "value": "john@doe.com" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.name", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "password", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "type": "submit", + "value": "github" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "github" + }, + "id": 1050002, + "text": "Link github", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "unlink", + "type": "submit", + "value": "google" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "google" + }, + "id": 1050003, + "text": "Unlink google", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "link", + "type": "submit", + "value": "ory" + }, + "group": "oidc", + "messages": [], + "meta": { + "label": { + "context": { + "provider": "ory" + }, + "id": 1050002, + "text": "Link ory", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/ui/node/fixtures/sort/schema/2.json b/ui/node/fixtures/sort/schema/2.json new file mode 100644 index 0000000..81a7490 --- /dev/null +++ b/ui/node/fixtures/sort/schema/2.json @@ -0,0 +1,39 @@ +{ + "$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": { + "abcd": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "website": { + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/ui/node/fixtures/sort/schema/3.json b/ui/node/fixtures/sort/schema/3.json new file mode 100644 index 0000000..81a7490 --- /dev/null +++ b/ui/node/fixtures/sort/schema/3.json @@ -0,0 +1,39 @@ +{ + "$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": { + "abcd": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "website": { + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": [ + "email", + "website" + ], + "additionalProperties": false + } + } +} diff --git a/ui/node/fixtures/sort/schema/4.json b/ui/node/fixtures/sort/schema/4.json new file mode 100644 index 0000000..9d0752b --- /dev/null +++ b/ui/node/fixtures/sort/schema/4.json @@ -0,0 +1,32 @@ +{ + "$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": { + "format": "email", + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "name": { + "type": "string", + "minLength": 2 + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/ui/node/helper.go b/ui/node/helper.go new file mode 100644 index 0000000..e2c3314 --- /dev/null +++ b/ui/node/helper.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +func PasswordLoginOrder(in []string) []string { + if len(in) == 0 { + return []string{"csrf_token", "password"} + } + if len(in) == 1 { + return append([]string{"csrf_token"}, in[0], "password") + } + return append([]string{"csrf_token", in[0], "password"}, in[1:]...) +} diff --git a/ui/node/identifiers.go b/ui/node/identifiers.go new file mode 100644 index 0000000..63bc941 --- /dev/null +++ b/ui/node/identifiers.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +const ( + TOTPCode = "totp_code" + TOTPSecretKey = "totp_secret_key" + TOTPQR = "totp_qr" + TOTPUnlink = "totp_unlink" +) + +const ( + LookupReveal = "lookup_secret_reveal" + LookupRegenerate = "lookup_secret_regenerate" + LookupDisable = "lookup_secret_disable" + LookupCodes = "lookup_secret_codes" + LookupConfirm = "lookup_secret_confirm" + LookupCodeEnter = "lookup_secret" +) + +const ( + ProfileChooseCredentials = "profile_choose_credentials" +) + +const ( + WebAuthnRegisterTrigger = "webauthn_register_trigger" + WebAuthnRegister = "webauthn_register" + WebAuthnLogin = "webauthn_login" + WebAuthnLoginTrigger = "webauthn_login_trigger" + WebAuthnRegisterDisplayName = "webauthn_register_displayname" + WebAuthnRemove = "webauthn_remove" + WebAuthnScript = "webauthn_script" +) + +const ( + DeviceAuthnRemove = "deviceauthn_remove" + DeviceAuthnNonce = "deviceauthn_nonce" +) + +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 new file mode 100644 index 0000000..b40963d --- /dev/null +++ b/ui/node/node.go @@ -0,0 +1,577 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "slices" + "sort" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/schema" + + "github.com/ory/kratos/text" +) + +// swagger:enum UiNodeType +type UiNodeType string + +const ( + Text UiNodeType = "text" + Input UiNodeType = "input" + Image UiNodeType = "img" + Anchor UiNodeType = "a" + Script UiNodeType = "script" + Division UiNodeType = "div" +) + +func (t UiNodeType) String() string { + return string(t) +} + +// swagger:enum UiNodeGroup +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" + PasskeyGroup UiNodeGroup = "passkey" + IdentifierFirstGroup UiNodeGroup = "identifier_first" + CaptchaGroup UiNodeGroup = "captcha" // Available in OEL + SAMLGroup UiNodeGroup = "saml" // Available in OEL + DeviceAuthnGroup UiNodeGroup = "deviceauthn" // Available in OEL +) + +func (g UiNodeGroup) String() string { + return string(g) +} + +// swagger:model uiNodes +type Nodes []*Node + +// Node represents a flow's nodes +// +// 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`. +// +// swagger:model uiNode +type Node struct { + // The node's type + // + // required: true + Type UiNodeType `json:"type" faker:"-"` + + // Group specifies which group (e.g. password authenticator) this node belongs to. + // + // required: true + Group UiNodeGroup `json:"group"` + + // The node's attributes. + // + // required: true + // swagger:type uiNodeAttributes + Attributes Attributes `json:"attributes" faker:"ui_node_attributes"` + + // The node's messages + // + // Contains error, validation, or other messages relevant to this node. + // + // required: true + Messages text.Messages `json:"messages"` + + // Meta contains a node meta information + // + // This might include a label and other information that can optionally + // be used to render UIs. + // + // required: true + Meta *Meta `json:"meta"` +} + +// A Node's Meta Information +// +// This might include a label and other information that can optionally +// be used to render UIs. +// +// swagger:model uiNodeMeta +type Meta struct { + // Label represents the node's label. + // + // Keep in mind that these values are autogenerated and can not be changed. + // If you wish to use other titles or labels implement that directly in + // your UI. + Label *text.Message `json:"label,omitempty"` +} + +// Used for en/decoding the Attributes field. +type jsonRawNode struct { + Type UiNodeType `json:"type"` + Group UiNodeGroup `json:"group"` + Attributes Attributes `json:"attributes"` + Messages text.Messages `json:"messages"` + Meta *Meta `json:"meta"` +} + +func (n *Node) ID() string { + return n.Attributes.ID() +} + +func (n *Node) Reset() { + n.Messages = nil + n.Attributes.Reset() +} + +func (n *Node) WithMetaLabel(label *text.Message) *Node { + if n.Meta == nil { + n.Meta = new(Meta) + } + n.Meta.Label = label + return n +} + +func (n *Node) GetValue() interface{} { + return n.Attributes.GetValue() +} + +func (n Nodes) Find(id string) *Node { + for _, nn := range n { + if nn.ID() == id { + return nn + } + } + + return nil +} + +func (n Nodes) FindAll(id string) []Node { + var nodes []Node + for _, nn := range n { + if nn.ID() == id { + nodes = append(nodes, *nn) + } + } + + return nodes +} + +func (n Nodes) Reset(exclude ...string) { + for k, nn := range n { + nn.Messages = nil + if !slices.Contains(exclude, nn.ID()) { + nn.Reset() + } + n[k] = nn + } +} + +func (n Nodes) ResetNodes(reset ...string) { + for k, nn := range n { + if slices.Contains(reset, nn.ID()) { + nn.Reset() + } + n[k] = nn + } +} + +func (n Nodes) ResetNodesWithPrefix(prefix string) { + for k := range n { + if strings.HasPrefix(n[k].ID(), prefix) { + n[k].Reset() + } + } +} + +func getStringSliceIndexOf(needle []string, haystack string) int { + for k := range needle { + if needle[k] == haystack { + return k + } + } + return -1 +} + +type sortOptions struct { + orderByGroups []string + schemaRef string + keysInOrder []string + keysInOrderAppend []string + keysInOrderPost func([]string) []string + stableGroups []string +} + +type SortOption func(*sortOptions) + +func SortByGroups(orderByGroups []UiNodeGroup) func(*sortOptions) { + return func(options *sortOptions) { + options.orderByGroups = make([]string, len(orderByGroups)) + for k := range orderByGroups { + options.orderByGroups[k] = string(orderByGroups[k]) + } + } +} + +func SortBySchema(schemaRef string) func(*sortOptions) { + return func(options *sortOptions) { + options.schemaRef = schemaRef + } +} + +func SortUseOrder(keysInOrder []string) func(*sortOptions) { + return func(options *sortOptions) { + options.keysInOrder = keysInOrder + } +} + +func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) { + return func(options *sortOptions) { + options.keysInOrderAppend = keysInOrder + } +} + +func SortUpdateOrder(f func([]string) []string) func(*sortOptions) { + return func(options *sortOptions) { + options.keysInOrderPost = f + } +} + +// SortStableGroups marks groups that arrive in a deterministic order and must +// keep it: within such a group the sort preserves insertion order instead of +// tiebreaking by value. Use it when the repeated nodes' only distinguishing +// value is opaque and non-display, so value ordering would be meaningless and unstable. +func SortStableGroups(groups ...UiNodeGroup) func(*sortOptions) { + return func(options *sortOptions) { + options.stableGroups = make([]string, len(groups)) + for k := range groups { + options.stableGroups[k] = string(groups[k]) + } + } +} + +func (n Nodes) SortBySchema(ctx context.Context, opts ...SortOption) error { + var o sortOptions + for _, f := range opts { + f(&o) + } + + if o.schemaRef != "" { + schemaKeys, err := schema.GetKeysInOrder(ctx, o.schemaRef) + if err != nil { + return err + } + + o.keysInOrder = append(o.keysInOrder, schemaKeys...) + } + + if o.keysInOrderPost != nil { + o.keysInOrder = o.keysInOrderPost(o.keysInOrder) + } + + o.keysInOrder = append(o.keysInOrder, o.keysInOrderAppend...) + + getKeyPosition := func(node *Node) int { + lastPrefix := len(o.keysInOrder) + + // Method should always be the last element in the list + if node.Attributes.ID() == "method" { + return len(n) + len(o.keysInOrder) + 1 + } + + for i, n := range o.keysInOrder { + if strings.HasPrefix(node.ID(), n) { + return i + } + } + + return lastPrefix + } + + if len(o.orderByGroups) > 0 { + // Sort by groups so that default is in front, then oidc, password, ... + // A stable sort keeps same-group nodes in their incoming order, so a + // group listed in stableGroups is not reshuffled before the tiebreak + // below has a chance to preserve it. + sort.SliceStable(n, func(i, j int) bool { + a := string(n[i].Group) + b := string(n[j].Group) + return getStringSliceIndexOf(o.orderByGroups, a) < getStringSliceIndexOf(o.orderByGroups, b) + }) + } + + sort.SliceStable(n, func(i, j int) bool { + a := n[i] + b := n[j] + + if a.Group == b.Group || + (a.Group == "default" && b.Group == "password") || (b.Group == "default" && a.Group == "password") { + pa, pb := getKeyPosition(a), getKeyPosition(b) + if pa < pb { + return true + } else if pa > pb { + return false + } + + // A stable group provides its own deterministic order; keep it + // rather than tiebreaking by the (meaningless, possibly unstable) + // value. + if a.Group == b.Group && slices.Contains(o.stableGroups, string(a.Group)) { + return false + } + + return fmt.Sprintf("%v", a.GetValue()) < fmt.Sprintf("%v", b.GetValue()) + } + + return false + }) + + return nil +} + +// Remove removes one or more nodes by their IDs. +func (n *Nodes) Remove(ids ...string) { + if n == nil { + return + } + + var r Nodes + for k, v := range *n { + var found bool + for _, needle := range ids { + if (*n)[k].ID() == needle { + found = true + break + } + } + if !found { + r = append(r, v) + } + } + *n = r +} + +// RemoveInGroup removes nodes whose ID is one of ids and that belong to the +// given group. Unlike Remove, which matches on ID alone, this leaves nodes with +// the same ID in other groups untouched — so one strategy clearing its own +// link/unlink nodes does not clobber another strategy's nodes that share the +// "link"/"unlink" input name but render in a different group. +func (n *Nodes) RemoveInGroup(group UiNodeGroup, ids ...string) { + if n == nil { + return + } + + var r Nodes + for _, v := range *n { + if v != nil && v.Group == group && slices.Contains(ids, v.ID()) { + continue + } + r = append(r, v) + } + *n = r +} + +// RemoveGroup removes all nodes belonging to a specific group. +func (n *Nodes) RemoveGroup(group UiNodeGroup) { + if n == nil { + return + } + + var r Nodes + for k, v := range *n { + if (*n)[k] != nil && string((*n)[k].Group) != string(group) { + r = append(r, v) + } + } + *n = r +} + +// RemoveGroup removes all nodes belonging to a set of groups. +func (n *Nodes) RemoveGroups(group ...UiNodeGroup) { + if n == nil { + return + } + + for _, g := range group { + n.RemoveGroup(g) + } +} + +// ClearTransientNodes removes all nodes that belong to transient groups. +func (n *Nodes) ClearTransientNodes() { + n.RemoveGroups( + DefaultGroup, + PasswordGroup, + OpenIDConnectGroup, + ProfileGroup, + LinkGroup, + CodeGroup, + TOTPGroup, + LookupGroup, + WebAuthnGroup, + PasskeyGroup, + IdentifierFirstGroup, + SAMLGroup, + ) +} + +// Upsert updates or appends a node. +func (n *Nodes) Upsert(node *Node) { + if n == nil { + *n = append(*n, node) + return + } + + for i := range *n { + if (*n)[i].ID() == node.ID() { + (*n)[i] = node + return + } + } + + *n = append(*n, node) +} + +// SetValueAttribute sets a node's attribute's value or returns false if no node is found. +func (n *Nodes) SetValueAttribute(id string, value interface{}) bool { + for i := range *n { + if (*n)[i].ID() == id { + (*n)[i].Attributes.SetValue(value) + return true + } + } + return false +} + +// Append appends a node. +func (n *Nodes) Append(node *Node) { + *n = append(*n, node) +} + +func (n *Nodes) RemoveMatching(node *Node) { + if n == nil { + return + } + + 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: Text, + } + case Input: + attr = &InputAttributes{ + NodeType: Input, + } + case Anchor: + attr = &AnchorAttributes{ + NodeType: Anchor, + } + case Image: + attr = &ImageAttributes{ + NodeType: Image, + } + case Script: + attr = &ScriptAttributes{ + NodeType: Script, + } + case Division: + attr = &DivisionAttributes{ + NodeType: Division, + } + default: + return fmt.Errorf("unexpected node type: %s", t) + } + + var d jsonRawNode + d.Attributes = attr + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&d); err != nil { + return err + } + + *n = Node(d) + + if n.Meta == nil { + n.Meta = new(Meta) + } + + return nil +} + +func (n *Node) MarshalJSON() ([]byte, error) { + var t UiNodeType + if n.Attributes != nil { + switch attr := n.Attributes.(type) { + case *TextAttributes: + t = Text + attr.NodeType = Text + case *InputAttributes: + t = Input + attr.NodeType = Input + case *AnchorAttributes: + t = Anchor + attr.NodeType = Anchor + case *ImageAttributes: + t = Image + attr.NodeType = Image + case *ScriptAttributes: + t = Script + attr.NodeType = Script + case *DivisionAttributes: + t = Division + attr.NodeType = Division + default: + return nil, errors.WithStack(fmt.Errorf("unknown node type: %T", n.Attributes)) + } + } + + if n.Type == "" { + n.Type = t + } else if n.Type != t { + return nil, errors.WithStack(fmt.Errorf("node type and node attributes mismatch: %T != %s", n.Attributes, n.Type)) + } + + if n.Meta == nil { + n.Meta = new(Meta) + } + + return json.Marshal((*jsonRawNode)(n)) +} diff --git a/ui/node/node_test.go b/ui/node/node_test.go new file mode 100644 index 0000000..bfe0c5a --- /dev/null +++ b/ui/node/node_test.go @@ -0,0 +1,560 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package node_test + +import ( + "bytes" + "context" + "embed" + "encoding/json" + "fmt" + "path/filepath" + "testing" + + "github.com/ory/x/snapshotx" + + "github.com/ory/kratos/text" + + "github.com/ory/x/assertx" + + "github.com/ory/kratos/corpx" + + "github.com/ory/kratos/ui/container" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/ui/node" +) + +func init() { + corpx.RegisterFakes() +} + +//go:embed fixtures/sort/* +var sortFixtures embed.FS +var ctx = context.Background() + +func TestNodesSort(t *testing.T) { + // use a schema compiler that disables identifiers + schemaCompiler := jsonschema.NewCompiler() + schemaPath := "fixtures/identity.schema.json" + + f, err := container.NewFromJSONSchema(ctx, "/foo", node.DefaultGroup, schemaPath, "", schemaCompiler, false) + require.NoError(t, err) + + f.UpdateNodeValuesFromJSON(json.RawMessage(`{}`), "traits", node.DefaultGroup) + f.SetCSRF("csrf_token") + + inputs, err := sortFixtures.ReadDir("fixtures/sort/input") + require.NoError(t, err) + + options := map[string][]node.SortOption{ + "1.json": { + node.SortUseOrder([]string{"password_identifier"}), + node.SortUpdateOrder(node.PasswordLoginOrder), + node.SortByGroups([]node.UiNodeGroup{ + node.DefaultGroup, + node.ProfileGroup, + node.OpenIDConnectGroup, + node.PasswordGroup, + node.LinkGroup, + node.LinkGroup, + }), + }, + "2.json": { + node.SortBySchema(filepath.Join("fixtures/sort/schema", "2.json")), + node.SortUpdateOrder(node.PasswordLoginOrder), + node.SortByGroups([]node.UiNodeGroup{ + node.DefaultGroup, + node.OpenIDConnectGroup, + node.PasswordGroup, + }), + }, + "3.json": { + node.SortBySchema(filepath.Join("fixtures/sort/schema", "3.json")), + node.SortByGroups([]node.UiNodeGroup{ + node.DefaultGroup, + node.OpenIDConnectGroup, + node.PasswordGroup, + }), + }, + "4.json": { + node.SortBySchema(filepath.Join("fixtures/sort/schema", "4.json")), + node.SortByGroups([]node.UiNodeGroup{ + node.DefaultGroup, + node.ProfileGroup, + node.PasswordGroup, + node.OpenIDConnectGroup, + node.LookupGroup, + node.WebAuthnGroup, + node.TOTPGroup, + }), + node.SortUseOrderAppend([]string{ + // Lookup + node.LookupReveal, + node.LookupRegenerate, + node.LookupCodes, + node.LookupConfirm, + + // Lookup + node.WebAuthnRemove, + node.WebAuthnRegisterDisplayName, + node.WebAuthnRegister, + + // TOTP + node.TOTPSecretKey, + node.TOTPQR, + node.TOTPUnlink, + node.TOTPCode, + }), + }, + } + + for _, in := range inputs { + t.Run("file="+in.Name(), func(t *testing.T) { + if in.IsDir() { + return + } + + fi, err := sortFixtures.Open(filepath.Join("fixtures/sort/input", in.Name())) + require.NoError(t, err) + defer func() { _ = fi.Close() }() + + var nodes node.Nodes + require.NoError(t, json.NewDecoder(fi).Decode(&nodes)) + require.NotEmpty(t, nodes) + + require.NoError(t, nodes.SortBySchema(ctx, options[in.Name()]...)) + + fe, err := sortFixtures.ReadFile(filepath.Join("fixtures/sort/expected", in.Name())) + require.NoError(t, err) + + assertx.EqualAsJSON(t, json.RawMessage(fe), nodes) + }) + } +} + +func TestNodesSortStableGroups(t *testing.T) { + t.Parallel() + + // Build count nodes in the same group, sharing an input name (as a list of + // remove buttons does). Insertion order is 0..count-1, recorded in each + // node's label; the value is the reverse, so a value tiebreak would flip the + // order. count exceeds Go's insertion-sort threshold (12) so the sort takes + // the quicksort path, where an unstable sort could otherwise reshuffle + // same-group nodes. + const count = 20 + build := func() node.Nodes { + var n node.Nodes + for i := range count { + value := fmt.Sprintf("%02d", count-1-i) + n = append(n, node.NewInputField(node.DeviceAuthnRemove, value, node.DeviceAuthnGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(&text.Message{Text: fmt.Sprintf("%02d", i)})) + } + return n + } + + labels := func(n node.Nodes) []string { + got := make([]string, 0, len(n)) + for _, nn := range n { + got = append(got, nn.Meta.Label.Text) + } + return got + } + + insertionOrder := make([]string, count) + valueOrder := make([]string, count) + for i := range count { + insertionOrder[i] = fmt.Sprintf("%02d", i) + valueOrder[i] = fmt.Sprintf("%02d", count-1-i) + } + + t.Run("case=stable group preserves insertion order", func(t *testing.T) { + t.Parallel() + n := build() + require.NoError(t, n.SortBySchema(ctx, + node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}), + node.SortStableGroups(node.DeviceAuthnGroup), + )) + assert.Equal(t, insertionOrder, labels(n)) + }) + + t.Run("case=without the option the value tiebreak reorders", func(t *testing.T) { + t.Parallel() + n := build() + require.NoError(t, n.SortBySchema(ctx, + node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}), + )) + assert.Equal(t, valueOrder, labels(n)) + }) +} + +func TestNodesUpsert(t *testing.T) { + var nodes node.Nodes + nodes.Upsert(node.NewCSRFNode("foo")) + require.Len(t, nodes, 1) + nodes.Upsert(node.NewCSRFNode("bar")) + require.Len(t, nodes, 1) + assert.EqualValues(t, "bar", nodes[0].Attributes.GetValue()) +} + +func TestNodesRemove(t *testing.T) { + var nodes node.Nodes + nodes.Append(node.NewInputField("other", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit)) + nodes.Append(node.NewInputField("link", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit)) + nodes.Append(node.NewInputField("link", "bar", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit)) + nodes.Append(node.NewInputField("unlink", "baz", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit)) + require.Len(t, nodes, 4) + + nodes.Remove("link", "unlink") + require.Len(t, nodes, 1) +} + +func TestNodesRemoveInGroup(t *testing.T) { + samlLink := node.NewInputField("link", "saml-provider", node.SAMLGroup, node.InputAttributeTypeSubmit) + nodes := node.Nodes{ + node.NewInputField("link", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit), + samlLink, + node.NewInputField("unlink", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit), + node.NewCSRFNode("token"), + } + + // Remove only the OIDC group's link/unlink nodes. + nodes.RemoveInGroup(node.OpenIDConnectGroup, "link", "unlink") + + require.Len(t, nodes, 2) + // The SAML link node shares the "link" name but a different group: it survives. + assert.Contains(t, nodes, samlLink) + // The CSRF node (DefaultGroup) is untouched. + var sawCSRF bool + for _, n := range nodes { + if n.Group == node.DefaultGroup { + sawCSRF = true + } + } + assert.True(t, sawCSRF, "CSRF node must not be removed") +} + +func TestNodeJSON(t *testing.T) { + t.Run("idempotent decode", func(t *testing.T) { + nodes := make(node.Nodes, 5) + for k := range nodes { + nodes[k] = new(node.Node) + expected := nodes[k] + + require.NoError(t, faker.FakeData(expected)) + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(expected)) + + var actual node.Node + require.NoError(t, json.NewDecoder(&b).Decode(&actual)) + + assert.EqualValues(t, *expected, actual) + assert.NotEmpty(t, actual.Type) + assert.NotNil(t, actual.Attributes) + } + }) + + t.Run("type mismatch", func(t *testing.T) { + n := &node.Node{Type: node.Image, Attributes: new(node.InputAttributes)} + var b bytes.Buffer + require.EqualError(t, json.NewEncoder(&b).Encode(n), "json: error calling MarshalJSON for type *node.Node: node type and node attributes mismatch: *node.InputAttributes != img") + }) + + t.Run("type empty", func(t *testing.T) { + n := &node.Node{Attributes: new(node.InputAttributes)} + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(n)) + assert.EqualValues(t, node.Input, gjson.GetBytes(b.Bytes(), "type").String()) + }) + + t.Run("type decode unknown", func(t *testing.T) { + var n node.Node + 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")) +} + +func TestNodeMarshalJSON(t *testing.T) { + tests := []struct { + name string + node *node.Node + wantErr bool + errMsg string + }{ + { + name: "text node", + node: &node.Node{ + Type: node.Text, + Group: node.DefaultGroup, + Attributes: &node.TextAttributes{ + NodeType: node.Text, + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "input node", + node: &node.Node{ + Type: node.Input, + Group: node.DefaultGroup, + Attributes: &node.InputAttributes{ + NodeType: node.Input, + Name: "password", + Type: "password", + FieldValue: "secret", + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "anchor node", + node: &node.Node{ + Type: node.Anchor, + Group: node.DefaultGroup, + Attributes: &node.AnchorAttributes{ + NodeType: node.Anchor, + HREF: "https://example.com", + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "image node", + node: &node.Node{ + Type: node.Image, + Group: node.DefaultGroup, + Attributes: &node.ImageAttributes{ + NodeType: node.Image, + Source: "image.jpg", + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "script node", + node: &node.Node{ + Type: node.Script, + Group: node.DefaultGroup, + Attributes: &node.ScriptAttributes{ + NodeType: node.Script, + Source: "script.js", + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "division node", + node: &node.Node{ + Type: node.Division, + Group: node.DefaultGroup, + Attributes: &node.DivisionAttributes{ + NodeType: node.Division, + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "type mismatch", + node: &node.Node{ + Type: node.Image, + Group: node.DefaultGroup, + Attributes: &node.InputAttributes{NodeType: node.Input}, + }, + wantErr: true, + errMsg: "node type and node attributes mismatch", + }, + { + name: "empty type inferred from attributes", + node: &node.Node{ + Group: node.DefaultGroup, + Attributes: &node.InputAttributes{ + NodeType: node.Input, + Name: "email", + }, + Messages: text.Messages{}, + Meta: &node.Meta{}, + }, + }, + { + name: "nil attributes", + node: &node.Node{ + Type: node.Image, + Group: node.DefaultGroup, + Attributes: nil, + Messages: text.Messages{}, + }, + wantErr: true, + errMsg: "node type and node attributes mismatch", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.node) + + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + return + } + + require.NoError(t, err) + + // Use snapshotx for testing serialization + snapshotx.SnapshotT(t, json.RawMessage(data)) + + // Verify roundtrip + var unmarshalled node.Node + err = json.Unmarshal(data, &unmarshalled) + require.NoError(t, err) + + // Re-marshal for comparison + remarshalled, err := json.Marshal(&unmarshalled) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(remarshalled)) + }) + } +} + +func TestNodes_FindAll(t *testing.T) { + nodes := node.Nodes{ + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}}, + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "b"}}, + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}}, + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "c"}}, + } + + t.Run("find matches multiple nodes", func(t *testing.T) { + got := nodes.FindAll("a") + assert.Equal(t, []node.Node{*nodes[0], *nodes[2]}, got) + }) + + t.Run("find matches one node", func(t *testing.T) { + got := nodes.FindAll("b") + assert.Equal(t, []node.Node{*nodes[1]}, got) + }) + + t.Run("no match returns empty slice", func(t *testing.T) { + got := nodes.FindAll("x") + assert.Empty(t, got) + }) +} + +func TestNodes_RemoveGroup(t *testing.T) { + nodes := node.Nodes{ + &node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "default"}}, + &node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha1"}}, + &node.Node{Type: node.Input, Group: node.IdentifierFirstGroup, Attributes: &node.InputAttributes{Name: "idfirst"}}, + &node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha2"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "password"}}, + nil, + } + + t.Run("removes single node", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroup(node.IdentifierFirstGroup) + assert.Equal(t, node.Nodes{nodes[0], nodes[1], nodes[3], nodes[4]}, n) + }) + + t.Run("removes multiple nodes", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroup(node.CaptchaGroup) + assert.Equal(t, node.Nodes{nodes[0], nodes[2], nodes[4]}, n) + }) + + t.Run("removes no nodes", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroup(node.PasskeyGroup) + assert.Equal(t, nodes[:5], n) + }) + + t.Run("removes multiple groups", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroups(node.CaptchaGroup, node.PasswordGroup) + assert.Equal(t, node.Nodes{nodes[0], nodes[2]}, n) + }) + + t.Run("removes multiple groups empty", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroups() + assert.Equal(t, nodes, n) + }) + + t.Run("removes multiple groups no matching", func(t *testing.T) { + n := append(node.Nodes(nil), nodes...) + n.RemoveGroups(node.PasskeyGroup) + assert.Equal(t, nodes[:5], n) + }) +} diff --git a/x/clean_url_test.go b/x/clean_url_test.go new file mode 100644 index 0000000..24ede50 --- /dev/null +++ b/x/clean_url_test.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCleanPath(t *testing.T) { + r := http.NewServeMux() + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(r.URL.String())) + }) + ts := httptest.NewServer(r) + defer ts.Close() + + for k, tc := range [][]string{ + {"//foo", "/foo"}, + {"//foo//bar", "/foo/bar"}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + res, err := ts.Client().Get(ts.URL + tc[0]) + require.NoError(t, err) + defer func() { _ = res.Body.Close() }() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, string(body), tc[1]) + }) + } +} diff --git a/x/cookie.go b/x/cookie.go new file mode 100644 index 0000000..8974011 --- /dev/null +++ b/x/cookie.go @@ -0,0 +1,95 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/http" + "time" + + "github.com/gorilla/sessions" + "github.com/pkg/errors" +) + +// SessionPersistValues adds values to the session store and persists the changes. +func SessionPersistValues(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id string, values map[string]interface{}) error { + // The error does not matter because in the worst case we're re-writing the session cookie. + cookie, _ := s.Get(r, id) + for k, v := range values { + cookie.Values[k] = v + } + + return errors.WithStack(cookie.Save(r, w)) +} + +// SessionGetString returns a string for the given id and key or an error if the session is invalid, +// the key does not exist, or the key value is not a string. +func SessionGetString(r *http.Request, s sessions.StoreExact, id string, key interface{}) (string, error) { + check := func(v map[interface{}]interface{}) (string, error) { + vv, ok := v[key] + if !ok { + return "", errors.Errorf("key %s does not exist in cookie: %+v", key, id) + } else if vvv, ok := vv.(string); !ok { + return "", errors.Errorf("value of key %s is not of type string in cookie", key) + } else { + return vvv, nil + } + } + + var exactErr error + cookie, err := s.GetExact(r, id, func(s *sessions.Session) bool { + _, exactErr = check(s.Values) + return exactErr == nil + }) + if err != nil { + return "", err + } else if exactErr != nil { + return "", exactErr + } + + return check(cookie.Values) +} + +// SessionGetStringOr returns a string for the given id and key or the fallback value if the session is invalid, +// the key does not exist, or the key value is not a string. +func SessionGetStringOr(r *http.Request, s sessions.StoreExact, id, key, fallback string) string { + v, err := SessionGetString(r, s, id, key) + if err != nil { + return fallback + } + return v +} + +func SessionUnset(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id string) 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 = -1 + cookie.Values = make(map[interface{}]interface{}) + 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 { + // No cookie was sent in the request. We have nothing to do. + return nil + } + + delete(cookie.Values, key) + return errors.WithStack(cookie.Save(r, w)) +} diff --git a/x/cookie_test.go b/x/cookie_test.go new file mode 100644 index 0000000..a7190a1 --- /dev/null +++ b/x/cookie_test.go @@ -0,0 +1,280 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/sessions" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSession(t *testing.T) { + const sid = "test_session" + + s := sessions.NewCookieStore([]byte("cyan cat walking over keyboard")) + s.Options.MaxAge = 78652871 + cj, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + // A dedicated transport so that a parallel test closing its httptest server + // (which calls http.DefaultTransport.CloseIdleConnections) cannot break our + // in-flight requests. + transport := http.DefaultTransport.(*http.Transport).Clone() + t.Cleanup(transport.CloseIdleConnections) + c := http.Client{Jar: cj, Transport: transport} + plainClient := &http.Client{Transport: transport} + + isExpiryCorrect := func(t *testing.T, r *http.Request) { + cookie, _ := s.Get(r, sid) + assert.EqualValues(t, 78652871, cookie.Options.MaxAge, "we ensure the options are always copied correctly.") + } + + router := http.NewServeMux() + router.HandleFunc("GET /set", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, SessionPersistValues(w, r, s, sid, map[string]interface{}{ + "string-1": "foo", + "string-2": "bar", + "string-3": "", + "int": 1234, + })) + isExpiryCorrect(t, r) + w.WriteHeader(http.StatusNoContent) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + mr := func(t *testing.T, path string) { + res, err := c.Get(ts.URL + "/" + path) + require.NoError(t, err) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + } + mr(t, "set") + + t.Run("case=GetString", func(t *testing.T) { + id := "get-string" + router.HandleFunc("GET /"+id, func(w http.ResponseWriter, r *http.Request) { + got, err := SessionGetString(r, s, sid, "string-1") + require.NoError(t, err) + assert.EqualValues(t, "foo", got) + + got, err = SessionGetString(r, s, sid, "string-2") + require.NoError(t, err) + assert.EqualValues(t, "bar", got) + + got, err = SessionGetString(r, s, sid, "string-3") + require.NoError(t, err) + assert.EqualValues(t, "", got) + + _, err = SessionGetString(r, s, sid, "int") + require.Error(t, err) + + _, err = SessionGetString(r, s, sid, "i-dont-exist") + require.Error(t, err) + + w.WriteHeader(http.StatusNoContent) + }) + }) + + t.Run("case=GetStringMultipleCookies", func(t *testing.T) { + id := "get-string-multiple" + + router.HandleFunc("GET /set/"+id, func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, SessionPersistValues(w, r, s, sid, map[string]interface{}{ + "multiple-string-1": "foo", + })) + require.NoError(t, SessionPersistValues(w, r, s, sid, map[string]interface{}{ + "multiple-string-2": "bar", + })) + isExpiryCorrect(t, r) + w.WriteHeader(http.StatusNoContent) + }) + + router.HandleFunc("GET /get/"+id, func(w http.ResponseWriter, r *http.Request) { + got, err := SessionGetString(r, s, sid, "multiple-string-1") + require.NoError(t, err) + assert.EqualValues(t, "foo", got) + + got, err = SessionGetString(r, s, sid, "multiple-string-2") + require.NoError(t, err) + assert.EqualValues(t, "bar", got) + + w.WriteHeader(http.StatusNoContent) + }) + + res, err := plainClient.Get(ts.URL + "/set/" + id) + require.NoError(t, err) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + + req, _ := http.NewRequest("GET", ts.URL+"/get/"+id, nil) + for _, c := range res.Cookies() { + req.AddCookie(c) + } + + res, err = plainClient.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("case=GetStringOr", func(t *testing.T) { + id := "get-string-or" + router.HandleFunc("GET /"+id, func(w http.ResponseWriter, r *http.Request) { + assert.EqualValues(t, "foo", SessionGetStringOr(r, s, sid, "string-1", "baz")) + assert.EqualValues(t, "bar", SessionGetStringOr(r, s, sid, "string-2", "baz")) + assert.EqualValues(t, "", SessionGetStringOr(r, s, sid, "string-3", "baz")) + assert.EqualValues(t, "baz", SessionGetStringOr(r, s, sid, "int", "baz")) + assert.EqualValues(t, "baz", SessionGetStringOr(r, s, sid, "i-dont-exist", "baz")) + + w.WriteHeader(http.StatusNoContent) + }) + mr(t, id) + }) + + mrCookie := func(t *testing.T, loc string, cks []*http.Cookie, f func(c []*http.Cookie, req *http.Request)) *http.Response { + req, err := http.NewRequest("GET", loc, nil) + require.NoError(t, err) + + // Mess with the signature + if f != nil { + f(cks, req) + } + + res, err := plainClient.Do(req) + require.NoError(t, err) + return res + } + + mrUnset := func(t *testing.T, ts *httptest.Server, id string, f func(c []*http.Cookie, req *http.Request)) *http.Response { + res, err := plainClient.Get(ts.URL + "/set") + require.NoError(t, err) + + res = mrCookie(t, ts.URL+"/"+id+"/unset", res.Cookies(), f) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + return res + } + + signatureScrambler := func(c []*http.Cookie, req *http.Request) { + for _, c := range c { + c.Value = strings.ReplaceAll(c.Value, "a", "b") + req.AddCookie(c) + } + } + + cookieCopier := func(c []*http.Cookie, req *http.Request) { + for _, c := range c { + req.AddCookie(c) + } + } + + t.Run("case=SessionSet", func(t *testing.T) { + res, err := plainClient.Get(ts.URL + "/set") + require.NoError(t, err) + + res = mrCookie(t, ts.URL+"/set", res.Cookies(), signatureScrambler) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("case=SessionUnset", func(t *testing.T) { + t.Run("prevent https://github.com/gorilla/sessions/pull/251", func(t *testing.T) { + w := httptest.NewRecorder() + require.NoError(t, SessionUnset(w, new(http.Request), s, "idonotexist")) + require.NoError(t, SessionUnset(w, new(http.Request), s, "idonotexist")) + require.Len(t, w.Result().Cookies(), 0) + }) + + id := "session-unset" + router.HandleFunc("GET /"+id+"/unset", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, SessionUnset(w, r, s, sid)) + w.WriteHeader(http.StatusNoContent) + cookie, _ := s.Get(r, sid) + assert.EqualValues(t, -1, cookie.Options.MaxAge, "we ensure the options are always copied correctly.") + }) + + router.HandleFunc("GET /"+id+"/get", func(w http.ResponseWriter, r *http.Request) { + require.Empty(t, SessionGetStringOr(r, s, sid, "string-1", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "string-2", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "string-3", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "int", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "i-dont-exist", "")) + w.WriteHeader(http.StatusNoContent) + }) + + t.Run("with invalid cookie signature", func(t *testing.T) { + res := mrUnset(t, ts, id, signatureScrambler) + assert.Len(t, res.Cookies(), 1) + mrCookie(t, ts.URL+"/"+id+"/get", res.Cookies(), cookieCopier) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("with valid cookie signature", func(t *testing.T) { + res := mrUnset(t, ts, id, cookieCopier) + assert.Len(t, res.Cookies(), 1) + mrCookie(t, ts.URL+"/"+id+"/get", res.Cookies(), cookieCopier) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + }) + + t.Run("case=SessionUnsetKey", func(t *testing.T) { + t.Run("prevent https://github.com/gorilla/sessions/pull/251", func(t *testing.T) { + w := httptest.NewRecorder() + require.NoError(t, SessionUnsetKey(w, new(http.Request), s, "idonotexist", "")) + require.NoError(t, SessionUnsetKey(w, new(http.Request), s, "idonotexist", "")) + require.Len(t, w.Result().Cookies(), 0) + }) + + id := "session-unset-key" + router.HandleFunc("GET /"+id+"/unset", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, SessionUnsetKey(w, r, s, sid, "string-1")) + w.WriteHeader(http.StatusNoContent) + isExpiryCorrect(t, r) + }) + + router.HandleFunc("GET /"+id+"/expect-unset", func(w http.ResponseWriter, r *http.Request) { + require.Empty(t, SessionGetStringOr(r, s, sid, "string-1", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "string-2", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "string-3", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "int", "")) + require.Empty(t, SessionGetStringOr(r, s, sid, "i-dont-exist", "")) + w.WriteHeader(http.StatusNoContent) + }) + + router.HandleFunc("GET /"+id+"/expect-one", func(w http.ResponseWriter, r *http.Request) { + require.Empty(t, SessionGetStringOr(r, s, sid, "string-1", "")) + assert.EqualValues(t, "bar", SessionGetStringOr(r, s, sid, "string-2", "baz")) + assert.EqualValues(t, "", SessionGetStringOr(r, s, sid, "string-3", "baz")) + assert.EqualValues(t, "baz", SessionGetStringOr(r, s, sid, "int", "baz")) + assert.EqualValues(t, "baz", SessionGetStringOr(r, s, sid, "i-dont-exist", "baz")) + w.WriteHeader(http.StatusNoContent) + }) + + t.Run("with invalid cookie signature", func(t *testing.T) { + res := mrUnset(t, ts, id, signatureScrambler) + assert.Len(t, res.Cookies(), 1) + mrCookie(t, ts.URL+"/"+id+"/expect-unset", res.Cookies(), cookieCopier) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("with valid cookie signature", func(t *testing.T) { + res := mrUnset(t, ts, id, cookieCopier) + assert.Len(t, res.Cookies(), 1) + mrCookie(t, ts.URL+"/"+id+"/expect-one", res.Cookies(), cookieCopier) + require.EqualValues(t, http.StatusNoContent, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + }) +} diff --git a/x/doc.go b/x/doc.go new file mode 100644 index 0000000..2e48474 --- /dev/null +++ b/x/doc.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/* +Package x provides various helpers that do not have an obvious home elsewhere. + +The contract implied here, is that: + - Package x does not depend on other parts of kratos + - Packages outside kratos do not depend on x. +*/ +package x diff --git a/x/edition_oss.go b/x/edition_oss.go new file mode 100644 index 0000000..d3ee127 --- /dev/null +++ b/x/edition_oss.go @@ -0,0 +1,9 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !commercial + +package x + +// Edition reports which build edition this binary was compiled as. +func Edition() string { return "oss" } diff --git a/x/edition_oss_test.go b/x/edition_oss_test.go new file mode 100644 index 0000000..6f15885 --- /dev/null +++ b/x/edition_oss_test.go @@ -0,0 +1,18 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !commercial + +package x_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/x" +) + +func TestEdition(t *testing.T) { + assert.Equal(t, "oss", x.Edition()) +} diff --git a/x/email.go b/x/email.go new file mode 100644 index 0000000..f216057 --- /dev/null +++ b/x/email.go @@ -0,0 +1,69 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "strings" + "unicode" + + "golang.org/x/net/idna" + + "github.com/ory/jsonschema/v3" +) + +func init() { + // email_loose accepts non-RFC carrier addresses (e.g. "foo.@docomo.ne.jp") + // that the strict RFC 5322 "email" format rejects, while still rejecting + // values that are unsafe in an SMTP envelope or an email header. + jsonschema.Formats["email_loose"] = func(v interface{}) bool { + s, ok := v.(string) + if !ok { + // Non-string values are handled by the "type" keyword. + return true + } + return IsEmailAddress(s) + } +} + +// emailLocalAtext is the RFC 5322 "atext" punctuation permitted in the local +// part. It deliberately excludes the RFC "specials" — "()<>[]:;@\,\"" — and all +// whitespace/control characters, which could break out of an SMTP envelope or +// inject an email header. +const emailLocalAtext = "!#$%&'*+/=?^_`{|}~.-" + +// maxEmailAddressLength caps accepted addresses. 320 is the historical RFC 3696 +// bound (64-octet local part + "@" + 255-octet domain). +const maxEmailAddressLength = 320 + +// IsEmailAddress reports whether s is shaped like an email address using a +// lenient, injection-safe check rather than a strict RFC 5322 parse: a local +// part of letters, digits, and atext punctuation (dot placement is not +// restricted, so carrier addresses like "foo.@docomo.ne.jp" pass), a single +// "@", and a domain that is a valid host name per idna.Lookup. It rejects +// control characters, whitespace, the RFC "specials" that enable SMTP-envelope +// or header injection, and addresses longer than 320 bytes. It is structural +// only and does not verify that the domain resolves or is deliverable. +func IsEmailAddress(s string) bool { + if len(s) == 0 || len(s) > maxEmailAddressLength { + return false + } + local, domain, ok := strings.Cut(s, "@") + if !ok || local == "" || domain == "" { + return false + } + for _, r := range local { + switch { + case unicode.IsLetter(r) || unicode.IsDigit(r): + case strings.ContainsRune(emailLocalAtext, r): + default: + return false + } + } + // idna.Lookup rejects any domain that is not a valid host name, so it also + // gates out embedded "@", control characters, whitespace, and specials. + if _, err := idna.Lookup.ToASCII(domain); err != nil { + return false + } + return true +} diff --git a/x/email_fuzz_test.go b/x/email_fuzz_test.go new file mode 100644 index 0000000..3f7fd36 --- /dev/null +++ b/x/email_fuzz_test.go @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "strings" + "testing" + "unicode" + + "golang.org/x/net/idna" +) + +// FuzzIsEmailAddress asserts the safety invariants of every string that +// IsEmailAddress accepts. These invariants — not the example table — are the +// actual injection-safety contract relied on by the courier SMTP path. +func FuzzIsEmailAddress(f *testing.F) { + for _, seed := range []string{ + "foo@ory.sh", + "foo.@docomo.ne.jp", + ".foo@docomo.ne.jp", + "foo..bar@docomo.ne.jp", + "a!#$%&'*+/=?^_`{|}~-@ory.sh", + "föö@münchen.de", + "foo@ory.sh\r\nBcc: evil@x", + "foo@ory.sh", + "foo@bar@ory.sh", + "@ory.sh", + "foo@", + "", + } { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, s string) { + if !IsEmailAddress(s) { + return + } + + if len(s) > maxEmailAddressLength { + t.Fatalf("accepted over-long address (%d bytes)", len(s)) + } + if strings.Count(s, "@") != 1 { + t.Fatalf("accepted address without exactly one @: %q", s) + } + local, domain, _ := strings.Cut(s, "@") + if local == "" || domain == "" { + t.Fatalf("accepted empty local part or domain: %q", s) + } + for _, r := range s { + if r < 0x20 || r == 0x7f || unicode.IsSpace(r) || unicode.IsControl(r) { + t.Fatalf("accepted control/whitespace rune %U in %q", r, s) + } + if r != '@' && strings.ContainsRune("()<>[]:;@\\,\"", r) { + t.Fatalf("accepted RFC specials rune %q in %q", r, s) + } + } + if _, err := idna.Lookup.ToASCII(domain); err != nil { + t.Fatalf("accepted domain that idna rejects (%v) in %q", err, s) + } + }) +} diff --git a/x/email_test.go b/x/email_test.go new file mode 100644 index 0000000..867a717 --- /dev/null +++ b/x/email_test.go @@ -0,0 +1,67 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsEmailAddress(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + in string + want bool + }{ + // Accepted: normal and non-RFC-but-safe carrier addresses. + {name: "plain", in: "foo@ory.sh", want: true}, + {name: "subdomain and plus", in: "foo.bar+tag@mail.ory.sh", want: true}, + {name: "docomo trailing dot", in: "foo.@docomo.ne.jp", want: true}, + {name: "docomo double dot", in: "foo..bar@docomo.ne.jp", want: true}, + {name: "docomo leading dot", in: ".foo@docomo.ne.jp", want: true}, + {name: "internationalized local part", in: "föö@ory.sh", want: true}, + {name: "atext punctuation", in: "a!#$%&'*+/=?^_`{|}~-@ory.sh", want: true}, + {name: "single label domain", in: "foo@localhost", want: true}, + {name: "idn domain accepted via idna", in: "foo@münchen.de", want: true}, + {name: "exactly 320 bytes", in: strings.Repeat("a", 313) + "@ory.sh", want: true}, + {name: "321 bytes over cap", in: strings.Repeat("a", 314) + "@ory.sh", want: false}, + + // Rejected: not an address. + {name: "empty", in: "", want: false}, + {name: "no at", in: "invalid-email", want: false}, + {name: "empty local", in: "@ory.sh", want: false}, + {name: "empty domain", in: "foo@", want: false}, + {name: "two at signs", in: "foo@bar@ory.sh", want: false}, + + // Rejected: injection / SMTP-envelope specials and whitespace. + {name: "CR injection", in: "foo@ory.sh\r\nBcc: evil@x", want: false}, + {name: "LF injection", in: "foo@ory.sh\nBcc: evil@x", want: false}, + {name: "space", in: "foo bar@ory.sh", want: false}, + {name: "tab", in: "foo\t@ory.sh", want: false}, + {name: "angle brackets", in: "foo@ory.sh", want: false}, + {name: "comma", in: "foo@ory.sh,evil@x", want: false}, + {name: "semicolon", in: "foo;bar@ory.sh", want: false}, + {name: "colon", in: "foo:bar@ory.sh", want: false}, + {name: "double quote", in: "\"foo\"@ory.sh", want: false}, + {name: "backslash", in: "foo\\bar@ory.sh", want: false}, + {name: "parenthesis", in: "foo(comment)@ory.sh", want: false}, + {name: "square bracket ip literal", in: "foo@[192.168.0.1]", want: false}, + + // Rejected: domain is not a valid host name (idna Lookup profile). + {name: "bang in domain", in: "foo@bar!baz", want: false}, + {name: "percent in domain", in: "foo@bar%baz", want: false}, + {name: "underscore in domain", in: "foo@bar_baz.com", want: false}, + {name: "backtick in domain", in: "foo@ba`r.com", want: false}, + {name: "over length cap", in: strings.Repeat("a", 310) + "@" + strings.Repeat("b", 20) + ".jp", want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, IsEmailAddress(tc.in)) + }) + } +} diff --git a/x/err.go b/x/err.go new file mode 100644 index 0000000..da524e5 --- /dev/null +++ b/x/err.go @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "errors" + "net/http" + + "github.com/gofrs/uuid" + + "github.com/ory/herodot" +) + +type WithIdentityIDError struct { + err error + identityID uuid.UUID +} + +func (e *WithIdentityIDError) Error() string { + return e.err.Error() +} + +func (e *WithIdentityIDError) Unwrap() error { + return e.err +} + +func (e *WithIdentityIDError) IdentityID() uuid.UUID { + return e.identityID +} + +func WrapWithIdentityIDError(err error, identityID uuid.UUID) error { + if err == nil { + return nil + } + + return &WithIdentityIDError{ + err: err, + identityID: identityID, + } +} + +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 + if errors.As(err, &sc) { + return sc.StatusCode() + } + return fallback +} + +func Must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/x/err_test.go b/x/err_test.go new file mode 100644 index 0000000..9437406 --- /dev/null +++ b/x/err_test.go @@ -0,0 +1,83 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x_test + +import ( + "errors" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestWrapWithIdentityIDError(t *testing.T) { + t.Run("case=wraps error with identity ID", func(t *testing.T) { + baseErr := errors.New("test error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + require.NotNil(t, wrappedErr) + assert.Equal(t, "test error", wrappedErr.Error()) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(wrappedErr, &withIDErr)) + assert.Equal(t, identityID, withIDErr.IdentityID()) + }) + + t.Run("case=unwraps to original error", func(t *testing.T) { + baseErr := errors.New("original error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + unwrappedErr := errors.Unwrap(wrappedErr) + assert.Equal(t, baseErr, unwrappedErr) + }) + + t.Run("case=returns nil when wrapping nil error", func(t *testing.T) { + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(nil, identityID) + + assert.Nil(t, wrappedErr) + }) + + t.Run("case=preserves identity ID with nil UUID", func(t *testing.T) { + baseErr := errors.New("test error") + var identityID uuid.UUID // nil UUID + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(wrappedErr, &withIDErr)) + assert.Equal(t, uuid.Nil, withIDErr.IdentityID()) + }) + + t.Run("case=can wrap already wrapped error", func(t *testing.T) { + baseErr := errors.New("base error") + firstID := uuid.Must(uuid.NewV4()) + secondID := uuid.Must(uuid.NewV4()) + + firstWrap := x.WrapWithIdentityIDError(baseErr, firstID) + secondWrap := x.WrapWithIdentityIDError(firstWrap, secondID) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(secondWrap, &withIDErr)) + // Should get the outermost identity ID + assert.Equal(t, secondID, withIDErr.IdentityID()) + }) + + t.Run("case=works with errors.Is", func(t *testing.T) { + baseErr := errors.New("base error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + assert.True(t, errors.Is(wrappedErr, baseErr)) + }) +} diff --git a/x/events/events.go b/x/events/events.go new file mode 100644 index 0000000..d5be5da --- /dev/null +++ b/x/events/events.go @@ -0,0 +1,596 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + "errors" + "net/url" + "time" + + "github.com/gofrs/uuid" + otelattr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" + "github.com/ory/x/jsonx" + "github.com/ory/x/otelx/semconv" +) + +const ( + IdentityCreated semconv.Event = "IdentityCreated" + IdentityDeleted semconv.Event = "IdentityDeleted" + IdentityUpdated semconv.Event = "IdentityUpdated" + JsonnetMappingFailed semconv.Event = "JsonnetMappingFailed" + LoginFailed semconv.Event = "LoginFailed" + LoginInitiated semconv.Event = "LoginInitiated" + LoginSucceeded semconv.Event = "LoginSucceeded" + RecoveryFailed semconv.Event = "RecoveryFailed" + RecoveryInitiatedByAdmin semconv.Event = "RecoveryInitiatedByAdmin" + RecoverySucceeded semconv.Event = "RecoverySucceeded" + RegistrationFailed semconv.Event = "RegistrationFailed" + RegistrationInitiated semconv.Event = "RegistrationInitiated" + RegistrationSucceeded semconv.Event = "RegistrationSucceeded" + SessionChanged semconv.Event = "SessionChanged" + SessionChecked semconv.Event = "SessionChecked" + SessionIssued semconv.Event = "SessionIssued" + SessionLifespanExtended semconv.Event = "SessionLifespanExtended" + SessionRevoked semconv.Event = "SessionRevoked" + SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" + SettingsFailed semconv.Event = "SettingsFailed" + SettingsSucceeded semconv.Event = "SettingsSucceeded" + VerificationFailed semconv.Event = "VerificationFailed" + VerificationSucceeded semconv.Event = "VerificationSucceeded" + WebhookDelivered semconv.Event = "WebhookDelivered" + WebhookFailed semconv.Event = "WebhookFailed" + WebhookSucceeded semconv.Event = "WebhookSucceeded" + CourierMessageAbandoned semconv.Event = "CourierMessageAbandoned" + CourierMessageDispatched semconv.Event = "CourierMessageDispatched" +) + +const ( + AttributeKeyErrorReason semconv.AttributeKey = "ErrorReason" + AttributeKeyFlowID semconv.AttributeKey = "FlowID" + AttributeKeyFlowRefresh semconv.AttributeKey = "FlowRefresh" + AttributeKeyFlowRequestedAAL semconv.AttributeKey = "FlowRequestedAAL" + AttributeKeyJsonnetInput semconv.AttributeKey = "JsonnetInput" + AttributeKeyJsonnetOutput semconv.AttributeKey = "JsonnetOutput" + AttributeKeyLoginRequestedAAL semconv.AttributeKey = "LoginRequestedAAL" + AttributeKeyLoginRequestedPrivilegedSession semconv.AttributeKey = "LoginRequestedPrivilegedSession" + AttributeKeyOrganizationID semconv.AttributeKey = "OrganizationID" + AttributeKeyReason semconv.AttributeKey = "Reason" // Deprecated, use AttributeKeyErrorReason + // AttributeKeySelfServiceFlowType is the type of self-service flow, e.g. "api" or "browser". + AttributeKeySelfServiceFlowType semconv.AttributeKey = "SelfServiceFlowType" + // AttributeKeySelfServiceFlowName is the client used in the self-service flow, e.g. "login" or "registration" + AttributeKeySelfServiceFlowName semconv.AttributeKey = "SelfServiceFlowName" + // AttributeKeySelfServiceMethodUsed is the strategy used in the self-service flow, e.g. "oidc" or "password". + AttributeKeySelfServiceMethodUsed semconv.AttributeKey = "SelfServiceMethodUsed" + AttributeKeySelfServiceSSOProviderUsed semconv.AttributeKey = "SelfServiceSSOProviderUsed" + // AttributeKeySelfServiceStrategyUsed is the name of the Flow used in the self-service flow, e.g. "login" or "registration". + // + // Deprecated: use AttributeKeySelfServiceFlowName instead. + AttributeKeySelfServiceStrategyUsed semconv.AttributeKey = "SelfServiceStrategyUsed" + AttributeKeySessionAAL semconv.AttributeKey = "SessionAAL" + AttributeKeySessionExpiresAt semconv.AttributeKey = "SessionExpiresAt" + AttributeKeySessionID semconv.AttributeKey = "SessionID" + AttributeKeyTokenizedSessionTTL semconv.AttributeKey = "TokenizedSessionTTL" + AttributeKeyWebhookAttemptNumber semconv.AttributeKey = "WebhookAttemptNumber" + AttributeKeyWebhookID semconv.AttributeKey = "WebhookID" + AttributeKeyWebhookRequestBody semconv.AttributeKey = "WebhookRequestBody" + AttributeKeyWebhookRequestID semconv.AttributeKey = "WebhookRequestID" + AttributeKeyWebhookResponseBody semconv.AttributeKey = "WebhookResponseBody" + AttributeKeyWebhookResponseStatusCode semconv.AttributeKey = "WebhookResponseStatusCode" + AttributeKeyWebhookTriggerID semconv.AttributeKey = "WebhookTriggerID" + AttributeKeyWebhookURL semconv.AttributeKey = "WebhookURL" + AttributeKeyCourierMessageID semconv.AttributeKey = "CourierMessageID" + AttributeKeyCourierMessageChannel semconv.AttributeKey = "CourierMessageChannel" + AttributeKeyCourierMessageTemplateType semconv.AttributeKey = "CourierMessageTemplateType" +) + +func attrSessionID(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeySessionID.String(), val.String()) +} + +func attrTokenizedSessionTTL(ttl time.Duration) otelattr.KeyValue { + return otelattr.String(AttributeKeyTokenizedSessionTTL.String(), ttl.String()) +} + +func attrSessionAAL(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeySessionAAL.String(), val) +} + +func attLoginRequestedAAL(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyLoginRequestedAAL.String(), val) +} + +func attrFlowRefresh(val bool) otelattr.KeyValue { + return otelattr.Bool(AttributeKeyFlowRefresh.String(), val) +} + +func attrOrganizationID(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyOrganizationID.String(), val) +} + +func attrFlowRequestedAAL(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyFlowRequestedAAL.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) +} + +func attrSelfServiceFlowType(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeySelfServiceFlowType.String(), val) +} + +func attrSelfServiceMethodUsed(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeySelfServiceMethodUsed.String(), val) +} + +func attrSelfServiceSSOProviderUsed(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeySelfServiceSSOProviderUsed.String(), val) +} + +func attrWebhookID(id string) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookID.String(), id) +} + +func attrWebhookURL(URL *url.URL) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookURL.String(), URL.Redacted()) +} + +func attrWebhookReq(body []byte) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookRequestBody.String(), string(body)) +} + +func attrWebhookRes(body []byte) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookResponseBody.String(), string(body)) +} + +func attrWebhookStatus(status int) otelattr.KeyValue { + return otelattr.Int(AttributeKeyWebhookResponseStatusCode.String(), status) +} + +func attrWebhookAttempt(n int) otelattr.KeyValue { + return otelattr.Int(AttributeKeyWebhookAttemptNumber.String(), n) +} + +func attrWebhookRequestID(id uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookRequestID.String(), id.String()) +} + +func attrWebhookTriggerID(id uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyWebhookTriggerID.String(), id.String()) +} + +// deprecated: use attrErrorReason instead +func attrReason(err error) otelattr.KeyValue { + return otelattr.String(AttributeKeyReason.String(), reasonForError(err)) +} + +func attrErrorReason(err error) otelattr.KeyValue { + return otelattr.String(AttributeKeyErrorReason.String(), reasonForError(err)) +} + +func attrJsonnetInput(in []byte) otelattr.KeyValue { + return otelattr.String(AttributeKeyJsonnetInput.String(), string(jsonx.Anonymize(in))) +} + +func attrJsonnetOutput(out string) otelattr.KeyValue { + return otelattr.String(AttributeKeyJsonnetOutput.String(), string(jsonx.Anonymize([]byte(out)))) +} + +func attrFlowID(id uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyFlowID.String(), id.String()) +} + +func attrCourierMessageID(id uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyCourierMessageID.String(), id.String()) +} + +func attrCourierMessageChannel(channel string) otelattr.KeyValue { + return otelattr.String(AttributeKeyCourierMessageChannel.String(), channel) +} + +func attrCourierMessageTemplateType(templateType string) otelattr.KeyValue { + return otelattr.String(AttributeKeyCourierMessageTemplateType.String(), templateType) +} + +func NewSessionIssued(ctx context.Context, aal string, sessionID, identityID uuid.UUID) (string, trace.EventOption) { + return SessionIssued.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attrSessionAAL(aal), + )..., + ) +} + +func NewSessionChanged(ctx context.Context, aal string, sessionID, identityID uuid.UUID) (string, trace.EventOption) { + return SessionChanged.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attrSessionAAL(aal), + )..., + ) +} + +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, FlowID uuid.UUID + FlowType, RequestedAAL, Method, SSOProvider string + IsRefresh bool +} + +func NewLoginSucceeded(ctx context.Context, o *LoginSucceededOpts) (string, trace.EventOption) { + return LoginSucceeded.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(o.IdentityID), + attrSessionID(o.SessionID), + attrSelfServiceFlowType(o.FlowType), + attLoginRequestedAAL(o.RequestedAAL), + attLoginRequestedPrivilegedSession(o.IsRefresh), + attrSelfServiceMethodUsed(o.Method), + attrSelfServiceSSOProviderUsed(o.SSOProvider), + attrFlowID(o.FlowID), + )..., + ) +} + +func NewRegistrationSucceeded(ctx context.Context, flowID, identityID uuid.UUID, flowType, method, provider string) (string, trace.EventOption) { + return RegistrationSucceeded.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + semconv.AttrIdentityID(identityID), + attrSelfServiceMethodUsed(method), + attrSelfServiceSSOProviderUsed(provider), + attrFlowID(flowID), + )...) +} + +func NewRecoverySucceeded(ctx context.Context, flowID, identityID uuid.UUID, flowType, method string) (string, trace.EventOption) { + return RecoverySucceeded.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + semconv.AttrIdentityID(identityID), + attrSelfServiceMethodUsed(method), + attrFlowID(flowID), + )...) +} + +func NewRecoveryInitiatedByAdmin(ctx context.Context, flowID, identityID uuid.UUID, flowType, method string) (string, trace.EventOption) { + return RecoveryInitiatedByAdmin.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + semconv.AttrIdentityID(identityID), + attrSelfServiceMethodUsed(method), + attrFlowID(flowID), + )...) +} + +func NewSettingsSucceeded(ctx context.Context, flowID, identityID uuid.UUID, flowType, method string) (string, trace.EventOption) { + return SettingsSucceeded.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + semconv.AttrIdentityID(identityID), + attrSelfServiceMethodUsed(method), + attrFlowID(flowID), + )...) +} + +func NewVerificationSucceeded(ctx context.Context, flowID, identityID uuid.UUID, flowType, method string) (string, trace.EventOption) { + return VerificationSucceeded.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceMethodUsed(method), + attrSelfServiceFlowType(flowType), + semconv.AttrIdentityID(identityID), + attrFlowID(flowID), + )...) +} + +func NewRegistrationFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { + return RegistrationFailed.String(), + trace.WithAttributes(append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + )...) +} + +func NewRecoveryFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return RecoveryFailed.String(), trace.WithAttributes(attrs...) +} + +func NewSettingsFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return SettingsFailed.String(), trace.WithAttributes(attrs...) +} + +func NewVerificationFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return VerificationFailed.String(), + trace.WithAttributes(attrs...) +} + +func NewIdentityCreated(ctx context.Context, identityID uuid.UUID) (string, trace.EventOption) { + return IdentityCreated.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + )..., + ) +} + +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( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + )..., + ) +} + +func NewLoginFailed(ctx context.Context, flowID uuid.UUID, flowType, method, requestedAAL string, isRefresh bool, err error) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attLoginRequestedAAL(requestedAAL), + attLoginRequestedPrivilegedSession(isRefresh), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return LoginFailed.String(), trace.WithAttributes(attrs...) +} + +func NewSessionRevoked(ctx context.Context, sessionID, identityID uuid.UUID) (string, trace.EventOption) { + return SessionRevoked.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + )..., + ) +} + +func NewSessionChecked(ctx context.Context, sessionID, identityID uuid.UUID) (string, trace.EventOption) { + return SessionChecked.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + )..., + ) +} + +func NewSessionJWTIssued(ctx context.Context, sessionID, identityID uuid.UUID, ttl time.Duration) (string, trace.EventOption) { + return SessionTokenizedAsJWT.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attrTokenizedSessionTTL(ttl), + )..., + ) +} + +func NewWebhookDelivered(ctx context.Context, URL *url.URL, reqBody []byte, status int, resBody []byte, attempt int, requestID, triggerID uuid.UUID, webhookID string) (string, trace.EventOption) { + return WebhookDelivered.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrWebhookReq(reqBody), + attrWebhookRes(resBody), + attrWebhookStatus(status), + attrWebhookURL(URL), + attrWebhookAttempt(attempt), + attrWebhookRequestID(requestID), + attrWebhookID(webhookID), + attrWebhookTriggerID(triggerID), + )..., + ) +} + +func NewWebhookSucceeded(ctx context.Context, triggerID uuid.UUID, webhookID string) (string, trace.EventOption) { + return WebhookSucceeded.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrWebhookID(webhookID), + attrWebhookTriggerID(triggerID), + )...) +} + +func NewWebhookFailed(ctx context.Context, err error, triggerID uuid.UUID, id string) (string, trace.EventOption) { + return WebhookFailed.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrWebhookID(id), + attrWebhookTriggerID(triggerID), + otelattr.String("Error", err.Error()), + attrErrorReason(err), + )..., + ) +} + +// NewJsonnetMappingFailed is used to log errors that occur during the Jsonnet +// mapping process. The jsonnetInput and jsonnetOutput is anonymized before +// emitting the event. +func NewJsonnetMappingFailed(ctx context.Context, err error, jsonnetInput []byte, jsonnetOutput, provider string, method string) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrErrorReason(err), + attrJsonnetInput(jsonnetInput), + attrSelfServiceSSOProviderUsed(provider), + attrSelfServiceMethodUsed(method), + ) + if jsonnetOutput != "" { + attrs = append(attrs, attrJsonnetOutput(jsonnetOutput)) + } + return JsonnetMappingFailed.String(), + trace.WithAttributes( + attrs..., + ) +} + +func NewLoginInitiated(ctx context.Context, flowID uuid.UUID, flowType string, refresh bool, organizationID uuid.NullUUID, requestedAAL string) (string, trace.EventOption) { + attrs := append(semconv.AttributesFromContext(ctx), + attrFlowID(flowID), + attrSelfServiceFlowType(flowType), + attrFlowRefresh(refresh), + attrFlowRequestedAAL(requestedAAL), + ) + + if organizationID.Valid { + attrs = append(attrs, + attrOrganizationID(organizationID.UUID.String())) + } + + return LoginInitiated.String(), + trace.WithAttributes(attrs...) +} + +func NewRegistrationInitiated(ctx context.Context, flowID uuid.UUID, flowType string, organizationID uuid.NullUUID) (string, trace.EventOption) { + attrs := append(semconv.AttributesFromContext(ctx), + attrFlowID(flowID), + attrSelfServiceFlowType(flowType), + ) + + if organizationID.Valid { + attrs = append(attrs, + attrOrganizationID(organizationID.UUID.String())) + } + + return RegistrationInitiated.String(), + trace.WithAttributes(attrs...) +} + +func reasonForError(err error) string { + if ve := new(schema.ValidationError); errors.As(err, &ve) { + return ve.Message + } + if r := *new(herodot.ReasonCarrier); errors.As(err, &r) { + return r.Reason() + } + return err.Error() +} + +func NewCourierMessageAbandoned(ctx context.Context, messageID uuid.UUID, channel string, templateType string) (string, trace.EventOption) { + return CourierMessageAbandoned.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrCourierMessageID(messageID), + attrCourierMessageChannel(channel), + attrCourierMessageTemplateType(templateType), + )..., + ) +} + +func NewCourierMessageDispatched(ctx context.Context, messageID uuid.UUID, channel string, templateType string) (string, trace.EventOption) { + return CourierMessageDispatched.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrCourierMessageID(messageID), + attrCourierMessageChannel(channel), + attrCourierMessageTemplateType(templateType), + )..., + ) +} diff --git a/x/events/events_test.go b/x/events/events_test.go new file mode 100644 index 0000000..15354c4 --- /dev/null +++ b/x/events/events_test.go @@ -0,0 +1,270 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package events_test + +import ( + "errors" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/otelx/semconv" +) + +func TestNewJsonnetMappingFailed(t *testing.T) { + tests := []struct { + name string + err error + jsonnetInput []byte + jsonnetOutput string + provider string + method identity.CredentialsType + expectedAttrs []attribute.KeyValue + }{ + { + name: "With all attributes", + err: errors.New("test error"), + jsonnetInput: []byte(`{"key": "PII value"}`), + jsonnetOutput: `{"key": 123}`, + provider: "test-provider", + method: identity.CredentialsTypeOIDC, + expectedAttrs: []attribute.KeyValue{ + attribute.String("SelfServiceSSOProviderUsed", "test-provider"), + attribute.String("SelfServiceMethodUsed", "oidc"), + attribute.String("ErrorReason", "test error"), + attribute.String("JsonnetInput", `{ + "key": "string" +}`), + attribute.String("JsonnetOutput", `{ + "key": "number" +}`), + }, + }, + { + name: "Without JsonnetOutput", + err: errors.New("another error"), + jsonnetInput: []byte(`{"key": "PII value"}`), + jsonnetOutput: "", + provider: "another-provider", + method: identity.CredentialsTypeSAML, + expectedAttrs: []attribute.KeyValue{ + attribute.String("SelfServiceSSOProviderUsed", "another-provider"), + attribute.String("SelfServiceMethodUsed", "saml"), + attribute.String("ErrorReason", "another error"), + attribute.String("JsonnetInput", `{ + "key": "string" +}`), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := t.Context() + eventName, opts := events.NewJsonnetMappingFailed(ctx, tt.err, tt.jsonnetInput, tt.jsonnetOutput, tt.provider, tt.method.String()) + + assert.Equal(t, events.JsonnetMappingFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + assert.ElementsMatch(t, tt.expectedAttrs, eventConfig.Attributes()) + }) + } +} + +func TestNewLoginFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("login failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewLoginFailed(ctx, flowID, "browser", "password", "aal1", false, baseErr) + + assert.Equal(t, events.LoginFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + // Should not contain IdentityID attribute + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + // Should contain other attributes + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("LoginRequestedAAL", "aal1")) + assert.Contains(t, attrs, attribute.Bool("LoginRequestedPrivilegedSession", false)) + assert.Contains(t, attrs, attribute.String("ErrorReason", "login failed")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "password")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewLoginFailed(ctx, flowID, "browser", "password", "aal1", false, wrappedErr) + + assert.Equal(t, events.LoginFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("ErrorReason", "login failed")) + }) +} + +func TestNewRecoveryFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("recovery failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewRecoveryFailed(ctx, flowID, "browser", "code", baseErr) + + assert.Equal(t, events.RecoveryFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "code")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewRecoveryFailed(ctx, flowID, "browser", "link", wrappedErr) + + assert.Equal(t, events.RecoveryFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "link")) + }) +} + +func TestNewSettingsFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("settings failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewSettingsFailed(ctx, flowID, "browser", "profile", baseErr) + + assert.Equal(t, events.SettingsFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "profile")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewSettingsFailed(ctx, flowID, "browser", "password", wrappedErr) + + assert.Equal(t, events.SettingsFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "password")) + }) +} + +func TestNewVerificationFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("verification failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewVerificationFailed(ctx, flowID, "browser", "code", baseErr) + + assert.Equal(t, events.VerificationFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "code")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewVerificationFailed(ctx, flowID, "browser", "link", wrappedErr) + + assert.Equal(t, events.VerificationFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "link")) + }) +} + +func TestNewCourierMessageDispatched(t *testing.T) { + t.Parallel() + nid := uuid.Must(uuid.NewV4()) + msgID := uuid.Must(uuid.NewV4()) + ctx := semconv.ContextWithAttributes(t.Context(), attribute.String("ProjectID", nid.String())) + + eventName, opts := events.NewCourierMessageDispatched(ctx, msgID, "smtp", "stub") + + assert.Equal(t, events.CourierMessageDispatched.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("ProjectID", nid.String())) + assert.Contains(t, attrs, attribute.String("CourierMessageID", msgID.String())) + assert.Contains(t, attrs, attribute.String("CourierMessageChannel", "smtp")) + assert.Contains(t, attrs, attribute.String("CourierMessageTemplateType", "stub")) +} + +func TestNewCourierMessageAbandoned(t *testing.T) { + t.Parallel() + nid := uuid.Must(uuid.NewV4()) + msgID := uuid.Must(uuid.NewV4()) + ctx := semconv.ContextWithAttributes(t.Context(), attribute.String("ProjectID", nid.String())) + + eventName, opts := events.NewCourierMessageAbandoned(ctx, msgID, "http", "recovery") + + assert.Equal(t, events.CourierMessageAbandoned.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("ProjectID", nid.String())) + assert.Contains(t, attrs, attribute.String("CourierMessageID", msgID.String())) + assert.Contains(t, attrs, attribute.String("CourierMessageChannel", "http")) + assert.Contains(t, attrs, attribute.String("CourierMessageTemplateType", "recovery")) +} diff --git a/x/fetcher.go b/x/fetcher.go new file mode 100644 index 0000000..bb6aeae --- /dev/null +++ b/x/fetcher.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "github.com/ory/x/jwksx" + +type JWKSFetchProvider interface { + JWKSFetcher() *jwksx.FetcherNext +} diff --git a/x/http.go b/x/http.go new file mode 100644 index 0000000..e980af4 --- /dev/null +++ b/x/http.go @@ -0,0 +1,148 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "cmp" + "context" + "net/http" + "net/url" + + "github.com/golang/gddo/httputil" + + "github.com/ory/herodot" +) + +type ctxKey struct{} + +var baseURLKey ctxKey + +// WithBaseURL stores the supplied base URL on the context. The URL's scheme +// is preserved verbatim — callers that need an https guarantee must enforce +// it themselves before calling. +func WithBaseURL(ctx context.Context, baseURL *url.URL) context.Context { + if baseURL == nil { + return ctx + } + return context.WithValue(ctx, baseURLKey, baseURL) +} + +func BaseURLFromContext(ctx context.Context) *url.URL { + if ctx == nil { + return nil + } + if v := ctx.Value(baseURLKey); v != nil { + if u, ok := v.(*url.URL); ok { + return u + } + } + return nil +} + +// BaseURLStringFromContext returns the captured base URL as a string, or an +// empty string if none is set. Used to copy the captured URL onto a flow +// row at creation time (where it lives next to the rest of the flow rather +// than being plumbed via context to every consumer). +func BaseURLStringFromContext(ctx context.Context) string { + if u := BaseURLFromContext(ctx); u != nil { + return u.String() + } + return "" +} + +// CourierBaseURL parses the courier base URL captured on a flow at init time +// (recovery.Flow.CourierBaseURL / verification.Flow.CourierBaseURL) and +// returns it as a *url.URL. When the captured value is empty or unparseable, +// it returns the supplied fallback (typically Config.SelfPublicURL). +func CourierBaseURL(courierBaseURL string, fallback *url.URL) *url.URL { + if courierBaseURL != "" { + if u, err := url.Parse(courierBaseURL); err == nil { + return u + } + } + return fallback +} + +func RequestURL(r *http.Request) *url.URL { + source := *r.URL + source.Host = cmp.Or(source.Host, r.Header.Get("X-Forwarded-Host"), r.Host) + + if proto := r.Header.Get("X-Forwarded-Proto"); len(proto) > 0 { + source.Scheme = proto + } + + if source.Scheme == "" { + source.Scheme = "https" + if r.TLS == nil { + source.Scheme = "http" + } + } + + return &source +} + +// RequestBaseURL returns the customer-facing base URL of the request. +// +// If a base URL was captured on the request context (e.g. by a proxy-aware +// middleware that validated an Ory-Base-URL-Rewrite / X-Ory-Original-Host +// header), that value wins — it is the URL the end user's browser actually +// used, which may differ from the host this service was reached at. This is +// the value an OIDC/SAML callback must be redirected back to. +// +// Otherwise it falls back to the request's own scheme://host[:port] (honoring +// X-Forwarded-Host / X-Forwarded-Proto via RequestURL), with no path, query, +// or fragment. +func RequestBaseURL(r *http.Request) string { + if captured := BaseURLStringFromContext(r.Context()); captured != "" { + return captured + } + u := RequestURL(r) + return (&url.URL{Scheme: u.Scheme, Host: u.Host}).String() +} + +// SendFlowCompletedAsRedirectOrJSON should be used when a login, registration, ... flow has been completed successfully. +// It will redirect the user to the provided URL if the request accepts HTML, or return a JSON response if the request is +// an SPA request +func SendFlowCompletedAsRedirectOrJSON( + w http.ResponseWriter, r *http.Request, writer herodot.Writer, out interface{}, redirectTo string, +) { + sendFlowAsRedirectOrJSON(w, r, writer, out, redirectTo, http.StatusOK) +} + +// SendFlowErrorAsRedirectOrJSON should be used when a login, registration, ... flow has errors (e.g. validation errors +// or missing data) and should be redirected to the provided URL if the request accepts HTML, or return a JSON response +// if the request is an SPA request. +func SendFlowErrorAsRedirectOrJSON( + w http.ResponseWriter, r *http.Request, writer herodot.Writer, out interface{}, redirectTo string, +) { + sendFlowAsRedirectOrJSON(w, r, writer, out, redirectTo, http.StatusBadRequest) +} + +func sendFlowAsRedirectOrJSON( + w http.ResponseWriter, r *http.Request, writer herodot.Writer, out interface{}, redirectTo string, jsonResponseCode int, +) { + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "application/json", + }, "text/html") { + case "application/json": + if err, ok := out.(error); ok { + writer.WriteError(w, r, err) + return + } + + writer.WriteCode(w, r, jsonResponseCode, out) + case "text/html": + fallthrough + default: + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } +} + +func AcceptsJSON(r *http.Request) bool { + return httputil.NegotiateContentType(r, []string{ + "text/html", + "application/json", + }, "text/html") == "application/json" +} diff --git a/x/http_test.go b/x/http_test.go new file mode 100644 index 0000000..10f5b3b --- /dev/null +++ b/x/http_test.go @@ -0,0 +1,135 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "context" + "crypto/tls" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/x/assertx" + "github.com/ory/x/logrusx" + + "github.com/stretchr/testify/assert" + + "github.com/ory/x/urlx" +) + +func TestWithBaseURL(t *testing.T) { + ctx := WithBaseURL(context.Background(), urlx.ParseOrPanic("https://www.ory.com/")) + assert.EqualValues(t, "https://www.ory.com/", BaseURLFromContext(ctx).String()) + assert.Nil(t, BaseURLFromContext(context.Background())) + + t.Run("preserves scheme", func(t *testing.T) { + for _, raw := range []string{"http://localhost:4000", "https://example.com/"} { + ctx := WithBaseURL(context.Background(), urlx.ParseOrPanic(raw)) + assert.EqualValuesf(t, raw, BaseURLFromContext(ctx).String(), "for input %q", raw) + } + }) + + t.Run("BaseURLStringFromContext", func(t *testing.T) { + assert.Equal(t, "", BaseURLStringFromContext(context.Background())) + assert.Equal(t, "https://www.ory.com/", + BaseURLStringFromContext(WithBaseURL(context.Background(), urlx.ParseOrPanic("https://www.ory.com/")))) + }) +} + +func TestRequestURL(t *testing.T) { + assert.EqualValues(t, RequestURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", TLS: &tls.ConnectionState{}, + }).String(), "https://foobar/foo") + assert.EqualValues(t, RequestURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", + }).String(), "http://foobar/foo") + assert.EqualValues(t, RequestURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", Header: http.Header{"X-Forwarded-Host": []string{"notfoobar"}, "X-Forwarded-Proto": {"https"}}, + }).String(), "https://notfoobar/foo") +} + +func TestRequestBaseURL(t *testing.T) { + t.Run("falls back to request scheme://host with no path", func(t *testing.T) { + assert.Equal(t, "https://foobar", RequestBaseURL(&http.Request{ + URL: urlx.ParseOrPanic("/self-service/login?foo=bar"), Host: "foobar", TLS: &tls.ConnectionState{}, + })) + assert.Equal(t, "http://foobar", RequestBaseURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", + })) + assert.Equal(t, "https://notfoobar", RequestBaseURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", + Header: http.Header{"X-Forwarded-Host": []string{"notfoobar"}, "X-Forwarded-Proto": {"https"}}, + })) + }) + + t.Run("context-captured customer base URL wins, scheme preserved", func(t *testing.T) { + // A proxy-aware middleware (e.g. the cloud courier middleware) validated + // an Ory-Base-URL-Rewrite / X-Ory-Original-Host header and stashed the + // real customer-facing base URL on the context. The OIDC/SAML state must + // capture *that*, not the oryapis host this service was reached at. + for _, captured := range []string{"http://localhost:4000", "https://login.customer.example.com"} { + req := (&http.Request{URL: urlx.ParseOrPanic("/self-service/login"), Host: "slug.projects.oryapis.com", TLS: &tls.ConnectionState{}}). + WithContext(WithBaseURL(context.Background(), urlx.ParseOrPanic(captured))) + assert.Equalf(t, captured, RequestBaseURL(req), "captured %q must win over the oryapis host", captured) + } + }) +} + +func TestAcceptToRedirectOrJSON(t *testing.T) { + wr := herodot.NewJSONWriter(logrusx.New("", "")) + + t.Run("case=browser", func(t *testing.T) { + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept", "text/html") + + t.Run("regular payload", func(t *testing.T) { + w := httptest.NewRecorder() + SendFlowCompletedAsRedirectOrJSON(w, r, wr, json.RawMessage(`{"foo":"bar"}`), "https://www.ory.com/redir") + loc, err := w.Result().Location() + require.NoError(t, err) + assert.Equal(t, "https://www.ory.com/redir", loc.String()) + }) + + t.Run("error payload", func(t *testing.T) { + w := httptest.NewRecorder() + SendFlowCompletedAsRedirectOrJSON(w, r, wr, errors.New("foo"), "https://www.ory.com/redir") + loc, err := w.Result().Location() + require.NoError(t, err) + assert.Equal(t, "https://www.ory.com/redir", loc.String()) + }) + }) + + t.Run("case=json", func(t *testing.T) { + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept", "application/json") + + t.Run("regular payload", func(t *testing.T) { + msg := json.RawMessage(`{"foo":"bar"}`) + w := httptest.NewRecorder() + SendFlowCompletedAsRedirectOrJSON(w, r, wr, msg, "https://www.ory.com/redir") + _, err := w.Result().Location() + require.ErrorIs(t, err, http.ErrNoLocation) + + body := MustReadAll(w.Result().Body) + assertx.EqualAsJSON(t, msg, json.RawMessage(body)) + }) + + t.Run("error payload", func(t *testing.T) { + ee := errors.WithStack(herodot.ErrBadRequest()) + w := httptest.NewRecorder() + SendFlowCompletedAsRedirectOrJSON(w, r, wr, ee, "https://www.ory.com/redir") + _, err := w.Result().Location() + require.ErrorIs(t, err, http.ErrNoLocation) + + body := MustReadAll(w.Result().Body) + assertx.EqualAsJSON(t, map[string]interface{}{"error": map[string]interface{}{"code": 400, "message": "The request was malformed or contained invalid parameters", "status": "Bad Request"}}, json.RawMessage(body)) + assert.EqualValues(t, http.StatusBadRequest, w.Result().StatusCode) + }) + }) +} diff --git a/x/httploadermiddleware.go b/x/httploadermiddleware.go new file mode 100644 index 0000000..ce3a908 --- /dev/null +++ b/x/httploadermiddleware.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "context" + "net/http" + + "github.com/urfave/negroni" + "golang.org/x/oauth2" + + "github.com/ory/jsonschema/v3/httploader" + "github.com/ory/x/httpx" +) + +func HTTPLoaderContextMiddleware(reg httpx.ClientProvider) negroni.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + hc := reg.HTTPClient(r.Context()) + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, hc) + ctx = context.WithValue(ctx, httploader.ContextKey, hc) + next(rw, r.WithContext(ctx)) + } +} diff --git a/x/ider.go b/x/ider.go new file mode 100644 index 0000000..2f19fdf --- /dev/null +++ b/x/ider.go @@ -0,0 +1,8 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +type Ider interface { + RequestID() string +} diff --git a/x/isjsonrequest.go b/x/isjsonrequest.go new file mode 100644 index 0000000..4b8f797 --- /dev/null +++ b/x/isjsonrequest.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/http" + + "github.com/golang/gddo/httputil" +) + +var offers = []string{"text/html", "text/*", "*/*", "application/json"} +var defaultOffer = "text/html" + +func IsJSONRequest(r *http.Request) bool { + return httputil.NegotiateContentType(r, offers, defaultOffer) == "application/json" || + r.Header.Get("Content-Type") == "application/json" +} + +func IsBrowserRequest(r *http.Request) bool { + return httputil.NegotiateContentType(r, offers, defaultOffer) == "text/html" +} diff --git a/x/isjsonrequest_test.go b/x/isjsonrequest_test.go new file mode 100644 index 0000000..9edcf2c --- /dev/null +++ b/x/isjsonrequest_test.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "fmt" + "net/http" + "testing" + + "github.com/golang/gddo/httputil" + "github.com/stretchr/testify/assert" +) + +func TestIsBrowserOrAPIRequest(t *testing.T) { + for k, tc := range []struct { + ua string + h string + e bool + }{ + {ua: "firefox-66", h: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", e: true}, + {ua: "safari-chrome", h: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", e: true}, + {ua: "ie8", h: "image/jpeg,application/x-ms-application,image/gif,application/xaml+xml,image/pjpeg,application/x-ms-xbap,application/x-shockwave-flash,application/msword,*/*", e: true}, + {ua: "ie8-any", h: "*/*", e: true}, + {ua: "edge", h: "text/html,application/xhtml+xml,image/jxr,*/*", e: true}, + {ua: "opera", h: "text/html,application/xml;q=0.9,application/xhtml+xml,image/png,image/webp,image/jpeg,image/gif,image/x-xbitmap,*/*;q=0.1", e: true}, + {ua: "json-api", h: "application/json", e: false}, + {ua: "no-accept", h: "", e: true}, + } { + t.Run(fmt.Sprintf("case=%d/ua=%s", k, tc.ua), func(t *testing.T) { + r := &http.Request{Header: map[string][]string{"Accept": {tc.h}}} + t.Logf("isBrowser: %s", httputil.NegotiateContentType(r, offers, defaultOffer)) + + t.Logf("isJSON: %s", httputil.NegotiateContentType(r, + []string{"application/json"}, + "text/html", + )) + + assert.Equal(t, tc.e, IsBrowserRequest(r)) + assert.Equal(t, !tc.e, IsJSONRequest(r)) + }) + } +} diff --git a/x/json_bool.go b/x/json_bool.go new file mode 100644 index 0000000..41db3bf --- /dev/null +++ b/x/json_bool.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "fmt" +) + +// ConvertibleBoolean can unmarshal both booleans and strings. +type ConvertibleBoolean bool + +func (bit *ConvertibleBoolean) UnmarshalJSON(data []byte) error { + asString := string(data) + switch asString { + case "true", `"true"`: + *bit = true + case "false", `"false"`: + *bit = false + default: + return fmt.Errorf("boolean unmarshal error: invalid input %s", asString) + } + return nil +} diff --git a/x/json_bool_test.go b/x/json_bool_test.go new file mode 100644 index 0000000..b43be04 --- /dev/null +++ b/x/json_bool_test.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type ConvertibleBooleanTest struct { + Verified ConvertibleBoolean `json:"verified,omitempty"` +} + +func TestUnmarshalBool(t *testing.T) { + data := `{"verified":true}` + c := ConvertibleBooleanTest{} + err := json.Unmarshal([]byte(data), &c) + require.NoError(t, err) + assert.Equal(t, ConvertibleBoolean(true), c.Verified) + + data = `{"verified":false}` + err = json.Unmarshal([]byte(data), &c) + require.NoError(t, err) + assert.Equal(t, ConvertibleBoolean(false), c.Verified) +} + +func TestUnmarshalString(t *testing.T) { + data := `{"verified":"true"}` + c := ConvertibleBooleanTest{} + err := json.Unmarshal([]byte(data), &c) + require.NoError(t, err) + assert.Equal(t, ConvertibleBoolean(true), c.Verified) + + data = `{"verified":"false"}` + err = json.Unmarshal([]byte(data), &c) + require.NoError(t, err) + assert.Equal(t, ConvertibleBoolean(false), c.Verified) +} + +func TestUnmarshalError(t *testing.T) { + data := `{"verified":1}` + c := ConvertibleBooleanTest{} + err := json.Unmarshal([]byte(data), &c) + assert.Error(t, err) +} diff --git a/x/json_marshal.go b/x/json_marshal.go new file mode 100644 index 0000000..65f2ef0 --- /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 0000000..4484699 --- /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/json_number.go b/x/json_number.go new file mode 100644 index 0000000..a440bfb --- /dev/null +++ b/x/json_number.go @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +// IsValidNumber reports whether s is a valid JSON number literal. +// +// Taken from encoding/json +func IsValidNumber(s string) bool { + // This function implements the JSON numbers grammar. + // See https://tools.ietf.org/html/rfc7159#section-6 + // and https://json.org/number.gif + + if s == "" { + return false + } + + // Optional - + if s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + + // Digits + switch { + default: + return false + + case s[0] == '0': + s = s[1:] + + case '1' <= s[0] && s[0] <= '9': + s = s[1:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // . followed by 1 or more digits. + if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { + s = s[2:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // e or E followed by an optional - or + and + // 1 or more digits. + if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { + s = s[1:] + if s[0] == '+' || s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // Make sure we are at the end. + return s == "" +} diff --git a/x/map_json.go b/x/map_json.go new file mode 100644 index 0000000..5420c4a --- /dev/null +++ b/x/map_json.go @@ -0,0 +1,79 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "bytes" + "encoding/json" + "regexp" + "strconv" + "strings" +) + +var ( + regexMatchInt = regexp.MustCompile(`^[0-9]+$`) + regexMatchFloat = regexp.MustCompile(`^[0-9]+\.[0-9]+$`) + regexMatchBool = regexp.MustCompile(`^(?i)false|true|on$`) +) + +func TypeMap(m map[string]string) (map[string]interface{}, error) { + jm := make(map[string]interface{}) + for k, v := range m { + if regexMatchInt.MatchString(v) { + vv, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return nil, err + } + jm[k] = vv + } else if regexMatchFloat.MatchString(v) { + vv, err := strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + jm[k] = vv + } else if regexMatchBool.MatchString(v) { + // Checkboxes have default values of `on` when checked, so set this to true. + if strings.ToLower(v) == "on" { + v = "true" + } + vv, err := strconv.ParseBool(strings.ToLower(v)) + if err != nil { + return nil, err + } + jm[k] = vv + } else { + jm[k] = v + } + } + + return jm, nil +} + +func UntypedMapToJSON(m map[string]string) (json.RawMessage, error) { + jm, err := TypeMap(m) + if err != nil { + return nil, err + } + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(jm); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func StructToMap(s interface{}) (map[string]interface{}, error) { + b, err := json.Marshal(s) + if err != nil { + return nil, err + } + + mapData := make(map[string]interface{}) + err = json.Unmarshal(b, &mapData) + if err != nil { + return nil, err + } + return mapData, nil +} diff --git a/x/map_json_test.go b/x/map_json_test.go new file mode 100644 index 0000000..61cd87e --- /dev/null +++ b/x/map_json_test.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStructToMap(t *testing.T) { + m := json.RawMessage(`{"string": "123"}`) + r, err := StructToMap(struct { + TestString string `json:"string"` + TestRaw *json.RawMessage `json:"raw"` + }{ + TestString: "string", + TestRaw: &m, + }) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "string": "string", + "raw": map[string]interface{}{ + "string": "123", + }, + }, r) +} + +func TestTypeMap(t *testing.T) { + r, err := TypeMap(map[string]string{ + "string": "string", + "int": "123", + "float": "123.123", + "bool": "TrUe", + "bool_on": "oN", + }) + require.NoError(t, err) + + assert.Equal(t, map[string]interface{}{ + "string": "string", + "int": int64(123), + "float": 123.123, + "bool": true, + "bool_on": true, + }, r) + + _, err = TypeMap(map[string]string{ + "int": "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + }) + assert.Error(t, err) + + _, err = TypeMap(map[string]string{ + "float": "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9", + }) + assert.Error(t, err) +} + +func TestUntypedMapToJSON(t *testing.T) { + r, err := UntypedMapToJSON(map[string]string{ + "string": "string", + "int": "123", + "float": "123.123", + "bool": "TrUe", + "bool_on": "oN", + }) + require.NoError(t, err) + assert.JSONEq(t, `{"string":"string","int":123,"float":123.123,"bool":true,"bool_on":true}`, string(r)) + + _, err = UntypedMapToJSON(map[string]string{ + "int": "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + }) + assert.Error(t, err) +} diff --git a/x/maxitems.go b/x/maxitems.go new file mode 100644 index 0000000..2c526a6 --- /dev/null +++ b/x/maxitems.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +// MaxItemsPerPage is used to prevent DoS attacks against large lists by limiting the items per page to 500. +func MaxItemsPerPage(is int) int { + if is > 500 { + return 500 + } + return is +} diff --git a/x/nocache.go b/x/nocache.go new file mode 100644 index 0000000..36fa864 --- /dev/null +++ b/x/nocache.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/http" +) + +// NoCache adds `Cache-Control: private, no-cache, no-store, must-revalidate` to the response header. +func NoCache(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate") +} + +// NoCacheHandlerFunc wraps http.HandlerFunc with `Cache-Control: private, no-cache, no-store, must-revalidate` headers. +func NoCacheHandlerFunc(handle http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + NoCache(w) + handle(w, r) + } +} + +// NoCacheHandler wraps http.HandlerFunc with `Cache-Control: private, no-cache, no-store, must-revalidate` headers. +func NoCacheHandler(handle http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NoCache(w) + handle.ServeHTTP(w, r) + }) +} diff --git a/x/normalize.go b/x/normalize.go new file mode 100644 index 0000000..7ab8273 --- /dev/null +++ b/x/normalize.go @@ -0,0 +1,167 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/mail" + "strings" + "unicode" + + "github.com/nyaruka/phonenumbers" + "github.com/pkg/errors" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "golang.org/x/text/runes" + "golang.org/x/text/secure/precis" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" + "golang.org/x/text/unicode/rangetable" +) + +// disallowedIdentifierRunes is the explicit set of rune categories the +// identifier profiles reject in addition to the FreeformClass default +// disposition. +var disallowedIdentifierRunes = rangetable.Merge( + unicode.Bidi_Control, // Directional overrides such as RLO/LRO that can spoof display order. + unicode.Other_Default_Ignorable_Code_Point, // Invisible code points like the zero-width joiner that browsers render as nothing. + unicode.Other, // Control, format, surrogate, private-use, and unassigned code points. + unicode.Noncharacter_Code_Point, // Permanently reserved code points (e.g. U+FFFE) that never represent a character. +) + +// emailProfile builds on PRECIS FreeformClass — it permits the punctuation +// and spaces that emails need (`@`, `.`, `+`, `-`, `_`) while disallowing +// every Default_Ignorable_Code_Point, bidi control, and pictographic symbol. +// +// `precis.BidiRule` (RFC 5893) is enforced in strict mode so that callers +// like `NormalizeIdentifier("email", …)` reject mixed LTR+RTL inputs at +// registration time. +var emailProfile = precis.NewFreeform( + precis.FoldWidth, + precis.LowerCase(), + precis.DisallowEmpty, + precis.BidiRule, + precis.Norm(norm.NFKC), + precis.Disallow(runes.In(disallowedIdentifierRunes)), +) + +// phoneProfile is FreeformClass plus width-fold only. Phone numbers reach +// phonenumbers.Parse which performs its own canonicalization to E.164, so +// the precis step here only width-folds fullwidth digits and removes any +// disallowed runes. +var phoneProfile = precis.NewFreeform(precis.FoldWidth) + +func applyPRECIS(profile *precis.Profile, value string, graceful bool) (string, error) { + value = strings.TrimSpace(value) + out, err := profile.String(value) + if err == nil { + return out, nil + } + // Strict mode reports any PRECIS error (disallowed rune or, for the + // email profile, an RFC 5893 bidi-rule violation) to the caller. + if !graceful { + return "", err + } + + // Graceful mode: strip the runes the profile forbids, then re-run the + // normalization pipeline on the cleaned value so casing, width folding, + // and NFKC still apply. Skipping the re-run would leave an uppercase or + // fullwidth poisoned variant (e.g. "ALICE​smith") distinct from the + // clean baseline ("alicesmith") and reopen the duplicate-account bypass. + // The bidi-less companion profile is used so mixed LTR+RTL input also + // normalizes instead of failing the bidi rule a second time. + chain := transform.Chain( + norm.NFKC, + runes.Remove(runes.Predicate(func(r rune) bool { return !profile.Allowed().Contains(r) })), + cases.Lower(language.Und), + ) + stripped, _, err := transform.String(chain, value) + if err != nil { + return value, nil + } + return stripped, nil +} + +// canonicalizeIdentifier gracefully applies the email profile to the string, +// stripping non-printable characters and emojis. +func canonicalizeIdentifier(value string) string { + v, _ := applyPRECIS(emailProfile, value, true) + return v +} + +// NormalizeEmailIdentifier normalizes an email address. +func NormalizeEmailIdentifier(value string) string { + return canonicalizeIdentifier(value) +} + +// NormalizePhoneIdentifier normalizes a phone number. +func NormalizePhoneIdentifier(value string) string { + value, _ = applyPRECIS(phoneProfile, value, true) + 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 canonicalizeIdentifier(value) +} + +// GracefulNormalization normalizes an identifier based on the format. +// +// Supported formats are: +// +// - email +// - phone +// - username +func GracefulNormalization(value string) string { + value = canonicalizeIdentifier(value) + if number, err := phonenumbers.Parse(value, ""); err == nil && phonenumbers.IsValidNumber(number) { + return NormalizePhoneIdentifier(value) + } 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": + value, err := applyPRECIS(emailProfile, value, false) + if err != nil { + return "", err + } + email, err := mail.ParseAddress(value) + if err != nil { + return "", err + } + return email.Address, nil + + case "sms": + value, err := applyPRECIS(phoneProfile, value, false) + if err != nil { + return "", err + } + 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 applyPRECIS(emailProfile, value, true) + } +} diff --git a/x/normalize_test.go b/x/normalize_test.go new file mode 100644 index 0000000..7c590ef --- /dev/null +++ b/x/normalize_test.go @@ -0,0 +1,317 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +// defaultIgnorableBypassRunes is the set of code points called out in the +// 2026-05-23 security report as surviving the Cf-only strip. Each rune has +// the Default_Ignorable_Code_Point property but lives in a category outside +// Cf (Lo, Mn, or Cn), so the predicate from PR #11885 misses them and the +// duplicate-account bypass still works. +var defaultIgnorableBypassRunes = []struct { + name string + r rune +}{ + {"Hangul Filler U+3164 (Lo)", 'ㅤ'}, + {"Hangul Choseong Filler U+115F (Lo)", 'ᅟ'}, + {"Hangul Jungseong Filler U+1160 (Lo)", 'ᅠ'}, + {"Khmer Vowel Inherent AQ U+17B4 (Mn)", '឴'}, + {"Khmer Vowel Inherent AA U+17B5 (Mn)", '឵'}, + {"Combining Grapheme Joiner U+034F (Mn)", '͏'}, + {"Reserved U+2065 (Cn, in DI_CP set)", '⁥'}, + {"Variation Selector-16 U+FE0F (Mn)", '️'}, + {"Mongolian Free Variation Selector One U+180B (Mn)", '᠋'}, + {"Variation Selector-17 U+E0100 (Mn)", '\U000E0100'}, +} + +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"}, + + // Invisible Unicode characters (category Cf) must not produce a + // distinct identifier — otherwise an attacker can register a second + // account with a visually identical email and bypass the duplicate- + // account check. + {"alice\u200B@example.com", "alice@example.com"}, // zero-width space + {"alice\u200C@example.com", "alice@example.com"}, // zero-width non-joiner + {"alice\u200D@example.com", "alice@example.com"}, // zero-width joiner + {"Alice\u00AD@example.com", "alice@example.com"}, // soft hyphen + {"alice\uFEFF@example.com", "alice@example.com"}, // zero-width no-break space (BOM) + {"alice\u2060@example.com", "alice@example.com"}, // word joiner + {"alice\u180E@example.com", "alice@example.com"}, // Mongolian vowel separator + {"\u202Aalice@example.com\u202C", "alice@example.com"}, // bidi LRE/PDF + {"al\u200Bice\u200C@ex\u200Dample.com", "alice@example.com"}, + + // NFKC compatibility decomposition collapses fullwidth and ligature + // lookalikes so they hit the same identifier as the ASCII canonical + // form. + {"alice@example.com", "alice@example.com"}, // fullwidth "alice" + {"office@example.com", "office@example.com"}, // ligature ffi + } + + for _, test := range tests { + assert.Equal(t, test.expected, NormalizeEmailIdentifier(test.input), "input=%q", test.input) + } +} + +func TestNormalizePhoneIdentifier(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"+1 650-253-0000", "+16502530000"}, + {"+1 (650) 253-0000", "+16502530000"}, + {"+44 20 7946 0958", "+442079460958"}, + {"+49 176 671 11 638", "+4917667111638"}, + {"+600136866982", "+60136866982"}, + {"+256 730 691 099", "+256730691099"}, + {"+256730691099", "+256730691099"}, + {"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"}, + {" ", ""}, + {" UsERNaME ", "username"}, + + // Invisible characters must not allow a duplicate username either. + {"al\u200Bice", "alice"}, // U+200B + {"al\u200Cice", "alice"}, // U+200C + {"al\u200Dice", "alice"}, // U+200D + {"al\u00ADice", "alice"}, // U+00AD soft hyphen + {"al\uFEFFice", "alice"}, // U+FEFF BOM + {"al\u2060ice", "alice"}, // U+2060 word joiner + {"al\u180Eice", "alice"}, // U+180E + {"alice", "alice"}, // fullwidth Latin (NFKC) + } + + for _, test := range tests { + assert.Equal(t, test.expected, NormalizeOtherIdentifier(test.input), "input=%q", 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)) + } +} + +// TestCanonicalizeIdentifier_Concurrent exercises the canonicalization +// path from many goroutines so the race detector catches any shared +// mutable state in the underlying transformer. A package-level +// transform.Chain would race on its internal link buffers; a per-call +// construction must not. +func TestCanonicalizeIdentifier_Concurrent(t *testing.T) { + t.Parallel() + + inputs := []struct { + input, want string + }{ + {"alice\u200B@example.com", "alice@example.com"}, + {"al\u200Cice\u200D@ex\u00ADample.com", "alice@example.com"}, + {"alice@example.com", "alice@example.com"}, + {"office@example.com", "office@example.com"}, + {"al\uFEFFice\u2060@example.com", "alice@example.com"}, + } + + const goroutines = 64 + const iterations = 200 + + var wg sync.WaitGroup + for range goroutines { + wg.Go(func() { + for range iterations { + for _, tc := range inputs { + got := NormalizeEmailIdentifier(tc.input) + if got != tc.want { + assert.Equal(t, tc.want, got, "input=%q", tc.input) + return + } + } + } + }) + } + wg.Wait() +} + +// TestNormalize_DefaultIgnorableBypass asserts that every Default_Ignorable_Code_Point +// rune called out in the 2026-05-23 security report is stripped — not just +// the Format (Cf) subset that PR #11885 covered. Each rune is injected mid- +// identifier; the canonical form must equal the rune-free baseline so that +// the UNIQUE constraint on identity_credential_identifiers catches the +// duplicate registration. +func TestNormalize_DefaultIgnorableBypass(t *testing.T) { + for _, tc := range defaultIgnorableBypassRunes { + t.Run(tc.name, func(t *testing.T) { + // Username path. + gotUser := NormalizeOtherIdentifier("alic" + string(tc.r) + "e") + assert.Equal(t, "alice", gotUser, + "username with %s must canonicalize to baseline", tc.name) + + // Email path. + gotEmail := NormalizeEmailIdentifier("alic" + string(tc.r) + "e@example.com") + assert.Equal(t, "alice@example.com", gotEmail, + "email with %s must canonicalize to baseline", tc.name) + + // Phone path: the rune must be removed so phonenumbers.Parse + // produces the canonical E.164 form. + gotPhone := NormalizePhoneIdentifier("+1 650-253" + string(tc.r) + "-0000") + assert.Equal(t, "+16502530000", gotPhone, + "phone with %s must canonicalize to baseline", tc.name) + + // GracefulNormalization must agree with the format-specific funcs. + assert.Equal(t, "alice", GracefulNormalization("alic"+string(tc.r)+"e")) + assert.Equal(t, "alice@example.com", + GracefulNormalization("alic"+string(tc.r)+"e@example.com")) + assert.Equal(t, "+16502530000", + GracefulNormalization("+1 650-253"+string(tc.r)+"-0000")) + }) + } +} + +// TestNormalize_PoisonedRuneStillFolds guards against a duplicate-account +// bypass in the graceful fallback: when an identifier carries a disallowed +// rune AND uppercase or fullwidth characters, stripping the rune is not +// enough — the result must still be lowercased, width-folded, and +// NFKC-composed so it collapses onto the clean baseline. Otherwise an +// attacker registers `ALICEsmith`, it stores as `ALICEsmith`, and the +// UNIQUE constraint never sees it as a duplicate of `alicesmith`. +func TestNormalize_PoisonedRuneStillFolds(t *testing.T) { + zwsp := string(rune(0x200B)) + tests := []struct { + name string + input string + baseline string + }{ + {"uppercase + zero-width space", "ALICE" + zwsp + "SMITH", "alicesmith"}, + {"fullwidth + zero-width space", "ALICE" + zwsp + "smith", "alicesmith"}, + {"uppercase + soft hyphen", "FOO" + string(rune(0x00AD)) + "BAR", "foobar"}, + {"fullwidth digits in email + ZWSP", "AL" + zwsp + "ICE@example.com", "alice@example.com"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.baseline, NormalizeOtherIdentifier(tc.input)) + assert.Equal(t, NormalizeOtherIdentifier(tc.baseline), NormalizeOtherIdentifier(tc.input)) + }) + } +} + +// TestNormalize_EmojiPreserved asserts that emoji and other pictographic +// symbols survive normalization rather than being stripped. They are only +// lowercased and NFKC-composed, so an identifier carrying an emoji stays +// distinct from the emoji-free baseline. +func TestNormalize_EmojiPreserved(t *testing.T) { + t.Run("emoji is preserved", func(t *testing.T) { + assert.Equal(t, "alice🎉", NormalizeOtherIdentifier("alice🎉")) + }) + + t.Run("emoji identifier is distinct from baseline", func(t *testing.T) { + assert.NotEqual(t, + NormalizeOtherIdentifier("alice"), + NormalizeOtherIdentifier("alice🎉"), + ) + }) +} + +// TestNormalize_RTLScripts asserts that identifiers containing RTL +// script letters (Hebrew, Arabic) — whether pure-RTL or mixed with LTR — +// canonicalize as distinct identifiers. The profile intentionally omits +// the RFC 5893 bidi rule so that international users can register email +// addresses and usernames in their native script. +func TestNormalize_RTLScripts(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"pure Hebrew username", "דוד", "דוד"}, + {"pure Arabic username", "محمد", "محمد"}, + {"pure Hebrew email", "דוד@example.com", "דוד@example.com"}, + {"pure Arabic email", "محمد@example.com", "محمد@example.com"}, + {"mixed LTR+Hebrew username", "aliceא", "aliceא"}, + {"mixed LTR+Hebrew email", "alice.דוד@example.com", "alice.דוד@example.com"}, + {"uppercase mixed lowercases", "ALICEא", "aliceא"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, NormalizeOtherIdentifier(tc.input)) + }) + } + + t.Run("mixed-script identifier is distinct from LTR-only baseline", func(t *testing.T) { + // `aliceא` and `alice` must NOT collide — Hebrew alef is not a + // homograph for any Latin letter, so they are different users. + assert.NotEqual(t, + NormalizeOtherIdentifier("alice"), + NormalizeOtherIdentifier("aliceא"), + ) + }) + + t.Run("bidi control characters are still stripped", func(t *testing.T) { + // Even without `precis.BidiRule`, explicit bidi-control + // characters (LRE, PDF, …) are disallowed at the rune level so + // they cannot be used to spoof the rendered identifier. The + // controls are built from code points so the test source itself + // stays free of bidirectional control characters (gosec G116). + lre, pdf := string(rune(0x202A)), string(rune(0x202C)) + assert.Equal(t, "alice@example.com", + NormalizeEmailIdentifier(lre+"alice@example.com"+pdf)) + }) +} + +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}, + {"+256 730 691 099", "sms", "+256730691099", 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/nosurfx/nosurf.go b/x/nosurfx/nosurf.go new file mode 100644 index 0000000..2d5760b --- /dev/null +++ b/x/nosurfx/nosurf.go @@ -0,0 +1,312 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package nosurfx + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + "github.com/ory/kratos/text" + + "github.com/ory/kratos/driver/config" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/nosurf" + "github.com/ory/x/randx" +) + +func ErrInvalidCSRFToken() *herodot.DefaultError { + return herodot.ErrForbidden(). + WithID(text.ErrIDCSRF). + WithError("the request was rejected to protect you from Cross-Site-Request-Forgery"). + WithDetail("docs", "https://www.ory.com/docs/troubleshooting/csrf"). + WithReason("Please retry the flow and optionally clear your cookies. The request was rejected to protect you from Cross-Site-Request-Forgery (CSRF) which could cause account takeover, leaking personal information, and other serious security issues.") +} + +func ErrGone() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusGone, + StatusField: http.StatusText(http.StatusGone), + ErrorField: "The requested resource is no longer available because it has expired or is otherwise invalid.", + } +} + +const ( + noCookie = "The HTTP Cookie Header is empty or not set." + cookieMissing = "The HTTP Cookie Header was set but did not include the anti-CSRF cookie." + tokenNotSent = "The anti-CSRF cookie was found but the CSRF token was not included in the HTTP request body (" + nosurf.CookieName + ") nor in the HTTP Header (" + nosurf.HeaderName + ")." + tokenMismatch = "The HTTP Cookie Header was set and a CSRF token was sent but they do not match. We recommend deleting all cookies for this domain and retrying the flow." +) + +func ErrInvalidCSRFTokenAJAX() *herodot.DefaultError { + return ErrInvalidCSRFToken(). + WithDetail("hint", "We detected an AJAX call, please ensure that CORS is enabled and configured correctly, and that your AJAX code sends cookies and has credentials enabled. For further debugging, check your Browser's Network Tab to see what cookies are included or excluded.") +} + +func ErrInvalidCSRFTokenAJAXNoCookies() *herodot.DefaultError { + return ErrInvalidCSRFTokenAJAX().WithDetail("reject_reason", noCookie) +} + +func ErrInvalidCSRFTokenAJAXCookieMissing() *herodot.DefaultError { + return ErrInvalidCSRFTokenAJAX().WithDetail("reject_reason", cookieMissing) +} + +func ErrInvalidCSRFTokenAJAXTokenNotSent() *herodot.DefaultError { + return ErrInvalidCSRFToken().WithDetail("hint", tokenNotSent) +} + +func ErrInvalidCSRFTokenAJAXTokenMismatch() *herodot.DefaultError { + return ErrInvalidCSRFTokenAJAX().WithDetail("reject_reason", tokenMismatch) +} + +func ErrInvalidCSRFTokenServer() *herodot.DefaultError { + return ErrInvalidCSRFToken(). + WithDetail("hint", "We detected a regular browser or server-side call. To debug browser calls check your Browser's Network Tab to see what cookies are included or excluded. If you are calling from a server ensure that the appropriate cookies are being forwarded and that the SDK method is called correctly.") +} + +func ErrInvalidCSRFTokenServerNoCookies() *herodot.DefaultError { + return ErrInvalidCSRFTokenServer().WithDetail("reject_reason", noCookie) +} + +func ErrInvalidCSRFTokenServerCookieMissing() *herodot.DefaultError { + return ErrInvalidCSRFTokenServer().WithDetail("reject_reason", cookieMissing) +} + +func ErrInvalidCSRFTokenServerTokenNotSent() *herodot.DefaultError { + return ErrInvalidCSRFToken().WithDetail("hint", tokenNotSent) +} + +func ErrInvalidCSRFTokenServerTokenMismatch() *herodot.DefaultError { + return ErrInvalidCSRFTokenServer().WithDetail("reject_reason", tokenMismatch) +} + +type CSRFTokenGeneratorProvider interface { + GenerateCSRFToken(r *http.Request) string +} + +type CSRFToken func(r *http.Request) string + +const CSRFTokenName = "csrf_token" + +func DefaultCSRFToken(r *http.Request) string { + return nosurf.Token(r) +} + +var FakeCSRFToken = base64.StdEncoding.EncodeToString([]byte(randx.MustString(32, randx.AlphaLowerNum))) + +func FakeCSRFTokenGenerator(r *http.Request) string { + return FakeCSRFToken +} + +func FakeCSRFTokenGeneratorWithToken(token string) func(r *http.Request) string { + return func(r *http.Request) string { + return token + } +} + +var _ nosurf.Handler = new(FakeCSRFHandler) + +type FakeCSRFHandler struct { + cookieNameFn func(r *http.Request) string +} + +func NewFakeCSRFHandler(reg interface { + config.Provider +}, +) *FakeCSRFHandler { + return &FakeCSRFHandler{ + cookieNameFn: func(r *http.Request) string { + return CSRFCookieName(r.Context(), reg) + }, + } +} + +func (f *FakeCSRFHandler) DisablePath(s string) { +} + +func (f *FakeCSRFHandler) DisableGlob(s string) { +} + +func (f *FakeCSRFHandler) DisableGlobs(s ...string) { +} + +func (f *FakeCSRFHandler) ExemptPath(s string) { +} + +func (f *FakeCSRFHandler) IgnorePath(s string) { +} + +func (f *FakeCSRFHandler) IgnoreGlob(s string) {} + +func (f *FakeCSRFHandler) IgnoreGlobs(s ...string) {} + +func (f *FakeCSRFHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {} + +func (f *FakeCSRFHandler) RegenerateToken(_ http.ResponseWriter, _ *http.Request) string { + return FakeCSRFToken +} + +func WithFakeCSRFCookie(t *testing.T, reg interface { + config.Provider +}, urlStr string, +) (*url.URL, []*http.Cookie) { + u, err := url.Parse(urlStr) + require.NoError(t, err) + + cookie := &http.Cookie{ + Name: CSRFCookieName(t.Context(), reg), + Value: FakeCSRFToken, + } + return u, []*http.Cookie{cookie} +} + +type CSRFProvider interface { + CSRFHandler() nosurf.Handler +} + +func CSRFCookieName(ctx context.Context, reg interface { + config.Provider +}, +) string { + return "csrf_token_" + fmt.Sprintf("%x", sha256.Sum256([]byte(reg.Config().SelfPublicURL(ctx).String()))) +} + +func NosurfBaseCookieHandler(reg interface { + config.Provider +}, +) func(w http.ResponseWriter, r *http.Request) http.Cookie { + return func(w http.ResponseWriter, r *http.Request) http.Cookie { + secure := reg.Config().CookieSecure(r.Context()) + + sameSite := reg.Config().CookieSameSiteMode(r.Context()) + if !secure { + sameSite = http.SameSiteLaxMode + } + + domain := "" + if d := reg.Config().CookieDomain(r.Context()); d != "" { + domain = d + } + + name := CSRFCookieName(r.Context(), reg) + cookie := http.Cookie{ + Name: name, + MaxAge: nosurf.MaxAge, + Path: reg.Config().CookiePath(r.Context()), + Domain: domain, + HttpOnly: true, + Secure: secure, + SameSite: sameSite, + } + + if alias := reg.Config().SelfPublicURL(r.Context()); reg.Config().SelfPublicURL(r.Context()).String() != alias.String() { + // If a domain alias is detected use that instead. + cookie.Domain = alias.Hostname() + cookie.Path = alias.Path + } + + return cookie + } +} + +func CSRFErrorReason(r *http.Request, reg interface { + config.Provider +}, +) error { + // Is it an AJAX request? + secFetchMode := r.Header.Get("Sec-Fetch-Mode") + isAjax := secFetchMode == "cors" || secFetchMode == "same-origin" + + if len(r.Header.Get("Cookie")) == 0 { + if isAjax { + return errors.WithStack(ErrInvalidCSRFTokenAJAXNoCookies()) + } + return errors.WithStack(ErrInvalidCSRFTokenServerNoCookies()) + } else if _, err := r.Cookie(CSRFCookieName(r.Context(), reg)); errors.Is(err, http.ErrNoCookie) { + if isAjax { + return errors.WithStack(ErrInvalidCSRFTokenAJAXCookieMissing()) + } + return errors.WithStack(ErrInvalidCSRFTokenServerCookieMissing()) + } + + // We can't parse the request body here, so we check if the content type is JSON, + // as we can't return a meaningful error in that case. + if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { + if len(r.Form.Get("csrf_token")+r.Header.Get(nosurf.HeaderName)) == 0 { + if isAjax { + return errors.WithStack(ErrInvalidCSRFTokenAJAXTokenNotSent()) + } + return errors.WithStack(ErrInvalidCSRFTokenServerTokenNotSent()) + } + } + + if isAjax { + return errors.WithStack(ErrInvalidCSRFTokenAJAXTokenMismatch()) + } + return errors.WithStack(ErrInvalidCSRFTokenServerTokenMismatch()) +} + +func CSRFFailureHandler(reg interface { + config.Provider + logrusx.Provider + httpx.WriterProvider +}, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := CSRFErrorReason(r, reg) + reg.Logger(). + WithError(err). + WithField("result", nosurf.VerifyToken(nosurf.Token(r), r.Form.Get("csrf_token"))). + WithField("expected_token", nosurf.Token(r)). + WithField("received_cookies", r.Cookies()). + WithField("received_token_form", r.Form.Get(nosurf.FormFieldName)). + WithField("received_token_body", r.PostForm.Get(nosurf.FormFieldName)). + WithField("received_token_header", r.Header.Get(nosurf.HeaderName)). + Warn("A request failed due to a missing or invalid csrf_token value") + + reg.Writer().WriteError(w, r, err) + } +} + +func NewCSRFHandler( + router http.Handler, + reg interface { + config.Provider + logrusx.Provider + httpx.WriterProvider + }, +) *nosurf.CSRFHandler { + n := nosurf.New(router) + + n.SetBaseCookieFunc(NosurfBaseCookieHandler(reg)) + n.SetFailureHandler(CSRFFailureHandler(reg)) + return n +} + +func NewTestCSRFHandler(router http.Handler, reg interface { + WithCSRFHandler(handler nosurf.Handler) + WithCSRFTokenGenerator(CSRFToken) + httpx.WriterProvider + logrusx.Provider + config.Provider +}, +) *nosurf.CSRFHandler { + n := NewCSRFHandler(router, reg) + reg.WithCSRFHandler(n) + reg.WithCSRFTokenGenerator(nosurf.Token) + return n +} diff --git a/x/nosurfx/nosurf_test.go b/x/nosurfx/nosurf_test.go new file mode 100644 index 0000000..51599d6 --- /dev/null +++ b/x/nosurfx/nosurf_test.go @@ -0,0 +1,189 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package nosurfx_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "testing" + + "github.com/ory/kratos/x/nosurfx" + + "github.com/tidwall/gjson" + + "github.com/ory/x/assertx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/nosurf" + "github.com/ory/x/randx" +) + +func TestNosurfBaseCookieHandler(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, "http://foo.com/bar")) + + cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil)) + assert.EqualValues(t, "csrf_token_01c86631efd1537ee34a98e75884a6e21dd8e2d9e944934bca21204106bfd32f", cookie.Name, "base64 representation of http://foo.com/bar") + assert.EqualValues(t, http.SameSiteLaxMode, cookie.SameSite, "is set to lax because https/secure is false - chrome rejects none samesite on non-https") + assert.EqualValues(t, nosurf.MaxAge, cookie.MaxAge) + assert.EqualValues(t, "/", cookie.Path, "cookie path is site root by default") + assert.EqualValues(t, "", cookie.Domain, "domain for the cookie is set to empty per default") + assert.False(t, cookie.Secure, "false because insecure dev mode") + assert.True(t, cookie.HttpOnly) + + alNum := regexp.MustCompile("[a-zA-Z_0-9]+") + for i := 0; i < 10; i++ { + require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, randx.MustString(16, randx.AlphaNum))) + cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil)) + + assert.NotEqual(t, "aHR0cDovL2Zvby5jb20vYmFy_csrf_token", cookie.Name, "should no longer be http://foo.com/bar") + assert.True(t, alNum.MatchString(cookie.Name), "does not have any special chars") + } + + require.NoError(t, conf.Set(ctx, config.ViperKeyCookieSameSite, "None")) + require.NoError(t, conf.Set(ctx, "dev", false)) + cookie = nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil)) + assert.EqualValues(t, http.SameSiteNoneMode, cookie.SameSite, "can be none because https/secure is true") + assert.True(t, cookie.Secure, "true because secure mode") + assert.True(t, cookie.HttpOnly) +} + +func TestNosurfBaseCookieHandlerAliasing(t *testing.T) { + ctx := context.Background() + conf, reg := pkg.NewFastRegistryWithMocks(t) + + require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, "http://foo.com/bar")) + + cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "http://foo.com/bar", nil)) + assert.EqualValues(t, "", cookie.Domain, "remains unset") + assert.EqualValues(t, "/", cookie.Path, "cookie path is site root by default") + + // Check root settings + require.NoError(t, conf.Set(ctx, config.ViperKeyCookieDomain, "bar.com")) + require.NoError(t, conf.Set(ctx, config.ViperKeyCookiePath, "/baz")) + cookie = nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "http://foo.com/bar", nil)) + assert.EqualValues(t, "bar.com", cookie.Domain, "domain doesn't change when request not from an alias but is overwritten by ViperKeyCookieDomain") + assert.EqualValues(t, "/baz", cookie.Path, "cookie path is site root by default but is overwritten by ViperKeyCookiePath") +} + +func TestNosurfBaseCookieErrorHandler(t *testing.T) { + _, reg := pkg.NewFastRegistryWithMocks(t) + + h := nosurfx.CSRFFailureHandler(reg) + expectError := func(t *testing.T, err error, req *http.Request) { + t.Helper() + rec := httptest.NewRecorder() + h(rec, req) + assertx.EqualAsJSON(t, err, json.RawMessage(gjson.Get(rec.Body.String(), "error").Raw)) + } + + newAjaxFormRequest := func() *http.Request { + req := httptest.NewRequest("GET", "https://foo/bar", nil) + req.Header.Set("Sec-Fetch-Mode", "cors") + return req + } + + newAjaxJSONRequest := func() *http.Request { + req := httptest.NewRequest("GET", "https://foo/bar", nil) + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Content-Type", "application/json") + return req + } + + newBrowserRequest := func() *http.Request { + req := httptest.NewRequest("GET", "https://foo/bar", nil) + return req + } + + t.Run("case=without cookie", func(t *testing.T) { + t.Run("source=ajax", func(t *testing.T) { + expectError(t, nosurfx.ErrInvalidCSRFTokenAJAXNoCookies(), newAjaxFormRequest()) + }) + + t.Run("source=browser", func(t *testing.T) { + expectError(t, nosurfx.ErrInvalidCSRFTokenServerNoCookies(), newBrowserRequest()) + }) + }) + + t.Run("case=ajax with cookie but without csrf cookie", func(t *testing.T) { + test := func(t *testing.T, req *http.Request, err error) { + req.Header.Set("Cookie", "foo=bar;") + expectError(t, err, req) + } + + t.Run("source=ajax", func(t *testing.T) { + test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXCookieMissing()) + }) + + t.Run("source=browser", func(t *testing.T) { + test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerCookieMissing()) + }) + }) + + t.Run("case=with correct cookie but token was not sent in header", func(t *testing.T) { + test := func(t *testing.T, req *http.Request, err error) { + req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;") + expectError(t, err, req) + } + + t.Run("source=ajax", func(t *testing.T) { + test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenNotSent()) + }) + + t.Run("source=ajax json", func(t *testing.T) { + test(t, newAjaxJSONRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch()) + }) + + t.Run("source=browser", func(t *testing.T) { + test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerTokenNotSent()) + }) + }) + + t.Run("case=ajax with correct cookie and token in header but they do not match", func(t *testing.T) { + test := func(t *testing.T, req *http.Request, err error) { + req.Header.Set(nosurf.HeaderName, "bar") + req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;") + expectError(t, err, req) + } + + t.Run("source=ajax", func(t *testing.T) { + test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch()) + }) + + t.Run("source=browser", func(t *testing.T) { + test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerTokenMismatch()) + }) + }) + + t.Run("case=correct cookie and token in body but they do not match", func(t *testing.T) { + test := func(t *testing.T, req *http.Request, err error) { + req.Header.Set("Accept", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;") + require.NoError(t, req.ParseForm()) + expectError(t, err, req) + } + + t.Run("source=ajax", func(t *testing.T) { + req := httptest.NewRequest("POST", "https://foo/bar", strings.NewReader(url.Values{nosurf.FormFieldName: {"bar"}}.Encode())) + req.Header.Set("Sec-Fetch-Mode", "cors") + test(t, req, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch()) + }) + + t.Run("source=browser", func(t *testing.T) { + req := httptest.NewRequest("POST", "https://foo/bar", strings.NewReader(url.Values{nosurf.FormFieldName: {"bar"}}.Encode())) + test(t, req, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch()) + }) + }) +} diff --git a/x/pagination.go b/x/pagination.go new file mode 100644 index 0000000..0161cec --- /dev/null +++ b/x/pagination.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/http" + "net/url" + + "github.com/ory/herodot" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/pagination/migrationpagination" + "github.com/ory/x/pagination/pagepagination" +) + +// The format we need to use in the Page tokens, as it's the only format that is understood by all DBs +const MapPaginationDateFormat = "2006-01-02 15:04:05.99999" + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func ParsePagination(r *http.Request) (page, itemsPerPage int) { + return migrationpagination.NewDefaultPaginator().ParsePagination(r) +} + +func PaginationHeader(w http.ResponseWriter, u url.URL, total int64, page, itemsPerPage int) { + migrationpagination.PaginationHeader(w, &u, total, page, itemsPerPage) +} + +type Page struct { + Page, ItemsPerPage int +} + +var PagePaginationLimit = 1000 + +func ParseKeysetOrPagePagination(r *http.Request) ([]keysetpagination.Option, *Page, error) { + q := r.URL.Query() + // If we have any new-style pagination parameters, use those and ignore the rest. + if q.Has("page_token") || q.Has("page_size") { + keyset, err := keysetpagination.Parse(q, keysetpagination.NewStringPageToken) + if err != nil { + return nil, nil, herodot.ErrBadRequest().WithReason(err.Error()) + } + return keyset, nil, nil + } + // allow fallback page pagination with upper limit + if q.Has("page") { + paginator := pagepagination.PagePaginator{MaxItems: 500, DefaultItems: 250} + page, perPage := paginator.ParsePagination(r) + if page*perPage > PagePaginationLimit { + return nil, nil, herodot.ErrBadRequest().WithReasonf("Legacy pagination is not supported for enumerating over %d items. Please switch to using page_token and page_size.", PagePaginationLimit) + } + return nil, &Page{page, perPage}, nil + } + // Allow passing per_page instead of page_size if only the former is set... + if q.Has("per_page") && !q.Has("page_size") { + q.Set("page_size", q.Get("per_page")) + q.Del("per_page") + r.URL.RawQuery = q.Encode() + } + // ... and defaul to keyset pagination + keyset, err := keysetpagination.Parse(q, keysetpagination.NewStringPageToken) + if err != nil { + return nil, nil, herodot.ErrBadRequest().WithReason(err.Error()) + } + return keyset, nil, nil +} diff --git a/x/pointer.go b/x/pointer.go new file mode 100644 index 0000000..e7360e8 --- /dev/null +++ b/x/pointer.go @@ -0,0 +1,13 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "github.com/gofrs/uuid" + +func PointToUUID(id uuid.UUID) *uuid.UUID { + if id == uuid.Nil { + return nil + } + return &id +} diff --git a/x/provider.go b/x/provider.go new file mode 100644 index 0000000..5c7eb04 --- /dev/null +++ b/x/provider.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "context" + + "github.com/gorilla/sessions" + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" +) + +type CookieProvider interface { + CookieManager(ctx context.Context) sessions.StoreExact + ContinuityCookieManager(ctx context.Context) sessions.StoreExact +} + +type BasicRegistry struct { + L *logrusx.Logger + C *retryablehttp.Client + T *otelx.Tracer +} + +func (s *BasicRegistry) Tracer(_ context.Context) *otelx.Tracer { return s.T } +func (s *BasicRegistry) Logger() *logrusx.Logger { return s.L } + +func (s *BasicRegistry) HTTPClient(_ context.Context, _ ...httpx.ResilientOptions) *retryablehttp.Client { + return s.C +} + +var _ logrusx.Provider = (*BasicRegistry)(nil) +var _ httpx.ClientProvider = (*BasicRegistry)(nil) +var _ otelx.Provider = (*BasicRegistry)(nil) diff --git a/x/readall.go b/x/readall.go new file mode 100644 index 0000000..3c013f7 --- /dev/null +++ b/x/readall.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "io" +) + +func MustReadAll(r io.Reader) []byte { + all, err := io.ReadAll(r) + if err != nil { + panic(err) + } + return all +} diff --git a/x/redir/port_redirect.go b/x/redir/port_redirect.go new file mode 100644 index 0000000..ba88536 --- /dev/null +++ b/x/redir/port_redirect.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package redir + +import ( + "net/http" + "path" + "strings" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httprouterx" +) + +func RedirectToAdminRoute(reg config.Provider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + admin := reg.Config().SelfAdminURL(r.Context()) + + dest := *r.URL + dest.Host = admin.Host + dest.Scheme = admin.Scheme + dest.Path = strings.TrimPrefix(dest.Path, httprouterx.AdminPrefix) + dest.Path = path.Join(admin.Path, httprouterx.AdminPrefix, dest.Path) + + http.Redirect(w, r, dest.String(), http.StatusTemporaryRedirect) + } +} + +func RedirectToPublicRoute(reg config.Provider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + public := reg.Config().SelfPublicURL(r.Context()) + + dest := *r.URL + dest.Host = public.Host + dest.Scheme = public.Scheme + dest.Path = strings.TrimPrefix(dest.Path, httprouterx.AdminPrefix) + dest.Path = path.Join(public.Path, dest.Path) + + http.Redirect(w, r, dest.String(), http.StatusTemporaryRedirect) + } +} diff --git a/x/redir/port_redirect_test.go b/x/redir/port_redirect_test.go new file mode 100644 index 0000000..182e804 --- /dev/null +++ b/x/redir/port_redirect_test.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package redir_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/redir" + "github.com/ory/x/configx" + "github.com/ory/x/httprouterx" +) + +func TestRedirectToPublicAdminRoute(t *testing.T) { + pub, adm := httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix() + + adminTS := httptest.NewServer(adm) + pubTS := httptest.NewServer(pub) + t.Cleanup(pubTS.Close) + t.Cleanup(adminTS.Close) + _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyAdminBaseURL: adminTS.URL, + config.ViperKeyPublicBaseURL: pubTS.URL, + })) + + pub.POST("/privileged", redir.RedirectToAdminRoute(reg)) + pub.POST("/admin/privileged", redir.RedirectToAdminRoute(reg)) + adm.POST("/privileged", func(w http.ResponseWriter, r *http.Request) { + _, _ = io.Copy(w, r.Body) + }) + + adm.POST("/read", redir.RedirectToPublicRoute(reg)) + pub.POST("/read", func(w http.ResponseWriter, r *http.Request) { + _, _ = io.Copy(w, r.Body) + }) + + for k, tc := range []struct { + source string + dest string + }{ + { + source: pubTS.URL + "/privileged?foo=bar", + dest: adminTS.URL + "/admin/privileged?foo=bar", + }, + { + source: pubTS.URL + "/admin/privileged?foo=bar", + dest: adminTS.URL + "/admin/privileged?foo=bar", + }, + { + source: adminTS.URL + "/admin/read?foo=bar", + dest: pubTS.URL + "/read?foo=bar", + }, + } { + t.Run(fmt.Sprintf("%d", k), func(t *testing.T) { + id := x.NewUUID().String() + res, err := adminTS.Client().Post(tc.source, "", strings.NewReader(id)) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, tc.dest, res.Request.URL.String()) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, id, string(body)) + }) + } +} diff --git a/x/redir/secure_redirect.go b/x/redir/secure_redirect.go new file mode 100644 index 0000000..82a6ca6 --- /dev/null +++ b/x/redir/secure_redirect.go @@ -0,0 +1,204 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package redir + +import ( + "cmp" + "net/http" + "net/url" + "strings" + + "github.com/ory/kratos/x" + + "github.com/ory/kratos/text" + + "github.com/golang/gddo/httputil" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" +) + +type secureRedirectOptions struct { + allowlist []url.URL + defaultReturnTo *url.URL + returnTo string + sourceURL string +} + +type SecureRedirectOption func(*secureRedirectOptions) + +// SecureRedirectAllowURLs allows the given URLs for redirects. +func SecureRedirectAllowURLs(urls []url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.allowlist = append(o.allowlist, urls...) + } +} + +// SecureRedirectUseSourceURL uses the given source URL (checks the `?return_to` value) +// instead of r.URL. +func SecureRedirectUseSourceURL(source string) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.sourceURL = source + } +} + +// SecureRedirectReturnTo uses the provided URL to redirect the user to it. +func SecureRedirectReturnTo(returnTo string) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.returnTo = returnTo + } +} + +// SecureRedirectAllowSelfServiceURLs allows the caller to define `?return_to=` values +// which contain the server's URL and `/self-service` path prefix. Useful for redirecting +// to the login endpoint, for example. +func SecureRedirectAllowSelfServiceURLs(publicURL *url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.allowlist = append(o.allowlist, *urlx.AppendPaths(publicURL, "/self-service")) + } +} + +// SecureRedirectOverrideDefaultReturnTo overrides the defaultReturnTo address specified +// as the second arg. +func SecureRedirectOverrideDefaultReturnTo(defaultReturnTo *url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.defaultReturnTo = defaultReturnTo + } +} + +// SecureRedirectToIsAllowedHost validates if the redirect_to param is allowed for a given wildcard. +// +// The DNS-label boundary on wildcard hosts is enforced upstream, where the +// allow-list is loaded: Config.SelfServiceBrowserAllowedReturnToDomains drops +// unsafe wildcards (via corsx.ClassifyOrigin) unless the project is +// grandfathered. This function trusts that and only matches; do not re-add a +// boundary check here, or grandfathered projects would be enforced +// inconsistently. +func SecureRedirectToIsAllowedHost(returnTo *url.URL, allowed url.URL) bool { + if allowed.Host != "" && allowed.Host[:1] == "*" { + return strings.HasSuffix(strings.ToLower(returnTo.Host), strings.ToLower(allowed.Host)[1:]) + } + return strings.EqualFold(allowed.Host, returnTo.Host) +} + +// TakeOverReturnToParameter carries over the return_to parameter to a new URL +// If `from` does not contain the `return_to` query parameter, the first non-empty value from `fallback` is used instead. +func TakeOverReturnToParameter(from string, to string, fallback ...string) (string, error) { + fromURL, err := url.Parse(from) + if err != nil { + return "", err + } + returnTo := cmp.Or(append([]string{fromURL.Query().Get("return_to")}, fallback...)...) + // Empty return_to parameter, return early + if returnTo == "" { + return to, nil + } + toURL, err := url.Parse(to) + if err != nil { + return "", err + } + toQuery := toURL.Query() + toQuery.Set("return_to", returnTo) + toURL.RawQuery = toQuery.Encode() + return toURL.String(), nil +} + +// SecureRedirectTo implements a HTTP redirector who mitigates open redirect vulnerabilities by +// working with allow lists. +func SecureRedirectTo(r *http.Request, defaultReturnTo *url.URL, opts ...SecureRedirectOption) (returnTo *url.URL, err error) { + o := &secureRedirectOptions{defaultReturnTo: defaultReturnTo} + for _, opt := range opts { + opt(o) + } + + if len(o.allowlist) == 0 { + return o.defaultReturnTo, nil + } + + source := x.RequestURL(r) + if o.sourceURL != "" { + source, err = url.ParseRequestURI(o.sourceURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to parse the original request URL: %s", err)) + } + } + + rawReturnTo := cmp.Or(o.returnTo, source.Query().Get("return_to")) + if rawReturnTo == "" { + return o.defaultReturnTo, nil + } + + returnTo, err = url.Parse(rawReturnTo) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to parse the return_to query parameter as an URL: %s", err)) + } + + returnTo.Host = cmp.Or(returnTo.Host, o.defaultReturnTo.Host) + returnTo.Scheme = cmp.Or(returnTo.Scheme, o.defaultReturnTo.Scheme) + + for _, allowed := range o.allowlist { + if strings.EqualFold(allowed.Scheme, returnTo.Scheme) && + SecureRedirectToIsAllowedHost(returnTo, allowed) && + strings.HasPrefix( + cmp.Or(returnTo.Path, "/"), + cmp.Or(allowed.Path, "/")) { + return returnTo, nil + } + } + + return nil, errors.WithStack(herodot.ErrBadRequest(). + WithID(text.ErrIDRedirectURLNotAllowed). + WithReasonf("Requested return_to URL %q is not allowed.", returnTo), + ) +} + +func SecureContentNegotiationRedirection( + w http.ResponseWriter, r *http.Request, out interface{}, + requestURL string, writer herodot.Writer, c *config.Config, + opts ...SecureRedirectOption, +) error { + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "application/json", + }, "text/html") { + case "application/json": + writer.Write(w, r, out) + case "text/html": + fallthrough + default: + ret, err := SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(r.Context()), + append([]SecureRedirectOption{ + SecureRedirectUseSourceURL(requestURL), + SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(r.Context())), + SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), + }, opts...)..., + ) + if err != nil { + return err + } + + http.Redirect(w, r, ret.String(), http.StatusSeeOther) + } + + return nil +} + +func ContentNegotiationRedirection( + w http.ResponseWriter, r *http.Request, out interface{}, writer herodot.Writer, returnTo string, +) { + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "application/json", + }, "text/html") { + case "application/json": + writer.Write(w, r, out) + case "text/html": + fallthrough + default: + http.Redirect(w, r, returnTo, http.StatusSeeOther) + } +} diff --git a/x/redir/secure_redirect_test.go b/x/redir/secure_redirect_test.go new file mode 100644 index 0000000..26604b0 --- /dev/null +++ b/x/redir/secure_redirect_test.go @@ -0,0 +1,322 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package redir_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/kratos/x/redir" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/pkg" + "github.com/ory/kratos/x" +) + +func TestSecureContentNegotiationRedirection(t *testing.T) { + conf, _ := pkg.NewFastRegistryWithMocks(t) + var jsonActual = json.RawMessage(`{"foo":"bar"}` + "\n") + writer := herodot.NewJSONWriter(nil) + + router := http.NewServeMux() + router.HandleFunc("GET /redir", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, redir.SecureContentNegotiationRedirection(w, r, jsonActual, x.RequestURL(r).String(), writer, conf)) + }) + router.HandleFunc("GET /default-return-to", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + router.HandleFunc("GET /return-to", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + ctx := context.Background() + + defaultReturnTo := ts.URL + "/default-return-to" + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, defaultReturnTo) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{ts.URL}) + + run := func(t *testing.T, href string, contentType string) (*http.Response, string) { + req, err := http.NewRequest("GET", href, nil) + require.NoError(t, err) + req.Header.Add("Accept", contentType) + res, err := ts.Client().Do(req) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + return res, string(body) + } + + t.Run("case=html browser causes redirect", func(t *testing.T) { + res, _ := run(t, ts.URL+"/redir", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + assert.EqualValues(t, res.Request.URL.String(), defaultReturnTo) + }) + + t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) { + res, _ := run(t, ts.URL+"/redir?return_to=/redirect-to", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redirect-to") + }) + + t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) { + res, body := run(t, ts.URL+"/redir?return_to=/redirect-to", "application/json") + assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redir?return_to=/redirect-to") + assert.EqualValues(t, body, jsonActual) + }) +} + +func TestSecureRedirectToIsAllowedHost(t *testing.T) { + type testCase struct { + allowedURL string + redirectURL string + valid bool + } + tests := map[string]testCase{ + "case=Domain is allowed": {allowedURL: "https://foo.bar", redirectURL: "https://foo.bar/redir", valid: true}, + "case=Domain prefix is allowed": {allowedURL: "https://*.bar", redirectURL: "https://foo.bar/redir", valid: true}, + "case=Subdomain prefix is allowed": {allowedURL: "https://*.foo.bar", redirectURL: "https://auth.foo.bar/redir", valid: true}, + "case=Domain is not allowed": {allowedURL: "https://foo.baz", redirectURL: "https://foo.bar/redir", valid: false}, + "case=Domain wildcard is not allowed": {allowedURL: "https://*.foo.baz", redirectURL: "https://foo.bar/redir", valid: false}, + "case=Subdomain is not allowed": {allowedURL: "https://*.foo.baz", redirectURL: "https://auth.foo.bar/redir", valid: false}, + // A labelled wildcard's leading-dot suffix keeps the label boundary: + // subdomains match, suffix-only hosts do not. (Unbound wildcards are + // dropped upstream by SelfServiceBrowserAllowedReturnToDomains, so the + // matcher never sees them outside grandfathered projects.) + "case=Labelled wildcard matches subdomain": {allowedURL: "https://*.foo.com", redirectURL: "https://sub.foo.com/redir", valid: true}, + "case=Labelled wildcard does not match suffix host": {allowedURL: "https://*.foo.com", redirectURL: "https://evilfoo.com/redir", valid: false}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + allowedURL, err := url.Parse(tc.allowedURL) + require.NoError(t, err) + redirectURL, err := url.Parse(tc.redirectURL) + require.NoError(t, err) + assert.Equal(t, redir.SecureRedirectToIsAllowedHost(redirectURL, *allowedURL), tc.valid) + }) + } +} + +func TestTakeOverReturnToParameter(t *testing.T) { + type testCase struct { + fromUrl string + toURL string + expectedOutputUrl string + } + tests := map[string]testCase{ + "case=return_to is taken over": {fromUrl: "https://original.bar?return_to=https://allowed.domain", toURL: "https://output.bar", expectedOutputUrl: "https://output.bar?return_to=https%3A%2F%2Fallowed.domain"}, + "case=only return_to is taken over when multiple query parameters are set": {fromUrl: "https://original.bar?return_to=https://allowed.domain&flow=12312", toURL: "https://output.bar", expectedOutputUrl: "https://output.bar?return_to=https%3A%2F%2Fallowed.domain"}, + "case=output query parameters are preserved": {fromUrl: "https://original.bar?return_to=https://allowed.domain", toURL: "https://output.bar?flow=123321", expectedOutputUrl: "https://output.bar?flow=123321&return_to=https%3A%2F%2Fallowed.domain"}, + "case=when original return_to is empty do nothing": {fromUrl: "https://original.bar?return_to=", toURL: "https://output.bar?flow=123123", expectedOutputUrl: "https://output.bar?flow=123123"}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + output, err := redir.TakeOverReturnToParameter(tc.fromUrl, tc.toURL) + require.NoError(t, err) + assert.Equal(t, output, tc.expectedOutputUrl) + }) + } +} + +func TestSecureRedirectTo(t *testing.T) { + newServer := func(t *testing.T, isTLS bool, isRelative bool, expectErr bool, opts func(ts *httptest.Server) []redir.SecureRedirectOption) *httptest.Server { + var ts *httptest.Server + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if opts == nil { + opts = func(ts *httptest.Server) []redir.SecureRedirectOption { + return nil + } + } + defaultReturnTo := "/default-return-to" + if !isRelative { + defaultReturnTo = ts.URL + defaultReturnTo + } + returnTo, err := redir.SecureRedirectTo(r, urlx.ParseOrPanic(defaultReturnTo), opts(ts)...) + if expectErr { + require.Error(t, err) + _, _ = w.Write([]byte("error")) + return + } + require.NoError(t, err) + + _, _ = w.Write([]byte(returnTo.String())) + }) + + if isTLS { + ts = httptest.NewTLSServer(handler) + } else { + ts = httptest.NewServer(handler) + } + + t.Cleanup(ts.Close) + return ts + } + + makeRequest := func(t *testing.T, ts *httptest.Server, path string) (*http.Response, string) { + res, err := ts.Client().Get(ts.URL + "/" + path) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + return res, string(body) + } + + t.Run("case=return to a relative path with anchor works", func(t *testing.T) { + returnTo, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "/?return_to=/foo/kratos%23abcd", nil), + urlx.ParseOrPanic("/default-return-to"), + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}), + ) + require.NoError(t, err) + assert.Equal(t, returnTo.String(), "/foo/kratos#abcd") + }) + + t.Run("case=return to default URL if nothing is allowed", func(t *testing.T) { + returnTo, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "/?return_to=/foo", nil), + urlx.ParseOrPanic("https://www.ory.sh/default-return-to"), + ) + require.NoError(t, err) + assert.Equal(t, returnTo.String(), "https://www.ory.sh/default-return-to") + }) + + t.Run("case=return to foo with server baseURL if allowed", func(t *testing.T) { + returnTo, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "/?return_to=/foo", nil), + urlx.ParseOrPanic("https://www.ory.sh/default-return-to"), + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh")}), + ) + require.NoError(t, err) + assert.Equal(t, returnTo.String(), "https://www.ory.sh/foo") + }) + + t.Run("case=return to a relative path works", func(t *testing.T) { + returnTo, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "/?return_to=/foo/kratos", nil), + urlx.ParseOrPanic("/default-return-to"), + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}), + ) + require.NoError(t, err) + assert.Equal(t, returnTo.String(), "/foo/kratos") + }) + + t.Run("case=return to a fully qualified domain is forbidden if allowlist is relative", func(t *testing.T) { + _, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "/?return_to=https://www.ory.sh/foo/kratos", nil), + urlx.ParseOrPanic("/default-return-to"), + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}), + ) + require.Error(t, err) + }) + + t.Run("case=return to another domain works", func(t *testing.T) { + returnTo, err := redir.SecureRedirectTo( + httptest.NewRequest("GET", "https://example.com/?return_to=https://www.ory.sh/foo/kratos", nil), + urlx.ParseOrPanic("https://www.ory.sh/default-return-to"), + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/foo")}), + ) + require.NoError(t, err) + assert.Equal(t, returnTo.String(), "https://www.ory.sh/foo/kratos") + }) + + t.Run("case=return to another domain fails if host mismatches", func(t *testing.T) { + s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.not-ory.sh/")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=return to another domain fails if path mismatches", func(t *testing.T) { + s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/not-kratos")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=return to another domain fails if scheme mismatches", func(t *testing.T) { + s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("http://www.ory.sh/")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=should work with self-service modifier", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectAllowSelfServiceURLs(urlx.ParseOrPanic(ts.URL))} + }) + _, body := makeRequest(t, s, "?return_to=/self-service/foo") + assert.Equal(t, body, s.URL+"/self-service/foo") + }) + + t.Run("case=should work with default return to", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic(ts.URL + "/another-default"))} + }) + _, body := makeRequest(t, s, "") + assert.Equal(t, body, s.URL+"/another-default") + }) + + t.Run("case=should override return_to", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{ + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic(ts.URL)}), + redir.SecureRedirectUseSourceURL("https://foo/bar?return_to=/override"), + } + }) + _, body := makeRequest(t, s, "?return_to=/original") + assert.Equal(t, body, s.URL+"/override") + }) + + t.Run("case=should work with subdomain wildcard", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://*.ory.sh/")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "https://www.ory.sh/kratos") + _, body = makeRequest(t, s, "?return_to=https://even.deeper.nested.ory.sh/kratos") + assert.Equal(t, body, "https://even.deeper.nested.ory.sh/kratos") + }) + + t.Run("case=should fallback to default return_to scheme", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{ + redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh")}), + redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic("https://www.ory.sh/docs")), + } + }) + _, body := makeRequest(t, s, "?return_to=//www.ory.sh/kratos") + assert.Equal(t, body, "https://www.ory.sh/kratos") + }) + + t.Run("case=should fallback to default return_to host", func(t *testing.T) { + s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption { + return []redir.SecureRedirectOption{ + redir.SecureRedirectAllowURLs([]url.URL{ + *urlx.ParseOrPanic("https://www.ory.sh"), + *urlx.ParseOrPanic("http://www.ory.sh"), + }), + redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic("https://www.ory.sh/docs")), + } + }) + _, body := makeRequest(t, s, "?return_to=http:///kratos") + assert.Equal(t, body, "http://www.ory.sh/kratos") + }) +} diff --git a/x/require.go b/x/require.go new file mode 100644 index 0000000..fb51941 --- /dev/null +++ b/x/require.go @@ -0,0 +1,18 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func RequireJSONMarshal(t *testing.T, in interface{}) []byte { + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(in)) + return b.Bytes() +} diff --git a/x/router.go b/x/router.go new file mode 100644 index 0000000..75dadd5 --- /dev/null +++ b/x/router.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "github.com/ory/x/httprouterx" +) + +type PublicHandler interface { + RegisterPublicRoutes(public *httprouterx.RouterPublic) +} + +type AdminHandler interface { + RegisterAdminRoutes(admin *httprouterx.RouterAdmin) +} + +type Handler interface { + PublicHandler + AdminHandler +} diff --git a/x/sql.go b/x/sql.go new file mode 100644 index 0000000..3c9a1c1 --- /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 0000000..f8c523d --- /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/stub_fs.go b/x/stub_fs.go new file mode 100644 index 0000000..d53f8e8 --- /dev/null +++ b/x/stub_fs.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "io" + "io/fs" + "time" +) + +type StubFS struct { + name string + data []byte + offset int +} + +func NewStubFS(name string, data []byte) fs.FS { + return &StubFS{ + name: name, + data: data, + } +} + +func (stub *StubFS) Mode() fs.FileMode { + return 0444 +} + +func (stub *StubFS) ModTime() time.Time { + return time.Time{} +} + +func (stub *StubFS) IsDir() bool { + return false +} + +func (stub *StubFS) Sys() interface{} { + return nil +} + +func (stub *StubFS) Stat() (fs.FileInfo, error) { + return stub, nil +} + +func (stub *StubFS) Read(bytes []byte) (int, error) { + if stub.offset >= len(stub.data) { + return 0, io.EOF + } + n := copy(bytes, stub.data[stub.offset:]) + stub.offset += n + return n, nil +} + +func (stub *StubFS) Close() error { + return nil +} + +func (stub *StubFS) Open(name string) (fs.File, error) { + return stub, nil +} + +func (stub *StubFS) Name() string { + return stub.name +} + +func (stub *StubFS) Size() int64 { + return int64(len(stub.data)) +} diff --git a/x/swagger/swagger_meta.go b/x/swagger/swagger_meta.go new file mode 100644 index 0000000..1c919b7 --- /dev/null +++ b/x/swagger/swagger_meta.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Ory Kratos +// +// Welcome to the Ory Kratos HTTP API documentation! +// +// Schemes: http, https +// Host: +// BasePath: / +// Version: latest +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// SecurityDefinitions: +// oryAccessToken: +// type: apiKey +// name: Authorization +// in: header +// +// Extensions: +// --- +// x-request-id: string +// x-forwarded-proto: string +// --- +// +// swagger:meta +package swagger diff --git a/x/swagger/swagger_types_global.go b/x/swagger/swagger_types_global.go new file mode 100644 index 0000000..8057ee4 --- /dev/null +++ b/x/swagger/swagger_types_global.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package swagger + +import "github.com/ory/herodot" + +// JSON API Error Response +// +// The standard Ory JSON API error format. +// +// swagger:model errorGeneric +type ErrorGeneric struct { + // Contains error details + // + // required: true + Error GenericError `json:"error"` +} + +// 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 204. +// +// swagger:response emptyResponse +type _ struct{} diff --git a/x/swagger/swagger_types_overrides.go b/x/swagger/swagger_types_overrides.go new file mode 100644 index 0000000..7949e79 --- /dev/null +++ b/x/swagger/swagger_types_overrides.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package swagger + +import "github.com/go-openapi/strfmt" + +// swagger:model UUID +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type uuid strfmt.UUID4 diff --git a/x/tests.go b/x/tests.go new file mode 100644 index 0000000..8657b4f --- /dev/null +++ b/x/tests.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func MustEncodeJSON(t *testing.T, in interface{}) string { + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(in)) + return b.String() +} + +const HostedHttpBin = "https://ory-network-httpbin-ijakee5waq-ez.a.run.app" diff --git a/x/time.go b/x/time.go new file mode 100644 index 0000000..4457e03 --- /dev/null +++ b/x/time.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "math" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var rnd = rand.New(rand.NewSource(time.Now().Unix())) //#nosec G404 + +func AssertEqualTime(t *testing.T, expected, actual time.Time) { + assert.EqualValues(t, expected.UTC().Round(time.Second), actual.UTC().Round(time.Second)) +} + +func RequireEqualTime(t *testing.T, expected, actual time.Time) { + require.EqualValues(t, expected.UTC().Round(time.Second), actual.UTC().Round(time.Second)) +} + +// RandomDelay returns a time randomly chosen from a normal distribution with mean of base and max/min of base +- deviation +// From the docstring for the rand.NormFloat64(): +// To produce a different normal distribution, callers can +// adjust the output using: +// +// sample = NormFloat64() * desiredStdDev + desiredMean +// +// Since 99.73% of values in a normal distribution lie within three standard deviations from the mean (https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule), +// by taking the standard deviation to be deviation/3, we can get a distribution which fits our bounds nicely with minimal clipping when we take max/mins to cut off the tails. +func RandomDelay(base, deviation time.Duration) time.Duration { + max := float64(base + deviation) + min := float64(base - deviation) + stddev := float64(deviation) / 3 + sample := rnd.NormFloat64() * (stddev + float64(base)) + boundedSample := math.Min(math.Max(sample, min), max) + return time.Duration(boundedSample) +} diff --git a/x/time_test.go b/x/time_test.go new file mode 100644 index 0000000..a5409e5 --- /dev/null +++ b/x/time_test.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRandomDelay(t *testing.T) { + base := time.Millisecond * 2 + deviation := time.Millisecond + for i := 0; i < 100; i++ { + delay := RandomDelay(base, deviation) + require.LessOrEqual(t, delay, base+deviation) + require.GreaterOrEqual(t, delay, base-deviation) + } +} diff --git a/x/token_prefixes.go b/x/token_prefixes.go new file mode 100644 index 0000000..577723e --- /dev/null +++ b/x/token_prefixes.go @@ -0,0 +1,7 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +const OrySessionToken = "ory_st_" +const OryLogoutToken = "ory_lo_" diff --git a/x/transaction/transaction.go b/x/transaction/transaction.go new file mode 100644 index 0000000..c796ee6 --- /dev/null +++ b/x/transaction/transaction.go @@ -0,0 +1,23 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package transaction holds the persistence transaction contracts. It is a leaf +// package so that importing it does not pull the SQL layer into API-only +// consumers such as the CLI. +package transaction + +import ( + "context" + + "github.com/ory/pop/v6" +) + +type ( + PersistenceProvider interface { + TransactionalPersisterProvider() Persister + } + + Persister interface { + Transaction(ctx context.Context, callback func(ctx context.Context, connection *pop.Connection) error) error + } +) diff --git a/x/uuid.go b/x/uuid.go new file mode 100644 index 0000000..19406a2 --- /dev/null +++ b/x/uuid.go @@ -0,0 +1,18 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "github.com/gofrs/uuid" +) + +var EmptyUUID uuid.UUID + +func NewUUID() uuid.UUID { + return uuid.Must(uuid.NewV4()) +} + +func ParseUUID(in string) uuid.UUID { + return uuid.FromStringOrNil(in) +} diff --git a/x/webauthnx/aaguid/aaguid.go b/x/webauthnx/aaguid/aaguid.go new file mode 100644 index 0000000..b3875fd --- /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" + "maps" + + "github.com/gofrs/uuid" +) + +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 0000000..8f7d83a --- /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/passkey-aaguids.json b/x/webauthnx/aaguid/passkey-aaguids.json new file mode 100644 index 0000000..8410d2e --- /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/x/webauthnx/errors.go b/x/webauthnx/errors.go new file mode 100644 index 0000000..53e3381 --- /dev/null +++ b/x/webauthnx/errors.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthnx + +import ( + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" +) + +var ErrNoCredentials = errors.New("required credentials not found") + +func ErrNotEnoughCredentials() *jsonschema.ValidationError { + return &jsonschema.ValidationError{Message: "unable to remove this security key because it would lock you out of your account", InstancePtr: "#/webauthn_remove"} +} + +// ErrCredentialAlreadyRegistered is returned when a settings flow submits a credential whose ID +// is already registered on the identity. Compliant browsers prevent this via excludeCredentials, +// so this guards against clients that ignore the exclusion list. +func ErrCredentialAlreadyRegistered(instancePtr string) *jsonschema.ValidationError { + return &jsonschema.ValidationError{Message: "this security key or passkey is already registered with your account", InstancePtr: instancePtr} +} diff --git a/x/webauthnx/handler.go b/x/webauthnx/handler.go new file mode 100644 index 0000000..44d2da8 --- /dev/null +++ b/x/webauthnx/handler.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthnx + +import ( + _ "embed" + "net/http" + + "github.com/ory/x/httprouterx" +) + +//go:embed js/webauthn.js +var jsOnLoad []byte + +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 +// +// # Get WebAuthn JavaScript +// +// This endpoint provides JavaScript which is needed in order to perform WebAuthn login and registration. +// +// If you are building a JavaScript Browser App (e.g. in ReactJS or AngularJS) you will need to load this file: +// +// ```html +//